The challenge
"A GitHub Actions workflow decides who can trigger it and which runner inherits the trust. Identify each trigger and trust boundary, follow the static handoff into the next language, and find the job that keeps coming back."
Every earlier day's script fields plus 44 new ones: 1,373 questions on the Single Solve (Day 20 had 1,329; none were dropped). Written from the solver's report, the start reply's question list, and the Game Master request log; the specimens weren't reopened.
The four new specimens each carried the lane's usual question family:
| answer_id (per specimen) | Question | Format |
|---|---|---|
…- |
"Classify the inert source role." | enum (benign_, downloader, dropper, impact, launcher, persistence_, relay, staged_, stager, uploader, advanced_) |
…- |
Map the decoded ordered behaviour graph to one primary ATT&CK technique, by the stated precedence | technique or sub-technique ID, or none for benign |
…-, …-, …- / …- |
Recover the modelled adapter, campaign ID, and indicator | exact text |
…- |
Recover the modelled stage count | base-10 integer |
…- |
Recover the ordered behaviour graph | tokens joined by > |
…- |
"Identify the script host or source container." | exact text |
…-, …-, …- |
Encoding profile, multi-stage profile, next language in the handoff | exact text |
Reading the prompt: what it said we'd need
| Clue | What it pointed to | What I did about it |
|---|---|---|
| "A GitHub Actions workflow" | A new language family: workflow YAML (.) |
Relied on the core chain's general reader; it handled the four . polyglots with no change |
| "who can trigger it", "which runner inherits the trust", "each trigger and trust boundary" | The workflow's on: triggers and the runner its jobs run on |
Not asked as fields today. The question set was the usual family; the host profile came back standalone for all four |
| "follow the static handoff into the next language" | The next- field |
The chain gave powershell, jscript, batch, and vbscript for the four specimens |
| "the job that keeps coming back" | Persistence (the challenge ID says github-), for example a scheduled workflow |
No persistence-specific field was asked; the persistence_ role exists in the enum but none of today's four was that role |
| "Actions Speak Louder" (title) | GitHub Actions | Confirmed the day's specimens |
Safety
Script specimens are hostile: static analysis only, on our Kali VM. The solver parses the text of the encrypted evidence.zip members; no specimen was executed and no workflow was run. No code excerpts are quoted here, only the answer values, with URLs defanged.
What was in the files
- Single Solve:
evidence.zip, 133,401 bytes, 149 encrypted members (Day 20's 145 plus four workflows). The 44 new questions came from four GitHub Actions polyglots, 11 fields each, all with host profilestandalone:
Specimen (polyglot-github_actions-…) |
Role | Attack | Adapter | Stages | Steps | Obfuscation | Stage profile | Next language |
|---|---|---|---|---|---|---|---|---|
benign- (1,373 bytes) |
benign_ |
none |
local_ |
2 | acquire_ |
base64 |
two_ |
powershell |
downloader. (2,598) |
downloader |
T1105 |
http_ |
3 | decode_ |
hex_ |
three_ |
jscript |
launcher. (2,094) |
launcher |
T1059 |
process_ |
3 | profile_ |
fragmented_ |
cross_ |
batch |
staged- (1,969) |
staged_ |
T1059 |
operation_ |
2 | decode_ |
plain_ |
single_ |
vbscript |
Indicators: campaign IDs GITHUB_ACTIONS-<8 hex>; URLs of the form hxxps://github_actions-…example[.]invalid/stage; the benign utility's file path /tmp/huntress-github_actions/.<16 hex>.stage.
- Time Trial: evidence.zip, 133,499 bytes, the same 1,373 questions. All four workflows had the same shapes; only the campaign IDs, URLs, and file path changed.
- Mastery: one 667,173-byte zip, 5 sets, 6,865 questions. Every set's four workflows matched the Single Solve's shapes with their own IDs.
An oddity, not looked into: two graphs don't fit their file names the obvious way. The "benign utility" has a download-and-execute graph but role benign_utility and attack none. The "staged chain" has the decode_configuration>return_configuration graph, which the attack rubric calls benign, but attack T1059. The Game Master accepted the core chain's values for all 44 fields, so the file names are not a guide to the answers. Why they differ wasn't checked, because the specimens weren't reopened.
Single Solve
The solver solved it with no stop. It picked up today's challenge ID with its core script solver, which uses the split-header reader added on Day 19, and sent two batches:
- Core batch, 1,243 answers →
accepted, 130 remaining. All 44 new fields were in it and all were accepted. The solver flagged 50 values it had seen rejected on other questions before (mostlynoneandT1105attack values, two of them on today's new specimens) and sent them anyway, as it is set to; none was rejected here. - Merged add-on batch, 130 answers → CORRECT. These were the fields the solver's add-on readers own: 72 JScript, 16 Batch, 9 VBScript, and 11 each for the WSF, HTA, and SCT hosts.
The split between the two batches moved by a few fields against Day 20 (129 add-on fields then, 130 now). Three moved to the add-on batch (batch-launcher-expansion, batch-impact-expansion, vbscript-relay-control) and two moved back to core (batch-dropper-expansion, batch-chain-expansion). No code changed, so this follows the day's evidence; the exact reason wasn't checked.
started_at 2026-10-21T13:02:58.627Z
deadline none (Single Solve)
answers 1,243 (core) + 130 (add-ons merged) = 1,373
start / artifact / solve timings: not recorded for the Single Solve
core submit 13:02:59 → 13:03:00 accepted, remaining 130
add-ons submit 13:03:01 → 13:03:02
TOTAL ~3 s from the start reply to CORRECT (solved_by 13:03:01.459Z, +2.83 s after started_at)
result CORRECT flag cb3b42c5…9530
No new solver, tests, or rehearsal were written today: the code that ran was the same as on Days 19 and 20.
Time Trial
CORRECT on the first attempt, ranked (90 s clock, up from 50 s on Day 20; 1,373 answers). The start reply showed unique_attempts_remaining: 31.
started_at 2026-10-21T13:03:07.534Z
deadline 2026-10-21T13:04:37.534Z (90 s)
answers 1373
start 579 ms
artifact 261 ms
solve 80 ms
submit 1332 ms
TOTAL 2253 ms (submit +1.98 s after started_at)
result CORRECT flag a4f3167f…3745
Mastery
CORRECT on the first attempt, ranked (5 sets, 170 s clock).
started_at 2026-10-21T13:03:14.925Z
deadline 2026-10-21T13:06:04.925Z (170 s)
answers 6865 across 5 sets
start 528 ms
artifact 386 ms
solve 380 ms
submit 1170 ms
TOTAL 2473 ms (submit +2.29 s after started_at)
result CORRECT flag ae1ff972…40c1
Time to solve
All three modes were solved by 09:03:17 ET, 20 s after the solver's first start request at 09:02:57 ET, and each went through on the first submit; both timed modes were ranked. The finish, 3 min 17 s after release, was set by the late start: the catch-up run began at 09:01:40 instead of 08:58 and handled the Metadata, Warmups, and Android lanes first.
Takeaways
- The general reader keeps paying off. A new host format (GitHub Actions YAML) went through the existing chain with no code change, for the third day running.
- The prompt's story and the question set differ. Triggers, trust boundaries, and "the job that keeps coming back" weren't asked as fields; the usual 11-field family was. Read the questions, not just the prompt.
- The rejected-before warning is advice, not a stop. 50 flagged values were right for these questions again.
- This lane finished last only because it runs last. The 20 s solve was fine; the 3 min 39 s late start set the finish time.