🏢Active DirectoryPermissionsAttack PathsTrustsGroups

Active Directory Dangerous ACE Orphaned SID Cross Domain Permissions Audit: The ACL Abuse Most Tools Can't Even Resolve

Some Active Directory ACEs grant full control to a SID that resolves to no principal at all. Most ACL reviews skip them silently. Here is how to find and fix them.

Younes AZABARBy Younes AZABAR18 min read
Active Directory Dangerous ACE Orphaned SID Cross Domain Permissions Audit: The ACL Abuse Most Tools Can't Even Resolve

An Active Directory dangerous ACE orphaned SID cross domain permissions audit asks a question a standard ACL review never reaches: which grants in your directory are held by trustees nobody can name? Every access control entry (ACE) in an AD DACL identifies its trustee by security identifier (SID), and review tooling almost always resolves that SID to a friendly name before showing it to you. But what happens when the SID resolves to nothing at all — and the ACE it sits in grants full control?

That is a different question from the one a normal review asks. A normal review asks "which live accounts hold GenericAll on Tier 0?" This one asks "which grants are held by trustees my directory cannot name?" The two produce completely different findings, and the second one is routinely skipped: an entry whose trustee will not resolve is easy for a report to render as a blank row, a raw string, or nothing at all.

What an Active Directory Dangerous ACE Orphaned SID Cross Domain Permissions Audit Checks

A SID is not a name. Microsoft's Security Identifiers reference describes the structure precisely: for a domain account, "the SID of a security principal is created by concatenating the SID of the domain with a relative identifier (RID) for the account." Everything before the final -RID segment is the domain identifier; the last segment identifies the principal inside that domain.

That structure is what makes a broken reference classifiable. Take a trustee SID that resolves to no user, group, computer, organizational unit or well-known principal, and compare its domain prefix to your own domain SID:

CaseDomain prefixWhat it most likely wasCan it be exercised today?
Orphaned local SIDMatches this domainA deleted account or group from this domainNo — nobody can log on as it
Cross-domain SIDDoes not match this domainA principal from another domain, reachable over a trustPossibly — you cannot tell from here

The distinction matters because of one guarantee Microsoft makes explicitly in the same reference: "SIDs always remain unique. Security authorities never issue the same SID twice, and they never reuse SIDs for deleted accounts." An orphaned local SID therefore cannot be re-acquired by creating a new account with the same name — the new account gets a new RID. That is genuinely reassuring, and it is the reason this case is not a five-alarm fire.

The cross-domain case has no such guarantee, because the principal may simply exist somewhere you cannot see. A legitimate cross-domain grant normally materialises in your directory as a Foreign-Security-Principal object — "the Security Principal from an external source." If no matching foreignSecurityPrincipal exists, you are looking at either an intentional grant whose companion object was never created, or a leftover from a trust that has since been removed or reconfigured.

⚠️

⚠️ Warning: "Unresolvable" is a statement about your directory, not about the attacker's. A SID that means nothing to your domain controller can still mean something to a domain controller on the other side of a trust.

The Rights That Make an Unresolvable Trustee Dangerous

Not every ACE with a broken trustee matters. The ones that matter carry rights that let the holder take over the object. Microsoft's ADS_RIGHTS_ENUM reference gives the exact bits:

RightHex valueWhat it grants
ADS_RIGHT_GENERIC_ALL0x10000000Create/delete children, read and write all properties, extended rights
ADS_RIGHT_GENERIC_WRITE0x40000000Write all properties and perform all validated writes
ADS_RIGHT_WRITE_DAC0x00040000Modify the object's DACL — i.e. grant yourself anything else
ADS_RIGHT_WRITE_OWNER0x00080000Take ownership of the object
ADS_RIGHT_DS_SELF0x00000008Perform an operation controlled by a validated write
ADS_RIGHT_DS_WRITE_PROP0x00000020Write properties, optionally scoped by an ObjectType GUID

There is a trap here that quietly ruins home-grown audit scripts. The generic bits above are the raw ADSI values. The .NET ActiveDirectoryRights enumeration that PowerShell surfaces uses the mapped directory-service values instead: GenericAll is 983551 (0xF01FF), not 0x10000000. And 0xF01FF is a composite that includes ReadProperty (16) and ListChildren (4).

So a script that writes $ace.ActiveDirectoryRights -band 'GenericAll' and treats a non-zero result as full control will flag essentially every read ACE in the directory. The correct test is an equality test against the full mask, plus separate checks for the raw generic bits and the standard write rights:

$mask = [int]$ace.ActiveDirectoryRights
$isDangerous =
    (($mask -band 0x10000000) -ne 0) -or          <# ADS_RIGHT_GENERIC_ALL   #>
    (($mask -band 0x40000000) -ne 0) -or          <# ADS_RIGHT_GENERIC_WRITE #>
    (($mask -band 0x000F01FF) -eq 0x000F01FF) -or <# full control (mapped)   #>
    (($mask -band 0x00040000) -ne 0) -or          <# WRITE_DAC               #>
    (($mask -band 0x00080000) -ne 0)              <# WRITE_OWNER             #>
💡

💡 Tip: Also filter on ACE type. A deny ACE grants nothing, so an inherited deny entry naming a dead SID is noise, not a finding.

Microsoft Shipped One of These Itself

The most convincing evidence that broken SID references are a systemic condition rather than a curiosity comes from Microsoft's own documentation of what adprep /domainprep does to your directory.

The Active Directory domain-wide schema updates reference lists the operations performed when a Windows Server 2016 domain controller is prepared. Operation 85 is described as modifying the domain naming context "to permit domain\Key Admins and rootdomain\Enterprise Key Admins to modify the msds-KeyCredentialLink attribute." The permissions column then records what actually happened:

(OA;CI;RPWP;5b47d60f-6090-40b2-9f37-2a4de88f3063;;Key Admins)
(OA;CI;RPWP;5b47d60f-6090-40b2-9f37-2a4de88f3063;;Enterprise Key Admins in root
 domain, but in nonroot domains resulted in a bogus domain-relative ACE with a
 nonresolvable -527 SID)

Read that last clause again. Microsoft is documenting that in every non-root domain of a multi-domain forest, this operation left behind an ACE whose trustee is a nonresolvable SID. Enterprise Key Admins is a forest-level group that lives in the forest root; the operation built the SID relative to the local domain, producing a -527 reference to a principal that domain does not have.

Two further operations clean up after it — and they do not ship together, which decides whether your domain received one fix or both. Operation 87 sits in the same Windows Server 2016 table (operations 82-88, after which the revision attribute on CN=ActiveDirectoryUpdate,CN=DomainUpdates,CN=System is set to 15); it deletes "the ACE granting Full Control to the incorrect domain-relative Enterprise Key Admins group" and adds a correct one. Operation 89 is listed in a different table — Windows Server (Semi-Annual Channel) domain-wide updates, which take that same revision attribute to 16 — and it goes further, deleting the broad (A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;Enterprise Key Admins) entry and replacing it with (OA;CI;RPWP;5b47d60f-6090-40b2-9f37-2a4de88f3063;;Enterprise Key Admins) — full control across the naming context, narrowed to write access on one attribute. That revision value is how you tell which of the two cleanups your domain actually received.

Those SDDL strings decode via Microsoft's ACE Strings reference: A is an access-allowed ACE, OA an object-specific one, CI marks it container-inheritable, and RPWPCRLCLOCCDCRCWDWOSDDTSW concatenates read property, write property, control access, list children, list object, create child, delete child, read control, write DAC, write owner, delete, delete tree and self-write. That is full control by another name — including the two rights that let a holder rewrite the DACL and seize ownership.

🚨 Danger: If your forest was prepared with the original Windows Server 2016 media and the later cleanup operations never ran, a -527 ACE granting broad rights over an entire domain naming context may still be sitting in a child domain, pointing at nothing.

Why the Two Cases Carry Different Weight

An orphaned local SID and a cross-domain SID look identical in a DACL — an unresolvable trustee — but they deserve different priorities.

Orphaned local SID. Nobody can exercise this grant through a normal logon. Because AD does not recycle RIDs, reclaiming the exact SID would require injecting it into an account's sIDHistory, and that presupposes the privileged access an attacker would be using the grant to obtain. It is a cleanup item and an audit-trail problem, not an active path. Leave it and it will outlive the incident that created it, quietly widening the blast radius of any future migration.

Cross-domain SID. This one is live if the referenced domain ever becomes reachable. Microsoft's SID filtering guidance states the mechanism plainly: "under some circumstances it is possible for attackers or rogue administrators that have compromised a domain controller in a trusted domain to use the SID history attribute (sIDHistory) to associate SIDs with new user accounts, granting themselves unauthorized rights." Microsoft Defender for Identity's accounts posture assessment restates the forest-level version: "If you have a forest trust without SID Filtering enabled (also called Quarantine), it's possible to inject a SID from another forest and it will be added to the user token when authenticated and used for access evaluations."

Put those together and a stale cross-domain ACE becomes a pre-positioned grant. An attacker who controls — or later re-establishes — the referenced domain does not need to modify your DACL at all. The permission is already written. They only need a token carrying that SID. This is the same trust-boundary reasoning covered in Active Directory trust attacks and the trust SID filtering and selective authentication audit; the difference is that here the grant survives the trust's deletion, because deleting a trust does not clean ACLs.

Two More Rights in the Same Blind Spot

Broken SID references share a blind spot with two other permission patterns that standard reviews skip for a different reason: they are not GenericAll, so keyword-driven audits never see them.

Self-membership on groups. The Self-Membership validated write, GUID bf9679c0-0de6-11d0-a285-00aa003049e2, is documented as "Validated write permission to enable updating membership of a group in terms of adding/removing one's own account." Its display name in the ACL editor is Add/Remove self as member, and it applies to the Group class only. Granted on a privileged group, it is a one-step escalation that never involves a third party writing member — the principal adds itself. Microsoft's Dsacls reference confirms the scoping: the WS permission is "meaningful only on group objects and when {ObjectType | Property} is a 'member.'" This is the rights family that makes dangerous group nesting worse than it looks.

Enterprise Key Admins over msDS-KeyCredentialLink. Per Active Directory security groups, Enterprise Key Admins carries the well-known RID 527, is a Global group in CN=Users, has no default members, and is protected by AdminSDHolder. The attribute it governs, msDS-KeyCredentialLink (schemaIdGuid 5b47d60f-6090-40b2-9f37-2a4de88f3063), "contains key material and usage information" and was "first implemented on Windows Server 2016."

Write access to that attribute is the whole of the Shadow Credentials attack. In Elad Shamir's original Shadow Credentials research: "if you can write to the msDS-KeyCredentialLink property of a user, you can obtain a TGT for that user" and "you can retrieve the NT hash of that user." The prerequisites are a Windows Server 2016 domain controller, a Server Authentication certificate on it, and Windows Server 2016 functional level — and the planted credential "would persist even if the user/computer changed their password." We cover the technique itself in Shadow Credentials: abusing msDS-KeyCredentialLink.

The audit question is narrower than the attack. Enterprise Key Admins ships empty. What you are checking is whether the group's ACE over your domain naming context is the scoped RPWP on 5b47d60f-… that Operation 89 installs, or the unscoped full-control entry the original preparation left behind — and whether the trustee actually resolves.

Detection

Auditing for this needs two passes: a point-in-time sweep of existing DACLs, and change auditing so new broken references are caught as they appear.

Sweep existing DACLs for unresolvable dangerous trustees

Request the raw SIDs rather than account names. Asking .NET for NTAccount objects up front throws on exactly the entries you are hunting: SecurityIdentifier.Translate raises IdentityNotMappedException when "some or all identity references could not be translated."

Import-Module ActiveDirectory

$domainSid = (Get-ADDomain).DomainSID.Value
$root      = (Get-ADRootDSE).defaultNamingContext

Get-ADObject -SearchBase $root -LDAPFilter '(objectClass=*)' -Properties nTSecurityDescriptor |
  ForEach-Object {
    $dn = $_.DistinguishedName
    $_.nTSecurityDescriptor.GetAccessRules(
        $true, $true, [System.Security.Principal.SecurityIdentifier]) |
      Where-Object { $_.AccessControlType -eq 'Allow' } |
      ForEach-Object {
        $mask = [int]$_.ActiveDirectoryRights
        $dangerous =
            (($mask -band 0x10000000) -ne 0) -or
            (($mask -band 0x40000000) -ne 0) -or
            (($mask -band 0x000F01FF) -eq 0x000F01FF) -or
            (($mask -band 0x00040000) -ne 0) -or
            (($mask -band 0x00080000) -ne 0)
        if (-not $dangerous) { return }

        $sid = $_.IdentityReference
        try {
            $null = $sid.Translate([System.Security.Principal.NTAccount])
            return
        } catch [System.Security.Principal.IdentityNotMappedException] { }

        $scope = if ($sid.Value.StartsWith("$domainSid-")) {
                     'Orphaned local SID'
                 } else {
                     'Cross-domain SID'
                 }

        [pscustomobject]@{
            ObjectDN = $dn
            Trustee  = $sid.Value
            Rights   = $_.ActiveDirectoryRights
            Scope    = $scope
        }
      }
  } | Sort-Object Scope, ObjectDN | Format-Table -AutoSize

Run this against a single OU first. A full naming-context sweep reads every security descriptor in the domain and is not something to fire at a production DC during business hours.

Check the two specific patterns

Both queries below ask for SecurityIdentifier trustees for the same reason the sweep does: the .Access property translates to NTAccount and throws IdentityNotMappedException on precisely the ACEs this article is about.

$root    = (Get-ADRootDSE).defaultNamingContext
$sidType = [System.Security.Principal.SecurityIdentifier]

<# Key Admins (-526) and Enterprise Key Admins (-527) on the domain NC:
   scoped, or full control? Match on the RID, not the name — in a non-root
   domain the bogus trustee is precisely the one that will not resolve to
   "Key Admins", so a name filter silently misses the finding. #>
(Get-ADObject $root -Properties nTSecurityDescriptor).nTSecurityDescriptor.GetAccessRules(
    $true, $true, $sidType) |
  Where-Object { $_.IdentityReference.Value -match '-52[67]$' } |
  Select-Object IdentityReference, ActiveDirectoryRights, ObjectType

<# Self-membership grants on groups: the validated write, or an unscoped DS_SELF #>
$selfMembership = [guid]'bf9679c0-0de6-11d0-a285-00aa003049e2'
Get-ADGroup -Filter * -Properties nTSecurityDescriptor | ForEach-Object {
    $g = $_
    $g.nTSecurityDescriptor.GetAccessRules($true, $true, $sidType) |
      Where-Object {
        $_.AccessControlType -eq 'Allow' -and (
          $_.ObjectType -eq $selfMembership -or
          ($_.ObjectType -eq [guid]::Empty -and
           ([int]$_.ActiveDirectoryRights -band 0x8) -ne 0)
        )
      } |
      Select-Object @{n='Group';e={$g.DistinguishedName}},
                    IdentityReference, ActiveDirectoryRights
}

An ObjectType of 5b47d60f-6090-40b2-9f37-2a4de88f3063 on the first query is the scoped, expected form. An empty ObjectType with a full-control mask is the unscoped entry that should have been replaced — and a -527 trustee you cannot match to a live Enterprise Key Admins group is the nonresolvable case itself.

Catch new ones as they appear

IndicatorEvent IDSourceWhat it tells you
Security descriptor modified on an AD object5136DC Security lognTSecurityDescriptor appears as the LDAP Display Name; the Value field carries the new SDDL
msDS-KeyCredentialLink written5136DC Security logA key credential was added — the Shadow Credentials primitive
sIDHistory written5136DC Security logThe mechanism by which a foreign SID becomes usable
Object created / deleted5137 / 5141DC Security logCorrelate to explain why a trustee stopped resolving

Microsoft's event 5136 reference sets out the prerequisites and the reading rules. The event belongs to the Audit Directory Service Changes subcategory, and "to generate this event, the modified object must have an appropriate entry in SACL: the 'Write' action auditing for specific attributes" — enabling the subcategory alone is not enough, the SACL must exist on the objects you care about.

Expect pairs, not singles: "for a change operation, you'll typically see two 5136 events for one action, with different Operation Type fields: 'Value Deleted' and then 'Value Added'." Microsoft's own recommendation is to alert on the Value Added event and use the Correlation ID to pull the previous value from its partner.

One detail from the same page doubles as a diagnostic for this whole article: "Event Viewer automatically tries to resolve SIDs and show the account name. If the SID can't be resolved, you'll see the source data in the event." A raw SID where you expected a name is the tell — in an event, and in an ACL report.

Remediation

💡

💡 Tip: Start with the domain naming context object itself. One -527 full-control ACE there outranks a hundred orphaned entries on leaf objects.

1. Confirm the adprep cleanup ran. In each non-root domain, inspect the DACL on the domain NC for ACEs whose trustee ends in -527. If you find a full-control entry — especially one that does not resolve — you are looking at the pre-Operation-87/89 state. The revision attribute on CN=ActiveDirectoryUpdate,CN=DomainUpdates,CN=System tells you how far the preparation got: 15 means operations 82-88 completed, 16 means Operation 89 did too.

2. Classify before you delete. For each unresolvable trustee, compare the domain prefix against your domain SID and against the SIDs of every domain you trust. A cross-domain SID matching a current trust partner is probably an intentional grant missing its foreignSecurityPrincipal object, and deleting it will break something. One that matches no current trust is a stale reference.

3. Remove by SID, not by name. This is where tooling fights you. Microsoft's Dsacls reference documents /R as taking a trustee specified "as User@Domain or as Domain\User" — a name form that by definition does not exist for a SID that will not resolve. Use PowerShell and hand the API a SecurityIdentifier directly:

$dn  = 'DC=child,DC=corp,DC=local'
$sid = [System.Security.Principal.SecurityIdentifier]'S-1-5-21-1004336348-1177238915-682003330-527'

$acl = Get-Acl -Path "AD:\$dn"
$acl.GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier]) |
  Where-Object { $_.IdentityReference -eq $sid } |
  ForEach-Object { $null = $acl.RemoveAccessRuleSpecific($_) }

Set-Acl -Path "AD:\$dn" -AclObject $acl

Note the $false for inherited rules: an inherited ACE cannot be removed at the child, only at the object that defines it. Fix the parent and let inheritance propagate.

4. Restore the intended scope, do not just delete. Where the entry was Enterprise Key Admins over the domain NC, the target state is documented: an object ACE granting RPWP scoped to 5b47d60f-6090-40b2-9f37-2a4de88f3063, container-inheritable. Deleting without replacing removes a permission Windows Hello for Business provisioning expects.

5. Keep SID filtering on — and do not confuse it with quarantining. A cross-domain ACE is only exploitable if a foreign SID can reach your access token, and SID filtering is what stops that. Across a forest trust the filtering is structural, not optional: MS-PAC specifies that a cross-forest trust "does not allow SIDs that are local to its forest to come over a cross-forest trust", and that ForestSpecific SIDs are "filtered out for QuarantinedWithinForest, CrossForest, External, and QuarantinedExternal trust boundaries." Defender for Identity, quoted above, treats the absence of that filtering as the finding. SID filter quarantining is a separate, stricter mode, and Microsoft's warnings attach to it alone: do not apply quarantining "to trusts within a forest that is not using the Windows Server 2003 forest functional level, because doing so removes SIDs that are required for Active Directory replication", and it "should not be applied to forest trusts." Read that as an instruction about the quarantine flag — never as a licence to turn SID filtering off.

6. Put a SACL on the objects you care about. Event 5136 needs it. At minimum: the domain NC object, CN=AdminSDHolder,CN=System, CN=Keys, and every Tier 0 group and OU. Auditing nTSecurityDescriptor and msDS-KeyCredentialLink writes turns the next occurrence into an alert instead of a finding a year later. The AdminSDHolder SDProp backdoor article covers why that container in particular earns a permanent watch.

7. Fold it into the recurring review. Orphaned trustees accumulate from ordinary operations — decommissioned service accounts, closed acquisitions, retired trusts. Treat the sweep as part of the standing Active Directory security audit, not as a one-off remediation project.

How EtcSec Detects This

EtcSec splits the broken-reference problem across two checks, precisely because the two cases carry different risk. ORPHANED_SID_DANGEROUS_ACE reports grant ACEs conveying GenericAll, full control, WriteDACL, WriteOwner or GenericWrite to a trustee whose domain prefix matches the audited domain but which resolves to no live principal — the likely-deleted-account case, rated High. CROSS_DOMAIN_SID_DANGEROUS_ACE reports the same rights held by a trustee from another domain entirely, rated Critical, because that reference becomes exercisable the moment the referencing domain is reachable again. Both deliberately exclude built-in administrative trustees and deny ACEs, so the output is the exposure and not the directory's normal shape.

Alongside them, ACL_SELF_MEMBERSHIP flags the Add/Remove-self-as-member validated write on group objects — scoped to groups only, and deliberately excluding full control so it does not simply re-report what ACL_GENERICALL already found. ENTERPRISE_KEY_ADMINS_FULL_ACCESS flags the -527 trustee holding either unscoped full control or write access to msDS-KeyCredentialLink on domain objects, the condition Operations 87 and 89 exist to correct. ACL_WRITEDACL and ACL_WRITEOWNER cover the same two rights when the trustee does resolve, and TRUST_SID_FILTERING_DISABLED covers the trust-side control that decides whether a cross-domain reference can ever reach a token.

Together with the broader ACL abuse and DCSync checks and the DPAPI, tombstone and schema permissions audit, these close the gap between "who has dangerous rights" and "which dangerous rights are held by principals nobody can name."

ℹ️

ℹ️ Note: EtcSec automatically checks for this vulnerability during every AD/Azure audit. Run a free audit to verify your environment.

Explore the identity security pages that support this topic