zwesty
Write-ups / Day 6 / Script
Day 6 · Tue, Oct 6 · Script

Shell We Begin

Prey slaughtered

Static triage of Bash droppers and cron chains, decoding packed configs, reading a behaviour graph, ATT&CK mapping, Batch/VBScript control-flow naming, 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-06-bash-behavior-graphs (+ /time_trial, /mastery)
Solved
2026-10-07 to 2026-10-10 (UTC): Single Solve, Time Trial, and Mastery all CORRECT. 57 h 31 min from the first start (2026-10-07 16:13 UTC) to the Mastery CORRECT (2026-10-10 01:44 UTC). Almost all of it was waiting on six Batch/VBScript control labels; the new Bash part was right on the first submit
Player
Zwesty (solo), with Claude Code (Opus 5.5) and the Homebase agent (GPT-6 Luna)

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-id, url, file-path, cron-path exact text
stage-count "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 .sh 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_output, sh_script …) Copied it from the decoded plan, not inferred from the curl/wget lines
"count the stages" The stage-count field Read, not counted. The plan has its own stage_count: 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 readonly variables: Material_<id> (the packed plan, as concatenated single-quoted pieces) and Codec_<id> (<representation>|<transform>|<key>). Then come Load_, Decode_, Recover_, Inspect_, and Plan_ functions. Plan_ builds curl/wget/chmod/cron command arrays and only prints them.
  • The decoded plans (Single Solve values):
Script Role Adapter Steps Stages Indicator
host-context.sh benign_utility readonly_configuration decode_configuration>return_configuration 1 host host-304d1b2abb
memory-stage.sh stager wget_stdout_pipe acquire_remote>dispatch_pipe 1 hxxps://8fe359fb1d[.]example[.]invalid/cff17820.sh
embedded-write.sh dropper printf_decode_redirect decode_embedded>write_file 1 /tmp/huntress-cache/.stage-eaa78515f3
remote-write.sh downloader wget_output acquire_remote>write_file 1 hxxps://521b2f4735[.]example[.]invalid/69c19c13.sh
local-dispatch.sh launcher sh_script locate_local>dispatch_process 1 /tmp/huntress-cache/.stage-23de755f9c
staged-chain.sh staged_chain wget_arch_fallback profile_host>acquire_remote>write_file>change_mode>dispatch_process 5 cron /etc/cron.d/update-9ea2805736
  • Codecs seen in Single Solve: representations base64_chunks, base64_literal, percent_hex, decimal_space, reversed_decimal; transforms reverse_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; VBScript reverse_then_xor and decimal_comma; Python base64_literal, integer_tuple, rolling_xor, repeating_key_xor; and a new VBScript wrapper, a one-pass For x = 0 To 0 … Next around Decode_* (later named for_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

  1. 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 defined prefix, the new VBScript and Python codecs), and all their regression tests still passed.
  2. 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, stager T1105, dropper T1140, downloader T1105: accepted together. - launcher T1059.004 (Unix Shell), sent alone: accepted. - chain T1059.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.
  3. Batch hand-offs (16:25 UTC): batch-chain-attack and batch-relay-attack = T1059.003, accepted (300).
  4. 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).
  5. 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 new For x = 0 To 0 wrapper: accepted, after for_loop and for_single_pass were rejected on 2026-10-08. It was first confirmed on Day 7's Node by Node, then applied here; it fits the _once family. - single_ampersand, separator_ampersand, command_separator on batch-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.
  6. More control tests (Homebase, 19:14–19:37 UTC): goto_dispatch accepted on batch-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.
  7. Batch expansion (Homebase, 19:56–19:59 UTC): for_indirect accepted for the benign and dropper scripts (FOR/CALL over fragment*/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.
  8. Reconciliation (Homebase, 20:19 UTC): seven answers the add-ons already produced but had never sent were accepted. 393 of 399, with 6 control answers left, all on scripts whose wrapper had no known name.
  9. 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-downloader/uploader/chain-control On Error Resume Next → Decode → On Error GoTo 0 → Err.Raise error_guard accepted
batch-dropper-control nothing in the control slot (the & line is a decoy) direct accepted
batch-impact-control set "HUNTRESS_READY=1" + if defined HUNTRESS_READY call set … if_guard accepted
batch-relay-control set "HUNTRESS_READY=1" + if defined HUNTRESS_READY set … if_guard CORRECT
  • error_guard came from Zwesty mirroring Batch's errorlevel_guard on Day 5.
  • direct came from the generator's own JScript style word, after splitting the open scripts by what was really in the slot.
  • if_guard came 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_dash had 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_once fits the _once family; if_guard fell 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.