zwesty
Write-ups / Day 9 / Script
Day 9 · Fri, Oct 9 · Script

All the World's a Stager

Prey slaughtered

Static triage of nine script languages (PowerShell, JScript incl. a .sct scriptlet, Batch, VBScript, Python, Bash, Node, PHP, and Lua polyglots), decoding packed plans, ATT&CK mapping, recovering stage graphs, working labels back from accepted answers, 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-09-stager-relationships (+ /time_trial, /mastery)
Solved
2026-10-09/10: Single Solve, Time Trial, and Mastery all CORRECT. 11 h 42 min from the first start (2026-10-09 13:59 UTC) to the Mastery CORRECT (2026-10-10 01:41 UTC). Most of that was the hunt for two control labels
Player
Zwesty (solo), with Claude Code (Opus 5.5) and the Homebase agent (GPT-6 Luna)

The challenge

"Every stager claims to know who came before it and who comes next. Build the relationship graph from final-byte evidence alone, and expose the stage whose claimed family tree is impossible."

695 questions, the largest Script day, about one AES-encrypted evidence.zip. It was Day 8's 615 plus 80 new ones: four Lua polyglots, two new host fixtures (a .sct scriptlet and an OpenResty Lua file), and two more advanced Bash fixtures.

Group Files Questions Format
PowerShell (Day 1's samples) resource- / collection- / impact-sample.ps1 15 enum, T####[.###], exact text
JScript 9 .js (host-context … relay-handoff) 72: role, ATT&CK, adapter, campaign, one indicator, three style questions enum, exact text
Batch 9 .bat / .cmd 72: the same five, plus expansion, control, graph exact text
VBScript 9 .vbs 72: the same five, plus representation, transform, control exact text
Python 14 recovered .py (pyc, zipapp, wheel, PyInstaller, py2exe, cx_Freeze, PyArmor …) 126: five plus syntax, obfuscation, carrier, chain enum, exact text
Bash 12 .sh (6 standard, 6 advanced: trap-handler, recursive-walk …) 84: role, ATT&CK, adapter or feature/primitive, campaign, count, steps enum, integer, ordered tokens
Node 8 .mjs 56: same shape as Bash enum, integer, ordered tokens
Polyglots 8 polyglot-php-*, 4 polyglot-lua-* 132 (11 each) see below
Host fixtures WSF JScript, WSF mixed VBScript, HTA JScript, PHP webshell, .sct scriptlet (JScript), OpenResty Lua 66 (11 each) see below

The 11 polyglot and host questions: role, ATT&CK, adapter, campaign, one indicator, stage count, steps ("ordered behavior graph using > separators"), host profile, obfuscation ("ordered encoding profile"), stage profile ("multi-stage relationship profile"), and next language ("the next script language in the inert handoff").

Reading the prompt: what it said we'd need

Clue What it pointed to What I did about it
"who came before it and who comes next" Each script's plan names its own steps and its next stage Answered next-language, stage-profile, and the hand-off indicators from the decoded plans
"Build the relationship graph" The graph (Batch), chain (Python), steps and stage-count fields Read from the plans, never from file names
"from final-byte evidence alone" Decode the real bytes; trust nothing on the surface Every decoder checks the script's own decode code against its Codec line before answering
"expose the stage whose claimed family tree is impossible" A stage whose header and body disagree Not a separate question in this delivery. The plan reader refuses a file whose header and body disagree; none did
".sct scriptlet" (new host profile) An XML scriptlet wrapping a JScript block The Homebase agent taught its JScript add-on to read the block (below)

Safety

Static only. No script was run: files were read straight out of the AES zip into memory and parsed as text. The .sct scriptlet was treated as XML; its JScript block was extracted and decoded as data, never handed to a script host. The artifacts stayed on our Kali VM and on the Homebase.

Claude's safety filter stopped one step of the analysis earlier in the event, and we didn't work around it; the Homebase agent took that part on.

What was in the files

  • Every older family is Day 8's template with new values. Each script carries encoded material, a decode function, and a plan (role, steps, adapter, values) that names the answers. Four small codec variants were new: Bash rolling_xor, advanced-Bash xor_hex|<key> (hex pairs XOR a key, checked against the Decode code), and Node json_numbers and reversed_decimal loaders.
  • Batch fixtures all follow one shape: setlocal EnableDelayedExpansion, fragments, a decoy separator line, a neutral_* no-op variable, a "control slot" between :HUNTRESS_SEPARATOR_DONE and the output, one rebuild line, then echo(. Example rebuild (staged-handoff): if defined HUNTRESS_READY call set "ReCoVeReD=%%FrAg0%%%%FrAg1%%…".
  • VBScript fixtures wrap their Decode_* call in a block (Select Case, Do … Exit Do, While … Wend, a one-pass For, or On Error Resume Next), and that block is the control answer.
  • Polyglots and host fixtures carry a HUNTRESS_ANALYSIS_V1:<ENC>-… plan in a header and again in the body, as on Day 8. The Lua polyglots and the OpenResty host used the same plan format as the PHP ones.
  • The .sct scriptlet (host-sct-scriptlet-jscript.js in the zip) is an XML scriptlet with a single JScript CDATA block. Its descriptor decodes to the same plan as its HUNTRESS_ANALYSIS_V1 header.
  • Time Trial and Mastery used the same templates with new values. The solver answered every timed set unchanged.

Single Solve

  1. Claude, from our Kali VM, 14:00 UTC: 565 of 695, no wrong submits. Day 8's solver (which wraps the whole chain back to Power Play) plus the four new codec variants. Eight batches, all accepted: PowerShell 15, Batch 55, VBScript 64, Python 126, Bash 84, Node 56, polyglots 132, host 33 (the three non-JScript host files). Left: 72 JScript, 33 for the three JScript host files, 25 Batch/VBScript wording.
  2. JScript went to the Homebase agent, which handled the JScript files throughout the event with an add-on that decodes the plans statically. 16:50 UTC, its run: 105 accepted (JScript 94 in four batches, its Batch-wording add-on 5 call_subroutine, its VBScript-control add-on 6 loop wrappers), no wrong submits. 670/695.
  3. The .sct scriptlet. The JScript add-on didn't recognise the new host at first. The agent extended it to pull the JScript CDATA block out of the scriptlet and feed it to the existing plan decoder. 18:01 UTC: all 11 host-sct-scriptlet-jscript-* answers accepted in one batch. The server said 681/695; the agent's report said 689, which was its offline coverage, not the server's count.
  4. Six descriptive names, all rejected (19:33–19:34 UTC, Homebase). For the four if defined Batch scripts and the two On Error VBScript scripts, the agent tried names that described the code (defined_percent_rebuild, ready_call_reparse, guarded_call_reparse, guarded_double_percent_set, error_policy_restore, saved_err_raise). These were this challenge's only wrong submits. The lesson: the generator uses a small fixed vocabulary, not descriptions.
  5. Batch expansion: 8 answers by rule. The labels follow the fragment variable family: set "x=!a!…" is delayed; FOR/CALL rebuilds are for_indirect; a direct call set from h0–h3 is call_double, and from fragment*, item*, or FrAg* it is delayed. Four came from the Homebase's coverage reconciliation (20:25), four from single-answer tests (20:48–20:49, each submitted alone). 689/695.
  6. The last six were control labels, found on the evening of 2026-10-09 by Zwesty and Claude working back from the accepted answers (see the next section). error_guard on the two VBScript scripts (00:35 UTC, 691). if_guard on batch-chain-control (01:36 UTC, 692): the cross-check. Then if_guard on benign, launcher, and impact; batch-impact-control returned CORRECT at 01:37:37 UTC (flag 9bb7e4e0…e29e).

6 wrong submits in total, all deliberate single-answer tests.

How the control labels were found (shared across Days 3–9)

The control questions are exact_text with no list, and they were the last open answers on every Script day.

Language What's in the slot Label
Batch call :HUNTRESS_RECOVER (or for /l … do call :HUNTRESS_RECOVER) call_subroutine
Batch ver >nul + if errorlevel 1 exit /b 1 errorlevel_guard
Batch goto :HUNTRESS_DISPATCH + its label goto_dispatch
Batch nothing direct
Batch set "HUNTRESS_READY=1" + if defined HUNTRESS_READY … if_guard
VBScript innermost block around Decode_* select_case, do_once, while_once, for_once
VBScript On Error Resume Next error_guard
VBScript nothing direct

Separator lines, the & rem HUNTRESS quote compound marker, neutral_* variables, and comments are decoys; they never decide the label.

  • error_guard came from Zwesty mirroring errorlevel_guard (first accepted on Day 5).
  • direct came from splitting the open scripts by what was really in the slot, then borrowing the generator's own word from the JScript style field.
  • if_guard came after 68 rejected names. Every accepted label is a keyword in the code plus one of five second words (subroutine, guard, dispatch, case, once). The readiness wrapper's keywords are set, if, and defined, so Zwesty and Claude listed that grid and sent the first cell: if_guard on Day 3 batch-relay-control was accepted.
  • The cross-check happened here. Before rolling if_guard out, it was sent on a different script in a different fragment family: Day 9's staged-handoff.bat (FrAg* fragments, call set rebuild). It was accepted, so the label belongs to the wrapper, not to one script. The rollout then went 11 for 11.

Support was never needed.

Day 9's 18 control answers:

Script Batch VBScript
benign (host-context) if_guard while_once
stager (memory-stage) call_subroutine while_once
dropper (embedded-write) call_subroutine do_once
downloader (remote-write) call_subroutine while_once
uploader (outbound-transfer) call_subroutine error_guard
launcher (local-dispatch) if_guard select_case
impact (impact-plan) if_guard for_once
chain (staged-handoff) if_guard error_guard
relay (relay-handoff) call_subroutine do_once

Solvers: Day 8's solver and its plan reader, wrapping the chain back through Day 7's Node solver and Day 6's Bash fixes, plus the Homebase agent's JScript (with the .sct extension), Batch-wording, and VBScript-control add-ons. No separate solver was written for this day.

Time Trial

Run from our Kali VM by our solver. Timings are client side; the request log doesn't show a deadline.

started_at  2026-10-10T01:41:33.922Z   deadline (not in the request log)
coverage    695/695, one submit of 695 answers
submitted   01:41:35 UTC
TOTAL       2.42 s (client side)
result      CORRECT   flag f1304487…c727

Mastery

Started straight after the Time Trial. One submit of 3,475 answers, five times the Single Solve's 695.

started_at  2026-10-10T01:41:40.206Z   deadline (not in the request log)
coverage    3,475/3,475, one submit
submitted   01:41:45 UTC
TOTAL       5.97 s (client side)
result      CORRECT   flag 3deb016b…0e4e

Both correct on the first attempt. The if defined wrapper that had stopped Day 4 and Day 5 timed runs earlier was already in the solver.

Time to solve

The Single Solve started on 2026-10-09 at 13:59:26 UTC, had 565 of 695 answers within a minute, and went CORRECT on 2026-10-10 at 01:37:37 UTC, 11 h 38 min later, after the hunt for the control labels. The Time Trial (2.42 s) and Mastery (5.97 s) followed on the first attempt each, for 11 h 42 min from the first start to the last CORRECT.

Takeaways

  • Compounding pays off: Day 8's solver answered 565 of 695 on the first pass with four small codec additions.
  • A new host is usually a new wrapper. The .sct scriptlet is XML around a JScript block; once the add-on could find the block, the existing decoder answered all 11.
  • The server's remaining is the count. The Homebase reported 689 after the scriptlet; the server said 681. Offline coverage isn't acceptance.
  • Descriptive names don't match a generator's vocabulary. Six careful descriptions failed; the right labels were short keyword_secondword pairs.
  • Cross-check a found label on a different script before rolling it out. Testing it on a different fragment family proved if_guard named the wrapper, and the 11-answer rollout then had no misses.
  • Split the work by who can do it. Claude did the solvers, the automated runs, and the reviews; the Homebase agent did the JScript and the scriptlet.