
Westbound - (OSINT) - Writeup
On this page
- 1.Challenge Overview
- 2.Step 0: Unpack and Read the Brief
- 3.Step 1: Decode the Phishing Email into an Image
- 4.Step 2: Pull the Hidden Layer with zsteg
- 5.Step 3: Enumerate the Sites — the "Locker"
- 6.Step 4: The "Desk" — a Drop Code (and bait #3)
- 7.Step 5: The "Voice" — an AES Key in the Spectrogram
- 8.Step 6: The "Wire" — pcap Confirms the Path
- 9.Step 7: Recover and Decrypt the Export
- 10.Step 8: Resurrect the Deleted Commit
- 11.Summary
- 12.Key Takeaways
Challenge Overview
| Event | KPMG CTF 2026 Campus Edition |
|---|---|
| Challenge | Westbound |
| Category | OSINT / Forensics |
| Points | 30 |
| File | westbound.zip |
"I packed forty-one gigs of Kestrel's westbound into one file and left a marker inside it, just so you'd know I was really there. Everything you need to reach it I already handed you: in the bundle, in the mail, in the things I was careless enough to leave lying around. Come take it off me."

This is a long one. An attacker ("nulltide") claims to have exfiltrated Kestrel Maritime's data and hidden a marker (the flag) deep inside an export. The whole thing is a breadcrumb trail: each artifact gives you the key to the next. There are also three fake flags planted along the way to catch anyone who stops early. The real marker is at the very end.
Step 0: Unpack and Read the Brief
unzip westbound.zip && cd westbound && ls
# index.html phish.eml README.txt sites/

README.txt sets the scene and, crucially, already contains a leaked clue with an embedded bait flag:
...the marker everyone submits is bait: KPMG_CTF{n0t_th3_r34l_0n3} <- don't.
the rest of me is in four pieces: a locker, a wire, a desk, a voice.
one holds the others. one is a door, one is a co...
🚩 Bait flag #1:
KPMG_CTF{n0t_th3_r34l_0n3}— explicitly labelled as bait. Ignore it.
So we're told up front the marker comes in four pieces: a locker, a wire, a desk, a voice. Hold onto that, it maps directly to files we'll recover.
Step 1: Decode the Phishing Email into an Image
phish.eml contains a large Base64 blob (an email attachment). Pull just the Base64 body into a file and decode it:
# copy the base64 section into b64.txt, then:
base64 -d b64.txt > loot/proof.png
Opening proof.png shows a fake terminal screenshot, an SSH session into kestrel-soc-01, reading /var/log/edge/proxy.log, running ./westbound.sh --pack, and a taunt: "three trophies, one marker. the marker is not on this screen."

The text on the image is just flavour. The taunt "the marker is not on this screen" is the hint that there's more hidden inside the PNG itself.
Step 2: Pull the Hidden Layer with zsteg
PNGs are a classic LSB-steganography carrier, so run zsteg, which scans every bit-plane/channel combination:
zsteg -a loot/proof.png

Buried in the b1,rgb,lsb,xy channel is the real clue (and bait flag #2):
you found the layer under the picture. most never get this far.
the marker everyone submits is bait: KPMG_CTF{n0t_th3_r34l_0n3} <- don't.
the rest of me is in four pieces: a locker, a wire, a desk, a voice.
one holds the others. one is a door, one is a co[de]
🚩 Bait flag #2: same
KPMG_CTF{n0t_th3_r34l_0n3}again, reinforcing the trap.
zsteg also lists a lot of noise (OpenPGP Secret Key, ddis/ddif, repeated-character runs). Those are decoys, false positives the author seeded to waste your time. The only line that reads as a real English sentence is the one above. Lesson: in zsteg -a output, trust the coherent text, not the "file: …" guesses.
Now we know the four pieces, and that "one holds the others, one is a door, one is a co[de]."
Step 3: Enumerate the Sites — the "Locker"
find sites -type f
# sites/git/index.html
# sites/vault/index.html
# sites/vault/7c41f9a2e0b3/index.html
# sites/files/index.html
# sites/blog/index.html
ls -la sites/files/index.html sites/git/index.html
# files/index.html is ~4.3 MB (!!)

A 4.3 MB HTML file is a dead giveaway: binary data is inlined as Base64. Checking the script at the bottom confirms it:
var B = {"pcap":"...","wav":"...","jpg":"..."};
var T = {"pcap":"application/vnd.tcpdump.pcap","wav":"audio/wav","jpg":"image/jpeg"};
function blob(k){var s=atob(B[k]);...return new Blob([u],{type:T[k]});}
// a[data-blob] links get URL.createObjectURL(blob(...))

The page (sites/files/index.html) is the "locker" — the one that holds the others. It rebuilds three files in-browser from Base64 and offers them as downloads: westbound.pcap, call.wav, desk.jpg, exactly the wire, the voice, and the desk. A fourth item, westbound-export.zip.enc, is shown but withheld.

Just open the HTML in a browser and download all four pieces. Now we work each one.
Step 4: The "Desk" — a Drop Code (and bait #3)
Open desk.jpg. It's a staged desktop photo with sticky notes:

- vault drop code
7c41f9a2e0b3— "code goes after /d/, no spaces." - A checklist: capture / call / desk done, marker → stays inside the export.
- "key is not written down. it is spoken." (this points us at the audio.)
That 7c41f9a2e0b3 matches the path sites/vault/7c41f9a2e0b3/, it's the door (the drop location), not the flag.
🚩 Bait trap #3: it's tempting to submit the drop code or treat it as the answer. It's just a path. The note itself says the marker stays inside the export.
Step 5: The "Voice" — an AES Key in the Spectrogram
The desk note said the key is spoken. Open call.wav in Audacity and switch to Spectrogram view. Text is drawn into the audio's frequency content:

ARCHIVE KEY s4lt3d-t1d35
So the archive key is s4lt3d-t1d35. (The short beeps at the very end are DTMF tones decoding to 0000, another decoy, ignore them.) This is the "co[de]" / key that unlocks the encrypted export.
Step 6: The "Wire" — pcap Confirms the Path
Open westbound.pcap in Wireshark and follow the HTTP streams (right-click → Follow → HTTP Stream):

One stream is a login to the vault API:

POST /api/login Host: vault.nulltide.example
user=nulltide&phrase=velvet-harbor
{"ok":true,"scope":"drop","note":"the drop path is a code, not a URL - and it is not in here"}
This confirms the model: the vault phrase is velvet-harbor, the drop path is the code from the desk (7c41f9a2e0b3), and the server flat-out tells us the marker is not in the pcap. The wire's job was just to corroborate the path and phrase, not to hold the flag.
Step 7: Recover and Decrypt the Export
The withheld westbound-export.zip.enc is downloadable from the git site (sites/git/index.html → "Download (encrypted)"):

Check what it is:
file westbound-export.zip.enc
# openssl enc'd data with salted password

OpenSSL salted data decrypts with the archive key from the spectrogram. Using modern OpenSSL defaults (PBKDF2):
openssl enc -d -aes-256-cbc -pbkdf2 -iter 200000 -md sha256 \
-pass pass:s4lt3d-t1d35 -in westbound-export.zip.enc -out export.zip
unzip export.zip

It extracts a tidewatch/ directory, and critically, it includes a full .git/ folder. A bundled .git directory is always worth investigating: it carries the project's entire history, including things that were later deleted.
Step 8: Resurrect the Deleted Commit
cd tidewatch
git log --oneline --all
# 72eb7a7 (HEAD -> main) remove notes before push
# 5ff35ab notes before i forget
# 6b26aa5 tag the fields i care about
# fabe99b init: host lookup

The story is obvious from the messages: a file was added in 5ff35ab ("notes before i forget") and deleted in 72eb7a7 ("remove notes before push"). Confirm what was removed:
git log --all --diff-filter=D --name-only
# commit 72eb7a7 ... remove notes before push
# notes.b64

The file is gone from the working tree but still lives in the object store. Pull it straight out of the commit where it existed:
git show 5ff35ab:notes.b64 | base64 -d
tidewatch stays public because nobody reads a recon tool, and nobody
reads the history of one.
kestrel paid for the audit and got the copy instead. the marker they are
hunting is in this export, planted where it will outlive the job:
KPMG_CTF{g1t_r3m3mb3r5_wh4t_y0u_d3l3t3}
that string is the whole point of the exercise. they will not read this far.
- t.v.
There it is, the real marker, hidden in deleted git history inside an encrypted archive.
Summary
| Piece | Artifact | Technique | Yields |
|---|---|---|---|
| — | phish.eml | Base64 decode | proof.png |
| — | proof.png | zsteg LSB | Four-pieces clue + bait flag |
| Locker | files/index.html (4.3 MB) | Inlined Base64 blobs | pcap, wav, jpg |
| Desk | desk.jpg | Visual inspection | Drop code 7c41f9a2e0b3 |
| Voice | call.wav | Audacity spectrogram | Key s4lt3d-t1d35 |
| Wire | westbound.pcap | Wireshark HTTP stream | Phrase velvet-harbor (confirm) |
| Code | westbound-export.zip.enc | OpenSSL AES decrypt | tidewatch/ + .git |
| Marker | deleted notes.b64 | git show + Base64 | Flag |
Key Takeaways
- Anomalous file sizes are signals. A 4.3 MB "HTML page" was the tell that files were inlined as Base64 blobs. Always sanity-check sizes during enumeration.
- Steganography lives in more than pixels. This challenge hid data in a PNG's LSB (zsteg) and in an audio spectrogram. When a clue says a key is "spoken," open the audio in a spectrogram view.
- Ignore the decoys deliberately. zsteg's
-aoutput was full of fake "OpenPGP Secret Key" hits, and there were three planted bait flags and a DTMF red herring. The author weaponised noise; staying on the one coherent thread is the skill. - A bundled
.gitis a treasure chest.git log --allandgit show <commit>:<file>recover anything ever committed. "Deleting" a file and committing the deletion does not remove it from history, which is the entire moral of the flag. - Know your OpenSSL defaults.
openssl encwith a salted header needs the matching KDF (-pbkdf2 -iter 200000 -md sha256here). Mismatched KDF settings make a correct password look wrong.
Flag: KPMG_CTF{g1t_r3m3mb3r5_wh4t_y0u_d3l3t3}
Bait flags to avoid: KPMG_CTF{n0t_th3_r34l_0n3} (README + stego layer), the vault drop code 7c41f9a2e0b3, and the DTMF 0000.