Skip to content
08 · Object & ACL SecurityPart 3 of 4Intermediate

AdminSDHolder and SDProp: cleanup and monitoring

Baseline the AdminSDHolder ACL, find orphaned adminCount=1 accounts, reset their ACLs safely, trigger SDProp on demand, and alert on AdminSDHolder changes.

Florian Amette7 min read

AdminSDHolder is the template ACL for every privileged account in a domain. Once an hour, the SDProp process on the PDC emulator copies it onto every member of the protected groups. That makes it one of the most effective persistence spots in Active Directory. An attacker who adds a single ACE to CN=AdminSDHolder,CN=System gets that right re-applied to every Domain Admin forever, even after defenders clean the individual accounts. The same mechanism also leaves a trail of orphaned adminCount=1 accounts with inheritance disabled and outdated privileged ACLs.

The ACL pillar guide explains what SDProp does and gives a first query for stale adminCount. This guide is the operational follow-up. It covers the exact protected set, a baseline of the AdminSDHolder ACL, a safe cleanup procedure, running SDProp on demand, and alerts that catch tampering.

What SDProp protects

On Windows Server 2016 and later domains, SDProp protects these objects and the transitive members of these groups:

  • Groups: Account Operators, Administrators, Backup Operators, Domain Admins, Domain Controllers, Enterprise Admins, Enterprise Key Admins, Key Admins, Print Operators, Read-only Domain Controllers, Replicator, Schema Admins, Server Operators.
  • Users: Administrator (RID 500) and krbtgt.

Each SDProp run:

  1. Reads the security descriptor of CN=AdminSDHolder,CN=System,<domain>.
  2. For each protected object and each transitive member, compares the ACL and overwrites it if it differs. Inheritance is disabled on the object.
  3. Sets adminCount=1.

It never reverses step 2 or 3 when an account leaves a protected group. The PDC emulator runs it every 60 minutes by default. The interval is set by AdminSDProtectFrequency (seconds) under HKLM\SYSTEM\CurrentControlSet\Services\NTDS\Parameters.

Nesting matters. A security group nested in Domain Admins is protected, and so are its members. A user who is a member only through a primary group assignment is a well-known edge case. Always check effective membership, not only the member attribute.

Measure: baseline the AdminSDHolder ACL

Export the current ACL and keep it as a signed, dated artifact:

PowerShell
$dn  = "CN=AdminSDHolder,CN=System," + (Get-ADDomain).DistinguishedName
$acl = Get-Acl -Path "AD:\$dn"
$acl.Sddl | Out-File "C:\Tier0\Baseline\AdminSDHolder-$(Get-Date -f yyyyMMdd).sddl"

$acl.Access | Select-Object IdentityReference, ActiveDirectoryRights, ObjectType,
    InheritedObjectType, AccessControlType, IsInherited |
    Sort-Object IdentityReference | Format-Table -AutoSize
$acl.Owner

Compare it with a freshly built lab domain at the same functional level and with the same optional products (Exchange, for example, adds its own ACEs). Expect ACEs for SYSTEM, Administrators, Domain Admins and Enterprise Admins, read access for Authenticated Users and Pre-Windows 2000 Compatible Access, Change Password for Everyone and SELF, and narrow property rights for Cert Publishers, Windows Authorization Access Group and Terminal Server License Servers. The owner should be Domain Admins.

Check two structural properties as well. The AdminSDHolder object normally has inheritance disabled, so ACEs set on the System container or the domain root do not flow into it. If inheritance has been turned on, every broad delegation at the domain root becomes part of the template for your admins. Also check the owner. An owner can always rewrite the DACL, so a non-default owner is as serious as a WriteDacl ACE. Record both in the baseline next to the SDDL so the weekly diff covers them.

Anything outside that set, especially GenericAll, GenericWrite, WriteDacl, WriteOwner, WriteProperty on member, or ResetPassword (00299570-246d-11d0-a768-00aa006e0529), needs an explanation. A user account or an unfamiliar group with write rights on AdminSDHolder should be treated as compromise until proven otherwise.

Audit: find the orphans

Build the list of objects that SDProp currently protects, then compare it with everything carrying adminCount=1. The pillar guide checks users only. Here groups and computers are included, and the protected groups are resolved by well-known SID so the query works in any language:

PowerShell
$domSid = (Get-ADDomain).DomainSID.Value
$protectedGroupSids = @(
    'S-1-5-32-544','S-1-5-32-548','S-1-5-32-549','S-1-5-32-550','S-1-5-32-551','S-1-5-32-552',
    "$domSid-512","$domSid-516","$domSid-521","$domSid-526"
)
$forestRootSid = (Get-ADDomain (Get-ADForest).RootDomain).DomainSID.Value
$protectedGroupSids += "$forestRootSid-518","$forestRootSid-519","$forestRootSid-527"

$protected = [System.Collections.Generic.HashSet[string]]::new()
foreach ($sid in $protectedGroupSids) {
    $g = Get-ADGroup -Filter "objectSid -eq '$sid'" -ErrorAction SilentlyContinue
    if (-not $g) { continue }
    [void]$protected.Add($g.DistinguishedName)
    Get-ADGroupMember $g -Recursive | ForEach-Object { [void]$protected.Add($_.distinguishedName) }
    # nested groups are protected too
    Get-ADGroup -LDAPFilter "(memberOf:1.2.840.113556.1.4.1941:=$($g.DistinguishedName))" |
        ForEach-Object { [void]$protected.Add($_.DistinguishedName) }
}

Get-ADObject -LDAPFilter '(adminCount=1)' -Properties adminCount, objectClass, whenChanged, nTSecurityDescriptor |
    Where-Object { -not $protected.Contains($_.DistinguishedName) -and $_.Name -notin 'Administrator','krbtgt' } |
    Select-Object Name, objectClass, whenChanged,
        @{ n = 'InheritanceBlocked'; e = { $_.nTSecurityDescriptor.AreAccessRulesProtected } },
        DistinguishedName

Enterprise Admins, Schema Admins and Enterprise Key Admins live in the forest root domain, which is why their SIDs use the root domain SID. In a child domain, members of Enterprise Admins are still protected because Enterprise Admins is a member of the child's Administrators group.

Typical orphans are former admins who changed roles, service accounts once "temporarily" added to Domain Admins, groups that were nested and later removed, and, in older domains, accounts that used to be in Account Operators or Print Operators.

Why adminCount is not a reliable admin inventory

It is tempting to use adminCount=1 as the list of privileged accounts. It is wrong in both directions. It includes orphans, as shown above. It also misses real privilege. An account with GenericAll on Domain Admins, a user with DCSync rights, a member of a group that controls a GPO linked to the Domain Controllers OU, and the Entra Connect connector account can all have full domain control without ever being in a protected group. SDProp never touches them, so they have adminCount unset, inheritance on, and whatever delegations their OU grants.

Use adminCount for what it is: a hint of past protected-group membership and a cleanup queue. Your real Tier 0 inventory comes from control relationships, which is what attack path analysis measures.

Enforce: clean up orphans properly

For each orphan, confirm with its owner that it no longer needs privilege. Then do three things together, because doing only one leaves the problem half fixed:

  1. Reset the explicit ACL to the schema default for its object class. This removes the copied AdminSDHolder ACEs.
  2. Re-enable inheritance, so OU-level delegations and your tiering ACLs apply again.
  3. Clear adminCount.
PowerShell
$dn = (Get-ADUser 'j.smith-old-admin').DistinguishedName

# 1. Back up, then reset to the schema default security descriptor
(Get-Acl "AD:\$dn").Sddl | Out-File "C:\Tier0\Backup\$(Get-Date -f yyyyMMdd)-j.smith.sddl"
dsacls $dn /S

# 2. Make sure inheritance is on (dsacls /S normally leaves it on; enforce anyway)
$acl = Get-Acl "AD:\$dn"
$acl.SetAccessRuleProtection($false, $false)
Set-Acl "AD:\$dn" -AclObject $acl

# 3. Clear the marker
Set-ADObject $dn -Clear adminCount

Also rotate the account's password. It was privileged, and its credentials may have been cached on systems outside Tier 0. Consider whether the account should exist at all.

Two more hardening steps reduce future orphans:

  • Empty the operator groups. Account Operators, Server Operators, Print Operators and Backup Operators give broad rights on DCs and are protected. Replace them with scoped delegation on OUs. This is better than excluding them from SDProp via the dSHeuristics mask.
  • Stop using protected groups for service accounts. Grant the specific rights needed instead, as described in defining Tier 0.

Run SDProp on demand

After changing AdminSDHolder or cleaning up, trigger a run on the PDC emulator instead of waiting up to an hour. On Windows Server 2008 R2 and later, write the runProtectAdminGroupsTask operational attribute on the rootDSE:

PowerShell
$pdc = (Get-ADDomain).PDCEmulator
$root = [ADSI]"LDAP://$pdc/RootDSE"
$root.Put('runProtectAdminGroupsTask', 1)
$root.SetInfo()

This needs Domain Admin rights and runs asynchronously.

Verify and monitor

Rerun the orphan query. It should return only accounts you have explicitly accepted. Then set up continuous monitoring:

  • SACL on AdminSDHolder. Add an audit entry for Everyone, Success, for Write all properties, Modify permissions and Modify owner on the AdminSDHolder object. With Audit Directory Service Changes enabled on DCs, every change produces event 5136 with the attribute nTSecurityDescriptor. Alert on it at high severity. Legitimate changes should be rare and ticketed.
  • Scheduled diff. A weekly job compares the live SDDL with the baseline file and opens a ticket on any difference. This also catches changes made while auditing was off.
  • adminCount drift. Alert on event 5136 where adminCount is set on an object outside your approved Tier 0 list. It tells you someone was added to a protected group, even briefly, which pairs with group-change events 4728, 4732 and 4756.

Microsoft Defender for Identity and PingCastle both flag AdminSDHolder anomalies too. Use them as cross-checks, not as the only control.

What it breaks

  • Helpdesk password resets on former admins. While an account is protected, SDProp blocks inherited OU delegations, so the helpdesk cannot reset its password. That is intended. Cleanup reverses it: once inheritance is re-enabled, OU delegations apply again and the helpdesk can reset the former admin's password. Check that this is what you want before resetting a sensitive account's ACL.
  • Custom ACEs placed directly on admin accounts. SDProp already overwrote them, so tools that expected them were already broken. The reset only makes this visible.
  • Applications that granted themselves rights through AdminSDHolder, for example some older identity management or PAM products. Removing unexplained ACEs from AdminSDHolder removes those rights from every admin at the next SDProp run. Confirm with the vendor first.
  • dSHeuristics changes. If someone previously set the exclusion mask, removing it re-protects the operator groups and their members, and delegations relying on the exclusion stop working.

Related reading: the ACL pillar guide, finding DCSync rights for the domain-root ACL, attack path management to see what orphaned rights still lead to, and the Tier 0 pillar for how protected accounts should be used.

Frequently asked questions

Can I shorten the SDProp interval?

Yes, with the AdminSDProtectFrequency DWORD (in seconds, 60 to 7200) under HKLM\SYSTEM\CurrentControlSet\Services\NTDS\Parameters on the PDC emulator. Microsoft advises against changing it in production because each run can generate significant replication and LSASS load in large domains. For testing or after a cleanup, trigger a single run through the runProtectAdminGroupsTask rootDSE operation instead.

Why does clearing adminCount not restore normal permissions?

Because adminCount is only a marker. When SDProp protected the account it also disabled ACL inheritance and copied the AdminSDHolder ACL onto it. Clearing adminCount changes neither. You must also re-enable inheritance and, ideally, reset the explicit ACEs to the schema default so the account picks up OU delegations again and loses the leftover privileged entries.

Should I exclude Account Operators or Backup Operators from SDProp with dSHeuristics?

Almost never. The dSHeuristics exclusion mask exists so those operator groups can be delegated in unusual scenarios, but it removes protection from groups that can already reach domain control. The better fix is to empty Account Operators, Server Operators, Print Operators and Backup Operators, replace them with scoped delegations, and leave SDProp protecting them.

AdminSDHolder and SDProp: cleanup and monitoring

Related guides

Group Policy & SYSVOL

Auditing GPO permissions and gPLink rights

Find who can edit, create and link GPOs in Active Directory: GPO ACLs, gPLink rights on OUs and sites, Group Policy Creator Owners and WMI filters.

Intermediate