>
av.Amit VijayanCYBERSECURITY & RESEARCHLet\u2019s connect
Back to the library

Threat Hunting Playbooks

Hunting WMI Event Subscriptions: Fileless Persistence That Survives Reboots

Thumbnail for: Hunting WMI Event Subscriptions: Fileless Persistence That Survives Reboots

Why this matters now

WMI event subscriptions are the persistence mechanism defenders are least likely to be looking at. No file on disk, no registry Run key, no scheduled task — a malicious __EventFilter, __EventConsumer and __FilterToConsumerBinding living inside the WMI database can fire a payload on logon, on process creation, or on a timer, and survive reboots and most "cleanups" that only chase files. It takes one command with red-team tooling: Impacket's WMIPersist.py, CrackMapExec's WMI module, or Metasploit's wmi_persistence all automate it. MITRE tracks it as T1546.003 (Event Triggered Execution: WMI Event Subscription), and it has real-world lineage — APT29 used it in the SolarWinds-era campaigns, Turla has relied on it, and SilentTrinity ships with it. If your detection stack has zero WMI-subscription coverage, this article fixes that.

The three-part trick

A permanent WMI event subscription is built from three linked components. Hunters who check only one of them will miss the persistence entirely.

1. __EventFilter — the trigger

A WQL query that decides when the payload fires. Classic attacker filters watch for __InstanceCreationEvent on Win32_Process (a new process launched), __InstanceModificationEvent on logon-related classes, or a simple __TimerEvent for periodic execution. The filter carries a CreatorSID — the account that created it, which is your first attribution lead.

2. __EventConsumer — the payload

The action that runs when the filter fires. Five standard consumer types exist, but two matter for persistence: CommandLineEventConsumer (ExecutablePath / CommandLineTemplate run a program or command) and ActiveScriptEventConsumer (ScriptText or ScriptFileName run script inline — ScriptText means the payload lives entirely inside the WMI database, fileless). Attackers can also subclass these with custom names, so don't hunt by consumer type alone.

3. __FilterToConsumerBinding — the link

Maps the filter to the consumer. A filter and a consumer can both exist without anything ever firing — the binding is what arms the weapon. This three-part split is why WMI persistence survives casual inspection: you have to enumerate all three, in the right namespace.

The namespace trap

Subscriptions live in the namespace where they were created — usually root\subscription, but attackers also hide them in root\default or sub-namespaces like root\subscription\ms_409. An audit that only checks root\subscription can walk right past a live backdoor. Enumerate namespaces first, then enumerate subscriptions inside each.

The KQL hunts

These run in Microsoft Defender XDR advanced hunting. Defender for Endpoint emits a dedicated event when a binding is created — WmiBindEventFilterToConsumer in DeviceEvents — so Hunt 1 is your cheapest, highest-fidelity query. Hunts 2 and 3 cover the gaps when the sensor or the table isn't there.

Hunt 1 — The binding: Defender sees the subscription being armed

The binding is the moment persistence goes live. Any WmiBindEventFilterToConsumer event in your estate deserves a look, because legitimate software almost never creates WMI event subscriptions at scale:

// Hunt: WMI event filter-to-consumer bindings created in Defender for Endpoint
DeviceEvents
| where Timestamp > ago(14d)
| where ActionType == "WmiBindEventFilterToConsumer"
| project Timestamp, DeviceName, InitiatingProcessAccountName,
    InitiatingProcessFileName, FileName, FolderPath, ProcessCommandLine
| order by Timestamp desc

Baseline this first: some management agents do register subscriptions legitimately. What you are hunting is a binding created by an unexpected parent — a script host, a LOLBin, or a process whose account has no business registering WMI subscriptions on that machine.

Hunt 2 — The creation tooling: catching the installer

Before a binding exists, something created the filter and consumer. Attackers reach for Set-WmiInstance / New-CimInstance in PowerShell, wmic, mofcomp (compiling a .mof file is the classic subscription-install path), or named red-team tooling. This catches the installer even when the binding event is missing:

// Hunt: processes creating WMI event subscription components
DeviceProcessEvents
| where Timestamp > ago(14d)
| where ProcessCommandLine has_any (
        "__EventFilter", "__EventConsumer", "__FilterToConsumerBinding",
        "WMIPersist", "wmi_persistence"
    )
    or (FileName in~ ("powershell.exe", "pwsh.exe")
        and ProcessCommandLine has_any ("Set-WmiInstance", "New-CimInstance", "Set-CimInstance")
        and ProcessCommandLine has_any ("EventFilter", "EventConsumer", "FilterToConsumerBinding"))
    or (FileName =~ "mofcomp.exe")
    or (FileName =~ "wmic.exe" and ProcessCommandLine has "namespace")
| project Timestamp, DeviceName, InitiatingProcessAccountName,
    InitiatingProcessFileName, FileName, ProcessCommandLine
| order by Timestamp desc

Note the double filter in the PowerShell branch: Set-WmiInstance alone is normal admin activity — it is the combination with subscription class names that makes it interesting. mofcomp.exe executing at all outside of an installer window is worth a look; compiling arbitrary .mof files is not something users do.

Hunt 3 — The trigger side: WmiPrvSE spawning shells

When a subscription fires, the WMI provider host (WmiPrvSE.exe) executes the consumer. Legitimate consumers rarely spawn interactive shells; a cmd.exe, powershell.exe, rundll32.exe or mshta.exe parented by WmiPrvSE.exe is the persistence revealing itself at runtime:

// Hunt: suspicious child processes of the WMI provider host
DeviceProcessEvents
| where Timestamp > ago(14d)
| where InitiatingProcessFileName =~ "WmiPrvSE.exe"
| where FileName in~ ("cmd.exe", "powershell.exe", "pwsh.exe",
    "rundll32.exe", "mshta.exe", "certutil.exe", "bitsadmin.exe")
| project Timestamp, DeviceName, InitiatingProcessAccountName,
    FileName, ProcessCommandLine
| order by Timestamp desc

This is your best shot at catching subscriptions that were installed long ago — before your logging or retention window — because it fires every time the persistence does its job.

Splunk equivalents

These assume Sysmon data flowing (Event IDs 19, 20 and 21 are the authoritative subscription records) plus the WMI-Activity operational log. A default Sysmon configuration — SwiftOnSecurity's, for example — already captures the WMI events; the WMI-Activity log ships with Windows.

SPL 1 — Sysmon 19/20/21: the authoritative subscription record

Sysmon logs WMI event activity natively: ID 19 fires on WmiEventFilter activity, ID 20 on WmiEventConsumer, ID 21 on WmiEventConsumerToFilter. This is the highest-fidelity Sysmon-side view:

index=wineventlog (EventCode=19 OR EventCode=20 OR EventCode=21) SourceName="Microsoft-Windows-Sysmon"
| eval component=case(EventCode=19, "WmiEventFilter",
    EventCode=20, "WmiEventConsumer", EventCode=21, "WmiEventConsumerToFilter")
| stats earliest(_time) AS first_seen, latest(_time) AS last_seen,
    values(component) AS components, values(User) AS users by host
| sort - first_seen

A host where all three components appear in a tight time window is a subscription being installed. A single host emitting 19/20/21s while your other hosts are silent is containment-grade evidence — most fleets generate almost none of these.

SPL 2 — WMI-Activity 5860/5861: consumer registration

The WMI-Activity Operational log records event consumer registrations directly: 5860 for temporary consumers, 5861 for permanent ones. Permanent is what attackers want:

index=wineventlog (EventCode=5860 OR EventCode=5861) SourceName="Microsoft-Windows-WMI-Activity"
| eval consumer_type=case(EventCode=5860, "temporary", EventCode=5861, "permanent")
| stats earliest(_time) AS first_seen, latest(_time) AS last_seen,
    count AS registrations, values(consumer_type) AS types by host
| where types="permanent" OR isnull(types)
| sort - registrations

Cross-reference these hosts against SPL 1 and the Defender hunts: a permanent consumer registration with no corresponding change ticket and no management-agent footprint is your case file.

SPL 3 — Subscription creation via process command lines

The Sysmon process-creation equivalent of KQL Hunt 2 — for environments where the Sysmon feed is the primary telemetry:

index=wineventlog (sourcetype="XmlWinEventLog:Microsoft-Windows-Sysmon/Operational"
    OR sourcetype="WinEventLog:Security")
(EventCode=1 OR EventCode=4688)
(CommandLine="*__EventFilter*" OR CommandLine="*__EventConsumer*"
    OR CommandLine="*__FilterToConsumerBinding*" OR CommandLine="*mofcomp*")
| stats earliest(_time) AS first_seen, latest(_time) AS last_seen,
    values(CommandLine) AS commands, values(ParentImage) AS parents by host, Image
| sort - first_seen

Triage: what to do when a hunt hits

A hit is a lead, not a verdict. Work it in this order:

1. Enumerate every namespace. Don't stop at root\subscription. List the namespaces, then pull all three component classes from each — Get-CimInstance is the modern cmdlet; legacy Get-WmiObject still works:

Get-CimInstance -Namespace root -ClassName __Namespace | Select-Object Name
Get-CimInstance -Namespace root\subscription -ClassName __EventFilter
Get-CimInstance -Namespace root\subscription -ClassName __EventConsumer
Get-CimInstance -Namespace root\subscription -ClassName __FilterToConsumerBinding

Repeat for root\default and any sub-namespaces you find. Compare against a known-good baseline image if you have one.

2. Read the consumer. For CommandLineEventConsumer, extract ExecutablePath and CommandLineTemplate; for ActiveScriptEventConsumer, extract ScriptText — this is the payload, and if it sits in ScriptText it never touched disk. Preserve it before you remove anything.

3. Attribute it. The CreatorSID on each component tells you which account created the subscription. Cross-check against the process tree from your hunts: who ran mofcomp or the PowerShell installer, and from where? That account's credential is compromised until proven otherwise.

4. Scope it. WMI works remotely over DCOM. Check for inbound connections to the host on 135/5985 around the creation time, and re-run the hunts fleet-wide — WMI persistence is a favorite for spreading laterally once one box is owned.

Remediation and hardening

Remove all three components, in order: binding first, then consumer, then filter — Remove-CimInstance each. Removing only the consumer leaves an orphaned binding and filter that an attacker can re-arm. If the WMI repository itself shows signs of tampering, winmgmt /resetrepository rebuilds it, but treat that as a last resort on production systems.

Reset the credential. The account behind CreatorSID must be rotated, and any session tokens it holds revoked. Then hunt for the other persistence the same actor likely planted — credential theft and WMI subscriptions travel together.

Lock the namespace down. Restrict write access to root\subscription via WMI namespace security so only admins can register subscriptions. Forward the WMI-Activity Operational log to your SIEM, keep Sysmon 19/20/21 enabled, and — cheapest win of all — turn KQL Hunt 1 into a Defender custom detection rule on WmiBindEventFilterToConsumer. A subscription that can never be created silently is a subscription your hunters will actually catch.

Keep exploring.

All articles