The challenge
"Six Bash behavior graphs, none of which will ever run. Read them like a native: map the adapters, count the stages, tag the ATT&CK techniques, and pull the exact indicators hiding in the final bytes."
399 questions (Single Solve and Time Trial) about one AES-encrypted evidence.zip (51 files, password infected), and 798 in Mastery (two sets). Every earlier Script day came back: Hatch Careful's 357 questions plus 42 new Bash ones.
| Group | Questions | Format |
|---|---|---|
| PowerShell (Day 1's samples) | 15 | enum, T####[., exact text |
| JScript | 72 | role, ATT&CK, adapter, campaign, indicator, three style questions |
| Batch | 72 | role, ATT&CK, plan fields, expansion, and control labels (no list given) |
| VBScript | 72 | role, ATT&CK, plan fields, control label (no list given) |
| Python | 126 | role, ATT&CK, plan fields, codec words |
| Bash (new) | 42: 6 scripts × 7 | below |
The Bash questions, per script (bash-<stem>-<field>):
| Field | Question | Format |
|---|---|---|
role |
benign_utility, stager, dropper, downloader, launcher, or staged_chain | enum (the list also offers impact, relay, uploader, and others) |
attack |
primary ATT&CK technique, or none |
T####[. |
adapter |
"the exact native Bash adapter named by the command plan" | exact text |
campaign |
the synthetic campaign ID | exact text |
| indicator | one of host-, url, file-, cron- |
exact text |
stage- |
"the exact number of modeled stages" | integer |
steps |
"the ordered behavior graph using > separators" | tokens joined by > |
Reading the prompt: what it said we'd need
| Clue | What it pointed to | What I did about it |
|---|---|---|
| "Six Bash behavior graphs" | Six . fixtures, each holding a plan whose steps list is the graph |
Built a Bash fixture decoder to decode each plan, then read the graph from it |
| "none of which will ever run" | Static only | Read the scripts as text out of the zip; never ran bash |
| "Read them like a native: map the adapters" | The adapter field: a name the plan itself gives (wget_, sh_ …) |
Copied it from the decoded plan, not inferred from the curl/wget lines |
| "count the stages" | The stage- field |
Read, not counted. The plan has its own stage_: 1 for every two-step plan, 5 for the chain. Counting the steps would have given 2 |
| "tag the ATT&CK techniques" | One technique per role | The rules from earlier days, with each doubtful one sent alone |
| "the exact indicators hiding in the final bytes" | The indicators exist only after decoding the packed material | Decode, then pick the field by name from the plan |
Safety
Static only. No script in any language was run: files were read straight out of the AES zip into memory and parsed. The Bash fixtures only build their command arrays and print them with declare -p, behind a HUNTRESS_INSPECT_ONLY guard, but they were treated as hostile anyway. The artifacts stayed on our Kali VM and on the Homebase. Indicators below are defanged.
What was in the files
- The PowerShell, JScript, Batch, VBScript, and Python fixtures are the earlier days' templates with new values, plus a few new variants (below).
- Each Bash fixture holds two
readonlyvariables:Material_<id>(the packed plan, as concatenated single-quoted pieces) andCodec_<id>(<representation>|<transform>|<key>). Then comeLoad_,Decode_,Recover_,Inspect_, andPlan_functions.Plan_buildscurl/wget/chmod/cron command arrays and only prints them. - The decoded plans (Single Solve values):
| Script | Role | Adapter | Steps | Stages | Indicator |
|---|---|---|---|---|---|
host- |
benign_utility | readonly_ |
decode_ |
1 | host host- |
memory- |
stager | wget_ |
acquire_ |
1 | hxxps:/ |
embedded- |
dropper | printf_ |
decode_ |
1 | / |
remote- |
downloader | wget_ |
acquire_ |
1 | hxxps:/ |
local- |
launcher | sh_ |
locate_ |
1 | / |
staged- |
staged_chain | wget_ |
profile_ |
5 | cron / |
- Codecs seen in Single Solve: representations
base64_chunks,base64_literal,percent_hex,decimal_space,reversed_decimal; transformsreverse_then_xor,add_modulo_256,single_byte_xor,identity. - New variants in the older languages: a Batch rebuild line prefixed with
if defined HUNTRESS_READY; VBScriptreverse_then_xoranddecimal_comma; Pythonbase64_literal,integer_tuple,rolling_xor,repeating_key_xor; and a new VBScript wrapper, a one-passFor x = 0 To 0 … NextaroundDecode_*(later namedfor_once). - Time Trial used the same template with new values: the solver answered all 399 unchanged. Mastery (two sets of 399) was the same too, except that one Bash script stored its material as
hex_dash(dash-separated hex bytes,57-c1-a8-…), a representation the Bash decoder had never met.
Single Solve
- Claude's solvers (our Kali VM, 16:13–16:16 UTC, 2026-10-07): nine submits, all accepted, 298 of 399, no wrong answers. The earlier solvers needed three small fixes first (the Batch
if definedprefix, the new VBScript and Python codecs), and all their regression tests still passed. - All 42 Bash answers were accepted on their first submit. The 30 plan fields went in one batch, the 6 roles in another, then ATT&CK in three steps:
- benign
none, stagerT1105, dropperT1140, downloaderT1105: accepted together. - launcherT1059.004(Unix Shell), sent alone: accepted. - chainT1059.004, sent alone: accepted, even with the cron descriptor in it. The own-interpreter rule held again, as it had for Batch, VBScript, JScript, and Python. - Batch hand-offs (16:25 UTC):
batch-chain-attackandbatch-relay-attack=T1059.003, accepted (300). - 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 the agent's JScript analyser decoded the plans statically and answered all 72 in four batches (372, 27 left).
- The wording answers (Homebase, 2026-10-08 and 2026-10-09 afternoon): single-answer tests.
-
errorlevel_guard(downloader),call_subroutine×4,do_once(impact),while_once(benign): accepted. -for_once×4 for the newFor x = 0 To 0wrapper: accepted, afterfor_loopandfor_single_passwere rejected on 2026-10-08. It was first confirmed on Day 7's Node by Node, then applied here; it fits the_oncefamily. -single_ampersand,separator_ampersand,command_separatoronbatch-dropper-control: all rejected. The&separator line was a decoy. - While Claude was writing up that run, Claude's safety filter stopped one step of the analysis, and we didn't work around it. - More control tests (Homebase, 19:14–19:37 UTC):
goto_dispatchaccepted onbatch-chain-control; 10 rejected (for_call_set,call_set_loop,if_defined_call_set,guarded_call_set,if_defined_set,conditional_set_rebuild,defined,on_error,saved_error_reraise,err_number_guard). Guessing had stopped paying. - Batch
expansion(Homebase, 19:56–19:59 UTC):for_indirectaccepted for the benign and dropper scripts (FOR/CALL overfragment*/v_a*variables; the labels follow the fragment variable family). One spillover miss: our runner's single-question mode also sent an add-on answer nobody had tested,batch-stager-expansion=call_double, rejected. After that, runs were filtered to one question. - Reconciliation (Homebase, 20:19 UTC): seven answers the add-ons already produced but had never sent were accepted. 393 of 399, with 6
controlanswers left, all on scripts whose wrapper had no known name. - The last six (our Kali VM, 2026-10-10 00:35–01:37 UTC), worked back from the accepted answers:
| Script | What sits around the rebuild/decode | Label | Result |
|---|---|---|---|
vbscript- |
On Error Resume Next → Decode → On Error GoTo 0 → Err. |
error_ |
accepted |
batch- |
nothing in the control slot (the & line is a decoy) |
direct |
accepted |
batch- |
set "HUNTRESS_ + if defined HUNTRESS_ |
if_ |
accepted |
batch- |
set "HUNTRESS_ + if defined HUNTRESS_ |
if_ |
CORRECT |
error_guardcame from Zwesty mirroring Batch'serrorlevel_guardon Day 5.directcame from the generator's own JScriptstyleword, after splitting the open scripts by what was really in the slot.if_guardcame from a pattern: every accepted label is a keyword in the code plus one of five second words (subroutine / guard / dispatch / case / once). It was the first one tried from that grid (on Day 3), after 68 rejected names across the 13 readiness questions on Days 3–9 (five of them on this day's two).- Separator lines,
& rem HUNTRESS quote compound,neutral_*variables, and comments never decided a label. Support was never needed.
61 Single Solve submits in total (16 from our Kali VM, 45 from the Homebase): 45 accepted, 16 rejected, every rejection a single deliberate test except the one spillover. CORRECT at 01:37:16 UTC on 2026-10-10, flag c156c79c…d0d9.
Solvers: our Bash decoder reads the two readonly lines, decodes the material, checks that the Decode_ function's own code matches the Codec line (operator and key), and splits the plan into fields. Our Day 6 solver wraps Hatch Careful's solver (which wraps Dim and Dimmer's and Power Play's) and answers the Bash questions from the plan. It stops if the steps don't match the script's stem. A small helper sends only confirmed answers. Its 2 tests check that it reproduces the first 298 exactly, and that a wrong key in Decode_ stops it. The timed runs use our runner with the Homebase agent's JScript, Batch-wording, and VBScript-control add-ons.
Time Trial
All timings below are the client's own.
started_at 2026-10-10T01:40:16.785Z deadline not recorded in the request log
coverage 399/399
submit 399 answers at 01:40:18
TOTAL 2.56 s (client side)
result CORRECT flag e520f067…8094
Correct on the first run, from our Kali VM, with our runner.
Mastery
Attempt 1 (01:40:22 UTC): stopped before submitting. One Bash script in a Mastery set stored its material as hex_dash. Our Bash decoder didn't know that representation and raised an error, so the run stopped instead of guessing. The attempt expired and nothing was sent. hex_dash wasn't new to the event: VBScript fixtures had already used it (Day 5 relay, this day's VBScript impact script), but the Bash decoder had its own, shorter list.
The fix: the Bash decoder now reads hex_dash as dash-separated hex bytes. It was checked offline against the saved Mastery set: 798/798. The first relaunch failed harmlessly: zsh on our Kali VM doesn't split words, so our runner got bad arguments and started nothing. It was re-run under bash -s.
Retry (01:44:47 UTC): CORRECT.
started_at 2026-10-10T01:44:47.562Z deadline not recorded in the request log
coverage 798/798 (two sets of 399)
submit 798 answers at 01:44:49
TOTAL 3.62 s (client side)
result CORRECT flag 81519846…0fcc
Time to solve
The Single Solve was completed at 01:37:16 UTC on 2026-10-10, 57 h 23 min after it started: 298 of 399 answers (all 42 Bash ones among them) were in within 3 minutes, and the last six control labels came two and a half days later. The Time Trial went through first time in 2.56 s; the first Mastery attempt stopped before submitting and expired, and the retry passed at 01:44:49 UTC in 3.62 s, 57 h 31 min after the first start.
Takeaways
- Read the number, don't count it. "Count the stages" was answered by the plan's own
stage_count(1 for a two-step plan). Counting the steps would have been wrong on five of six scripts. - The own-interpreter rule held for Bash: launchers and chains map to
T1059.004, even when the chain also writes a cron entry. - A decoder per language means one list of codecs per language.
hex_dashhad appeared in VBScript but the Bash decoder didn't know it. A shared codec table, or a check of every solver against every known word, would have caught it. - Stopping beats guessing in a timed run. The Mastery stop cost one expired attempt and nothing else. The fix plus an offline check on the saved set (798/798) made the retry safe.
- Control labels are a keyword plus a purpose word.
for_oncefits the_oncefamily;if_guardfell out of the keyword × second-word grid. Decoys (separators, comments,neutral_*) never decide. - Tools must send only what was asked. A bulk run's spillover sent one untested answer, and it was wrong. Runs were filtered to one question after that.