July 12, 2026
TryHackMe — Scheme Catcher (Advent of Cyber 2025 · Side Quest 2)
🔗 Room: https://tryhackme.com/room/sq2-aoc2025-JxiOKUSD9R — AoC 2025 Side Quest 🃏 Theme: “The Silent Control System of the Jester” ·…
By Harry Mike
4 min read
🔗 Room: https://tryhackme.com/room/sq2-aoc2025-JxiOKUSD9R — AoC 2025 Side Quest 🃏 Theme: "The Silent Control System of the Jester" · Format: four-question Q&A 🏷️ Tags:
reversing packer c2 pwn heap glibc-2.40 house-of-water
A silent control system. Connections that drop, logs that rewrite themselves. Somewhere behind the Jester's beacon is a payload store you're meant to break.
This side quest is two boxes in one: a reverse-engineering half (defeat a self-decrypting C2 beacon to recover a hidden endpoint) and a binary-exploitation half (a leakless glibc-2.40 heap pwn against the payload service). We solved the RE half from first principles; the pwn half is the intended technique, and we credit the write-ups we followed for it in the footer.
Enumeration
$ nmap -sV -sC -Pn <MACHINE_IP>
22/tcp open ssh OpenSSH 9.6p1 Ubuntu # publickey-only — not the way in
80/tcp open http Apache/2.4.58 (Ubuntu) # “Under Construction” page
9004/tcp open unknown “Payload Storage Malhare’s / Version 4.2.0”
$ nmap -sV -sC -Pn <MACHINE_IP>
22/tcp open ssh OpenSSH 9.6p1 Ubuntu # publickey-only — not the way in
80/tcp open http Apache/2.4.58 (Ubuntu) # “Under Construction” page
9004/tcp open unknown “Payload Storage Malhare’s / Version 4.2.0”
The web root is a static placeholder, but directory listing is enabled, and that's the whole opening:
/dev/ -> 4.2.0.zip -> latest/beacon.bin
/dev/ -> 4.2.0.zip -> latest/beacon.bin
beacon.bin is an ELF64, not stripped — a C2 beacon. That's our reversing target.
The beacon — first flag in plain sight
Before any real analysis, the cheap check pays off. The "packing" never actually protected the first flag — it sits in cleartext in .rodata:
$ strings beacon.bin | grep THM
THM{[REDACTED]} # Q1
$ strings beacon.bin | grep THM
THM{[REDACTED]} # Q1
Q1 captured with nothing more than strings.
Unpacking the self-decrypting ELF
Disassemble the beacon and the code region is garbage — because it's self-decrypting. The real entry point is a custom .easter section that XOR-decrypts the code at runtime, then jumps into it:
.easter @ 0x804000:
XOR region 0x401370 .. 0x401bc4 with key 0x0D
push 0x401370 ; ret # jump into the freshly-decrypted code
.easter @ 0x804000:
XOR region 0x401370 .. 0x401bc4 with key 0x0D
push 0x401370 ; ret # jump into the freshly-decrypted code
Replicate that XOR (single-byte key 0x0d) over the same region to produce a clean, decrypted binary. Now the logic reads plainly:
action()promptsEnter key:,reads 32 bytes, andstrcmps them against a hardcoded key ([REDACTED]). On a match it prints "Access granted" and binds a socket server on0.0.0.0:4444with a small command menu.payload_load()openslocalhost:80and requests a path that is assembled from two immediate values in the code — amovabsof one 8-byte chunk plus amovof a 3-byte tail — which reassemble into a secret web directory (/[REDACTED]/).
That hidden directory is the point: it isn't linked anywhere, and you only learn it by reversing the decrypted beacon — exactly what the room rewards (no fuzzing required).
The hidden directory — second flag + the pwn kit
Browsing the recovered path (directory listing is on here too):
/[REDACTED]/foothold.txt -> THM{[REDACTED]} # Q2
/[REDACTED]/4.2.0-R1–1337-server.zip -> server + libc.so.6 (glibc 2.40) + ld
/[REDACTED]/foothold.txt -> THM{[REDACTED]} # Q2
/[REDACTED]/4.2.0-R1–1337-server.zip -> server + libc.so.6 (glibc 2.40) + ld
Q2 captured. foothold.txt reads "bypass and pack is: pwn you need" — the room spelling out its own plan: the bypass (the hardcoded key), the pack (the self-decrypting beacon), and the pwn (of port 9004) are the intended foothold. The zip conveniently ships the server binary plus its exact libc/ld — a complete offline exploitation kit.
The port-9004 "Payload Storage" service
server (behind the 9004 banner) is a classic heap-note manager — create / update / delete slots:
- create —
malloc(size), stores pointer + size, no zero-init. - update — strictly bounds-checked, writes in-chunk only. No overflow.
- delete —
free(chunk)but never clears the pointer → use-after-free + double-free. This is the only memory-safety bug.
The mitigations are deliberately maxed out: Full RELRO, PIE, stack canary, NX, on glibc 2.40 (tcache safe-linking, key-checked double-free, the 2.40 unsorted-bin checks). Crucially there is no output/leak function anywhere in the binary, and ASLR is on — so there's no in-band information leak to bootstrap from.
Where our own solve ended. We characterised all of the above black-box — the UAF, the full 2.40 mitigation set, the absence of a leak, and (empirically) that ASLR is enabled — and we captured Q1/Q2. We did not land the 9004 exploit ourselves. What follows is the intended solution, per the published write-ups credited in the footer.
Q3 / Q4 — the intended pwn (House of Water → container → host)
The bug is a leakless UAF against a fully-mitigated 2.40 heap. The intended technique is House of Water, which keeps the entire bootstrap heap-local so ASLR costs only a small brute:
- Forge a chunk inside tcache_perthread_struct. Free specific sizes so the struct's counts field reads as a fake chunk header of size 0x10001. The tcache struct's entries[] aren't safe-linked, so pointers there are usable raw.
- Link the fake chunk into the unsorted bin with 2-byte partial overwrites, then a targeted malloc returns control of the tcache_perthread_struct itself — now holding a raw libc pointer, i.e. a libc-relative write with no prior leak.
- Beat ASLR with a 1/256 nibble brute (two 4-bit nibbles — heap-local, not the far larger heap→PIE bridge the naïve read of the box suggests).
- Leak then win in one connection — corrupt IO_2_1_stdout so a flush prints an 8-byte libc pointer, fixing the libc base, then drop a House of Apple 2 payload _IO_wfile_jumps → system) for a shell.
A prerequisite that's easy to miss: the rapid connections the brute needs are throttled by a keyed firewall on port 21337 (the literal source of the "connections drop" theme) — removed with a key recovered from a different AoC room (Day 9). That's the real defence, not fail2ban.
The shell lands as root inside a privileged Docker container CapEff: 000001ffffffffff):
# Q3 — user.txt, inside the container
cat user.txt -> THM{[REDACTED]}
# Q4 — privileged container, so mount the host disk directly:
mount /dev/nvme0n1p1 /mnt/host
cat /mnt/host/root/root.txt -> THM{[REDACTED]}
# Q3 — user.txt, inside the container
cat user.txt -> THM{[REDACTED]}
# Q4 — privileged container, so mount the host disk directly:
mount /dev/nvme0n1p1 /mnt/host
cat /mnt/host/root/root.txt -> THM{[REDACTED]}
(The "intended" root is a kagent.ko kernel-module ioctl exploit; the privileged-container disk mount is the shortcut.)
Wrap-up
Scheme Catcher is a genuinely two-discipline side quest:
- RE — defeat a self-decrypting C2 beacon .easter XOR) to recover a hidden endpoint; Q1 was cleartext, Q2 lives behind the reversed path.
- Pwn — a leakless glibc-2.40 UAF, solved with House of Water + House of Apple 2, gated behind a keyed firewall, landing in a privileged container that owns the host disk.
Defensive takeaways
- A packer is not protection. The first flag was cleartext in .rodata; the self-decrypting stub only slows an analyst down.
- Don't ship your exploitation kit. The service binary and its exact libcld sat in a listable web directory — everything an attacker needs to build the pwn offline. Disable directory listing; don't publish binaries.
- Hardcoded keys and "secret" paths are security-by-obscurity. Both were recoverable by trivial reversing.
- Null pointers on free(). The whole chain starts at a UAF from a dangling pointer — clear it, and add double-free / one-object guards.
- Never run a memory-unsafe network service as root in a privileged container. Container RCE became host root via a raw disk mount. Drop capabilities; no privileged containers.
- (Positives: ASLR, Full RELRO, and the connection-drop firewall genuinely raised the bar — they're why the leakless pwn is hard.)
Credits & sources
Q1 and Q2 were solved from first principles here (beacon reversing + hidden-directory discovery). The Q3/Q4 exploitation chain (House of Water, the port-21337 firewall, and the container-to-host step) follows the intended solution as documented in these excellent write-ups — full credit to their authors:
- jaxafed — https://jaxafed.github.io/posts/tryhackme-aoc2025_sidequest_two/
- rootagi — https://github.com/rootagi/Scheme-Catcher
Room: TryHackMe — Advent of Cyber 2025, Side Quest 2 "Scheme Catcher." Flags, keys, and the hidden path are redacted. 🃏