zwesty
Write-ups / Day 15 / Script
Day 15 · Thu, Oct 15 · Script

Crumb Trail

Prey slaughtered

Compounding script triage; Perl as a new polyglot host; browser credential-store access (ATT&CK T1555.003); Batch variable-expansion forms; finding one wrong answer among 1,026 by batching

STTMClaude Homebase Both

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

Challenge ID
script-analysis-day-15-credential-store-clues (+ /time_trial, /mastery)
Solved
2026-10-15: all three modes CORRECT by 09:26:55 ET, 25 min 56 s after our automated morning solver started the Single Solve. Single Solve 09:10:50, Time Trial 09:20:50, and Mastery 09:26:55 (both timed modes on untimed, unranked attempts)
Player
Zwesty (solo), with Claude Code (Opus 5.5): our automated morning solver (a scheduled script on our Kali VM) did most of the Single Solve; a Claude helper agent finished it and made the first Time Trial; Claude ran the audit, the untimed retries, and the Batch expansion fix

The challenge

"Some credential-access-shaped code keeps rummaging through the browser's cupboards, and synthetic store artifacts are sitting there waiting. Connect each code path to the artifact it targets, and find the reference that points at a store that was never there."

1,026 questions about the specimens in one evidence.zip (Day 14 had 971). The lane compounds: every earlier day's specimens come back. Today adds a new polyglot host, Perl: four polyglot-perl-* specimens plus a host-perl-cgi one, 11 questions each = 55 new questions.

Question family Example question Format
Perl role, attack "Classify the inert source role."; map the decoded behaviour graph to one technique by the stated precedence enum; ATT&CK ID
Perl adapter, campaign, url / file-path, stage-count, steps "Recover the exact url indicator." exact text, integer, >-joined tokens
Perl host-profile, obfuscation, stage-profile, next-language "Identify the script host or webshell source container." exact text
collection-attack (since Day 1) Treat access to the browser credential store as the central behaviour ATT&CK ID (T1555.003; the broader T1005 also accepted)
batch-*-expansion (since Day 3) "Classify only the expansion form used by the descriptor reconstruction statement." one label; the question now lists percent, delayed, call_double, for_indirect

Reading the prompt: what it said we'd need

Clue What it pointed to What I did about it
"credential-access-shaped code", "browser's cupboards" Credentials from web browsers (T1555.003): the collection specimen's descriptor names the Chromium stores Login Data and Cookies Nothing new: collection-attack = T1555.003 has been answered since Day 1
"synthetic store artifacts", "a store that was never there" We expected new questions about store files None came. No question asked about a store; the question ids and text were the same in all three modes. The prompt's theme didn't match new fields this time
"Connect each code path to the artifact it targets" Per-specimen indicators (URL, file, note) from each decoded descriptor Our core solver already reads those
(unstated) a new host Perl polyglots, the same template as PHP, Lua, AutoIt, MSBuild The Day 8 core read the HUNTRESS_ANALYSIS_V1 plan in each Perl file without a change

Safety

  • The specimens are inert, malware-styled scripts. They were read as text in memory on our Kali VM; nothing was run, and nothing was copied off the VM.
  • This write-up was built from our own records of the run. The only artifact text quoted is two short, defanged lines.

What was in the files

  • Single Solve: one password-protected evidence.zip, 116 members: every lane-to-date specimen (PowerShell, Bash, Python, Node, JScript, VBScript, Batch, and the polyglot hosts) plus the five new Perl files.
  • The credential-store clue lives in the collection specimen. Its PowerShell decodes a Base64 JSON descriptor holding a URL, a file list (update-<id>.dat, Login Data, Cookies), a stage zip name, a mutex, and a user agent. The .NET types it lists (IO.File, IO.Compression.ZipFile, Net.HttpWebRequest) tell the story: read the store, zip it, send it.
  • The Perl specimens carry the plan twice: a # HUNTRESS_ANALYSIS_V1:B64-… header and the same Base64 string in a my $descriptor_<id> = "…"; variable, wrapped in a build_plan_<id> sub. Decoded, it's the familiar key=value|… plan (language, role, attack, adapter, profiles, steps, campaign, url=hxxps://perl-<id>.example[.]invalid/stage).
  • Timed variants: the same 1,026 questions with identical text and formats, but 115 of 116 files differ (new values and new forms everywhere). Mastery was 5 sets, 5,130 questions.
  • The Batch line that mattered (Time Trial, host-context.cmd), inside a :HUNTRESS_RECOVER subroutine called once by for /l %%Q in (1,1,1) do call :HUNTRESS_RECOVER: set "cfg=%h0%%h1%%h2%%h3%".

Single Solve

Our automated morning solver (a scheduled script on our Kali VM) did most of it; a transport timeout left it unfinished.

Batch Answers Result
core (Day 8 chain) 899, the 55 Perl answers among them accepted (remaining 127)
Batch vocabulary, one at a time 10 9 accepted (remaining 118); the 10th timed out
JScript plug-ins (plain, styles, roles, attack) 60 + 27 + 9 + 9 accepted (helper agent)
Batch vocabulary, one at a time 6 accepted (helper agent)
VBScript control, one at a time 6 accepted; the last → CORRECT (helper agent)
  • 13:01:17 UTC: the 10th single submit (batch-uploader-control = call_subroutine) stalled. curl gave up at 60 s with 74 KB of a 567 KB reply received. The solver dropped the lane on the transport error.
  • Did it land? status shows no per-answer progress, so the Claude helper agent didn't resend. It sent the next batch first: that reply's cumulative accepted_ids held batch-uploader-control (118 − 1 − 60 = 57 remaining). It had landed, so it wasn't sent again.
  • Offline coverage before sending: 1,026/1,026, and the core matched every accepted answer. No solver change.
  • Single Solve CORRECT at 09:10:50 ET, flag c6577c2f…0f12. Nothing rejected.

Time Trial

First attempt (ranked): INCORRECT. 50 s clock, 31 unique attempts left. The helper agent stopped there and didn't retry.

started_at  2026-10-15T13:11:29.877Z
deadline    2026-10-15T13:12:19.877Z   (50 s, ranked)
answers     1026
start 1035 ms · artifact 608 ms · solve 126 ms · submit 794 ms · TOTAL 2564 ms
stamped     +1.84 s after started_at
result      incorrect   (no detail)

Finding the wrong answer. With no per-answer detail and 115 of 116 files changed, comparing files didn't help. Claude audited offline: every Time Trial label had been accepted on some earlier day; VBScript codecs matched each script's own Codec_* line; Batch control matched the confirmed control-slot rules. Not checkable offline: JScript styles, Batch graphs, adapters.

So we retried untimed and batched: a plain retry on this route has no clock and isn't ranked. Sending plug-in by plug-in, as in a Single Solve, makes the failing batch name the wrong plug-in.

  • Core 896 → accepted (remaining 130).
  • First plug-in batch rejected: batch-benign-expansion = delayed. That was the line above: a plain %h0%… rebuild, which our Batch vocabulary maps to delayed. On Day 14 the same line with the same question was accepted as delayed.
  • Next candidates in order: call_double (the h0–h3 fragment-family rule) → incorrect; percent (the literal form, now in the question's own list) → accepted.
  • Every Batch expansion in this variant wanted the literal form: delayed was rejected 7 more times. Then call_double was accepted for the 4 call set %%x%% rebuilds and percent for the 3 %x% ones. The relay needed none. All 13 other plug-in groups were accepted first time.
attempt     untimed retry   (no deadline, unranked)
started_at  2026-10-15T13:16:26.929Z
submits     40 in this mode (incl. the ranked attempt): 29 accepted, 10 incorrect, 1 CORRECT
result      CORRECT 13:20:50Z   flag 885e3125…c0d8

Mastery

Untimed and batched too, with the same fallback (literal form first, then delayed).

attempt     untimed   (no deadline, unranked)
started_at  2026-10-15T13:21:23.086Z
answers     5 sets, 5130 questions, 122 batches
rejected    21 singles, all Batch expansion: 20× delayed, 1× call_double (set 5 chain); each then accepted on the fallback
result      CORRECT 13:26:55Z   flag 9c5f28ac…9ecb

Time to solve

All three modes were solved by 09:26:55 ET, 25 min 56 s after the Single Solve was started. The Single Solve was finished by hand at 09:10:50 after a transport timeout; the ranked Time Trial was incorrect, so both timed modes were then solved on untimed, unranked retries (Time Trial 09:20:50, Mastery 09:26:55).

Takeaways

  • A transport timeout isn't a rejection. The server had replied; we just didn't read it all. The next submit's cumulative accepted_ids answered "did it land?" without resending. The solver should check and resume, not stop.
  • The same line can take a different label on a different variant. set "cfg=%h0%…" was delayed on Day 14 and this morning's Single Solve, and percent on every timed variant today. A label rule that has always held can still be the wrong one.
  • To find one wrong answer in 1,026, batch it. An untimed retry sent plug-in by plug-in pointed at the Batch vocabulary in the first rejected batch. Without that the audit had no way to narrow it down.
  • A prompt's theme isn't always a new question. "Credential store clues" brought no store questions; the real change was the Perl host, and the existing template solver handled it.
  • Cost: both timed modes ended unranked, and this lane's last CORRECT (09:26:55 ET, also the day's finish) came ~25 min later than the solver's usual ~09:02. Open item at the time: our Batch vocabulary still answered delayed first, and Claude was changing it to try the literal form first before the next day's run.