Windows Service Recovery Actions: The Persistence Vector Hiding in Plain Sight
Attackers abuse the Service Control Manager's failure-recovery actions to run arbitrary commands as SYSTEM. Here is how the technique works and how to audit for it across a fleet.
When defenders hunt for persistence on Windows, they check the usual suspects: Run keys, scheduled tasks, WMI event subscriptions, startup folders. One mechanism that almost never makes the checklist is the Service Control Manager's failure-recovery actions — the "what should Windows do when this service crashes?" configuration buried three tabs deep in services.msc. It has let attackers run arbitrary commands as NT AUTHORITY\SYSTEM for two decades, and most EDR tooling still doesn't watch it.
How recovery actions work
Every Windows service carries a recovery configuration that tells the SCM how to react when the service process exits with a non-zero code. The options are Take No Action, Restart the Service, Restart the Computer, and — the interesting one — Run a Program. That last action lets an administrator specify an arbitrary command line that the SCM executes as SYSTEM the moment the service fails.
It exists for a legitimate reason: a service that dies should be able to trigger a cleanup script, page an on-call engineer, or roll a log. But the SCM does not care whether the program you point it at is a diagnostic script or cmd.exe /c net user .... The command runs in the LocalSystem context of the machine, with no interactive logon and no obvious parent-process breadcrumb tying it back to "a service crashed."
The attack in three commands
An attacker who already has local administrator rights can weaponize a benign, low-noise service in seconds. The classic single-command version uses the built-in sc.exe:
REM Configure a payload to run when the service "fails"
sc.exe failure Fax reset= 0 actions= restart/60000/run/1000
sc.exe failureflag Fax 1
sc.exe qc Fax REM confirm FAILURE_ACTIONS is set
REM Point the "run a program" action at the payload
reg add "HKLM\SYSTEM\CurrentControlSet\Services\Fax" ^
/v FailureCommand /t REG_SZ ^
/d "C:\Windows\Temp\beacon.exe" /f
Now the attacker just needs the service to fail — which they can force by killing the service process, or simply waiting for a flaky third-party service to crash on its own. When it does, beacon.exe launches as SYSTEM. The persistence survives reboots because it lives in the service's registry key, and it blends in because services.exe is a legitimate, signed parent for the spawned process.
The tell is subtle: a SYSTEM-owned process whose grandparent is services.exe but which corresponds to no installed service binary. Most alerting never looks at recovery-action commands at all, so this rarely fires.
Why this slips past normal auditing
- It's not an autostart entry. Sysinternals Autoruns, the tool most persistence hunts lean on, does not enumerate
FailureCommandorFailureActions. The payload is invisible to the standard sweep. - The trigger is decoupled from the install. The malicious command is written today but may not execute for days, until the service happens to fail. Timeline correlation breaks.
- The parentage looks clean. Spawning from
services.exeas SYSTEM is exactly what a legitimate recovery action looks like — because it is one. - Registry ACLs are permissive. Any local admin can set a recovery command on most services without tripping a privilege boundary.
Auditing for it manually
You can enumerate every service's recovery configuration and flag any that point at a Run a Program action. A quick PowerShell sweep of the raw registry values gives you the ground truth (the GUI hides the command, and sc qfailure doesn't always show it):
Get-ChildItem 'HKLM:\SYSTEM\CurrentControlSet\Services' |
ForEach-Object {
$p = $_.PSPath
$cmd = (Get-ItemProperty $p -Name FailureCommand -ErrorAction SilentlyContinue).FailureCommand
if ($cmd) {
[pscustomobject]@{
Service = $_.PSChildName
FailureCommand = $cmd
}
}
} | Format-Table -Auto
Any row here deserves scrutiny. Legitimate recovery commands are rare in the wild; a command pointing into C:\Windows\Temp, a user profile, ProgramData, or an unsigned binary is a strong indicator of abuse. Cross-reference the command target against your allowlist and check whether the binary is signed and expected.
Where WinSentinel fits
Running that query once, by hand, on one box is easy. The hard part is doing it continuously and everywhere — because a recovery-action backdoor planted on a single forgotten workstation is exactly the kind of thing that never gets re-checked. WinSentinel audits service recovery configuration as part of its persistence and service-hardening modules: it enumerates every service's failure actions, flags Run a Program commands that point outside trusted, signed locations, and surfaces them alongside the rest of your service-privilege findings (unquoted paths, weak service ACLs, writable binaries).
On a single machine that's the full picture — every one of WinSentinel's audit modules runs locally with no limits, free. You get the complete recovery-action inventory and the finding, on your own box, today.
Where the story changes is scale. If you run WinSentinel Pro with a central node, that same recovery-action check becomes a fleet-wide control: every agent reports its service-recovery inventory to the central node, so a single anomalous FailureCommand planted on one of a hundred machines shows up in the compliance rollup instead of hiding in the noise. When a new recovery-action backdoor appears on any endpoint, it registers as posture drift and alerts — you don't have to remember to re-run the sweep, because the agents never stop running it.
Hardening checklist
- Inventory every service with a non-empty
FailureCommandand treat unexpected entries as suspicious until proven otherwise. - Validate that any recovery command points at a signed binary in a protected directory — never a temp, profile, or world-writable path.
- Enforce least privilege: fewer local admins means fewer principals who can plant a recovery-action payload in the first place.
- Enable process-creation logging (Event ID 4688 with command line) so that when a recovery action does fire, you have the command line on record.
- Re-audit continuously, not once. Persistence that's decoupled from its trigger only shows up if you keep looking.
Service recovery actions are a reminder that persistence doesn't need a novel zero-day — sometimes it just needs a legitimate feature nobody audits. Add FailureCommand to your hunt, and don't let it stay a blind spot on the machines you've stopped checking.