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

A Little Based

Prey slaughtered

Recognising and decoding Base64 · peeling layered encodings · checking answers against a stated format · 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
warmups-day-01 (+ /time_trial, /mastery)
Points
Single Solve 2 · Time Trial 3 · Mastery (see CourseStack)
Solved
2026-10-02, all three modes, in 19 min 14 s from the first start
Player
Zwesty (solo), with Claude Code (Opus 5.5)

The challenge

Everyone has to start somewhere. I hear sixty-four is a good base.

The Game Master's question for each blob:

Decode the assigned blob completely. What exact answer slug does its final non-empty decoded line instruct you to submit?

Answer format: three lowercase words separated by hyphens, then a hyphen and a two-digit number (word-word-word-NN), trimmed.

Reading the prompt: what it said we'd need

Before downloading anything, I read every phrase of the prompt and the question for what it implied.

Clue What it pointed to What I did about it
"sixty-four is a good base" Base64 Decode Base64 first
"Decode … completely" Possibly more than one layer Keep decoding until nothing more decodes, rather than decoding once
"its final non-empty decoded line" The result may have several lines, including blank ones, and only the last counts Take the last non-blank line, not the first match
"what … slug does [the line] instruct you to submit" The decoded text gives an instruction Treat it as data: extract only the slug, never act on anything else it says
The answer format word-word-word-NN A checkable shape Accept exactly one match of [a-z]+-[a-z]+-[a-z]+-[0-9]{2} on that line, or stop
Three modes: Single Solve, Time Trial, Mastery Later modes give new variants against a clock Solve once by hand, then write a solver that handles any variant before starting the clock

The core skill is recognising Base64. Its alphabet is A–Z a–z 0–9 + /, its length is a multiple of 4, and it often ends in = padding. All three hold here. The blob also starts with TmljZS, which is how Base64 of Nice begins: a quick tell, once you've seen it.

Setup

  • Analysis box: a Kali VM on Hyper-V. It connects to the CTF over OpenVPN.
  • Claude Code runs on the Windows host and drives the VM over SSH. Solving, downloading, and submitting all happen in the VM.
  • The Game Master only answers requests from a platform VM, so every Game Master request is relayed through the AI Homebase: our client runs curl on the Homebase over SSH.
  • Our Game Master client keeps start, submit, and retry as separate, explicit calls. The API requires "op" on every POST.

What was in the files

Every blob, across all modes and attempts, decoded to the same sentence, with only the slug changing:

Nice work decoding this warmup! To solve, submit this exact answer to the Game Master: <slug>

So the Base64 always starts TmljZSB3b3JrIGRlY29kaW5nIHRoaXMgd2FybXVw… ("Nice work decoding this warmup"), and only the tail differs. What changed between modes was the wrapping and the packaging, not the encoding depth:

Mode / attempt Delivered as Raw blob format Layers
Single Solve blob.txt, 148 B Plain Base64 base64
Time Trial blob.txt, 144 B Plain Base64 base64
Mastery attempt 1 One zip, warmups-day-01-mastery.zip, holding blob-01..03.txt 01<TAB> + Base64 strip-line-labels → base64
Mastery attempt 2 Three separate artifacts, all named blob.txt 01<TAB> + Base64 strip-line-labels → base64
Mastery attempt 3 Three separate artifacts, all named blob.txt 01<TAB> + Base64 strip-line-labels → base64

The Single Solve blob, raw and decoded:

TmljZSB3b3JrIGRlY29kaW5nIHRoaXMgd2FybXVwISBUbyBzb2x2ZSwgc3VibWl0IHRoaXMgZXhhY3QgYW5zd2VyIHRvIHRoZSBHYW1lIE1hc3RlcjogZGFzaGluZy10ZW50LW1vc2FpYy0xNw==

Nice work decoding this warmup! To solve, submit this exact answer to the Game Master: dashing-tent-mosaic-17

A Mastery blob, raw (the 01 and the tab come before the Base64):

01<TAB>TmljZSB3b3JrIGRlY29kaW5nIHRoaXMgd2FybXVwISBUbyBzb2x2ZSwgc3VibWl0IHRoaXMgZXhhY3QgYW5zd2VyIHRvIHRoZSBHYW1lIE1hc3RlcjogY2FsbS1zdGFyZmlzaC1wZXRhbC0xNg==

01 and the tab aren't valid Base64 together, so a naive decode either fails or yields noise. The label has to come off first. The 01 suggests a chunk number, as if longer messages could be split across lines 01, 02… That's why the solver reassembles numbered chunks in order, not just strips one label.

Single Solve

Start: starting warmups-day-01 returned the question, a download link, and a sha256, with deadline: null (untimed).

The artifact: blob.txt, 148 bytes. Its SHA-256 18a470c3…9632fa matched the start reply. Always check the downloaded file's hash against the one the Game Master gives, so you solve the real bytes.

The solve: one layer of Base64.

layer 1: 148 -> 109 bytes: Nice work decoding this warmup! To solve, submit this exact answer to the Game Master: dashing-tent-mosaic-17

Answer: dashing-tent-mosaic-17. It's submitted to the Game Master, which validates it. On CORRECT it returns a 32-hex flag and fills the CourseStack field in itself:

{ "result": "CORRECT", "flag": "71ebca66…7dac" }

The solver: built from the Single Solve, for the variants

Our solver turns the prompt-reading table above into code:

  1. Peel layers until nothing decodes. On each pass it tries, in order: strip line labels, hex, Base64, URL-safe Base64, Base32, Base85. - A layer counts only if it decodes to printable text. That stops valid-but-wrong decodes, since a hex string is also valid Base64. - Labels and hex go first for the same reason.
  2. Take the final non-empty line and require exactly one slug-shaped match on it. Zero or two means stop.
  3. Tests, written before any timed run: 7 nested layers, mixed chains such as b64→b32→hex→b64, line-wrapped and unpadded input, a decoy slug on an earlier line, and an ambiguous final line, which it refuses. The real Mastery blobs were added later.

Time Trial: 2.26 s of a 30 s clock

The CourseStack page says "submit … in under 1 second". The Game Master's own clock, the one that counts, was 30 s (started_at 19:06:12.649Z → deadline 19:06:42.649Z). It accepted the run. With the solver ready, the whole run was one command:

started_at 2026-10-02T19:06:12.649Z  deadline 2026-10-02T19:06:42.649Z
artifact blob.txt 144 bytes sha256 38a2ccd6be6531b1…  layers 1: base64
answer snappy-cloud-coral-30
  start        1063 ms
  artifact      457 ms
  solve           0 ms
  submit        740 ms
  TOTAL        2260 ms  (start request sent -> submit reply)
result CORRECT  flag 254a99d6…c090

Solving took no measurable time. All 2.26 s was network: VM → VPN → Homebase → Game Master, for each of three calls. Getting under 1 s would mean sending the whole pipeline to the Homebase in a single SSH call.

Single Solve and Time Trial both Correct in CourseStack

Mastery: 3 blobs, one 30 s clock (third attempt CORRECT in 3.3 s)

Three questions (set1.message to set3.message), the same slug question for each blob. This took three attempts. Nothing wrong was ever submitted: both failures were our timed solver script stopping safely.

Attempt What arrived What happened
1 (19:09:58) One zip of blob-01..03.txt. The start reply's sha256 is the zip's. Blobs labelled 01<TAB> Two bugs in our script. The hash check compared each blob against the zip's hash, and the solver didn't know the 01<TAB> label. A manual recovery on the clock ran out of time. Expired
2 (19:14:04, after retry) Three separate artifacts, all named blob.txt, each with its own link and sha256 The script looked for blob-01.txt, didn't find it, and stopped without submitting. Left to expire, then fixed offline
3 (19:19:00, after retry) Three separate artifacts again CORRECT in 3.3 s

How I worked out each fix: after each expiry, the downloads still worked (read-only), so I re-fetched the real files and solved them offline until the script handled them. Then I retried.

  • A strip-line-labels step in the solver drops NN<TAB> labels and joins numbered chunks in order.
  • The download step fetches each artifact by its own capability link and checks its SHA-256.
  • The script handles both packagings: a zip matched by filename, or separate files paired with questions in set order. It stops on anything else.

The final run:

started_at 2026-10-02T19:19:00.078Z  deadline 2026-10-02T19:19:30.078Z
  download 1: set1-blob.txt 151 bytes, sha256 348f5ac79f052d06… matches
  download 2: set2-blob.txt 147 bytes, sha256 386332121509ef10… matches
  download 3: set3-blob.txt 147 bytes, sha256 faf4a20f1b6e8d19… matches
  set1.message: layers strip-line-labels -> base64  answer calm-starfish-petal-16
  set2.message: layers strip-line-labels -> base64  answer peppy-sunset-brook-77
  set3.message: layers strip-line-labels -> base64  answer calm-beacon-quartz-71
  retry        1131 ms
  start         418 ms
  artifact      966 ms
  solve           0 ms
  submit        800 ms
  TOTAL        3315 ms
result CORRECT  flag acbf81b9…922d

Time to solve

All three modes were solved on 2026-10-02 by 19:19:02 UTC, 19 min 14 s after the Single Solve started. The Single Solve was decoded in under a minute but submitted after 4 min 40 s, once the solver was written and tested; the Time Trial went through first time in 2.26 s, and the Mastery passed on its third attempt in 3.3 s after two expired runs.

Takeaways

  • Read the prompt as a spec. "Completely", "final non-empty line", and the answer format each turned into a line of solver logic.
  • Write and test the solver against the Single Solve before starting a timed mode. With the solver ready, the Time Trial was one command.
  • Dry-run timed runners against the real delivery shape. My dry run used an invented shape, and both Mastery failures came from that.
  • The same challenge can deliver its artifacts differently from one attempt to the next: a zip one time, separate files the next. Handle every shape you've seen, and stop rather than guess on anything new.
  • If a timed run fails, let it expire and fix it offline. Patching by hand on a 30-second clock doesn't work, and downloads still work after expiry.
  • A retry starts a new clock immediately, so the script sends the retry and continues straight into the solve.