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- |
15 | enum, T####[., exact text |
| JScript | 9 . (host- … relay-) |
72: role, ATT&CK, adapter, campaign, one indicator, three style questions | enum, exact text |
| Batch | 9 . / . |
72: the same five, plus expansion, control, graph |
exact text |
| VBScript | 9 . |
72: the same five, plus representation, transform, control |
exact text |
| Python | 14 recovered . (pyc, zipapp, wheel, PyInstaller, py2exe, cx_Freeze, PyArmor …) |
126: five plus syntax, obfuscation, carrier, chain | enum, exact text |
| Bash | 12 . (6 standard, 6 advanced: trap-, recursive- …) |
84: role, ATT&CK, adapter or feature/primitive, campaign, count, steps | enum, integer, ordered tokens |
| Node | 8 . |
56: same shape as Bash | enum, integer, ordered tokens |
| Polyglots | 8 polyglot-, 4 polyglot- |
132 (11 each) | see below |
| Host fixtures | WSF JScript, WSF mixed VBScript, HTA JScript, PHP webshell, . 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-, stage-, and the hand-off indicators from the decoded plans |
| "Build the relationship graph" | The graph (Batch), chain (Python), steps and stage- 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-Bashxor_hex|<key>(hex pairs XOR a key, checked against the Decode code), and Nodejson_numbersandreversed_decimalloaders. - Batch fixtures all follow one shape:
setlocal EnableDelayedExpansion, fragments, a decoy separator line, aneutral_*no-op variable, a "control slot" between:HUNTRESS_SEPARATOR_DONEand the output, one rebuild line, thenecho(. 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-passFor, orOn Error Resume Next), and that block is thecontrolanswer. - 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
.sctscriptlet (host-sct-scriptlet-jscript.jsin the zip) is an XML scriptlet with a single JScript CDATA block. Its descriptor decodes to the same plan as itsHUNTRESS_ANALYSIS_V1header. - Time Trial and Mastery used the same templates with new values. The solver answered every timed set unchanged.
Single Solve
- 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.
- 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. - The
.sctscriptlet. 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 11host-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. - Six descriptive names, all rejected (19:33–19:34 UTC, Homebase). For the four
if definedBatch scripts and the twoOn ErrorVBScript 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. - Batch
expansion: 8 answers by rule. The labels follow the fragment variable family:set "x=!a!…"isdelayed; FOR/CALL rebuilds arefor_indirect; a directcall setfromh0–h3iscall_double, and fromfragment*,item*, orFrAg*it isdelayed. Four came from the Homebase's coverage reconciliation (20:25), four from single-answer tests (20:48–20:49, each submitted alone). 689/695. - The last six were
controllabels, found on the evening of 2026-10-09 by Zwesty and Claude working back from the accepted answers (see the next section).error_guardon the two VBScript scripts (00:35 UTC, 691).if_guardonbatch-chain-control(01:36 UTC, 692): the cross-check. Thenif_guardon benign, launcher, and impact;batch-impact-controlreturned CORRECT at 01:37:37 UTC (flag9bb7e4e0…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_ (or for /) |
call_ |
| Batch | ver >nul + if errorlevel 1 exit / |
errorlevel_ |
| Batch | goto :HUNTRESS_ + its label |
goto_ |
| Batch | nothing | direct |
| Batch | set "HUNTRESS_ + if defined HUNTRESS_ |
if_ |
| VBScript | innermost block around Decode_ |
select_, do_, while_, for_ |
| VBScript | On Error Resume Next |
error_ |
| VBScript | nothing | direct |
Separator lines, the & rem HUNTRESS quote compound marker, neutral_* variables, and comments are decoys; they never decide the label.
error_guardcame from Zwesty mirroringerrorlevel_guard(first accepted on Day 5).directcame from splitting the open scripts by what was really in the slot, then borrowing the generator's own word from the JScriptstylefield.if_guardcame 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 areset,if, anddefined, so Zwesty and Claude listed that grid and sent the first cell:if_guardon Day 3batch-relay-controlwas accepted.- The cross-check happened here. Before rolling
if_guardout, it was sent on a different script in a different fragment family: Day 9'sstaged-handoff.bat(FrAg*fragments,call setrebuild). 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-) |
if_ |
while_ |
stager (memory-) |
call_ |
while_ |
dropper (embedded-) |
call_ |
do_ |
downloader (remote-) |
call_ |
while_ |
uploader (outbound-) |
call_ |
error_ |
launcher (local-) |
if_ |
select_ |
impact (impact-) |
if_ |
for_ |
chain (staged-) |
if_ |
error_ |
relay (relay-) |
call_ |
do_ |
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
.sctscriptlet is XML around a JScript block; once the add-on could find the block, the existing decoder answered all 11. - The server's
remainingis 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_secondwordpairs. - Cross-check a found label on a different script before rolling it out. Testing it on a different fragment family proved
if_guardnamed 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.