Persistence Removal
Systematically hunting and removing attacker persistence — scheduled tasks, services, registry runkeys, WMI subscriptions, startup folders, DLL hijacking, and web shells — and how to know when you've found all of it.
You've contained the known compromised hosts and reset the known compromised credentials. The incident is in "eradication" phase. 48 hours later, the attacker is active again on the same subnet. The containment worked — but eradication didn't. The attacker had a secondary persistence mechanism: a WMI subscription that triggers a PowerShell download-and-execute payload whenever a specific process runs. You only looked at scheduled tasks and registry run keys during eradication. This chapter is the comprehensive checklist of every persistence location to hunt before declaring eradication complete.
Registry Persistence
# Enumerate all common registry persistence locations
$runKeys = @(
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run",
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce",
"HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run",
"HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce",
"HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Run",
"HKLM:\SYSTEM\CurrentControlSet\Services",
"HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon",
"HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options"
)
foreach ($key in $runKeys) {
if (Test-Path $key) {
Write-Host "`n=== $key ===" -ForegroundColor Cyan
Get-ItemProperty $key -ErrorAction SilentlyContinue |
Select-Object * -ExcludeProperty PS*
}
}
# Winlogon Userinit and Shell (classic persistence via value replacement)
$winlogon = Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"
Write-Host "`nUserinit: $($winlogon.Userinit)"
Write-Host "Shell: $($winlogon.Shell)"
# Expected: Userinit = C:\Windows\system32\userinit.exe,
# Expected: Shell = explorer.exe
# Anything extra appended = persistence
Scheduled Task Persistence
# List all non-Microsoft scheduled tasks — focus on recently created ones
$sinceDate = Get-Date "2026-08-01"
Get-ScheduledTask | Where-Object {
$_.TaskPath -notlike "\Microsoft\*"
} | ForEach-Object {
$info = Get-ScheduledTaskInfo $_ -ErrorAction SilentlyContinue
$actions = ($_.Actions | ForEach-Object { "$($_.Execute) $($_.Arguments)" }) -join "; "
[PSCustomObject]@{
Path = $_.TaskPath + $_.TaskName
Status = $info.LastRunTime
Actions = $actions
RunAs = ($_.Principal.UserId)
}
} | Where-Object {
# Flag tasks with suspicious characteristics
$_.Actions -match "powershell|cmd|wscript|cscript|mshta|rundll32|regsvr32"
} | Format-List
# Raw XML for a specific task (reveals everything, including obfuscated commands)
Export-ScheduledTask -TaskName "SuspiciousTask" | Out-File "task_audit.xml"
Service-Based Persistence
# Enumerate non-Microsoft services — compare against known baseline
Get-WmiObject Win32_Service | Where-Object {
# Filter to non-Microsoft, non-built-in services
$_.PathName -notmatch "^[Cc]:\\[Ww]indows\\" -and
$_.State -eq "Running"
} | Select-Object Name, DisplayName, PathName, StartName, Description |
Sort-Object Name | Format-List
# Also check for services with ImagePath in suspicious locations
$suspiciousPaths = @("\\Temp\\","\\AppData\\","\\Users\\","\\ProgramData\\")
Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Services\*" |
Where-Object { $p = $_.ImagePath; $suspiciousPaths | Where-Object { $p -like "*$_*" } } |
Select-Object PSChildName, ImagePath, Start |
Format-Table
WMI Subscription Persistence
WMI subscriptions are one of the most missed persistence mechanisms in incident response. They run entirely in the WMI infrastructure, leave no file on disk in obvious locations, and survive reboots.
WMI Event Subscription: How It Works
═══════════════════════════════════════════════════════════════════
Three WMI objects must exist to create event-driven persistence:
1. Event Filter — defines the trigger condition
"When SystemUptime > 200 seconds after boot"
"When process calc.exe starts"
"At a time interval (every N seconds)"
2. Event Consumer — defines what to execute
CommandLineEventConsumer: runs a command
ActiveScriptEventConsumer: runs VBScript/JScript directly
LogFileEventConsumer: writes to a log file
3. Binding — links filter to consumer
Attack chain:
Attacker registers WMI subscription (requires admin)
System reboots → filter triggers → consumer executes payload
No scheduled task, no service, no registry runkey
Defensive detection: Event 5861 (WMI persistence created) in SIEM
# Enumerate all WMI event subscriptions
Write-Host "=== Event Filters ===" -ForegroundColor Cyan
Get-WMIObject -Namespace root\subscription -Class __EventFilter |
Select-Object Name, Query, QueryLanguage | Format-List
Write-Host "=== Event Consumers ===" -ForegroundColor Cyan
Get-WMIObject -Namespace root\subscription -Class CommandLineEventConsumer |
Select-Object Name, CommandLineTemplate | Format-List
Get-WMIObject -Namespace root\subscription -Class ActiveScriptEventConsumer |
Select-Object Name, ScriptText | Format-List
Write-Host "=== Filter-Consumer Bindings ===" -ForegroundColor Cyan
Get-WMIObject -Namespace root\subscription -Class __FilterToConsumerBinding |
Select-Object Filter, Consumer | Format-List
# Remove a specific WMI persistence entry
# Get-WMIObject -Namespace root\subscription -Class __EventFilter -Filter "Name='EvilFilter'" | Remove-WMIObject
# Get-WMIObject -Namespace root\subscription -Class CommandLineEventConsumer -Filter "Name='EvilConsumer'" | Remove-WMIObject
# Get-WMIObject -Namespace root\subscription -Class __FilterToConsumerBinding | Remove-WMIObject
Web Shell Identification and Removal
If the initial access vector was a web application, a web shell may persist even after host isolation. Web shells are files placed on the web server that provide remote code execution via HTTP.
# Find recently modified files in IIS web root (web shells often written by web server user)
$webRoot = "C:\inetpub\wwwroot"
$sinceDate = Get-Date "2026-08-01"
Get-ChildItem $webRoot -Recurse -Include "*.php","*.asp","*.aspx","*.ashx","*.asmx","*.config" |
Where-Object { $_.LastWriteTime -gt $sinceDate } |
Select-Object FullName, LastWriteTime, Length |
Sort-Object LastWriteTime -Descending
# Look for web shell signatures in PHP files
Get-ChildItem $webRoot -Recurse -Include "*.php" | ForEach-Object {
$content = Get-Content $_.FullName -Raw -ErrorAction SilentlyContinue
if ($content -match "eval\(|exec\(|system\(|passthru\(|shell_exec\(|base64_decode\(") {
Write-Warning "Suspicious: $($_.FullName)"
Select-String -InputObject $content -Pattern "eval\(|exec\(|system\(|passthru\(|shell_exec\(" |
ForEach-Object { Write-Host " Line $($_.LineNumber): $($_.Line.Trim())" }
}
}
# Check web server access logs for web shell access patterns
# Requests to .php/.aspx files returning 200 with POST body are suspicious
# Log location: C:\inetpub\logs\LogFiles\W3SVC1\
Startup Folder and DLL Hijacking
# Check all startup folders (all users)
$startupPaths = @(
"$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Startup",
"C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp",
"$env:ALLUSERSPROFILE\Microsoft\Windows\Start Menu\Programs\Startup"
)
foreach ($path in $startupPaths) {
if (Test-Path $path) {
Write-Host "`n=== $path ===" -ForegroundColor Cyan
Get-ChildItem $path | Select-Object Name, LastWriteTime, Length | Format-Table
}
}
# Check all user profiles' startup folders
Get-ChildItem "C:\Users" -Directory | ForEach-Object {
$startup = "$($_.FullName)\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup"
if (Test-Path $startup) {
$items = Get-ChildItem $startup
if ($items) {
Write-Host "`n=== $startup ===" -ForegroundColor Yellow
$items | Select-Object Name, LastWriteTime | Format-Table
}
}
}
Persistence Removal Verification
After removing persistence, verify it's gone — and then monitor for 24-48 hours for re-establishment before declaring eradication complete.
| Persistence type | Removal action | Verification |
|---|---|---|
| Registry run key | Remove-ItemProperty on the specific value | Re-run registry audit; confirm value absent; reboot and re-check |
| Scheduled task | Unregister-ScheduledTask -Confirm:$false | Get-ScheduledTask for the task name returns nothing; check Task Scheduler UI |
| Service | Stop-Service + sc delete [name] | Get-Service returns nothing; check registry HKLM\SYSTEM\CurrentControlSet\Services |
| WMI subscription | Remove-WMIObject on all three objects (filter, consumer, binding) | Re-run WMI enumeration; all three objects return empty; monitor Event 5861 |
| Web shell | Delete the file; restore from known-good backup | Hash check vs. original; access log shows no more requests to that path |
| Startup folder shortcut | Delete the .lnk file | Startup folder empty; no process launch on next user logon |
Sophisticated attackers deploy multiple independent persistence mechanisms precisely because they expect one to be found. Finding and removing the WMI subscription doesn't mean the scheduled task they also created is gone. The eradication checklist must be comprehensive and applied to every confirmed compromised host — not just the ones where you found specific persistence. Running the full persistence audit (this chapter's scripts) on all contained hosts, even if you've already identified one mechanism on each, is the minimum bar for eradication confidence. After completing all removal actions, continue monitoring for 24-48 hours before declaring the environment clean.
Q & A
Q: You find a persistence mechanism but don't know what payload it ran. Do you remove it or let it run so you can see the payload?
Remove it — but capture the artifact first. Document the persistence mechanism fully (WMI subscription text, scheduled task XML, registry value content), extract any encoded or obfuscated command, and then remove the mechanism. If the payload is a URL, don't access it from within your corporate environment. If it's a base64-encoded script, decode it in an isolated analysis sandbox (Flare VM, REMnux). The risk of letting it run again to "see the payload" is not worth it — you already have the payload in the persistence object itself. The only exception is controlled deception operations, which are out of scope for most IR teams.
Q: You found 12 persistence mechanisms across 6 hosts. What's the safest removal order?
Remove all persistence simultaneously across all hosts, ideally as part of the broader simultaneous eradication action. For a single host, the order matters less than completeness — but generally: (1) services first (they're running and could re-establish other persistence), (2) scheduled tasks (similar reason), (3) registry run keys, (4) WMI subscriptions, (5) startup folder items. After removal on all hosts, do a full re-scan before concluding. The key constraint is not order within a host — it's ensuring you've identified all persistence before starting removal, rather than finding it incrementally while the attacker re-establishes what you remove.