Active Directory AdminSDHolder ACL drift allowing shadow persistence via SDProp inheritance bypass
01 Incident diagnostics
- The Symptom
- Security review flags a foreign ACE — often a decommissioned service account or a broad principal like Authenticated Users — holding GenericAll or WriteDacl on Domain Admins members. An engineer strips the ACE from the affected user object with dsacls, confirms it is gone, then finds it back in place within the hour, still granting full control, with adminCount remaining stuck at 1 on accounts long removed from protected groups.
- Detection
- Run dsacls "CN=AdminSDHolder,CN=System,DC=corp,DC=com" and diff the output against a known-good export. Cross-check propagation with:
Get-ADUser-LDAPFilter "(admincount=1)" -Properties admincount,nTSecurityDescriptor | Select Name,@{n='Owner';e={$_.nTSecurityDescriptor.Owner}}. A rising modification counter on repadmin /showobjmeta "CN=AdminSDHolder,CN=System,DC=corp,DC=com" without a corresponding change ticket confirms unauthorised drift on the template object rather than the downstream accounts. - Root Cause
- AdminSDHolder is the master ACL template that the SDProp process (running on the PDC emulator, default interval 60 minutes via AdminSDProtectFrequency) copies onto the nTSecurityDescriptor of every member of a protected group, simultaneously setting adminCount=1 and disabling ACL inheritance (SE_DACL_PROTECTED) on the target object. When an attacker or a stale delegation adds an ACE to AdminSDHolder itself rather than the downstream object, every remediation performed on the protected account is cosmetic — the next SDProp pass overwrites the account's DACL from the drifted template, silently re-establishing the malicious ACE and defeating any review that inspects only the group member, never the AdminSDHolder source object.
- Prevention
- Apply a SACL to CN=AdminSDHolder,CN=System that audits write-DACL and write-property operations, feeding Event ID 4670/5136 into your SIEM with an alert on any principal outside a two-person change-control list. Lock delegation on the object using dsacls to permit modification only from a break-glass administrative group, then schedule a nightly comparison job (ldifde export piped to Compare-Object against adminsdholder_baseline.ldf) that pages on-call the moment the DACL diverges, rather than relying on periodic manual review of downstream group members.
- Blast Radius
- Every current member of Domain Admins, Enterprise Admins, Schema Admins, BUILTINAdministrators and Account Operators inherits the drifted ACE on the next cycle
- Accounts historically removed from protected groups but still flagged admincount=1 retain the disabled-inheritance state indefinitely, hiding orphaned exposure from standard group audits
- Nested service accounts used by backup agents, AD-integrated monitoring, or Azure AD Connect sync inherit elevated rights if they were ever added to a protected group
- Cross-domain trusts relying on this domain's privileged accounts inherit the exposure transitively through Kerberos delegation and SID history
- Any downstream tooling that trusts adminCount=1 as a signal of "currently protected" produces false negatives for stale, unprotected-but-flagged accounts
Apply the intervention
Before you execute
Confirm the incident scope, obtain approved privileged access, and capture the current configuration before applying changes.
Potentially destructive change. Confirm scope, backup and rollback access before copying these commands.
Fix commands and configuration
# 1. Enumerate the live ACL on the master object, not the downstream accounts dsacls "CN=AdminSDHolder,CN=System,DC=corp,DC=com" # 2. Remove the rogue ACE from AdminSDHolder itself dsacls "CN=AdminSDHolder,CN=System,DC=corp,DC=com" /R "CORPsvc-legacybackup" # 3. Snapshot the corrected baseline for future diffing ldifde -f adminsdholder_baseline.ldf -d "CN=AdminSDHolder,CN=System,DC=corp,DC=com" -p base # 4. Force an immediate SDProp run instead of waiting up to 60 minutes $rootDSE = [ADSI]"LDAP://RootDSE" $rootDSE.Put("RunProtectAdminGroupsTask", 1) $rootDSE.SetInfo() # 5. Verify the corrected ACL has replicated onto protected accounts Get-ADUser -LDAPFilter "(admincount=1)" -Properties admincount,nTSecurityDescriptor | Select-Object Name,@{n='ACECount';e={$_.nTSecurityDescriptor.Access.Count}} # 6. Release orphaned accounts stuck with admincount=1 and re-enable inheritance $obj = [ADSI]"LDAP://CN=Old Svc Account,OU=Legacy,DC=corp,DC=com" $obj.psbase.ObjectSecurity.SetAccessRuleProtection($false,$true) $obj.CommitChanges() Set-ADObject -Identity "CN=Old Svc Account,OU=Legacy,DC=corp,DC=com" -Clear admincountVerify, roll back or escalate
Verify recovery
Repeat the detection checks after the intervention and confirm that the original symptom no longer occurs.
Rollback
Stop if impact increases. Restore the captured pre-change configuration or approved backup, then reassess before retrying.
Escalate when
Escalate to the platform owner or security incident lead when recovery is unavailable, scope is uncertain, or privileged access cannot be verified.