Auditing Active Directory ACLs before an attacker does
How to find dangerous AD ACLs like GenericAll and DCSync rights, clean up stale adminCount flags, and use BloodHound defensively.
Group Policy hardening and patching get most of the attention, but a huge share of real-world Active Directory compromises never touch a CVE at all — they abuse permissions that were granted, inherited, or forgotten. An over-permissioned service account, a stale delegation from a decommissioned helpdesk tool, or a DCSync-capable service principal can hand an attacker domain dominance without a single exploit. This guide covers how to find and fix those paths before someone else does.
The ACLs that matter most
Active Directory objects carry discretionary access control lists (DACLs) just like files do, but the rights that matter for security are a small, well-known set:
| Right | What it allows | Why it's dangerous |
|---|---|---|
GenericAll | Full control of the object | Can reset passwords, modify group membership, edit any attribute |
GenericWrite | Write any non-protected attribute | Can set scriptPath for logon-script execution, alter group membership attributes |
WriteDACL | Modify the object's own ACL | Grantee can add themselves GenericAll at will, bypassing future review |
WriteOwner | Take ownership of the object | New owner implicitly gains the ability to grant themselves any right |
DS-Replication-Get-Changes + DS-Replication-Get-Changes-All | Request directory replication data | Together, these enable DCSync — pulling password hashes for any account, including krbtgt, without touching a DC's disk |
Any of the first four rights held on a group like Domain Admins, or on the AdminSDHolder object itself, is equivalent to full domain compromise. The replication rights are dangerous specifically because they are rarely audited — most admins only think to check group membership, not replication permissions on the domain object.
Auditing ACLs with PowerShell
Start with the domain root object itself, since that's where DCSync rights live:
$domainDN = (Get-ADDomain).DistinguishedName
$acl = Get-Acl -Path "AD:\$domainDN"
$acl.Access | Where-Object {
$_.ActiveDirectoryRights -match "ExtendedRight" -and
($_.ObjectType -eq "1131f6aa-9c07-11d1-f79f-00c04fc2dcd2" -or # DS-Replication-Get-Changes
$_.ObjectType -eq "1131f6ad-9c07-11d1-f79f-00c04fc2dcd2") # DS-Replication-Get-Changes-All
} | Select-Object IdentityReference, ActiveDirectoryRights, ObjectTypeThese two GUIDs are not the only path to DCSync: GenericAll, AllExtendedRights (an extended-right ACE with an empty ObjectType) and WriteDACL on the domain root grant or let the holder grant the same rights. The DCSync rights audit covers the full query and the expected default holders.
Then sweep for dangerous rights across high-value OUs (Domain Controllers, Tier 0 admin OU, privileged groups):
$targets = @("OU=Domain Controllers,DC=corp,DC=example,DC=com",
"CN=Domain Admins,CN=Users,DC=corp,DC=example,DC=com")
foreach ($dn in $targets) {
Write-Host "== $dn ==" -ForegroundColor Cyan
(Get-Acl -Path "AD:\$dn").Access |
Where-Object { $_.ActiveDirectoryRights -match "GenericAll|GenericWrite|WriteDacl|WriteOwner" } |
Select-Object IdentityReference, ActiveDirectoryRights, AccessControlType
}Cross-reference every IdentityReference returned against a known-good list of built-in principals (SYSTEM, Enterprise Admins, Domain Admins, Administrators). Anything else needs a documented business reason.
AdminSDHolder, SDProp, and adminCount
Active Directory protects a fixed set of privileged groups (Domain Admins, Enterprise Admins, Schema Admins, Administrators, Account Operators, Backup Operators, and a few others) using a mechanism called SDProp (SD Propagation). Every 60 minutes, the process:
- Reads the ACL on the special
AdminSDHolderobject. - Applies that ACL to every current member of a protected group, overwriting any ACL customizations made directly on those accounts.
- Sets
adminCount=1on each protected member, and does not automatically clear it when the account leaves the protected group.
This is why permission changes you make directly on a Domain Admin's user object keep reverting — SDProp stomps them on its next cycle. It's also why adminCount=1 becomes a durable, if imperfect, marker of "this account was privileged at some point."
To change effective permissions on protected accounts, edit the ACL on AdminSDHolder itself (carefully — it propagates to everyone), not on individual accounts.
$adminSDHolderDN = "CN=AdminSDHolder,CN=System," + (Get-ADDomain).DistinguishedName
(Get-Acl -Path "AD:\$adminSDHolderDN").Access |
Where-Object { $_.ActiveDirectoryRights -match "GenericAll|WriteDacl|WriteOwner" } |
Select-Object IdentityReference, ActiveDirectoryRightsFinding and cleaning stale adminCount=1 objects
Accounts removed from privileged groups keep adminCount=1 and, critically, keep inheritance disabled on their ACL — meaning they no longer receive permission changes made at the OU level, and their last-known privileged ACL sticks around indefinitely. This is a common "shadow admin" path: an account that looks ordinary in group membership but still carries residual privileged ACL entries.
# Find users flagged adminCount=1 that are NOT current members of protected groups
$protectedMembers = @()
foreach ($g in "Domain Admins","Enterprise Admins","Schema Admins","Administrators","Account Operators","Backup Operators") {
$protectedMembers += (Get-ADGroupMember -Identity $g -Recursive -ErrorAction SilentlyContinue).SamAccountName
}
Get-ADUser -Filter "adminCount -eq 1" -Properties adminCount, whenChanged |
Where-Object { $_.SamAccountName -notin $protectedMembers } |
Select-Object SamAccountName, DistinguishedName, whenChangedFor each stale hit: confirm the account genuinely no longer needs elevated rights, then re-enable ACL inheritance and let it inherit from its OU again:
$obj = Get-ADUser -Identity "svc_legacybackup"
$acl = Get-Acl -Path "AD:\$($obj.DistinguishedName)"
$acl.SetAccessRuleProtection($false, $true) # unprotect, inherit from parent
Set-Acl -Path "AD:\$($obj.DistinguishedName)" -AclObject $acl
Set-ADUser -Identity "svc_legacybackup" -Clear adminCountDo this in batches with change review — clearing adminCount on an account that's still legitimately privileged (just via nested group membership SDProp didn't catch cleanly) will strip protections it still needs.
Using BloodHound and PingCastle defensively
Manual ACL review scales poorly past a few thousand objects. Assessment tools built for attackers work just as well pointed inward:
- BloodHound ingests AD ACLs, group membership, session data, and GPO links, then graphs attack paths ("shortest path to Domain Admins"). Run it read-only, against a dedicated collection account with no write rights, and treat the output as an internal audit artifact, not something to leave lying around — the graph itself is a target map.
- PingCastle produces a scored health report covering stale objects, ACL misconfigurations, trust risks, and AdminSDHolder anomalies, with a trend score you can track release over release.
Run these on a recurring schedule (monthly is reasonable for most environments), store historical reports, and track the count of "critical" findings as a metric — not just a point-in-time cleanup exercise.
Verification
After remediating a finding, re-run the exact query that found it and confirm a clean result:
# Re-check DCSync-equivalent rights (replication GUIDs, AllExtendedRights, GenericAll, WriteDACL) after remediation
$domain = Get-ADDomain
$rootSid = (Get-ADDomain (Get-ADForest).RootDomain).DomainSID.Value
$expected = @(
'S-1-5-18', # SYSTEM
'S-1-5-9', # Enterprise Domain Controllers
'S-1-5-32-544', # Administrators (built-in)
"$($domain.DomainSID.Value)-516", # Domain Controllers
"$($domain.DomainSID.Value)-512", # Domain Admins (default WriteDACL)
"$rootSid-519", # Enterprise Admins (default GenericAll)
"$rootSid-498" # Enterprise Read-only Domain Controllers (Get-Changes only)
)
$repl = @([guid]'1131f6aa-9c07-11d1-f79f-00c04fc2dcd2', [guid]'1131f6ad-9c07-11d1-f79f-00c04fc2dcd2')
$acl = Get-Acl -Path "AD:\$($domain.DistinguishedName)"
$acl.GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier]) | Where-Object {
$r = $_.ActiveDirectoryRights
$_.AccessControlType -eq 'Allow' -and
-not $_.PropagationFlags.HasFlag([System.Security.AccessControl.PropagationFlags]::InheritOnly) -and
$_.IdentityReference.Value -notin $expected -and (
$r.HasFlag([System.DirectoryServices.ActiveDirectoryRights]::GenericAll) -or
$r.HasFlag([System.DirectoryServices.ActiveDirectoryRights]::WriteDacl) -or
($r.HasFlag([System.DirectoryServices.ActiveDirectoryRights]::ExtendedRight) -and
($_.ObjectType -in $repl -or $_.ObjectType -eq [guid]::Empty))
)
}
# Expect no output, apart from documented exceptions such as the Entra Connect AD DS connector accountWhat it breaks
- Legitimate delegated administration. Helpdesk groups scoped to reset passwords in a specific OU, application teams that manage their own service accounts, and backup software that needs replication-adjacent rights can all show up as "findings." Removing these without review breaks real workflows — validate against a change owner before revoking.
- Third-party AD-integrated tools (backup, identity governance, PAM solutions) sometimes require broad rights by design; confirm vendor documentation before stripping their service account's permissions.
- Automation that relies on adminCount clearing — if a script or provisioning tool expects
adminCountto reflect current group membership in real time, it will need to account for SDProp's one-hour cycle and non-automatic clearing.
Pair this audit with the identity boundaries described in Tier 0 & privileged access, and revisit it any time you decommission a delegated admin tool or helpdesk workflow — those are the most common sources of orphaned ACL grants.
Frequently asked questions
What is the fastest way to find who can DCSync in my domain?
Run Get-Acl against the domain naming context object and filter for principals holding both DS-Replication-Get-Changes and DS-Replication-Get-Changes-All, or GenericAll, AllExtendedRights or WriteDACL, which include or can grant them. By default, domain controllers hold both rights through the Domain Controllers and Enterprise Domain Controllers groups, the built-in Administrators group holds both (covering Domain Admins and Enterprise Admins), and Enterprise Read-only Domain Controllers hold Get-Changes only. Anyone else holding both rights, or one of the broader rights, can perform DCSync.
Why do permissions I remove from a Domain Admin's account keep coming back?
This is AdminSDHolder and the SDProp process. Every 60 minutes, SDProp resets the ACL on every member of a protected group (Domain Admins, Enterprise Admins, etc.) to match the AdminSDHolder object's ACL, overwriting any custom permissions you added directly on that user.
Is it safe to just strip GenericAll and WriteDACL from every non-admin account?
Not without review. Some of those grants are legitimate delegated administration, such as a helpdesk OU-admin group that needs GenericAll scoped to a specific OU. Audit each finding, map it to an intended business purpose, and remove only what has no justification.
Auditing Active Directory ACLs before an attacker does