Privilege Escalation · advanced · ~11 min

Windows privesc: token privileges and UAC

**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.

Overview

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.

Why it matters

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:

  • Reaching SYSTEM lets an attacker read LSASS memory or the SAM hive and harvest password hashes and Kerberos tickets. Those credentials fuel lateral movement to every other machine that trusts them — the difference between one compromised web server and a domain-wide incident.
  • Getting the scoping right matters professionally. Reporting a "UAC bypass" as a boundary-crossing privilege escalation is inaccurate and damages a report's credibility, because Microsoft explicitly does not treat UAC as a security boundary. A good finding names the real boundary that was crossed (standard user → SYSTEM) and the real primitive (a dangerous token privilege), not the cosmetic prompt.
  • The fixes here — least-privilege service accounts, Credential Guard, RunAsPPL — are cheap, high-impact, and frequently missing. Being able to recommend and verify them is directly billable value.

Core concepts

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.

Access tokens and privileges (the foundation)

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."

SeImpersonatePrivilege / SeAssignPrimaryTokenPrivilege — the "potato" primitive

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.

SeBackupPrivilege / SeRestorePrivilege — read-any / write-any file

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.

SeDebugPrivilege — open any process

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.

SeTakeOwnershipPrivilege — become the owner, then re-permission

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.

UAC is not a security boundary

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

  1. What asset is ultimately being protected here, and which single token privilege most directly threatens the credential store in LSASS?
  2. Where is the real trust boundary in the diagram, and why does a UAC bypass not cross it?
  3. A service account shows SeImpersonatePrivilege as Disabled in whoami /priv. What insecure assumption would lead you to call it safe, and why is it wrong?
  4. Which log source would let you notice a process opening a handle to LSASS, and why is that event only meaningful when correlated (some backup/AV tools open LSASS legitimately)?

Syntax notes

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.

Lesson

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.

Dangerous token privileges

Check your current privileges with whoami /priv. Several are effectively equivalent to SYSTEM:

  • SeImpersonatePrivilege / SeAssignPrimaryToken — the "potato" family of attacks. Trick a SYSTEM process into authenticating to you, impersonate its token, and become SYSTEM. Service accounts (IIS, SQL) often hold this privilege, which is why a web or database compromise so often reaches SYSTEM.
  • SeBackupPrivilege — read any file (for example, dump the SAM/SYSTEM hives or NTDS.dit).
  • SeRestorePrivilege — write any file (for example, overwrite a privileged binary).
  • SeDebugPrivilege — open any process and inject into it (for example, dump LSASS for credentials).
  • SeTakeOwnership — take ownership of objects, then re-permission them.

The pattern is consistent: a privilege that looks narrow is actually a generic primitive for reading, writing, or impersonating across the trust boundary.

UAC bypasses

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 fixes

  • Grant token privileges sparingly — especially SeImpersonate on service accounts. Prefer virtual or managed accounts with modern protections.
  • Enable Credential Guard.
  • Keep LSASS protected.
  • Do not rely on UAC as a boundary.

Code examples

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.

1. WARNING: intentionally vulnerable — use only in a local, isolated, authorized lab. Do not deploy.

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.

2. SECURE — detect the dangerous privileges and remove them

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.

3. VERIFY — prove the fix rejects the bad state and accepts the good state

# 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).

Line by line

Walking the defensive workflow (detect → harden → verify):

  1. $dangerous = @(...) — a hard-coded allow-list-by-exclusion: the six privilege names that are effectively SYSTEM. Keeping this list explicit makes the audit auditable.
  2. 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.
  3. ($_ -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.
  4. 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.
  5. Why we don't trust "Disabled." The audit flags a privilege even when its State is Disabled, because the token owner can enable it at runtime via AdjustTokenPrivileges. Trusting the Disabled flag is the classic false-negative.
  6. Remediation (a) — the strongest fix is removing the privilege assignment from the account (User Rights Assignment) and moving the service to a Virtual/gMSA account scoped to only what it needs. This breaks the escalation chain at the precondition, so even a future app bug cannot reach SYSTEM by this path.
  7. Remediation (b) RunAsPPL=1 — marks LSASS as a Protected Process Light, so an attacker who reaches SYSTEM still cannot trivially open a handle to read LSASS memory (blunting SeDebug-based credential dumping). Requires a reboot to take effect.
  8. Remediation (c) Credential Guard — uses virtualization-based security to hold secrets outside the normal LSASS address space, so even a SYSTEM-level read of LSASS yields far less.
  9. Verificationverify-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 $nullPASS. 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

Common mistakes

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

Debugging tips

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.
  • Removed the user right but 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.
  • A legitimate tool suddenly breaks after enabling RunAsPPL / Credential Guard. Some drivers or plugins that hook LSASS will fail — that is expected. Inventory those tools first in the lab; this is a false-positive on your functionality, not a security failure.
  • PowerShell parsing of 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:

  1. Am I in the same token/session the finding used (elevated vs filtered, service vs interactive)?
  2. Is the privilege assigned (policy) or merely present-but-disabled on this one token?
  3. Did the change take effect yet (new logon / reboot required)?
  4. Is a security control (RunAsPPL, Credential Guard, EDR) now blocking the step — i.e. is the fix already working?

Memory safety

Security & safety — detection, logging, and authorization for this topic.

Authorization checklist (before any lab work):

  • The host is one you own or have written authorization to test.
  • Work is confined to localhost / an isolated lab VM / a container / a deliberately-vulnerable target (CTF). No production, no third-party systems.
  • Scope and time window are agreed; you have a rollback plan.
  • No real credentials or secrets are used — placeholders only (e.g. SVC_PASSWORD=<development-placeholder>).

What to log (so token abuse is detectable):

  • Windows Security 4672 — "Special privileges assigned to new logon." Fires when a logon receives sensitive privileges (SeDebug, SeBackup, SeRestore, SeImpersonate, SeTakeOwnership). Baseline which accounts legitimately trigger it; alert on new ones.
  • Security 4673 / 4674 — a privileged service was called / an operation was attempted on a privileged object. High-volume; use for correlation, not standalone alerting.
  • Security 4688 — process creation with command line; watch for SYSTEM processes spawned by service accounts.
  • Sysmon Event ID 10 — process accessed another process; alert on non-baseline processes opening a handle to lsass.exe with read/dump access masks. This is the strongest single signal for SeDebug-style credential dumping.
  • Security 4720/4732/4728 — account created or added to a privileged group (post-escalation persistence).
  • For each event capture: timestamp, source host, subject account/SID, target resource (process/file/object), the security decision (granted/denied), the privilege involved, and a correlation id so one escalation chain can be reconstructed.

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.

Real-world uses

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.

Practice tasks

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.

  • Objective: read and interpret a token's privileges.
  • Requirements: run whoami /priv and whoami /all in your lab VM; identify which listed privileges appear on the "dangerous" list from the lesson.
  • Output: a short table of privilege name → Enabled/Disabled → "benign or high-risk."
  • Hints: SeChangeNotifyPrivilege is benign and near-universal; SeImpersonate/SeDebug are high-risk.
  • Concepts: tokens, privileges, Enabled vs Disabled.

Beginner 2 — Baseline event 4672.

  • Objective: connect a privilege to its log signal.
  • Requirements: with auditing on, log off and back on in the lab; find the Security event 4672 for your logon and note which sensitive privileges it lists.
  • Output: the 4672 event ID, subject account, and the privilege list.
  • Constraints: read-only; do not clear logs.
  • Hints: 4672 fires at logon when sensitive privileges are assigned.
  • Concepts: detection/logging, special-privileges audit.

Intermediate 1 — Audit script.

  • Objective: automate detection.
  • Requirements: adapt the lesson's 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.
  • Input/Output: input = current token; output = log line time, account, privileges-found, result.
  • Constraints: read-only enumeration; never log secrets.
  • Hints: split whoami /priv on \s{2,}; treat Disabled as still-dangerous.
  • Concepts: least privilege, audit-as-code.

Intermediate 2 — Remediate and retest a lab service.

  • Objective: break the escalation chain and prove it.
  • Requirements: in the lab, create a throwaway service account granted "Impersonate a client after authentication," confirm with whoami /priv (as that account) that SeImpersonatePrivilege is present, then remove the right, restart, and re-run the check.
  • Output: before/after whoami /priv evidence showing the privilege gone.
  • Constraints: lab VM only; use a placeholder password.
  • Hints: privilege changes need a new logon/restart to take effect.
  • Concepts: mitigation verification, same-account retest.
  • Defensive conclusion: state the finding, the fix applied, and the retest evidence.

Challenge — Build a detection + hardening runbook.

  • Objective: end-to-end defensive package for token-privilege escalation.
  • Requirements: (1) enable RunAsPPL and (where the edition supports it) Credential Guard in the lab and verify with the registry/VBS checks; (2) write a detection that correlates 4672 (SeImpersonate on a service account) → 4688 (SYSTEM spawn) → Sysmon 10 (handle to lsass.exe) using a correlation id; (3) document baseline known-good actors to suppress false positives; (4) write CLEANUP/RESET steps to restore the VM (remove the throwaway account, revert RunAsPPL if desired, snapshot rollback).
  • Constraints: lab only; no exploit code; log metadata only, never secrets.
  • Hints: baseline AV/backup before alerting; a Disabled privilege still counts.
  • Concepts: layered defense, correlation, verification, false-positive tuning.
  • Defensive conclusion: deliver the runbook as remediation + detection + retest, and state residual risk honestly (do not claim "completely secure").

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.

Summary

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.