October 29, 2026
CTF Chronicles: Walkthrough on Log Correlation, Privilege Escalation, and a Sealed Vault
There’s a particular kind of satisfaction in a box that doesn’t just hand you a shell and call it a day. Gargantua (the room I’m writing up…
By Nathamayil Natesh
5 min read
There's a particular kind of satisfaction in a box that doesn't just hand you a shell and call it a day. Gargantua (the room I'm writing up here) does something I don't see often enough in CTF design: it makes you think like an analyst before it lets you think like an attacker. Half the challenge isn't exploitation — it's correlation, pattern-spotting, and resisting the urge to brute-force your way past a puzzle that actually wants you to read the data.
Here's how it went, stage by stage.
Stage 1: A Custom Protocol with an Old Bug Wearing a New Coat
The entry point was a bespoke TCP service on port 50505 — the Gargantua Sync Protocol (GSP) — speaking a tiny command set: HELP, STATUS, FETCH, QUIT. FETCH pulls a "manifest" from a relay store, and the service politely filters out .. in the path to stop directory traversal.
Except it doesn't filter absolute paths. The code boiled down to this:
python
target = os.path.join(BASE, arg)target = os.path.join(BASE, arg)If arg is relative, you stay sandboxed inside BASE. If arg is an absolute path, os.path.join silently discards BASE entirely and just uses arg. It's one of those bugs that's almost too simple — but it's also a bug real codebases ship with regularly, which is probably the point.
That one line turned a read-only "status manifest" service into an arbitrary file read primitive, running as the user edmunds. A quick script hunting for SSH keys across every plausible home directory:
python
for u in users:
for k in keynames:
p = f"/home/{u}/.ssh/{k}"
r = fetch(p)for u in users:
for k in keynames:
p = f"/home/{u}/.ssh/{k}"
r = fetch(p)…turned up edmunds's own private Ed25519 key sitting in his .ssh folder, readable because the FETCH handler didn't care whose files it was serving. SSH in with that key, and flag.txt was sitting right there:
TFG_Kp3Vn7Qz_GPTFG_Kp3Vn7Qz_GPLesson: path-sanitization that blocks .. but not absolute paths is a classic near-miss. If your input validation thinks in terms of "forbidden substrings" instead of "resolved destination," it's not validation, it's a suggestion.
Stage 2: Signal in the Noise
This is where the room earns its name. Edmunds' home directory held an 8.9MB file, activity.log.enc, described in the task only as: find the anomaly in a sea of beacon traffic, using a smaller VPN login record as ground truth.
First problem: it was openssl enc'd with a salted password, and the salted header confirmed the format but not the cipher or key-derivation settings. The task hint — "your flag unlocks doors" — turned out to be literal. The Stage 1 flag was the encryption passphrase:
bash
openssl enc -d -aes-256-cbc -pbkdf2 -iter 10000 \
-in activity.log.enc -out activity.log -k 'TFG_Kp3Vn7Qz_GP'openssl enc -d -aes-256-cbc -pbkdf2 -iter 10000 \
-in activity.log.enc -out activity.log -k 'TFG_Kp3Vn7Qz_GP'That decrypted to a gzip-compressed tar archive, which unpacked into two files:
beacons.log— 300,001 lines of beacon trafficvpn_auth.log— 4,001 legitimate VPN sign-ins
My first instinct was to correlate operator + IP + nearest-prior-login — classic "does this beacon's source match that operator's most recent VPN session" logic. It failed spectacularly: every single beacon flagged as a mismatch, because VPN clients rotate IPs constantly. Good theory, wrong signal — a reminder that in real telemetry, "doesn't match the most recent session" is normal, not suspicious.
Second attempt: look for exact IP collisions where a beacon claims one operator but the IP belongs to a different real VPN session. That cut it down to 286 candidates, almost entirely falling inside private RFC1918 ranges — expected noise on a shared office NAT, not evidence of anything.
The actual signal turned out to be structural, not relational. A quick date-range sanity check:
bash
awk '{print $1}' beacons.log | cut -c1-10 | sort -uawk '{print $1}' beacons.log | cut -c1-10 | sort -u…returned every date from 2067-08-20 through 2067-08-31, plus one outlier: 2067-09-01. One line, sitting a full day outside the rest of the dataset:
2067-09-01T04:17:52Z beacon operator=mann src=10.50.24.199 \
session=deadbeef token=TFG_Rw9Ld2Xf_GP2067-09-01T04:17:52Z beacon operator=mann src=10.50.24.199 \
session=deadbeef token=TFG_Rw9Ld2Xf_GPNotice the session ID too: deadbeef. Not even subtle, once you're looking at the right thing — but buried under 300,000 lines, it took several wrong turns to narrow down to it.
Lesson: when correlating two noisy datasets, don't assume the anomaly lives in the relationship between them. Sometimes it's a much simpler, orthogonal property — a timestamp outside the valid window, a value that's clearly been hand-planted (deadbeef isn't hex noise, it's a wink from the challenge author).
Stage 3: The Operator Roster
The next clue was deliberately vague: a roster synced locally, not in a home directory. This pointed back to something I'd already stumbled across but not opened — /var/lib/gargantua/.sync/telemetry.cache, another openssl enc'd file.
Following the established pattern, the previous flag was the key:
bash
openssl enc -d -aes-256-cbc -pbkdf2 -iter 10000 \
-in telemetry.cache -out telemetry.raw -k 'TFG_Rw9Ld2Xf_GP'openssl enc -d -aes-256-cbc -pbkdf2 -iter 10000 \
-in telemetry.cache -out telemetry.raw -k 'TFG_Rw9Ld2Xf_GP'This one decrypted straight to a SQLite database, with two tables: operators and sessions. The operators table had handles, roles, and SHA-512 password hashes:
1|edmunds|relay|$6$...
2|romilly|observer|$6$...
3|doyle|observer|$6$...
4|miller|controller|$6$...
5|getty|relay|$6$...1|edmunds|relay|$6$...
2|romilly|observer|$6$...
3|doyle|observer|$6$...
4|miller|controller|$6$...
5|getty|relay|$6$...Four roles, three tiers: relay, observer, and one solitary controller — miller. Combined with a sessions table entry showing miller logged in right around the Stage 2 anomaly's timestamp, the "strongest operator" was clearly the target.
Cracking the hash with john and rockyou.txt took under two seconds:
sunshine1 (miller)sunshine1 (miller)Weak passwords age about as well as you'd expect.
Getting in as miller needed one more twist: the box's sshd_config only allows password authentication for the sysadmin account — everyone else is key-only. So the path wasn't ssh miller@host, it was ssh edmunds@host followed by su - miller using the cracked password locally.
Stage 4: Housekeeping
Once inside as miller, id revealed membership in the operators group — which mattered, because a maintenance script discovered earlier was owned root:operators and group-writable:
-rwxrwxr-x 1 root operators 166 sync_cleanup.sh-rwxrwxr-x 1 root operators 166 sync_cleanup.shRoot ran it every minute via cron. That's about as textbook a privilege escalation as they come: anything appended to that script executes as root, sixty seconds later, guaranteed.
Rather than drop a reverse shell or touch a password (both explicitly off-limits for this live instance), I generated a fresh SSH keypair and had the cron job append it to root's authorized_keys:
bash
echo 'mkdir -p /root/.ssh && echo "<pubkey>" >> /root/.ssh/authorized_keys \
&& chmod 700 /root/.ssh && chmod 600 /root/.ssh/authorized_keys' \
>> /opt/gargantua/maintenance/sync_cleanup.shecho 'mkdir -p /root/.ssh && echo "<pubkey>" >> /root/.ssh/authorized_keys \
&& chmod 700 /root/.ssh && chmod 600 /root/.ssh/authorized_keys' \
>> /opt/gargantua/maintenance/sync_cleanup.shA minute later:
bash
ssh -i vault_key root@10.49.170.172
root@gargantua:~#ssh -i vault_key root@10.49.170.172
root@gargantua:~#Root flag retrieved from /root/flag.txt.
Lesson: a group-writable file that root executes on a timer is one of the cleanest privilege escalation primitives there is, precisely because it requires no exploit — just patience and one correctly-placed line.
Stage 5: The Vault
The last door. /root/.gargantua/vault.enc — another salted openssl blob, 816 bytes, clearly the final payload. True to form, the key wasn't new; it was the flag from the step before it (the Housekeeping flag), continuing the chain-of-keys pattern that ran through the entire room:
openssl enc -d -aes-256-cbc -pbkdf2 -iter 10000 \
-in vault.enc -out vault.raw -k '<housekeeping flag>'openssl enc -d -aes-256-cbc -pbkdf2 -iter 10000 \
-in vault.enc -out vault.raw -k '<housekeeping flag>'That decrypted cleanly into another layer of gzip compression — consistent with every other payload in this room, which never handed over plaintext without at least one extra wrapper to peel back.
Closing Thoughts
What made Gargantua stand out wasn't any single technique — the path-traversal bug, the cron privesc, and the hash cracking are all things you've seen in a dozen other rooms. What stood out was the discipline the room demanded: every wrong turn (brute-force IP correlation, chasing session-ID collisions, assuming the VPN log lived off-box) cost real time, and the room never bailed you out with an obvious "aha." The flags themselves doubling as decryption keys for the next stage was a nice structural touch too — it meant sloppy notes or a missed flag didn't just cost you points, it locked you out of the next door entirely.