zwesty
Write-ups / Day 1 / Script
Day 1 · Thu, Oct 1 · Script

Power Play

Prey slaughtered

Static PowerShell triage · decoding embedded config · extracting IOCs · mapping behaviour to MITRE ATT&CK (and choosing parent vs sub-technique) · handling password-protected sample archives safely

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-01-native-signals (+ /time_trial, /mastery)
Points
Single Solve 2 · Time Trial 3 · Mastery 5
Solved
2026-10-02, all three modes, in 31 min 45 s from the first start
Player
Zwesty (solo), with Claude Code (Opus 5.5)

The challenge

Three PowerShell specimens are on the table, and none of them will ever run. That's fine, because you only need to read them. Classify each script's intent, map its primary ATT&CK technique, and pull out the exact indicators sitting in plain sight.

15 questions, five per script (resource-sample.ps1, collection-sample.ps1, and impact-sample.ps1):

Per script Answer format
Scenario One of resource_hijacking, synthetic_collection, or synthetic_impact
Primary ATT&CK technique T#### or T####.###
Three exact indicators Resource: URL, wallet, mutex. Collection: URL, first filename, user agent. Impact: extension, recovery-note name, URL

The Time Trial had the same 15 questions (the page said "under 8 seconds"). Mastery had 30, as two sets of 15 ("2 tasks in under 7 seconds").

Reading the prompt: what it said we'd need

Clue What it pointed to What I did about it
"none of them will ever run … you only need to read them" Static analysis only Never execute. Kali ships pwsh, and it stayed unused. The scripts were decrypted to memory and read as text
"Classify each script's intent" Identify the objective: mining, theft, ransomware Look at what the code would touch and use, not the file names
"map its primary ATT&CK technique" One best-fit technique per script, which is a judgement call Map from evidence, then work out parent vs sub-technique (see below)
"exact indicators sitting in plain sight" IOCs that are present, maybe lightly encoded, and must be copied byte for byte Decode, then extract programmatically. Never retype
"native signals" (in the challenge ID) The .NET types each script references are the behavioural signal Base the classification on the $nativeTypes list

Safety: a locked sample archive

evidence.zip was AES-256 encrypted (zip method 99, WinZip AE-2). The Game Master API gave no password. The archive's unencrypted parts held no hint either: no comments, and only the standard AES extra field.

The lesson page: "The password for this archive is infected"

The CourseStack lesson page had it: "The password for this archive is infected." That's the security industry's standard password for zipped malware samples. The convention keeps antivirus and mail filters from acting on samples in transit.

Handling: decrypted with 7z x -pinfected -so straight to stdout, so no .ps1 ever touched the disk. Nothing was executed. Artifacts stayed in the analysis VM.

What was in the files

Every script, in every mode, was one of three fixed templates. Each defines a single function that: 1. decodes a Base64 JSON "descriptor" holding the indicators, 2. passes it through a $reconstruct block (the identity function in every sample we saw), 3. returns it alongside a $nativeTypes list: the .NET classes a real version would use.

The resource sample, with the Base64 shortened:

# Huntress Script Triage -- inert composite-analysis fixture (windows_powershell_5_1)
function artifact_6ad60457ad {
  $encodedMaterial = [Convert]::FromBase64String('eyJhIjoiYTg4YmZkYTcy…')
  $reconstruct = {
    param([byte[]]$candidateBytes)
    return [byte[]]$candidateBytes
  }
  [byte[]]$plainMaterial = @(& $reconstruct $encodedMaterial)
  $decodedText = [Text.Encoding]::UTF8.GetString($plainMaterial)
  $descriptor = $decodedText | ConvertFrom-Json
  $record = $descriptor
  $nativeTypes = @([Diagnostics.Process],[Microsoft.Win32.Registry],[Net.WebClient])
  [pscustomobject]@{ A = $record.a; B = $record.b; C = $record.c; D = $record.d; Native = $nativeTypes }
}

The three samples, decoded (Single Solve values, defanged):

Sample $nativeTypes Descriptor contents
resource Diagnostics.Process, Microsoft.Win32.Registry, Net.WebClient URL hxxps://cdn-03ce326948[.]example[.]invalid/3e59094b24e2.bin · wallet wallet_d327bc11…6b77 · file update-03ce326948.dat · Run key HKCU\Software\HuntressLab\Run\03ce326948 · user agent Mozilla/5.0 HuntressLab/3.6 · 64 (a thread count) · mutex Global\HuntressLab_6460b4f6aa72
collection IO.File, IO.Compression.ZipFile, Net.HttpWebRequest URL hxxps://497b6b9b43[.]example[.]invalid/3f890532dd42.dat · target files ["update-497b6b9b43.dat", "Login Data", "Cookies"] · archive stage-e536c782dd01.zip · mutex · user agent Mozilla/5.0 HuntressLab/3.3
impact IO.File, Security.Cryptography.Aes, IO.DirectoryInfo extension .locked-1d685 · note RECOVERY-1d6858b777.txt · URL hxxps://1d6858b777[.]example[.]invalid/a0e20204ed17.dat · the same ["update-…", "Login Data", "Cookies"] list · mutex

Two details that mattered: - JSON escaping: the descriptor shows Global\\HuntressLab_…, but the real value has one backslash. - "Composite" fixtures: strings like Login Data and Cookies appear in both the collection and impact samples. I first wrote them off as decoys. They turned out to be the key to the hardest answer.

Was every mode the same style, just with different data?

Yes. I compared all 9 scripts (3 per mode, across 4 zips). With the random function name and the Base64 removed, there were exactly 3 distinct script skeletons. The $nativeTypes list per sample type never changed, and neither did the descriptor's layout: in every resource sample, b is the URL, c the wallet, and h the mutex. Only the values changed: URLs, wallets, mutexes, user-agent versions, and the random hex IDs.

So the scenario and ATT&CK answers were the same in all three modes: T1496, T1555.003, and T1486. That was tested, not assumed: the solver matched each script's type list against the three Game Master-confirmed lists, and would have stopped on any mismatch.

Single Solve: 12 indicators straight away, ATT&CK the hard way

The scenarios and indicators were straightforward: - The scenarios came from the native types. Process, Registry, and WebClient plus a wallet and mutex mean mining. File, ZipFile, and HttpWebRequest mean gather, pack, and send. File, AES, and DirectoryInfo plus .locked and a recovery note mean ransomware. - The indicators were picked out of the decoded descriptor by shape: https://, wallet_, Global\, Mozilla/, and so on. "The first filename" was the first entry of the collection target list.

The ATT&CK IDs were the hard part. The Game Master judges all 15 answers together and says only incorrect, with no detail. Two full submits failed (collection as T1005, then T1560).

The breakthrough was a partial submit. I sent only the 12 non-ATT&CK answers, and the reply was accepted, remaining: 3. That proved all 12 indicators and scenarios were right, and that Single Solve keeps partial progress, the same as Mastery. From then on, each ATT&CK answer could be tested on its own.

Each candidate was reasoned in advance, never cycled through blindly. The event forbids brute-forcing:

Technique Tried alone Result Lesson
impact T1486 Data Encrypted for Impact ✅ accepted Textbook: AES, directory walking, a renamed extension, a note
resource T1496.001 Compute Hijacking ❌ Nothing in the sample proves which resource is hijacked
resource T1496 Resource Hijacking ✅ accepted The parent, because the evidence doesn't narrow it
collection T1005, T1560, T1560.002, T1119, T1074.001 ❌ all five These describe the mechanism (read, zip, stage), not the objective
collection T1555 Credentials from Password Stores ❌ The right family, but too vague
collection T1555.003 Credentials from Web Browsers ✅ CORRECT Login Data and Cookies are Chrome's password and cookie stores: the evidence names the browser

The reframe that cracked collection: the "first filename" answer had been accepted from the list ["update-…dat", "Login Data", "Cookies"]. So that list was the collection script's real target list, not decoys. A script that reads Chrome's credential stores, zips them, and sends them is browser credential theft (T1555.003), even though the challenge labelled the scenario "collection".

12 requests in total: 2 full submits, 1 diagnostic, and 9 single ATT&CK tests. CORRECT, flag 1fb306c1…11a4.

Parent or sub-technique? The rule this taught

Use the most specific technique the evidence actually proves, and no more specific. The test: "If I picked the sub-technique, could I point to the line that proves it?"

  • Login Data / Cookies prove "browser", so the sub-technique T1555.003 wins. The parent was rejected.
  • A wallet and a mutex prove "mining", but not CPU vs GPU vs cloud, so the parent T1496 wins. The sub-technique was rejected.

Time Trial: 1.49 s

CourseStack after the Single Solve: Time Trial "under 8 seconds", Mastery "2 new dynamic tasks in under 7 seconds"

With the judgement calls confirmed, the timed modes were pure extraction. Our solver recognises each script by its $nativeTypes set, using only the three confirmed pairs. It pulls indicators by shape, and stops on an unknown type list, a changed $reconstruct block, or an indicator not found exactly once.

started_at 2026-10-02T20:57:14.179Z  deadline 2026-10-02T20:57:44.179Z
  artifact evidence.zip 2472 bytes sha256 76980d6a5ad47ff4… (checked)
  resource-attack: T1496      collection-attack: T1555.003      impact-attack: T1486
  … 12 indicators …
  start         455 ms
  artifact      322 ms
  solve           9 ms   (including AES decryption of all three scripts)
  submit        700 ms
  TOTAL        1485 ms
result CORRECT  flag c46b165d…2505

Mastery: 2.15 s, first attempt

The delivery was one outer zip holding evidence-01.zip and evidence-02.zip, each AES-encrypted. That's one of the two delivery shapes the solver was tested against before the first attempt, a lesson carried over from the earlier Day 1 challenges. Each set was answered only from its own zip.

started_at 2026-10-02T20:58:52.474Z  deadline 2026-10-02T20:59:22.474Z
  artifact script-analysis-day-01-native-signals-mastery.zip 5166 bytes sha256 f96ad10b598a63dc… (checked)
  … 30 answers (2 sets × 15) …
  start         530 ms
  artifact      763 ms
  solve          22 ms
  submit        829 ms
  TOTAL        2145 ms
result CORRECT  flag 6fe2d6ec…8a52

Time to solve

All three modes were solved on 2026-10-02 by 20:58:54 UTC, 31 min 45 s after the Single Solve started. The Single Solve took 19 min 49 s and 12 requests, most of them spent pinning down the ATT&CK IDs; the Time Trial (1.49 s) and the Mastery (2.15 s) each went through on the first submit.

Takeaways

  • Read the lesson page as well as the API. The archive password was only there.
  • When a checker only says "incorrect", use partial submits to find out which answers are wrong. Then test the uncertain ones one at a time.
  • Reason every guess in advance. Twelve requests, each with a stated reason, isn't brute force. Cycling through technique IDs would be.
  • Map ATT&CK from the objective, not the mechanism. Zipping and uploading is how. Stealing browser credentials is what.
  • Pick the most specific technique the evidence proves, and stop there.
  • Re-examine your "decoys" once an answer has been accepted. If a validated answer came from them, they're evidence.
  • Once the judgement calls are confirmed, timed modes are just extraction. Build the solver around the confirmed answers, and have it stop on anything new.