- The 5 files (
INDEX.BTR,MAPPING*.MAP,OBJECTS.DATA) are the Windows WMI repository: the persistence is a fileless WMI Event Subscription, invisible to the classic checks (Startup / Scheduled Tasks / Run keys) and a historical blind spot of autoruns tooling. - A
CommandLineEventConsumerrunspowershell -enc; the command (base64/UTF-16LE) is a loader reading a property off a WMI class. - The custom class
Win32_HardwareTelemetry(fake-legit name) stores the payload in itsConfigDataproperty = base64(deflate(.NET assembly)). - Rebuild the assembly and inside is
net user patch <base64> /add: the backdoor password is the flag (base64). There's an anti-analysis guard too.
An offline forensics case: everything runs on your own machine against the attachment files, no target to attack. The attachment is a password-protected archive: extract it with 7z x -pAft3rH0ursAtt4chm3ntP4ss <attachment> (or unzip -P Aft3rH0ursAtt4chm3ntP4ss <attachment>; if unsure of the format, run file <attachment> first) → you get INDEX.BTR, MAPPING*.MAP, OBJECTS.DATA. Then only standard tools: strings, grep, python3, base64, iconv.
[01] The artifact: it's a WMI repository
Forensics rule one: identify what you're holding. Those five files aren't a random dump — they're the exact layout of the Windows WMI repository, which lives in C:\Windows\System32\wbem\Repository\. OBJECTS.DATA holds class definitions and instances; INDEX.BTR is the B-tree index; the .MAP files are page maps.
ls -la # INDEX.BTR MAPPING1.MAP MAPPING2.MAP MAPPING3.MAP OBJECTS.DATA = C:\Windows\System32\wbem\Repository\ # il repository WMI
ls -la # INDEX.BTR MAPPING1.MAP MAPPING2.MAP MAPPING3.MAP OBJECTS.DATA = C:\Windows\System32\wbem\Repository\ # the WMI repository
The room's hints ("nothing in Startup/Scheduled Tasks/Run keys", "tools don't think to check there") all point at the same technique: WMI Event Subscription persistence. It's fileless — no executable on disk, it all lives inside the WMI DB — and historically one of the blind spots of autoruns tooling. @0xMia's tip ("dig through the raw data by hand") is literal.
[02] Confirm the persistence
A WMI Event Subscription has three parts: an __EventFilter (the trigger, a WQL query — here on a "small hours" theme), an __EventConsumer (the action) and a __FilterToConsumerBinding tying them. No exotic parser needed: strings + grep on OBJECTS.DATA surface the real instances.
strings -a OBJECTS.DATA \ | grep -iE 'EventConsumer|EventFilter|FilterToConsumerBinding' \ | sort | uniq -c | sort -rn | head __FilterToConsumerBinding # c'e una subscription REALE CommandLineEventConsumer # il consumer che ESEGUE un comando # mostra la query WQL del filtro: il trigger a tema «ore piccole» strings -a OBJECTS.DATA | grep -iE 'SELECT .*FROM .*(Win32_LocalTime|__InstanceModification)' # es.: SELECT * FROM __InstanceModificationEvent ... Win32_LocalTime ... ← il trigger «ore piccole»
strings -a OBJECTS.DATA \ | grep -iE 'EventConsumer|EventFilter|FilterToConsumerBinding' \ | sort | uniq -c | sort -rn | head __FilterToConsumerBinding # there IS a real subscription CommandLineEventConsumer # the consumer that RUNS a command # show the filter's WQL query: the "small hours" trigger strings -a OBJECTS.DATA | grep -iE 'SELECT .*FROM .*(Win32_LocalTime|__InstanceModification)' # e.g.: SELECT * FROM __InstanceModificationEvent ... Win32_LocalTime ... ← the "small hours" trigger
[03] The command: obfuscated PowerShell
The CommandLineEventConsumer carries a CommandLineTemplate: the command fired on trigger. I extract it and find a classic powershell -enc — where -enc expects base64 of UTF-16LE text.
strings -a OBJECTS.DATA | grep -oE 'cmd /C powershell.exe .*-enc [A-Za-z0-9+/=]+' # cmd /C powershell.exe -Sta -Nop -Window Hidden -enc JABmAGkAbABlAC... # -enc = base64 di UTF-16LE: decodifico ENC=$(strings -a OBJECTS.DATA | grep -oE '[-]enc [A-Za-z0-9+/=]+' | head -1 | awk '{print $2}') echo $ENC | base64 -d | iconv -f UTF-16LE -t UTF-8
strings -a OBJECTS.DATA | grep -oE 'cmd /C powershell.exe .*-enc [A-Za-z0-9+/=]+' # cmd /C powershell.exe -Sta -Nop -Window Hidden -enc JABmAGkAbABlAC... # -enc = base64 of UTF-16LE: decode it ENC=$(strings -a OBJECTS.DATA | grep -oE '[-]enc [A-Za-z0-9+/=]+' | head -1 | awk '{print $2}') echo $ENC | base64 -d | iconv -f UTF-16LE -t UTF-8
# ← output del comando sopra: il loader decodificato (da leggere, non da eseguire) $file = ([WmiClass]'ROOT\cimv2:Win32_HardwareTelemetry').Properties['ConfigData'].Value; $o = New-Object IO.MemoryStream; $d = New-Object IO.Compression.DeflateStream( [IO.MemoryStream][Convert]::FromBase64String($file), [IO.Compression.CompressionMode]::Decompress); # ...legge i byte decompressi... [Reflection.Assembly]::Load($o.ToArray()).EntryPoint.Invoke($null,...) # .NET in memoria
# ← output of the command above: the decoded loader (read it, don't run it) $file = ([WmiClass]'ROOT\cimv2:Win32_HardwareTelemetry').Properties['ConfigData'].Value; $o = New-Object IO.MemoryStream; $d = New-Object IO.Compression.DeflateStream( [IO.MemoryStream][Convert]::FromBase64String($file), [IO.Compression.CompressionMode]::Decompress); # ...reads the decompressed bytes... [Reflection.Assembly]::Load($o.ToArray()).EntryPoint.Invoke($null,...) # .NET in memory
It never touches disk. It reads a property off a WMI class (ConfigData of Win32_HardwareTelemetry), base64-decodes it, deflate-decompresses it, and loads the result as a .NET assembly straight into memory via Reflection.Assembly::Load. The payload exists as no file: it lives inside the WMI DB. That's the "hidden custom configuration data" from the itinerary.
[04] The malicious class and its payload
Win32_HardwareTelemetry is not a standard WMI class: the attacker created it with a legit-looking name (mimicry). Its ConfigData property is the payload store. I look for it in the repository: a long base64 string (starts with 7VZ..., typical of a deflate stream).
# la classe custom e la proprieta che tiene il payload strings -a OBJECTS.DATA | grep -iE 'Win32_HardwareTelemetry|ConfigData' | sort -u Win32_HardwareTelemetry ... ConfigData # nome finto-legittimo, dato embedded
# the custom class and the property holding the payload strings -a OBJECTS.DATA | grep -iE 'Win32_HardwareTelemetry|ConfigData' | sort -u Win32_HardwareTelemetry ... ConfigData # fake-legit name, embedded data
[05] Rebuild the .NET payload
I redo by hand what the loader would do: extract the ConfigData blob, base64-decode and decompress it. Careful: DeflateStream is raw deflate (no zlib/gzip header) → in Python that's zlib.decompress(data, -15). The result starts with MZ: it's a PE, a .NET assembly.
# 0) individua il blob: prime 40 lettere delle stringhe base64 lunghe (spunta il prefisso 7VZ) strings -a OBJECTS.DATA | grep -oE '[A-Za-z0-9+/]{200,}' | cut -c1-40 # 7VZ... ← il blob ConfigData inizia cosi # 1) estrai il blob ConfigData (filtro NON ancorato sul prefisso 7VZ) strings -a OBJECTS.DATA | grep -oE '7VZ[A-Za-z0-9+/]{200,}={0,2}' > cfg.b64 wc -lc cfg.b64 # atteso: 1 riga, byte > 0 (se e vuoto, il filtro non ha agganciato nulla) # 2) base64 -> RAW deflate (-15) -> assembly python3 -c "import base64,zlib; open('payload.bin','wb').write(zlib.decompress(base64.b64decode(open('cfg.b64').read().strip()), -15))" file payload.bin PE32 executable ... Mono/.Net assembly # payload ricostruito
# 0) locate the blob: first 40 chars of the long base64 strings (spot the 7VZ prefix) strings -a OBJECTS.DATA | grep -oE '[A-Za-z0-9+/]{200,}' | cut -c1-40 # 7VZ... ← the ConfigData blob starts like this # 1) extract the ConfigData blob (filter NOT anchored, keyed on the 7VZ prefix) strings -a OBJECTS.DATA | grep -oE '7VZ[A-Za-z0-9+/]{200,}={0,2}' > cfg.b64 wc -lc cfg.b64 # expected: 1 line, bytes > 0 (if empty, the filter matched nothing) # 2) base64 -> RAW deflate (-15) -> assembly python3 -c "import base64,zlib; open('payload.bin','wb').write(zlib.decompress(base64.b64decode(open('cfg.b64').read().strip()), -15))" file payload.bin PE32 executable ... Mono/.Net assembly # payload rebuilt
[06] The flag inside the assembly
The .NET string literals are UTF-16, so I search with strings -e l. Out comes the malware's real action — and a textbook detail.
strings -a -e l payload.bin | grep -iE 'net user|halt|mismatch|bytelotus' /c net user patch VEhN████████████████████████████████████ /add # crea un backdoor; la password E la flag (base64, oscurata) Execution halted: Environment mismatch. # guard: gira solo sulla macchina giusta (bytelotusdc)
strings -a -e l payload.bin | grep -iE 'net user|halt|mismatch|bytelotus' /c net user patch VEhN████████████████████████████████████ /add # creates a backdoor; the password IS the flag (base64, masked) Execution halted: Environment mismatch. # guard: only runs on the right host (bytelotusdc)
The payload creates a local user patch (an ironic name: the "patch" that opens the door) and sets its password. That password is the flag, base64-encoded. I decode it on my own instance — the value stays with you:
echo 'VEhN████████████████████████████████████' | base64 -d THM{████████████████████} # oscurata: policy
echo 'VEhN████████████████████████████████████' | base64 -d THM{████████████████████} # redacted: policy
THM{████████████████████}
You won't find the flag here: the value itself teaches nothing, and half the fun is getting there. The methodology above is complete and reproducible — the last Enter is yours to press.
[07] Defense
| Weakness / TTP | Detection & fix |
|---|---|
| WMI Event Subscription persistence (fileless) | Sysmon events 19/20/21 (WmiFilter/Consumer/Binding); Get-WmiObject -Namespace root\subscription __EventConsumer; Autoruns "WMI" tab |
| Payload hidden in a custom WMI class | Baseline the classes in root\cimv2; alert on odd classes/properties holding large blobs; offline parsers (python-cim / PyWMIPersistenceFinder) |
powershell -enc + in-memory Assembly::Load | ScriptBlock & Module logging; AMSI; alert on -enc/FromBase64String/Reflection.Assembly in EventConsumers |
Backdoor via net user ... /add | Event ID 4720 (user created); alert on off-hours user creation / from WMI processes |
No suspicious file, no Run key, nothing in Scheduled Tasks — yet the box "clocks in" every night. The whole chain lives inside the WMI DB: a trigger, a consumer launching PowerShell, and a fake-legit class acting as the payload's hard drive. The lesson: the WMI repository is executable code and data just like the filesystem — include it in triage, don't treat it as system noise. And beware names that "sound Microsoft": Win32_ is not a free pass.