zwesty
Write-ups / Day 4 / Script
Day 4 · Sun, Oct 4 · Script

Dim and Dimmer

Prey slaughtered

Static triage of VBScript, Batch, JScript, and PowerShell; decoding character-code arrays and byte transforms from a script's own constants; control-flow classification; ATT&CK mapping for hand-off scripts; single-answer wording tests; scripted timed runs

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-04-shell-contrast (+ /time_trial, /mastery)
Solved
Single Solve CORRECT 2026-10-10 00:50 UTC (the evening of 2026-10-09 in ET), Time Trial CORRECT 01:13 UTC, Mastery CORRECT 01:39 UTC on a retry. 57 h 41 min from the first start (2026-10-07 15:59 UTC) to the Mastery CORRECT; 226 of 231 answers were in by the evening of 2026-10-09 and the rest of the time went on five control labels
Player
Zwesty (solo), with Claude Code (Opus 5.5) and the Homebase agent (GPT-6 Luna)

The challenge

"These VBScript specimens hide their material behind character codes, reversible transforms, and winding control flow. Unwind every representation on paper, classify each behavior, and find out which stage passes the baton to which."

231 questions about one AES-encrypted evidence.zip (password infected, 30 scripts). Days 2 and 3 came back whole, and VBScript was new: nine .vbs files with the same stems as the JScript and Batch ones (host-context, memory-stage, embedded-write, remote-write, outbound-transfer, local-dispatch, impact-plan, staged-handoff, relay-handoff).

Group Files Questions Format
PowerShell (Power Play's samples) 3 .ps1 15: scenario, ATT&CK, three indicators each enum, T####[.###], exact text
JScript (Host With the Most's) 9 .js 72: role, ATT&CK, adapter, campaign, indicator, three style questions as Day 2
Batch (Expansion Pack's format, new values) 9 .cmd / .bat 72: role, ATT&CK, adapter, campaign, indicator, expansion, control, graph role enum; the rest exact text
VBScript (new) 9 .vbs 72: role, ATT&CK, adapter ("WSH, COM, or LOLBin"), campaign, indicator, representation, transform, control role enum; the rest exact text, no vocabulary given

The three new VBScript questions were "Identify the exact VBScript material representation", "Identify the exact reversible byte transform", and "Identify the exact VBScript control-flow form". None of them lists its allowed values.

Reading the prompt: what it said we'd need

Clue What it pointed to What I did about it
"VBScript specimens" A new language on top of Days 1–3 A new VBScript decoder; everything older reused from the solver chain
"hide their material behind character codes" The representation question: how the encoded bytes are written Read from each script's Codec_ line (decimal_pipe, hex_compact …), checked against its Load code
"reversible transforms" The transform question, and the key needed to undo it Same Codec_ line; the decode checked against the script's own Decode code
"winding control flow" The control question The block around the Decode_* call. This was the hard part: three days of wording tests
"Unwind every representation on paper" Decode statically, never run Every script read as text from the zip in memory
"classify each behavior" Role and ATT&CK per script The decoded plan's steps give the role; Day 3's role → ATT&CK pairs held
"which stage passes the baton to which" The two hand-off scripts staged-handoff.vbs → resource-sample.ps1, relay-handoff.vbs → local-dispatch.vbs; ATT&CK is the hand-off script's own interpreter

Safety

Static only. No script was run: the zip was read into memory and each file parsed as text. The VBScript fixtures are inert anyway (nothing calls their functions, and there is no WScript or CreateObject use), but the rule held. The artifacts stayed on our Kali VM and on the Homebase. Indicators point at the reserved .invalid domain and are defanged here.

What was in the files

  • The PowerShell and JScript files are Power Play's and Host With the Most's formats with new values.
  • The Batch files are Expansion Pack's format with new values, randomised adapters (certutil.exe for the dropper, regsvr32.exe for the chain, rundll32.exe for the relay, cmd.exe for the launcher), and varying fragment names (fragment0, v_a0, h0, FrAg0). Some lines are unquoted with ^| escapes, and some end in & rem HUNTRESS quote compound. Each one's HUNTRESS-MATERIAL header decodes to exactly the fragment plan (checked with our header decoder).
  • Each VBScript file holds Const Material_<id> (the plan, encoded), Const Codec_<id> = <representation>|<transform>|<key hex>, and Load_, Decode_, Recover_, and Inspect_ functions. The decoded plan is the same key=value|… format as Batch's (steps, adapter, stage_count, campaign_id, indicators).
Script Role Representation / transform Around Decode_* in Recover_*
host-context.vbs benign_utility decimal_pipe / subtract_modulo_256 Select Case 1 (inside an If True Then)
memory-stage.vbs stager hex_compact / repeating_key_xor nothing (an If (2 > 1) one-liner sits before it)
embedded-write.vbs dropper hex_compact / subtract_modulo_256 Select Case 1
remote-write.vbs downloader hex_compact / reverse_bytes Do … Exit Do … Loop
outbound-transfer.vbs uploader decimal_dash / repeating_key_xor Do … Exit Do … Loop
local-dispatch.vbs launcher decimal_pipe / rolling_xor nothing
impact-plan.vbs impact reversed_decimal / reverse_bytes While p < 1 … Wend
staged-handoff.vbs staged_chain hex_colon / single_byte_xor Do … Exit Do … Loop (case-mangled names like SoUrCeByTeS)
relay-handoff.vbs staged_chain (relay) hex_compact / rolling_xor Select Case 1

A short excerpt of the downloader's recover step (inert, flattened to one line):

Do; item2 = Decode_1190821c(item1); Exit Do; Loop; For item5 = 0 To UBound(item2); Next

The For … Next after it only turns the decoded bytes into text, so it never decides the label.

  • Time Trial and Mastery used the same template with new values, but drew their control forms from the whole Script lane. The Time Trial's VBScript included the On Error Resume Next form (first seen on Day 5), and the first Mastery's Batch scripts included the if defined HUNTRESS_READY wrapper (first seen on Day 3).

Single Solve

Day 1 of the attempt (2026-10-07): 139 of 231, from the solvers

  1. Claude, 15:59–16:04 UTC, from our Kali VM. Certain answers first, in batches, then judgement answers one at a time: - PowerShell 15 (Power Play's solver), Batch plan fields 45, Batch ATT&CK 7 (Day 3's role → ID pairs), Batch expansion = delayed ×4, VBScript plan fields 36, and all 18 VBScript representation and transform answers straight from the Codec_ lines. All accepted, nothing rejected: 125 of 231 after six submits. - VBScript ATT&CK: the six roles with a Batch-confirmed ID went together (none, T1105, T1140, T1105, T1041, T1486): accepted. The hand-offs alone: launcher T1059.005 accepted; chain T1059.001 (the next stage's PowerShell) rejected, then T1059.005 accepted; relay T1059.005 accepted. Hand-off scripts take their own interpreter's ID. - VBScript control: select_case accepted on all three Select Case 1 scripts (the If True wrapper doesn't change it). Then two spellings per form, all rejected: do_loop, do_exit (Do … Exit Do), while_wend, while_loop (While … Wend), straight_line, linear (nothing). Claude stopped there.
  2. The VBScript hand-off lesson unlocked Day 3's two open Batch hand-offs. By the same logic they'd be the Batch interpreter's T1059.003. Both went together at 16:25: accepted (139).
  3. JScript: the Homebase agent (17:58 UTC). Claude's safety filter had stopped one step of the JScript analysis on Day 2, and we didn't work around it, so Claude didn't attempt them here. The Homebase agent's JScript analyser decoded the plans statically and answered all 72 in four batches: 211 of 231.

Left: 6 VBScript control and 14 Batch (5 expansion, 9 control). None of these was blocked by the safety filter. The answers are free text with no list, so the only way in was tests.

The wording tests (2026-10-08 to 2026-10-10)

Rules for every test: one answer per submit, and a rejected value never resent for that question.

VBScript, the _once labels (found on this day). The Homebase agent kept testing, and on its 2026-10-09 run it found the pattern:

Script (form) Rejected first Accepted
remote-write.vbs (Do … Exit Do) 7: do_loop, do_exit, do_exit_loop, exit_do_loop, single_pass_loop, early_exit_do, single_iteration do_once (then on outbound-transfer and staged-handoff too)
impact-plan.vbs (While … Wend) 6: while_wend, while_loop, while_guard, while_condition, bounded_while, while while_once

The label names what the block does (it runs once), not just its keywords. The same family later gave for_once for the For … Next wrapper on Days 6–7.

Batch expansion. The rebuild line alone didn't predict the label. Two scripts with identical for %%G in (0 1 2 3) do call set … lines got different answers: for_indirect was accepted for host-context.cmd and rejected for the dropper (Claude, 2026-10-08). The difference was the fragment variable family. The dropper uses h0–h3: delayed rejected, call_double accepted. The fragment* FOR rebuilds (benign, impact, chain) are for_indirect, and the uploader's direct call set over FrAg* is delayed (found by the Homebase agent). Claude's safety filter stopped one step of the analysis here, and we didn't work around it; the Homebase agent finished that part.

Batch control, the dispatch form. goto :HUNTRESS_DISPATCH + its label is goto_dispatch (found on Day 6; applied to this day's uploader and relay on 2026-10-09, 226 of 231). The call :HUNTRESS_RECOVER scripts are call_subroutine. Wrong guesses here were named after decoys (findstr_filter, matcher_pipe, quoted_assignment_chain, dispatch_filter …): the | findstr separators and quote-compound markers never decide the label.

The last five: nothing in the slot → direct. At 226 of 231 the open ones were the two VBScript scripts with nothing around Decode_* (memory-stage, local-dispatch) and three Batch scripts with an empty control slot (the lines between :HUNTRESS_SEPARATOR_DONE and echo(). The stager alone had 12 rejected names (direct_sequence, unwrapped, no_loop, direct_call, no_control_flow, no_wrapper …), and the launcher 5. On the evening of 2026-10-09 Zwesty and Claude worked back from the accepted answers instead:

  • Each accepted label is a keyword in the code plus a purpose word. error_guard came from Zwesty mirroring errorlevel_guard on Day 5.
  • Splitting the open scripts by what was really in the slot showed the "nothing" words had only ever been tried on scripts that did have something (an if defined check). For the truly empty slot, the candidates came from the generator's own "plain" vocabulary in other fields: JScript invocation-style direct, transform identity, codec plain_*.
  • direct was accepted on Day 5's empty Batch slot, then on this day's VBScript stager (227 of 231). The rollout: Batch benign, dropper and launcher and the VBScript launcher: Single Solve CORRECT, 231/231, at 00:50:10 UTC, flag 0f1ee976…0b3a.

Day 4 was the first Script day after Day 2 to reach Single Solve CORRECT. The last Batch label, if_guard for the set "HUNTRESS_READY=1" + if defined HUNTRESS_READY wrapper, wasn't needed here (no Day 4 Single Solve script has it). It was found later that evening after 68 rejected names, from the pattern "a keyword in the code + one of five second words (subroutine / guard / dispatch / case / once)". Support was never needed.

Final control labels for this day:

Labels
VBScript select_case ×3, do_once ×3, while_once ×1, direct ×2
Batch call_subroutine ×4, goto_dispatch ×2, direct ×3

Wrong answers in total: the request logs hold 46 incorrect replies (48 answers, one a batch of 3): 10 answers from Claude on our Kali VM, 38 from the Homebase agent. Every rejection narrowed the next test.

Solvers: our VBScript decoder reads a .vbs from its Codec_ line, then checks the line against the script's own Load and Decode code (the key array, the + N) Mod 256, the separator), and stops if they disagree. Rolling XOR uses the previous stored byte. A Batch reader handles the fragments (any prefix, quoted or ^|-escaped). Our Day 4 solver joins them to Power Play's solver and gives only confirmed answers (4 tests), and a small helper submits a filtered subset. Later the labels moved into shared add-ons (Batch wording rules, VBScript control, plus the Homebase agent's JScript analyser), run by our runner with the whole Script chain.

Time Trial

Run from our Kali VM with our solver once the Single Solve was CORRECT. Timings are the client's.

started_at  2026-10-10T01:13:06.101Z
coverage    231/231 (incl. error_guard and direct, both learned on other days)
submitted   01:13:07 UTC, 231 answers
TOTAL       2.24 s (client side)
result      CORRECT   flag 9c0f6678…d96f

Correct on the first run.

Mastery

Attempt 1 (01:13:13 UTC): stopped before submitting. Three of the four outbound-transfer / embedded-write Batch scripts across its two sets had the if defined HUNTRESS_READY wrapper, whose label was still unknown then. The solver left them out, our runner stopped instead of guessing, and the attempt expired with nothing sent.

Retry (01:39:48 UTC, after if_guard was found and folded into our Batch wording rules): CORRECT.

started_at  2026-10-10T01:39:48.370Z   (retry; attempt 1 started 01:13:13 UTC and expired unsent)
coverage    462/462 (two sets of 231)
submitted   01:39:50 UTC, 462 answers
TOTAL       2.95 s (client side)
result      CORRECT   flag e2617d26…8f3e

Time to solve

The Single Solve was completed at 00:50:10 UTC on 2026-10-10, 56 h 51 min after it started: 211 of 231 answers were in within two hours on the first day, and the last 20 expansion and control labels took two more days of wording tests. The Time Trial went through first time in 2.24 s; the first Mastery attempt stopped before submitting and expired, and the retry passed at 01:39:50 UTC in 2.95 s, 57 h 41 min after the first start.

Takeaways

  • Read the answer from the artifact when it's there. All 18 representation/transform answers were the Codec_ words, accepted first try, and our VBScript decoder checks those words against the script's own code.
  • Hand-off scripts take their own interpreter's ATT&CK ID (T1059.005 for VBScript). That lesson, learned here, also closed Day 3's two Batch hand-offs as T1059.003.
  • Name what the code does, in the generator's words. do_once and while_once describe a block that runs once; direct describes an empty slot and came from another field's vocabulary. Descriptive synonyms (single_iteration, no_wrapper) all failed.
  • Split by what is really in the slot. Decoys (separators, quote-compound markers, neutral_* variables, comments, the byte-to-text loop) never decide the label. "Nothing" words had failed only because they'd been tried on scripts that had something.
  • Send one at a time, never resend a rejection. 48 rejections over three days, each one a test.
  • A timed solver must stop rather than guess. Mastery attempt 1 met a form with no known label, sent nothing, and the retry passed once the label was found.