TryHackMeMedium

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.

Green ghost mascot with a pointed hood

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:

  1. Parse the provided system artifacts for hidden custom configuration data
  2. Locate the malicious class and extract its embedded payload
  3. 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. Parsing OBJECTS.DATA by 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 strings output 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}