Privilege Escalation · advanced · ~11 min
**What you will learn** - Read a Windows access token with `whoami /priv` and recognise which privileges are effectively equal to SYSTEM (SeImpersonate, SeBackup, SeRestore, SeDebug, SeTakeOwnership). - Explain *why* a narrow-sounding privilege is really a generic read / write / impersonate primitive that crosses a trust boundary. - State accurately that UAC is a convenience split, not a security boundary, and scope a "UAC bypass" correctly in a report. - Apply the defensive controls that break these paths: least-privilege token assignment, virtual/managed service accounts, Credential Guard, and LSASS protection (RunAsPPL). - Detect abuse: which Windows Security and Sysmon events reveal token-privilege escalation and LSASS access, and how to verify a hardening fix actually holds.
Security objective. The asset you are protecting is the local machine's SYSTEM-level control and the credentials cached in memory (LSASS) and on disk (the SAM/SYSTEM hives, NTDS.dit on domain controllers). The threat is a local attacker who already has a foothold as a low-to-medium privileged account — very often a service account for IIS or SQL Server — and wants to become NT AUTHORITY\SYSTEM so they can dump credentials and move laterally. By the end you will be able to detect the token-privilege abuse and prevent it by removing the dangerous privilege and hardening credential storage.
What this is. Every Windows thread runs with an access token: a kernel object listing the account's SID, its groups, and a set of named privileges (SeXxxPrivilege). Most privileges are boring. A handful are so powerful that holding one is equivalent to owning the box. This lesson teaches you to identify those privileges, understand the mechanism that makes them dangerous, and — the core of the lesson — shut them down and watch for their abuse.
Where it fits. This builds directly on your prereqs. "The Windows privilege model: UAC and tokens" gave you the token structure and the two-token UAC model; here we weaponise (conceptually) and then defend specific privileges on that token. "Windows privesc: service misconfigurations" showed how you land on a service account in the first place — a misconfigured or compromised service is the usual entry point that then hands you a token carrying SeImpersonatePrivilege. This lesson connects those two: bad service, powerful token, SYSTEM.
How we teach it. Windows privilege abuse is not something you reproduce with a portable snippet the way you do a C buffer overflow — the real primitives are OS-level. So the code section models the pattern in a lab-safe, defensive way: an enumeration/audit script that detects dangerous privileges, a hardened configuration that removes them, and a verification step that proves the removal. Everything runs on a machine you own or are explicitly authorised to test.
In authorised engagements this is one of the most reliable Windows escalation paths in existence, which is exactly why defenders must understand it. A penetration tester who lands code execution as an IIS worker or a SQL Server service account will check whoami /priv first — because those accounts are designed to hold SeImpersonatePrivilege so the service can impersonate its clients, and that same privilege is the entry ticket to the "potato" family of SYSTEM escalations.
From the defender's side, the stakes are concrete:
Each privilege below is taught as: what it is, plain explanation, how it works, when it applies, and a pitfall. The recurring theme: a privilege that looks narrow is really a generic primitive for reading, writing, or impersonating across a trust boundary.
Definition. An access token is a kernel object attached to every process/thread describing who is running: user SID, group SIDs, and a list of privileges — named capabilities like SeShutdownPrivilege. Privileges are per-token and can be enabled or disabled.
Plain explanation. Groups say who you are; privileges say what special actions the OS lets you take regardless of file/object permissions. whoami /priv prints them.
How it works. When code calls a protected API, the kernel checks whether the required privilege is present and enabled on the caller's token. A few privileges bypass normal ACL checks entirely.
When / when-not. Normal users hold harmless privileges. Service and admin accounts accumulate powerful ones — that is the risk surface.
Pitfall. A privilege listed as Disabled is still dangerous: most escalation code simply calls AdjustTokenPrivileges to enable it, which the token owner is allowed to do. "Disabled" is not "revoked."
Definition. The privilege to act on behalf of (impersonate) another account after that account authenticates to your thread.
Plain explanation. It exists for a legitimate reason: a web/database service needs to impersonate the user who connected to it. But if you can coerce a SYSTEM process to authenticate to you, you can impersonate SYSTEM.
How it works (conceptually, no exploit). The "potato" attacks trick a privileged service into performing an authentication handshake toward an attacker-controlled endpoint; the attacker captures the resulting SYSTEM token and, because they hold SeImpersonate, uses it to spawn a SYSTEM process. The details are OS-version specific and out of scope — what matters defensively is the precondition: the account holds the privilege.
When / when-not. Held by IIS app-pool identities and SQL Server service accounts by design. Rarely needed by interactive users.
Pitfall. Teams "fix" a web app vulnerability but leave the service account holding SeImpersonate, so the next code-exec bug still reaches SYSTEM. The privilege, not the app bug, is the escalation.
Definition. SeBackup grants read access to any file bypassing ACLs; SeRestore grants write access to any file bypassing ACLs.
Plain explanation. Meant for backup software. But "read any file" means an attacker can copy the SAM and SYSTEM registry hives (local password hashes) or NTDS.dit (all domain hashes) on a DC. "Write any file" means overwriting a privileged binary or service DLL.
How it works. Opening a handle with the FILE_FLAG_BACKUP_SEMANTICS intent tells the kernel to honour the backup/restore privilege instead of the file's ACL.
When / when-not. Backup operators and some service identities. A normal app never needs it.
Pitfall. Assuming file ACLs protect the SAM hive. They do not when the token carries SeBackup.
Definition. The privilege to open a handle to any process, including protected system processes, and read/write its memory.
Plain explanation. Debuggers need it. Attackers use it to open LSASS and dump the credentials it caches.
Pitfall. Granting SeDebug to a monitoring or "admin helper" account is effectively granting credential-theft capability.
Definition. Take ownership of any securable object regardless of its DACL; the owner can then rewrite the DACL and grant themselves full control.
Pitfall. It is a two-step primitive — take ownership, then re-permission — so it looks harmless in isolation but yields full control of protected objects.
Definition. User Account Control gives an administrator two tokens: a filtered standard token for everyday use and an elevated token available after consent.
Plain explanation. It reduces accidental privileged actions; it is a convenience split. A "UAC bypass" auto-elevates from the standard token to the elevated token within the same admin account — using auto-elevating binaries or hijacked registry/COM entries. No trust boundary is crossed.
Pitfall / misconception to correct. Do not report a UAC bypass as "privilege escalation across a trust boundary." Microsoft states UAC is not a security boundary. Scope it as escalation within an already-administrative account. (Related misconception in this space: dumping and reading a token or a JWT tells you what it claims, but only the kernel's privilege check — or a signature verification for a JWT — actually enforces anything.)
THREAT MODEL — local token-privilege escalation on one Windows host
ENTRY POINT TRUST BOUNDARY ASSET
----------- -------------- -----
[ Internet user ] --HTTP/SQL--> ( web / DB app code )
|
code exec as ↓
+----------------------------+
| Service account token | <-- foothold
| SID: IIS AppPool / MSSQL |
| Priv: SeImpersonate (ON) |
+----------------------------+
|
========== TRUST BOUNDARY: non-admin → SYSTEM ==========
|
potato-style impersonation
v
+----------------------------+
| NT AUTHORITY\SYSTEM token | <-- objective
+----------------------------+
| | |
read SAM/ open LSASS write any
SYSTEM hive (SeDebug) file (SeRestore)
(SeBackup) |
v
+----------------------------+
| CREDENTIALS = protected asset |
| hashes, Kerberos tickets |
| -> lateral movement |
+----------------------------+
UAC prompt sits INSIDE the admin account — it is not any of
these boundaries. A "UAC bypass" never crosses the double line.
Knowledge check
SeImpersonatePrivilege as Disabled in whoami /priv. What insecure assumption would lead you to call it safe, and why is it wrong?The key command for enumeration and for verifying a fix is whoami /priv. It prints the current token's privileges and their enabled state.
> whoami /priv
PRIVILEGES INFORMATION
----------------------
Privilege Name Description State
============================= ==================================== ========
SeImpersonatePrivilege Impersonate a client after auth Enabled <-- potato precondition
SeChangeNotifyPrivilege Bypass traverse checking Enabled (benign, everyone has it)
SeCreateGlobalPrivilege Create global objects Enabled
# ^ name ^ human description ^ Enabled/Disabled
# Disabled still counts: code can enable it via AdjustTokenPrivileges.
Related read-only lab commands (all safe, information-only):
whoami /priv # privileges on the CURRENT token
whoami /groups # group SIDs and integrity level
whoami /all # everything: user, groups, privileges
For auditing service-account privilege assignment policy (not a single token), the privilege "user rights" live in Local Security Policy under Security Settings > Local Policies > User Rights Assignment, exportable with secedit /export. That export is what you review to see who is granted SeImpersonatePrivilege machine-wide.
The most distinctly "Windows" class of privilege escalation abuses access-token privileges directly. For the underlying token model, see the Windows Fundamentals privilege-model lesson.
Check your current privileges with whoami /priv. Several are effectively equivalent to SYSTEM:
The pattern is consistent: a privilege that looks narrow is actually a generic primitive for reading, writing, or impersonating across the trust boundary.
UAC (User Account Control) gives an admin two tokens — a standard one and an elevated one. It is a convenience split, not a security boundary; Microsoft states this explicitly.
Many "bypasses" auto-elevate without showing a prompt, using techniques such as auto-elevating binaries or hijacked registry and COM entries.
In a report, describe a UAC bypass accurately: it is escalation within an already-admin account, not the crossing of a trust boundary.
The three parts below model the defensive workflow: detect → harden → verify. Run only on a host you own or are explicitly authorised to test (a local lab VM). These are auditing scripts, not exploits — there is deliberately no attack code here.
This shows the misconfiguration an attacker looks for: a custom service running under an account that has been granted SeImpersonatePrivilege (or SeDebug, SeBackup, SeRestore) and is reachable from lower-privileged code. Presented as configuration to recognise, never to weaponise.
# INSECURE lab setup (conceptual, for a throwaway VM only):
# - A custom "HelperSvc" runs as a normal domain user 'svc_app'
# - Admin granted svc_app the "Impersonate a client after authentication"
# right so a quick prototype would work.
# - svc_app is also reachable via a web endpoint that can run commands.
#
# whoami /priv (as svc_app) shows:
# SeImpersonatePrivilege ......... Enabled
#
# Why this is dangerous: any code-exec as svc_app now satisfies the
# precondition for a potato-style escalation to SYSTEM. The app bug and
# the privilege TOGETHER = SYSTEM. Removing the privilege breaks the chain.
Detection (PowerShell, read-only) — flags any dangerous privilege on the current token:
# audit-token-privs.ps1 — read-only enumeration, safe to run
$dangerous = @(
'SeImpersonatePrivilege','SeAssignPrimaryTokenPrivilege',
'SeBackupPrivilege','SeRestorePrivilege',
'SeDebugPrivilege','SeTakeOwnershipPrivilege'
)
$found = whoami /priv |
Select-String -Pattern ($dangerous -join '|') |
ForEach-Object { ($_ -split '\s{2,}')[0].Trim() }
if ($found) {
Write-Warning ("Dangerous privileges present: " + ($found -join ', '))
exit 1 # non-zero so CI / a scheduled audit can alert
} else {
Write-Host 'OK: no high-risk token privileges on this account.'
exit 0
}
Remediation — the fix has three independent layers:
# a) LEAST PRIVILEGE: run the service under a Virtual Account or a
# Group Managed Service Account (gMSA) and grant it ONLY what it needs.
# If the service does not genuinely need to impersonate clients,
# remove the 'Impersonate a client after authentication' right from
# that account in User Rights Assignment (secpol.msc).
#
# b) PROTECT LSASS from SeDebug-style dumping — enable RunAsPPL so LSASS
# runs as a Protected Process Light:
# Registry: HKLM\SYSTEM\CurrentControlSet\Control\Lsa
# RunAsPPL = 1 (DWORD) then reboot
#
# c) ENABLE Credential Guard (VBS-backed) so secrets are isolated from
# the normal LSASS process even for an attacker who reaches SYSTEM.
# Deploy via Group Policy / MDM per your Windows edition.
# verify-hardening.ps1 — run AS the service account after remediation
$bad = whoami /priv | Select-String 'SeImpersonatePrivilege|SeDebugPrivilege|SeBackupPrivilege|SeRestorePrivilege'
if ($bad) {
Write-Error 'FAIL: dangerous privilege still assigned — remediation incomplete.'
} else {
Write-Host 'PASS: service account no longer holds high-risk privileges.'
}
# Confirm LSASS protection took effect (expects RunAsPPL = 1):
$ppl = (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa' -Name RunAsPPL -ErrorAction SilentlyContinue).RunAsPPL
if ($ppl -eq 1) { Write-Host 'PASS: LSASS RunAsPPL enabled.' }
else { Write-Warning 'CHECK: RunAsPPL not set to 1 (reboot required after setting).' }
Expected output. Before remediation, audit-token-privs.ps1 prints a warning listing e.g. SeImpersonatePrivilege and exits 1. After you remove the right and reboot, running verify-hardening.ps1 as the same service account prints PASS: service account no longer holds high-risk privileges. and PASS: LSASS RunAsPPL enabled. — the same evidence you attach to a retest in a report. The bad state is rejected (privilege gone), the good state is accepted (service still starts and functions with only the rights it needs).
Walking the defensive workflow (detect → harden → verify):
$dangerous = @(...) — a hard-coded allow-list-by-exclusion: the six privilege names that are effectively SYSTEM. Keeping this list explicit makes the audit auditable.whoami /priv | Select-String -Pattern (...) — prints the current token's privileges and keeps only lines matching a dangerous name. This reads token state; it changes nothing.($_ -split '\s{2,}')[0].Trim() — the whoami table is column-aligned with 2+ spaces between columns, so splitting on two-or-more spaces isolates the privilege name (column 1). We ignore the Enabled/Disabled column on purpose — see step 5.if ($found) { exit 1 } — a non-zero exit lets a scheduled task or CI job treat "dangerous privilege present" as a failing check and raise an alert.AdjustTokenPrivileges. Trusting the Disabled flag is the classic false-negative.SeDebug-based credential dumping). Requires a reboot to take effect.verify-hardening.ps1 re-runs the same enumeration as the service account. Trace of expected values: before fix $bad is non-empty → FAIL; after fix $bad is $null → PASS. The RunAsPPL read returns 1 once set and rebooted, else the script tells you to reboot. Same-account re-test is what makes the evidence trustworthy — auditing from an admin console can show a different token than the service actually receives.| Stage | Command | Before fix | After fix |
|---|---|---|---|
| Detect | audit-token-privs.ps1 |
warns, exit 1 | OK, exit 0 |
| Verify priv | verify-hardening.ps1 |
FAIL | PASS |
| Verify LSASS | read RunAsPPL |
absent / 0 | 1 |
| Wrong approach | Why it's wrong | Corrected approach | How to recognise / prevent |
|---|---|---|---|
| Patch the web-app RCE and consider the SYSTEM path closed | The privilege (SeImpersonate), not the app bug, is the escalation primitive; the next bug reaches SYSTEM again |
Remove the privilege from the service account and move it to a scoped Virtual/gMSA account | Re-run whoami /priv as the service account and confirm the privilege is gone, not just disabled |
Treat a Disabled privilege as harmless |
Code can enable it at runtime via AdjustTokenPrivileges; Disabled ≠ revoked |
Flag the privilege regardless of state; revoke the assignment | Audit User Rights Assignment (secedit /export), not just the live token's Enabled column |
| Report a UAC bypass as "privilege escalation across a trust boundary" | Microsoft does not treat UAC as a security boundary; it's escalation within an admin account | Scope it as escalation within an admin context; identify the real boundary (standard user → SYSTEM) | If both tokens belong to the same admin account, it is not a boundary crossing |
| Rely on file ACLs to protect the SAM hive / NTDS.dit | SeBackup reads any file regardless of ACL |
Restrict who holds SeBackup; enable Credential Guard; monitor hive access |
Check which accounts hold backup/restore rights; alert on Volume Shadow copies of hives |
Grant SeDebug to a monitoring or "admin helper" account for convenience |
SeDebug = open any process = dump LSASS = credential theft |
Grant only to genuine debuggers, temporarily; enable RunAsPPL | Alert on non-baseline processes opening a handle to lsass.exe |
| "The vulnerability scanner passed, so we're secure" | A clean scan proves the scanner found nothing it checks for — not that the token model is safe | Manually audit privileges and service accounts; verify each fix | Treat scanner output as one input, never as proof of security |
Common problems and how to work through them:
whoami /priv shows fewer privileges than expected. You are likely reading a filtered (non-elevated) token. Run from the context that actually matters — for a service, run the audit as the service account (e.g. via PsExec -i -u in the lab, or a scheduled task running as that identity), not from your interactive admin prompt.whoami /priv still lists it. Privilege changes apply at next logon/token creation. Restart the service (or reboot) so a fresh token is minted, then re-check.RunAsPPL set but LSASS still openable. RunAsPPL requires a reboot and Secure Boot for full effect. Confirm with the RunAsPPL registry read after rebooting; check the System event log for LSA protection start messages.whoami /priv returns empty. Locale changes the column text. Prefer matching on the stable privilege name (SeImpersonatePrivilege) rather than the description, and split on \s{2,} as shown.Questions to ask when a privesc finding won't reproduce:
Security & safety — detection, logging, and authorization for this topic.
Authorization checklist (before any lab work):
SVC_PASSWORD=<development-placeholder>).What to log (so token abuse is detectable):
lsass.exe with read/dump access masks. This is the strongest single signal for SeDebug-style credential dumping.What to NEVER log: account passwords, LSASS memory contents, password hashes, Kerberos tickets, session cookies/tokens, private keys, or unneeded PII. Logging these turns your SIEM into a second credential store for the attacker. Log the fact and metadata of an access, never the secret itself.
Which events signal abuse: a service account (IIS/MSSQL identity) suddenly triggering 4672 with SeImpersonate followed by a SYSTEM 4688 spawn; any non-AV/non-backup process opening lsass.exe (Sysmon 10); Volume Shadow copies or reads of the SAM/SYSTEM hive or NTDS.dit.
How false positives arise: legitimate backup software holds SeBackup and reads hives; AV/EDR opens LSASS by design; administrators legitimately trigger 4672. Baseline these known-good actors first so alerts fire on deviation, not on the capability existing.
Authorized real-world use case. During an internal penetration test, a tester gains limited code execution through a vulnerable intranet web app running as an IIS app-pool identity. whoami /priv shows SeImpersonatePrivilege: Enabled. The tester documents (in a lab-equivalent) that this satisfies the precondition for SYSTEM escalation, and — the deliverable that matters — recommends removing the privilege, moving the app pool to a scoped virtual account, enabling RunAsPPL and Credential Guard, then retests to confirm the account no longer holds the privilege. The client's blue team uses events 4672 and Sysmon 10 to build a detection for the same pattern.
Professional best-practice habits:
| Habit | Beginner | Advanced |
|---|---|---|
| Least privilege | Run services as Virtual/gMSA accounts, not LocalSystem; remove SeImpersonate/SeDebug unless truly required |
Continuously audit User Rights Assignment via GPO/DSC; alert on drift; enforce tiered admin model |
| Secure defaults | Enable RunAsPPL for LSASS on new builds | Deploy Credential Guard fleet-wide via GPO/MDM; verify VBS is active |
| Validation | Re-run whoami /priv as the service account after every change |
Automated post-deploy checks in CI that fail the build if a dangerous privilege is assigned |
| Logging | Turn on 4672 and Sysmon process-access logging | Correlate 4672 → 4688 → Sysmon 10 into a single escalation-chain detection with correlation ids |
| Error handling / accuracy | Never call a system "completely secure"; scope UAC bypass correctly | Track exploitability-based severity, retest, and record residual risk |
Accuracy reminders to carry into every report: UAC is not a security boundary; a passing scanner is not proof of security; and reading a token's contents (or decoding a JWT) tells you what it claims, not what the OS will enforce — only the kernel's privilege check (or a real signature verification for a JWT) enforces anything.
All tasks are lab-only — a Windows VM you own or a provided CTF box — and each ends with a defensive conclusion (remediate + verify). Do not run on production or third-party systems.
Beginner 1 — Enumerate your token.
whoami /priv and whoami /all in your lab VM; identify which listed privileges appear on the "dangerous" list from the lesson.SeChangeNotifyPrivilege is benign and near-universal; SeImpersonate/SeDebug are high-risk.Beginner 2 — Baseline event 4672.
Intermediate 1 — Audit script.
audit-token-privs.ps1 so it (a) exits non-zero when any dangerous privilege is present and (b) writes a timestamped line to a log file.time, account, privileges-found, result.whoami /priv on \s{2,}; treat Disabled as still-dangerous.Intermediate 2 — Remediate and retest a lab service.
whoami /priv (as that account) that SeImpersonatePrivilege is present, then remove the right, restart, and re-run the check.whoami /priv evidence showing the privilege gone.Challenge — Build a detection + hardening runbook.
Lab cleanup / reset: delete any throwaway service account and its user-right grant; revert test registry keys if your baseline requires it; restore the VM from a pre-lab snapshot; clear only your test artifacts, never production logs.
Main concepts. A Windows access token carries privileges, and a handful are effectively SYSTEM: SeImpersonate/SeAssignPrimaryToken (the "potato" precondition, common on IIS/SQL service accounts), SeBackup/SeRestore (read/write any file — SAM, SYSTEM, NTDS.dit), SeDebug (open any process — dump LSASS), and SeTakeOwnership (own then re-permission). Each is a generic read/write/impersonate primitive that crosses the non-admin → SYSTEM boundary. Reaching SYSTEM lets an attacker harvest credentials and move laterally.
Key commands. whoami /priv (enumerate + verify), whoami /all, secedit /export (audit assignments); RunAsPPL registry value and Credential Guard for LSASS protection; Windows event 4672 and Sysmon Event ID 10 for detection.
Common mistakes. Treating a Disabled privilege as safe (code can enable it); fixing the app bug but leaving the privilege; reporting a UAC bypass as a boundary crossing (UAC is not a security boundary); trusting file ACLs against SeBackup; equating a clean scanner run with "secure."
What to remember. Remove the privilege, don't just disable it; run services under least-privilege Virtual/gMSA accounts; enable RunAsPPL and Credential Guard; verify every fix by re-running whoami /priv as the service account; log privilege assignment and LSASS access (metadata only, never secrets); baseline known-good actors to tame false positives; and never claim a system is "completely secure." All hands-on work stays on systems you own or are explicitly authorised to test.