Back to KPMG CTF 2026 Campus Edition
Westbound - (OSINT) - Writeup

Westbound - (OSINT) - Writeup

Oct 12, 2026 6 min readWriteup 16 of 22
On this page
  1. 1.Challenge Overview
  2. 2.Step 0: Unpack and Read the Brief
  3. 3.Step 1: Decode the Phishing Email into an Image
  4. 4.Step 2: Pull the Hidden Layer with zsteg
  5. 5.Step 3: Enumerate the Sites — the "Locker"
  6. 6.Step 4: The "Desk" — a Drop Code (and bait #3)
  7. 7.Step 5: The "Voice" — an AES Key in the Spectrogram
  8. 8.Step 6: The "Wire" — pcap Confirms the Path
  9. 9.Step 7: Recover and Decrypt the Export
  10. 10.Step 8: Resurrect the Deleted Commit
  11. 11.Summary
  12. 12.Key Takeaways

Challenge Overview

EventKPMG CTF 2026 Campus Edition
ChallengeWestbound
CategoryOSINT / Forensics
Points30
Filewestbound.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."

Westbound challenge card

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/

Unzipped folder contents

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."

proof.png - the fake SSH screenshot

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

zsteg output revealing the hidden layer

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  (!!)

find sites output

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(...))

Inlined blob-rebuilding script

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.

Trophy locker page

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:

desk.jpg with the vault drop code

  • 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:

call.wav spectrogram showing the archive key

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):

Wireshark HTTP stream - nulltide.example

One stream is a login to the vault API:

Wireshark - POST /api/login

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)"):

git site with encrypted download

Check what it is:

file westbound-export.zip.enc
# openssl enc'd data with salted password

file shows OpenSSL salted data

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

openssl decrypt and unzip

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

git log

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

git diff-filter=D showing 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

PieceArtifactTechniqueYields
—phish.emlBase64 decodeproof.png
—proof.pngzsteg LSBFour-pieces clue + bait flag
Lockerfiles/index.html (4.3 MB)Inlined Base64 blobspcap, wav, jpg
Deskdesk.jpgVisual inspectionDrop code 7c41f9a2e0b3
Voicecall.wavAudacity spectrogramKey s4lt3d-t1d35
Wirewestbound.pcapWireshark HTTP streamPhrase velvet-harbor (confirm)
Codewestbound-export.zip.encOpenSSL AES decrypttidewatch/ + .git
Markerdeleted notes.b64git show + Base64Flag

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 -a output 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 .git is a treasure chest. git log --all and git 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 enc with a salted header needs the matching KDF (-pbkdf2 -iter 200000 -md sha256 here). 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.