Hacker Holidays — Day 12: After Hours
Persistence hiding outside every autoruns tool's coverage — a malicious WMI class buried in the raw CIM repository carries a compressed .NET payload that only detonates on the domain controller by name.

Event: Hacker Holidays — The Byte Lotus Hotel · Day 12 · Forensics Points: 90
Briefing
Bar closed. Guests asleep. Something on the network just clocked in for a shift off the rotation.
Back-office machines were being logged into during the small hours. Nothing showed up
in Startup, Scheduled Tasks, or the registry Run keys — persistence was hiding
somewhere quieter, “in a corner of the system most tools don’t think to check.” A hint
from @0xMia on the room page: “the usual autoruns/persistence tools straight up
don’t catch this one — you’re gonna have to dig through the raw data by hand.”
Itinerary:
- Parse the provided system artifacts for hidden custom configuration data
- Locate the malicious class and extract its embedded payload
- Decode the payload and submit the recovered flag
Recon — Identifying the Artifacts
Room attachments unzipped with the provided passphrase:
unzip attachments-1784136288483.zip
# -> INDEX.BTR MAPPING1.MAP MAPPING2.MAP MAPPING3.MAP OBJECTS.DATA
That exact set of files — INDEX.BTR, MAPPING*.MAP, OBJECTS.DATA — is the raw
WMI CIM repository (normally found at C:\Windows\System32\wbem\Repository\).
Combined with the briefing (“not in Startup/Scheduled Tasks/Run keys”, “autoruns
doesn’t catch this”), this pointed straight at WMI persistence — a custom class or
event consumer stashed in the repository, which most autoruns-style tooling has
historically had weak or no coverage for.
Hunting the Malicious Class
First pass — grep for known WMI persistence class names inside the raw repository blob:
strings OBJECTS.DATA | grep -i -E "EventConsumer|EventFilter|FilterToConsumerBinding|ActiveScriptEventConsumer|CommandLineEventConsumer"
This confirmed the presence of __EventFilter, __FilterToConsumerBinding,
CommandLineEventConsumer, and ActiveScriptEventConsumer definitions/instances in
the repository — standard WMI event subscription persistence building blocks.
Plain strings on the whole file was too noisy (full of default Windows/GPO policy
class definitions that ship with every CIM repository). Narrowed down by looking for
base64-shaped long strings instead:
strings -n 20 OBJECTS.DATA | grep -E "^[A-Za-z0-9+/]{40,}={0,2}$"
One ~1450-character base64 blob stood out, repeated 4 times in the file (unlike the surrounding one-off Defender/GPO policy noise) — clearly an embedded payload property on a class instance.
Decoding the Payload
Base64-decoding the blob produced binary data with no recognizable magic bytes (not
gzip 1f 8b, not zlib 78 9c):
echo "<blob>" | base64 -d > /tmp/blob.bin
file /tmp/blob.bin # -> data
xxd /tmp/blob.bin | head -5
No header suggested raw DEFLATE (no wrapper) — the output of
System.IO.Compression.DeflateStream, a very common choice for keeping payloads
compact and un-fingerprintable:
import zlib
data = open('/tmp/blob.bin', 'rb').read()
out = zlib.decompress(data, -15) # -15 = raw deflate, no zlib/gzip header
open('/tmp/updates.exe', 'wb').write(out)
Decompression succeeded, producing a 4096-byte PE file.
Analyzing the Dropped Binary
file /tmp/updates.exe
# -> PE32 executable for MS Windows 4.00 (GUI), Intel i386 Mono/.Net assembly, 3 sections
It’s a small .NET assembly (internal name updates.exe, per its embedded
VERSIONINFO resource). Since .NET string literals are UTF-16LE, plain ASCII
strings/grep for the flag came up empty — expected, not a dead end.
Disassembled the IL directly with monodis (already present via mono-devel, no need
for the full dotnet SDK or ilspycmd):
monodis --output=/tmp/updates.il /tmp/updates.exe
grep -i -A3 -B3 "ldstr" /tmp/updates.il
The Main method logic:
call string class [mscorlib]System.Environment::get_MachineName()
ldstr "bytelotusdc"
call bool string::Equals(string, string, valuetype [mscorlib]System.StringComparison)
brfalse.s IL_0045
newobj instance void class [System]System.Diagnostics.ProcessStartInfo::'.ctor'()
ldstr "cmd.exe"
callvirt instance void ...set_FileName(string)
ldstr "/c net user patch <base64> /add"
callvirt instance void ...set_Arguments(string)
...
call class [System]System.Diagnostics.Process::Start(...)
Logic: the payload only fires if Environment.MachineName == "bytelotusdc" (an
environment/anti-analysis check) — if it matches, it silently runs cmd.exe to add a
new local user account named patch, using a base64 string as the “password”
argument.
Recovering the Flag
The base64 string passed as the backdoor account’s password is the flag:
echo "<base64 password argument>" | base64 -d
# THM{redacted}
Takeaways
- WMI event subscriptions (
__EventFilter/__EventConsumer/__FilterToConsumerBinding) live in the CIM repository, not the registry — that’s exactly why Startup, Scheduled Tasks, and Run-key checks (and a lot of autoruns tooling) miss this persistence mechanism entirely. ParsingOBJECTS.DATAby hand is sometimes the only way to find it. - A repeated, unusually long base64 blob inside a binary repository dump is worth
isolating on its own — filtering
stringsoutput for base64-shaped lines cut through a huge amount of legitimate GPO/Defender policy noise. - Headerless decompression isn’t a dead end. No magic bytes just meant raw
DEFLATE instead of a gzip/zlib-wrapped stream —
zlib.decompress(data, -15)handles exactly that case. - Anti-analysis checks can double as a chain-of-custody clue. Gating the payload on
Environment.MachineName == "bytelotusdc"confirms the intended target (the domain controller) and explains why the backdoor never fired anywhere else on the network.
Flag
THM{redacted}