-
Notifications
You must be signed in to change notification settings - Fork 0
feat: validate T1546.003 WMI event subscriptions #22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| $ErrorActionPreference = 'Continue' | ||
| $names = 'AtomicRedTeam-WMIPersistence-CommandLineEventConsumer-Example','PT-2026-013-Variant' | ||
| $namespace = 'root/subscription' | ||
| $bindingResidue = @() | ||
|
|
||
| function Get-NamedBindings { | ||
| param([string]$ObjectName) | ||
| $found = @() | ||
| $consumers = @(Get-WmiObject -Namespace $namespace -Class CommandLineEventConsumer -Filter "Name = '$ObjectName'" -ErrorAction SilentlyContinue) | ||
| foreach ($consumer in $consumers) { | ||
| $found += @(Get-WmiObject -Namespace $namespace -Query "REFERENCES OF {$($consumer.__RELPATH)} WHERE ResultClass = __FilterToConsumerBinding" -ErrorAction SilentlyContinue) | ||
| } | ||
| $filters = @(Get-WmiObject -Namespace $namespace -Class __EventFilter -Filter "Name = '$ObjectName'" -ErrorAction SilentlyContinue) | ||
| foreach ($filter in $filters) { | ||
| $found += @(Get-WmiObject -Namespace $namespace -Query "REFERENCES OF {$($filter.__RELPATH)} WHERE ResultClass = __FilterToConsumerBinding" -ErrorAction SilentlyContinue) | ||
| } | ||
| @($found | Sort-Object -Property __PATH -Unique) | ||
| } | ||
|
|
||
| foreach ($name in $names) { | ||
| @(Get-NamedBindings -ObjectName $name) | Remove-WmiObject -ErrorAction SilentlyContinue | ||
| Start-Sleep -Milliseconds 250 | ||
| $remainingBindings = @(Get-NamedBindings -ObjectName $name) | ||
| if ($remainingBindings.Count -eq 0) { | ||
| @(Get-WmiObject -Namespace $namespace -Class CommandLineEventConsumer -Filter "Name = '$name'" -ErrorAction SilentlyContinue) | | ||
| Remove-WmiObject -ErrorAction SilentlyContinue | ||
| @(Get-WmiObject -Namespace $namespace -Class __EventFilter -Filter "Name = '$name'" -ErrorAction SilentlyContinue) | | ||
| Remove-WmiObject -ErrorAction SilentlyContinue | ||
| } else { | ||
| $bindingResidue += "binding:$name" | ||
| } | ||
| } | ||
| Remove-Item 'C:\Windows\Temp\pt-2026-013-original-completion.json','C:\Windows\Temp\pt-2026-013-variant-completion.json' -Force -ErrorAction SilentlyContinue | ||
| Start-Sleep -Seconds 2 | ||
| $remaining = foreach ($name in $names) { | ||
| if (Get-WmiObject -Namespace $namespace -Class __EventFilter -Filter "Name = '$name'" -ErrorAction SilentlyContinue) { "filter:$name" } | ||
| if (Get-WmiObject -Namespace $namespace -Class CommandLineEventConsumer -Filter "Name = '$name'" -ErrorAction SilentlyContinue) { "consumer:$name" } | ||
| } | ||
| $remaining = @($bindingResidue) + @($remaining) | ||
| $completionFiles = @( | ||
| 'C:\Windows\Temp\pt-2026-013-original-completion.json', | ||
| 'C:\Windows\Temp\pt-2026-013-variant-completion.json' | ||
| ) | Where-Object { Test-Path $_ } | ||
| [pscustomobject]@{ Remaining=@($remaining); CompletionFiles=@($completionFiles); Clean=(@($remaining).Count -eq 0 -and @($completionFiles).Count -eq 0) } | ConvertTo-Json -Compress | ||
| if (@($remaining).Count -ne 0 -or @($completionFiles).Count -ne 0) { throw 'WMI subscription cleanup residue remains' } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| $ErrorActionPreference = 'Stop' | ||
| Get-CimInstance Win32_OperatingSystem | Select-Object Caption,Version,LastBootUpTime | ConvertTo-Json -Compress |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| $ErrorActionPreference = 'Stop' | ||
| @(Get-CimInstance -Namespace root/subscription -ClassName __EventFilter | Select-Object -ExpandProperty Name) | ConvertTo-Json -Compress |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| $ErrorActionPreference = 'Stop' | ||
| $id = 'PT-2026-013-Transient' | ||
| Register-CimIndicationEvent -Query "SELECT * FROM Win32_ProcessStartTrace WHERE ProcessName='definitely-not-created-pt-2026-013.exe'" -SourceIdentifier $id | Out-Null | ||
| Start-Sleep -Seconds 1 | ||
| Unregister-Event -SourceIdentifier $id | ||
| Get-Job -Name $id -ErrorAction SilentlyContinue | Remove-Job -Force | ||
| [pscustomobject]@{ SourceIdentifier=$id; Permanent=$false } | ConvertTo-Json -Compress |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| $ErrorActionPreference = 'Stop' | ||
| Set-ExecutionPolicy -Scope Process Bypass -Force | ||
| $testGuid = '3c64f177-28e2-49eb-a799-d767b24dd1e0' | ||
| $name = 'AtomicRedTeam-WMIPersistence-CommandLineEventConsumer-Example' | ||
| Import-Module Invoke-AtomicRedTeam -Force | ||
| Invoke-AtomicTest T1546.003 -TestGuids $testGuid -PathToAtomicsFolder 'C:\Tools\AtomicRedTeam\atomics' -Confirm:$false | ||
| $deadline = (Get-Date).AddSeconds(30) | ||
| do { | ||
| $filter = Get-WmiObject -Namespace root/subscription -Class __EventFilter -Filter "Name = '$name'" -ErrorAction SilentlyContinue | ||
| $consumer = Get-WmiObject -Namespace root/subscription -Class CommandLineEventConsumer -Filter "Name = '$name'" -ErrorAction SilentlyContinue | ||
| $binding = if ($consumer) { | ||
| Get-WmiObject -Namespace root/subscription -Query "REFERENCES OF {$($consumer.__RELPATH)} WHERE ResultClass = __FilterToConsumerBinding" -ErrorAction SilentlyContinue | ||
| } | ||
| if ($filter -and $consumer -and $binding) { break } | ||
| Start-Sleep -Seconds 1 | ||
| } while ((Get-Date) -lt $deadline) | ||
| $result = [pscustomobject]@{ | ||
| TestGuid = $testGuid | ||
| FilterPresent = [bool]$filter | ||
| ConsumerPresent = [bool]$consumer | ||
| BindingPresent = [bool]$binding | ||
| CompletedAt = (Get-Date).ToUniversalTime().ToString('o') | ||
| } | ||
| if (-not ($result.FilterPresent -and $result.ConsumerPresent -and $result.BindingPresent)) { throw 'Atomic WMI subscription was not fully created' } | ||
| $completionPath = 'C:\Windows\Temp\pt-2026-013-original-completion.json' | ||
| $result | ConvertTo-Json -Compress | Set-Content -Path $completionPath -Encoding UTF8 | ||
| $result | ConvertTo-Json -Compress |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| $ErrorActionPreference = 'Stop' | ||
| $name = 'PT-2026-013-Variant' | ||
| $namespace = 'root/subscription' | ||
| $filterArgs = @{ | ||
| Name = $name | ||
| EventNameSpace = 'root\CimV2' | ||
| QueryLanguage = 'WQL' | ||
| Query = "SELECT * FROM __InstanceModificationEvent WITHIN 60 WHERE TargetInstance ISA 'Win32_PerfFormattedData_PerfOS_System' AND TargetInstance.SystemUpTime > 99999999" | ||
| } | ||
| $consumerArgs = @{ | ||
| Name = $name | ||
| CommandLineTemplate = "$env:SystemRoot\System32\cmd.exe /c exit 0" | ||
| } | ||
| $filter = New-CimInstance -Namespace $namespace -ClassName __EventFilter -Property $filterArgs | ||
| $consumer = New-CimInstance -Namespace $namespace -ClassName CommandLineEventConsumer -Property $consumerArgs | ||
| $binding = New-CimInstance -Namespace $namespace -ClassName __FilterToConsumerBinding -Property @{ Filter=[Ref]$filter; Consumer=[Ref]$consumer } | ||
| $deadline = (Get-Date).AddSeconds(30) | ||
| do { | ||
| $persistedFilter = Get-WmiObject -Namespace $namespace -Class __EventFilter -Filter "Name = '$name'" -ErrorAction SilentlyContinue | ||
| $persistedConsumer = Get-WmiObject -Namespace $namespace -Class CommandLineEventConsumer -Filter "Name = '$name'" -ErrorAction SilentlyContinue | ||
| $persistedBinding = if ($persistedConsumer) { | ||
| Get-WmiObject -Namespace $namespace -Query "REFERENCES OF {$($persistedConsumer.__RELPATH)} WHERE ResultClass = __FilterToConsumerBinding" -ErrorAction SilentlyContinue | ||
| } | ||
| $filterPresent = [bool]$persistedFilter | ||
| $consumerPresent = [bool]$persistedConsumer | ||
| $bindingPresent = [bool]$persistedBinding | ||
| if ($filterPresent -and $consumerPresent -and $bindingPresent) { break } | ||
| Start-Sleep -Seconds 1 | ||
| } while ((Get-Date) -lt $deadline) | ||
| $result = [pscustomobject]@{ | ||
| FilterPresent = $filterPresent | ||
| ConsumerPresent = $consumerPresent | ||
| BindingPresent = $bindingPresent | ||
| CompletedAt = (Get-Date).ToUniversalTime().ToString('o') | ||
| } | ||
| if (-not ($result.FilterPresent -and $result.ConsumerPresent -and $result.BindingPresent)) { throw 'Variant WMI subscription was not fully created' } | ||
| $completionPath = 'C:\Windows\Temp\pt-2026-013-variant-completion.json' | ||
| $result | ConvertTo-Json -Compress | Set-Content -Path $completionPath -Encoding UTF8 | ||
| $result | ConvertTo-Json -Compress |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| any where winlog.event_data.Operation:"Created" and (winlog.event_data.EventType like~ ("WmiFilterEvent", "WmiConsumerEvent", "WmiBindingEvent")) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| search index=main source="WinEventLog:Microsoft-Windows-Sysmon/Operational" (_raw="*<EventID>19</EventID>*" OR _raw="*<EventID>20</EventID>*" OR _raw="*<EventID>21</EventID>*") (_raw="*<Data Name='Operation'>*Created*") AND ((_raw="*<Data Name='EventType'>*WmiFilterEvent*" OR _raw="*<Data Name='EventType'>*WmiConsumerEvent*" OR _raw="*<Data Name='EventType'>*WmiBindingEvent*")) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
In the generated Mayuri live query, Useful? React with 👍 / 👎. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Operation="Created" EventType IN ("WmiFilterEvent", "WmiConsumerEvent", "WmiBindingEvent") |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| title: Permanent WMI Event Subscription Created | ||
| id: 45742e29-7c43-4a65-bd91-b04fa7a8d9dc | ||
| status: test | ||
| description: >- | ||
| Detects creation of a permanent WMI event filter, consumer, or filter-to-consumer binding. | ||
| Permanent WMI subscriptions can provide stealthy event-triggered persistence. | ||
| references: | ||
| - https://attack.mitre.org/techniques/T1546/003/ | ||
| - https://github.com/redcanaryco/atomic-red-team/blob/master/atomics/T1546.003/T1546.003.yaml | ||
| author: mell0wx | ||
| logsource: | ||
| product: windows | ||
| category: wmi_event | ||
| detection: | ||
| selection_created: | ||
| Operation: Created | ||
| selection_type: | ||
| EventType: | ||
| - WmiFilterEvent | ||
| - WmiConsumerEvent | ||
| - WmiBindingEvent | ||
| condition: selection_created and selection_type | ||
| falsepositives: | ||
| - Legitimate endpoint-management, monitoring, or software-deployment products creating permanent WMI subscriptions | ||
| - Controlled administrative or purple-team validation activity | ||
| date: 2026-08-02 | ||
| level: high | ||
| tags: | ||
| - attack.persistence | ||
| - attack.privilege-escalation | ||
| - attack.t1546.003 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,133 @@ | ||
| { | ||
| "scenario_id": "PT-2026-013", | ||
| "validation_run_id": "VAL-2026-013", | ||
| "technique_id": "T1546.003", | ||
| "atomic_test_guid": "3c64f177-28e2-49eb-a799-d767b24dd1e0", | ||
| "atomic_test_index": 1, | ||
| "atomic_test_name": "Persistence via WMI Event Subscription - CommandLineEventConsumer", | ||
| "validation_date_utc": "2026-08-03", | ||
| "target": "approved Windows victim", | ||
| "rollback_snapshot": "verified; identifier retained in private evidence", | ||
| "scope": { | ||
| "local_only": true, | ||
| "domain_controller_targeted": false, | ||
| "network_payload": false, | ||
| "credential_access": false, | ||
| "lateral_movement": false, | ||
| "payload_triggered": false | ||
| }, | ||
| "preflight": { | ||
| "victim_running": true, | ||
| "secure_channel": true, | ||
| "sensors_running": ["Sysmon64", "SplunkForwarder", "Velociraptor", "WazuhSvc", "WinDefend"], | ||
| "splunk_healthy": true, | ||
| "fresh_victim_telemetry_confirmed": true, | ||
| "wmi_object_name_collisions": false, | ||
| "victim_boot_time_utc": "2026-07-17T14:40:33.501419Z", | ||
| "uptime_seconds_at_original_start": 1422387.012, | ||
| "atomic_payload_trigger_window_seconds": [240, 324] | ||
| }, | ||
| "original": { | ||
| "start_time_utc": "2026-08-03T01:47:00.513788Z", | ||
| "end_time_utc": "2026-08-03T01:47:08.334013Z", | ||
| "exec_output": "Exact Atomic UUID completed; filter, CommandLineEventConsumer, and binding were verified through a durable completion record.", | ||
| "detection_results": [ | ||
| { | ||
| "_time": "2026-08-03 01:47:05.000 UTC", | ||
| "host": "approved-windows-victim", | ||
| "source": "WinEventLog:Microsoft-Windows-Sysmon/Operational", | ||
| "event_codes": [19, 20], | ||
| "event_types": ["WmiFilterEvent", "WmiConsumerEvent"], | ||
| "event_count": 3 | ||
| } | ||
| ], | ||
| "subscription_state": "filter, consumer, and binding created and verified", | ||
| "detection_fired": true, | ||
| "detection_latency_seconds": 4.486, | ||
| "payload_verification": { | ||
| "process": "notepad.exe", | ||
| "event_id": 1, | ||
| "source": "WinEventLog:Microsoft-Windows-Sysmon/Operational", | ||
| "earliest_utc": "2026-08-03T01:46:55Z", | ||
| "latest_utc": "2026-08-03T01:47:15Z", | ||
| "query_scope": "bounded Sysmon process-creation search", | ||
| "match_count": 0 | ||
| } | ||
| }, | ||
| "variant": { | ||
| "start_time_utc": "2026-08-03T01:48:46.005927Z", | ||
| "end_time_utc": "2026-08-03T01:48:49.604107Z", | ||
| "exec_output": "Modified local CommandLineEventConsumer subscription completed with a deliberately non-triggering WQL condition; durable completion record verified.", | ||
| "detection_results": [ | ||
| { | ||
| "_time": "2026-08-03 01:48:47.000 UTC", | ||
| "host": "approved-windows-victim", | ||
| "source": "WinEventLog:Microsoft-Windows-Sysmon/Operational", | ||
| "event_codes": [19, 20, 21], | ||
| "event_types": ["WmiFilterEvent", "WmiConsumerEvent", "WmiBindingEvent"], | ||
| "event_count": 3 | ||
| } | ||
| ], | ||
| "subscription_state": "filter, consumer, and binding created and verified", | ||
| "detection_fired": true, | ||
| "detection_latency_seconds": 0.994 | ||
| }, | ||
| "negatives": [ | ||
| { | ||
| "name": "read-only CIM operating-system inventory", | ||
| "start_time_utc": "2026-08-03T01:49:52.263193Z", | ||
| "end_time_utc": "2026-08-03T01:49:54.749420Z", | ||
| "exec_output": "read-only operating-system inventory completed", | ||
| "detection_results": [], | ||
| "detection_fired": false, | ||
| "detection_latency_seconds": null | ||
| }, | ||
| { | ||
| "name": "read-only permanent subscription inventory", | ||
| "start_time_utc": "2026-08-03T01:50:34.234812Z", | ||
| "end_time_utc": "2026-08-03T01:50:36.844661Z", | ||
| "exec_output": "existing subscription inventory completed", | ||
| "detection_results": [], | ||
| "detection_fired": false, | ||
| "detection_latency_seconds": null | ||
| }, | ||
| { | ||
| "name": "transient in-process CIM indication subscription", | ||
| "start_time_utc": "2026-08-03T01:51:16.343586Z", | ||
| "end_time_utc": "2026-08-03T01:51:21.796557Z", | ||
| "exec_output": "temporary in-process subscription registered and removed; no permanent namespace objects created", | ||
| "detection_results": [], | ||
| "detection_fired": false, | ||
| "detection_latency_seconds": null | ||
| } | ||
| ], | ||
| "deviations": [ | ||
| { | ||
| "description": "The initial harness used direct CIM-reference string matching to verify and remove the filter-to-consumer binding. The Atomic created the subscription, but the harness could not reliably recognize the association and did not produce a completion record.", | ||
| "response": "Execution stopped. Cleanup was replaced with the upstream-style REFERENCES OF association query, a regression contract was added, and filter, consumer, binding, and completion-file counts were all verified as zero before rerun.", | ||
| "residue_after_cleanup": false | ||
| }, | ||
| { | ||
| "description": "A subsequent controlled rerun confirmed that fixed-delay polling did not correct the binding comparison because the issue was association representation rather than ingestion delay.", | ||
| "response": "The positive verification path was changed to the same REFERENCES OF association query and tested before the successful evidence-producing replay.", | ||
| "residue_after_cleanup": false | ||
| } | ||
| ], | ||
| "cleanup": { | ||
| "start_time_utc": "2026-08-03T01:52:17.025253Z", | ||
| "end_time_utc": "2026-08-03T01:52:21.487320Z", | ||
| "exec_output": "filters=0; consumers=0; bindings=0; completion_files=0; victim secure channel=true; endpoint sensors running; dcdiag quiet", | ||
| "filters_remaining": 0, | ||
| "consumers_remaining": 0, | ||
| "bindings_remaining": 0, | ||
| "completion_files_remaining": 0, | ||
| "idempotence_recheck": { | ||
| "start_time_utc": "2026-08-03T02:18:46.789298Z", | ||
| "end_time_utc": "2026-08-03T02:18:53.085340Z", | ||
| "remaining": [], | ||
| "completion_files": [], | ||
| "clean": true | ||
| } | ||
| }, | ||
| "status": "validated" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| # Permanent WMI Event Subscription Detection Validation | ||
|
|
||
| - Scenario: `PT-2026-013` | ||
| - Validation: `VAL-2026-013` | ||
| - Technique: `T1546.003` | ||
| - Atomic UUID: `3c64f177-28e2-49eb-a799-d767b24dd1e0` | ||
| - Result: **passed live validation** | ||
|
|
||
| ## Assertions | ||
| - Exact Atomic `CommandLineEventConsumer` subscription: detected. | ||
| - Modified non-triggering permanent subscription: detected. | ||
| - Read-only CIM operating-system inventory: not detected. | ||
| - Read-only permanent-subscription inventory: not detected. | ||
| - Transient in-process indication subscription: not detected. | ||
| - Explicit association cleanup and postflight health: passed. | ||
|
|
||
| The live Mayuri query uses raw Sysmon XML because equivalent normalized WMI fields are not yet verified in Splunk. The canonical Sigma rule remains field-based and backend-neutral. | ||
|
|
||
| See the [live JSON record](live/VAL-2026-013-PT-2026-013.json), [scenario results](../../purple-team/scenarios/PT-2026-013-wmi-event-subscription/RESULTS.md), and [sanitized evidence](../../evidence/sanitized/PT-2026-013/README.md). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If a final WMI query fails because the provider is unavailable, permissions changed, or the namespace cannot be queried,
-ErrorAction SilentlyContinueconverts that failure into an empty result, so$remainingstays empty and the script reportsClean=trueeven though the preceding removals may also have failed. For this persistence scenario, make the verification queries terminating or explicitly check their success before certifying cleanup.Useful? React with 👍 / 👎.