zwesty
Write-ups / Day 1 / Metadata
Day 1 · Thu, Oct 1 · Metadata

You Must Be This Hash to Ride

Prey slaughtered

File identity (size, MD5, SHA-1, SHA-256) · zip structure (local headers against the central directory, nested and stored zips) · not trusting labels · building a timed solver

STTMClaude Homebase Both

Flags are shortened to their first 8 and last 4 characters because they're unique per team.

Challenge ID
metadata-forensics-day-01-hash-to-ride (+ /time_trial, /mastery)
Solved
2026-10-02, all three modes, in 24 min 49 s from the first start
Player
Zwesty (solo), with Claude Code (Opus 5.5)

The challenge

Three ordinary files arrived without trustworthy labels. Build an identity inventory from the bytes themselves and prove which labels can—and cannot—be trusted.

Single Solve: five questions, all about one file inside metadata-day-01-evidence.zip:

answer_id Question Format
evidence-note-filename What is the exact filename of the first evidence note? Exact base filename with its extension
evidence-note-size-bytes What is its exact size in bytes? Base-10 integer
evidence-note-md5 What is its MD5 digest? 32 hex characters, any case
evidence-note-sha1 What is its SHA-1 digest? 40 hex characters, any case
evidence-note-sha256 What is its SHA-256 digest? 64 hex characters, any case

Each size and hash question names its target as metadata-day-01-evidence.zip -> evidence-note.txt.

Reading the prompt: what it said we'd need

Clue What it pointed to What I did about it
"identity inventory" The standard file-identity set in forensics: name, size, and cryptographic hashes Compute all of them for every file, not just the one asked about
"from the bytes themselves" Hash the exact member bytes, not a re-saved or re-encoded copy Read each member in memory, straight from the zip, without extracting to disk
"without trustworthy labels" / "which labels can—and cannot—be trusted" Names, extensions, and the question's own -> member label may lie Take names from the zip's own headers. Compare what the content is (its magic bytes) with what the extension claims. Check that the zip's two name records agree
"exact filename" The answer is checked character for character Use the name exactly as stored: no case changes, no trimming, no path
"Three ordinary files", though only one is asked about The later modes probably bring the other files Build the solver for several files and several zips, not just one
Hex-length formats (32/40/64) and "integer" Checkable shapes Check every answer's format before it's submitted

The zip detail behind "labels": every zip records each file's name twice. Once in the local header in front of the file's data, and again in the central directory at the end. Most tools read only the central directory, and a crafted zip can make the two disagree. So the solver checks both, and stops if they differ, rather than pick one.

Setup

The same as A Little Based: a Kali VM over the CTF VPN, driven by Claude Code from Windows over SSH, with every Game Master request relayed through the Homebase. This challenge was where our shared runner was built. It runs start or resume, downloads and hash-checks the artifacts, solves, checks formats, and submits once, so each challenge only needs a small solver.

What was in the files

Every evidence file followed the same template. Each variant randomised four values, the case marker, endpoint, sequence, and analyst, and kept their widths fixed. So the size stayed 240 bytes while every hash changed.

The Single Solve's evidence-note.txt, inside metadata-day-01-evidence.zip:

Huntress CTF 2026 — Metadata Forensics
Case marker: CASE-DED50DC2F828
Endpoint: WS-0142E587
Collected sequence: 8273759354
Analyst: analyst-457fe211

Preserve these bytes exactly. Filename, length, and cryptographic digests are evidence.

The Time Trial's roster.csv (129 bytes) is the same kind of record as CSV:

case_marker,endpoint_id,analyst_id,collected_sequence,status
CASE-30F928381244,WS-B8BBB60B,analyst-29ef710a,2181529668,collected

The zip structure, the same in every mode: - One member per evidence zip, deflated, created on Unix. - A fixed timestamp of 2026-01-01 00:00 UTC, in the DOS field and the UT extra field (555405000100b95569). - A data descriptor (flag bit 3), so the local header's CRC and sizes are zero. The real values live after the data and in the central directory.

Mastery attempt 1 wrapped the two set zips in an outer zip. Those inner zips were stored (no compression) and carry 1980-01-01 DOS dates with a FAT origin, unlike the inner files.

Things that looked odd but were fine: - zipinfo called evidence-note.txt "binary". The cause is the em dash in the title line: 3 non-ASCII UTF-8 bytes. file correctly says UTF-8 text. The extension and the content agree. - In Mastery, the "renamed evidence-note copy" isn't a copy. It's 240 bytes too, but it carries different markers from the evidence note, so the hashes differ. The word "copy" was a label that couldn't be trusted.

Single Solve

Start: shape A, one download and one sha256, with no deadline. The zip was 372 bytes, its SHA-256 a511416d…5b64e5 matching the start reply.

Static checks before trusting anything: - One local header, one central directory entry, one data descriptor, so no hidden extra entries. - The local and central names match: evidence-note.txt, 17 bytes, plain ASCII. - No path tricks (../, absolute paths), and the CRC checks out (af710f76).

Answers, computed in Python from the member's bytes, then cross-checked with unzip -p … | wc -c | md5sum | sha1sum | sha256sum. The digests are shortened here the same way as the flags:

Field Answer
Filename evidence-note.txt
Size 240
MD5 6b5074cd…a7d5
SHA-1 11f594f6…92d9
SHA-256 a6d2d239…4dc6

CORRECT (flag 8b4c7712…954f). This was the first live use of our shared runner, and the run took 1.4 s.

The solver

Our solver builds the inventory for every file it can reach, opening nested zips. For each file: name, size, MD5, SHA-1, SHA-256, and the type its magic bytes say, set against the extension. Then, for each question:

  1. Which field: from the answer_id first (-md5, -size-bytes…), then the wording.
  2. Which file, in order:
  3. the question's own -> member label, or a sibling question's, if that member really exists;
  4. an ordinal ("first", "second");
  5. a member whose name contains the noun asked about ("the roster" → *roster*);
  6. the only file there is.
  7. Which zip (for Mastery): the set's own artifact, or the zip the question names, wherever it sits.
  8. Stop, never guess, on: local and central names that differ, unsafe paths, a question it doesn't understand, or a file it can't identify with certainty.

Time Trial: 2.77 s of a 30 s clock

The questions moved to the roster, roster-filename to roster-sha256. The filename question gave no ordinal: "What is the exact filename of the roster?" My solver would have stopped on that, so before starting the clock I added the sibling-label and noun-match steps above.

started_at 2026-10-02T20:05:46.501Z  deadline 2026-10-02T20:06:16.501Z
  artifact metadata-day-01-evidence.zip 269 bytes sha256 f075726ccf34543c… (checked)
  roster-filename: roster.csv
  roster-size-bytes: 129
  roster-md5: d1d7be11…07e3
  roster-sha1: 0bd2a514…b14c
  roster-sha256: ca8713b5…f887
  start        1486 ms
  artifact      479 ms
  solve           1 ms
  submit        799 ms
  TOTAL        2765 ms
result CORRECT  flag 3713666b…0996

The zip held only roster.csv. The prompt's "three files" are spread across the modes, not packed into one zip.

Mastery: 2 sets, 10 questions, one 30 s clock (second attempt CORRECT in 2.62 s)

Set Zip File asked about
1 metadata-day-01-evidence-01.zip "the renamed evidence-note copy", labelled evidence-note-copy.txt
2 metadata-day-01-evidence-02.zip "the first evidence note", labelled evidence-note.txt

Prepared before the first attempt, because A Little Based's Mastery had switched delivery shapes between attempts: - Nested zips: a member that is itself a zip is listed and then opened. - Each question is answered only from its own zip. - The noun match ignores words like "renamed" and "first". - Tests for every shape I could expect: two separate zips (distinct names, and identical names), and one outer zip holding both.

Attempt What arrived What happened
1 (20:08:19) One outer zip, …-mastery.zip (1,052 B), holding the two set zips stored, uncompressed A false stop, my bug. My name check scanned the raw bytes for PK\x03\x04 header signatures. Because the inner zips were stored, their headers appear verbatim in the outer zip's bytes, so it found 4 "local headers" against 2 central entries and stopped. Left to expire
2 (20:09:39, after retry) Two separate artifacts, both named metadata-day-01-evidence.zip CORRECT in 2.62 s

The fix between attempts: read each local header at the exact offset the central directory records for it (header_offset), instead of scanning for signatures. After the expiry I re-ran the fixed solver on the real attempt-1 zip offline: all 10 answers came out and passed their formats. Then I retried.

started_at 2026-10-02T20:09:39.846Z  deadline 2026-10-02T20:10:09.846Z
  artifact 01-metadata-day-01-evidence.zip 384 bytes sha256 7d9fd74dec861538… (checked)
  artifact 02-metadata-day-01-evidence.zip 373 bytes sha256 19d84b78883e5c1c… (checked)
  set1.evidence-note-copy-filename: evidence-note-copy.txt
  set1.evidence-note-copy-size-bytes: 240
  set1.evidence-note-copy-md5: b7700f4a…2a77
  set1.evidence-note-copy-sha1: b840c63e…16cd
  set1.evidence-note-copy-sha256: f1e8c5bd…1681
  set2.evidence-note-filename: evidence-note.txt
  set2.evidence-note-size-bytes: 240
  set2.evidence-note-md5: 4eaa40b7…c44d
  set2.evidence-note-sha1: 9563ebf1…a2ea
  set2.evidence-note-sha256: f613ae22…fe17
  retry         990 ms
  start         334 ms
  artifact      565 ms
  solve           1 ms
  submit        727 ms
  TOTAL        2617 ms
result CORRECT  flag c394e917…10e9

Time to solve

All three modes were solved on 2026-10-02 by 20:09:41 UTC, 24 min 49 s after the Single Solve started. The five answers were found in about 2 minutes, but the Single Solve was submitted at 10 min 32 s, after the shared runner and solver were built and tested; the Time Trial went through first time in 2.77 s, and the Mastery passed on its second attempt in 2.62 s after a false stop.

Takeaways

  • "Identity" in forensics means name, size, and hashes, taken from the exact bytes. Read members in memory and cross-check with a second tool.
  • Don't trust labels, including the question's own. The -> member hint, the extension, and a word like "copy" can all be wrong. Here, the "copy" wasn't a copy.
  • A zip names each file twice. Compare the local header and the central directory, reading each local header at its recorded offset. Never scan for signatures: stored inner zips put their headers verbatim in the outer zip.
  • Read the later modes' questions before starting them. The Time Trial's "the roster", with no ordinal, would have stopped the solver. Reading the status for free found that before the clock.
  • Mastery deliveries switch shapes between attempts, the same as in A Little Based. Prepare for every shape, stop safely on anything new, and fix it offline.