The challenge
"BeanShell source sits inside an inert JSP host container, and it's the last piece of a thirty-language puzzle. Analyze both, then rebuild the complete execution, host, and evidence graph, and find the edge that holds everything together."
Script is cumulative: one evidence.zip holds every day's members, and every earlier question is asked again. The Single asked 1,802 questions. Day 29's Single had 1,747, all still present, so Day 30 added 55: 11 questions on each of five new BeanShell members.
- polyglot-beanshell-benign-utility.bsh, polyglot-beanshell-downloader.bsh, polyglot-beanshell-launcher.bsh, polyglot-beanshell-staged-chain.bsh (standalone BeanShell)
- host-jsp-beanshell-beanshell.bsh (the BeanShell in a JSP host)
The 11 questions per member (answer id = member stem + suffix):
| Suffix | Question | Format |
|---|---|---|
- |
"Classify the inert source role." | enum of 11 (benign_, downloader, … advanced_) |
- |
"Map the decoded ordered behavior graph to one primary MITRE ATT&CK technique using this precedence: …" (a long rule list; the parent technique is also accepted when a language sub-technique is canonical) | pattern: a technique ID, or none for the benign graph |
- |
"Recover the modeled language adapter." | exact text |
- |
"Recover the exact synthetic campaign identifier." | exact text |
- or - |
"Recover the exact url indicator." / "Recover the exact file_path indicator." | exact text |
- |
"Recover the modeled stage count." | integer |
- |
"Recover the ordered behavior graph using > separators." | tokens joined by > |
- |
"Identify the script host or source container." ("… or webshell source container" for the hosted member) | exact text |
- |
"Recover the ordered encoding profile." | exact text |
- |
"Recover the multi-stage relationship profile." | exact text |
- |
"Recover the next script language in the inert handoff." | exact text |
The Time Trial asked 1,898 questions on its own zip (65 of them on BeanShell members). The Mastery asked 1,898 per set over 14 sets (26,572 answers).
Reading the prompt: what it said we'd need
Nobody read the prompt on the day; the solver answered from the questions. This table is written afterwards.
| Clue | What it pointed to | How the solver met it |
|---|---|---|
| "BeanShell source" | A new language: Java-style BeanShell (.) |
Four new polyglot- members, read like every polyglot- member since Day 8: from their analysis-plan header |
| "inside an inert JSP host container" | A hosted-source member, like the earlier host- files |
host-; its accepted host- was jsp_ (the four standalone ones: standalone) |
| "the last piece of a thirty-language puzzle" | Cumulative: every earlier language is still asked | The 1,747 older questions were unchanged; the solver and plug-ins already covered them |
| "rebuild the complete execution, host, and evidence graph" | The ordered behaviour graph (-, -), the host field, and the zip as a whole |
The same 11 fields every polyglot- member has had; nothing new in shape |
| "find the edge that holds everything together" | Possibly the handoff to the next language (-) |
No new question type matched it. The question list had only the usual 11 fields per member |
| "Spill the Beans" (title) | Beans → BeanShell | Confirmed the language |
Safety
Static analysis only. Nothing was executed. The solver reads each member as text: it decodes the HUNTRESS_ANALYSIS_V1 plan header and compares it with any plan found in the body. The header reader only interprets bounded literal expressions and never evaluates challenge code. For this write-up only the solver's report, the saved Game Master replies, and the solver source were read; the evidence zips were not opened. The URL and file-path answers are synthetic (example[.]invalid hosts, /tmp/huntress-beanshell/… paths) and are left out here.
What was in the files
Described from the question list and the accepted answers.
Single Solve: evidence.zip, 162,886 bytes, 1,802 questions. The five BeanShell members' accepted values:
| Member | role | attack | adapter | steps | stages | obfuscation | next-language |
|---|---|---|---|---|---|---|---|
| benign-utility | benign_ |
none |
local_ |
decode_ |
3 | hex_ |
powershell |
| downloader | downloader |
T1105 |
http_ |
profile_ |
3 | fragmented_ |
jscript |
| launcher | launcher |
T1059 |
process_ |
decode_ |
2 | plain_ |
batch |
| staged-chain | staged_ |
T1059 |
operation_ |
acquire_ |
2 | base64 |
vbscript |
| host-jsp-beanshell | staged_ |
T1059 |
operation_ |
profile_ |
3 | fragmented_ |
jscript |
Campaigns are BEANSHELL- plus 8 hex digits. The role and ATT&CK answers that were accepted are the header's own, even where the step list read by the stated precedence would point elsewhere (the launcher's steps are decode_configuration>return_configuration, which the precedence calls benign; T1059 was accepted).
- Time Trial: a fresh
evidence.zip, 185,953 bytes, 1,898 questions with a different family mix, 65 of them on BeanShell members. - Mastery: one zip of 2,572,436 bytes, 14 sets of 1,898 questions, 65 BeanShell questions per set.
Single Solve: the automated solver
The solver ran our Script solver's core first, as one batch, then one batch from the plug-ins.
What the core does. It is our Day 8 solver, extended through Day 23. Every polyglot-* and host-* member is answered from its HUNTRESS_ANALYSIS_V1 plan: the header is decoded (HEX, B64, HB64, FRAG, Day 18's split form, Day 19's literal expressions, Day 23's LYR- stacks) into v=3|key=value|…, and the 11 answer suffixes map to plan keys. If any plan found in the body disagrees with the header, it stops instead of answering. The older languages (PowerShell, Batch, VBScript, Python, Bash, Node) go through our Day 7 solver. Plug-ins run after the core and may only fill ids it left open: here the JScript files, the WSF/HTA/SCT hosts, and some Batch wording answers.
All 55 BeanShell answers came from the core batch; none needed a plug-in.
Single Solve
started_at 09:01:29.970 ET deadline none
answers 1,672 core (accepted, 130 remaining), then 130 plug-in answers
start 625 ms
artifact 418 ms (162,886 bytes)
solve 889 ms
submit 2574 ms (core batch 532 ms, plug-in batch 2039 ms)
TOTAL 4510 ms
result CORRECT flag 9cb5bb16…f7c2 (09:01:33.3 ET)
Time Trial
CORRECT on the first attempt, ranked, on a fresh variant.
Time Trial (ranked)
started_at 09:01:40.028 ET deadline 09:02:30.028 (50 s clock)
answers 1,898
start 727 ms
artifact 305 ms (185,953 bytes)
solve 90 ms
submit 1026 ms
TOTAL 2151 ms (submit +1,652 ms after started_at)
result CORRECT flag 1d0ebe47…5cb7 (09:01:40.9 ET; 0.901 s on the server's stamps)
Mastery
CORRECT on the first attempt, ranked.
Mastery (ranked)
started_at 09:01:47.839 ET deadline 09:11:27.839 (9 min 40 s clock)
answers 26,572 (14 sets × 1,898)
start 2270 ms
artifact 676 ms (2,572,436 bytes)
solve 1334 ms
submit 2752 ms
TOTAL 7082 ms (submit +6,227 ms after started_at)
result CORRECT flag defe4693…9576 (09:01:51.8 ET; 4.007 s on the server's stamps)
Why no change was needed
- The core keys on the plan header's marker, not on the language. It finds
HUNTRESS_ANALYSIS_V1in the text whatever the comment syntax, so a new language whose header uses a known encoding is read like every language before it. - The new members brought no new field. The 55 new ids were the usual 11 suffixes. Despite the prompt's "graph" and "edge", the diff against Day 29 was 55 new ids and 0 removed.
- The header and body agreed on every BeanShell member, so the fail-closed check didn't stop anything.
- The older 1,747 were already solved by the same core and plug-ins on Day 29.
Time to solve
All three modes were solved by 09:01:51.8 ET, 1 min 51.8 s after release and 21.9 s after the Single started. Each mode went through on its first submit, and both timed modes were ranked.
Takeaways
- A reader that keys on the data, not the syntax, survives new languages. BeanShell was language 30 and cost no code.
- Flavour text isn't a new field. "Find the edge that holds everything together" sounded new; diffing the question ids against the day before (55 new, 0 gone, all the usual suffixes) showed it wasn't.
- The header's own role and ATT&CK were accepted, even where the step list alone, read by the precedence, suggests something else. Trusting the header, with a body cross-check, was the right rule all event.
- The lane ran last and still finished by 09:01:52, with both timed modes ranked (4th and 3rd).