Query Details

HUNT 09 AD Admin SD Holder ACL Backdoor History 90d

Query

// =========================================================
// HUNT-09 | AD-AdminSDHolder-ACL-Backdoor-History-90d
// Description : Full audit of attribute changes (5136) to
//               the AdminSDHolder container and SDProp
//               propagation (sdPropagationControl), plus
//               permission grants targeting privileged group
//               objects. Identifies backdoor ACLs that
//               SDProp will propagate to protected objects.
// Period      : 90 days
// Use Case    : AdminSDHolder persistence detection,
//               ACL backdoor post-compromise forensics
// Tables      : SecurityEvent
// =========================================================

let Period = 90d;

// All attribute modifications on AdminSDHolder object
let AdminSDHolderChanges = SecurityEvent
    | where TimeGenerated > ago(Period)
    | where EventID == 5136
    | where ObjectName has "CN=AdminSDHolder,CN=System"
    | extend
        Actor       = strcat(SubjectDomainName, "\\", SubjectUserName),
        AttrChanged = tostring(column_ifexists("AttributeLDAPDisplayName", "")),
        OldValue    = OldValue,
        NewValue    = tostring(column_ifexists("AttributeValue", "")),
        OpType      = case(
            OperationType == "%%14675", "ValueAdded",
            OperationType == "%%14676", "ValueDeleted",
            "Unknown"
        );

// Protected objects where SDProp propagates (AdminCount=1)
let SDPropObjects = SecurityEvent
    | where TimeGenerated > ago(Period)
    | where EventID == 5136
    | where tostring(column_ifexists("AttributeLDAPDisplayName", "")) =~ "adminCount"
       and tostring(column_ifexists("AttributeValue", "")) == "1"
    | summarize SDPropAccounts = make_set(ObjectName, 50) by Computer;

// ACL grants on the domain root and privileged groups
// (nTSecurityDescriptor / nTSD changes)
let PrivGroupACLChanges = SecurityEvent
    | where TimeGenerated > ago(Period)
    | where EventID == 5136
    | where tostring(column_ifexists("AttributeLDAPDisplayName", "")) =~ "nTSecurityDescriptor"
    | where ObjectName has_any ("CN=Domain Admins,", "CN=Enterprise Admins,",
                               "CN=Schema Admins,", "CN=Administrators,",
                               "CN=Group Policy Creator Owners,",
                               "CN=Account Operators,", "CN=Backup Operators,",
                               "CN=Server Operators,", "CN=DnsAdmins,")
    | extend
        Actor       = strcat(SubjectDomainName, "\\", SubjectUserName),
        AffectedDN  = ObjectName;

// Combine and tag
AdminSDHolderChanges
| summarize
    Changes         = count(),
    LastChange      = max(TimeGenerated),
    FirstChange     = min(TimeGenerated),
    Actors          = make_set(Actor, 10),
    Changes_nTSD    = countif(AttrChanged =~ "nTSecurityDescriptor"),
    Changes_Other   = countif(AttrChanged !~ "nTSecurityDescriptor"),
    ChangedAttrs    = make_set(AttrChanged, 20),
    DCs             = make_set(Computer, 10)
    by ObjectName = "CN=AdminSDHolder"
| extend
    RiskLevel = case(
        Changes_nTSD >= 1, "Critical - SecurityDescriptor_Modified",
        Changes >= 3,      "High - Multiple_Changes",
        "Medium"
    ),
    RiskScore = (Changes_nTSD * 50) + (Changes * 5),
    Technique = "T1546.007 - AdminSDHolder"
| union (
    PrivGroupACLChanges
    | summarize
        Changes         = count(),
        LastChange      = max(TimeGenerated),
        FirstChange     = min(TimeGenerated),
        Actors          = make_set(Actor, 10),
        Changes_nTSD    = count(),
        Changes_Other   = 0,
        ChangedAttrs    = pack_array("nTSecurityDescriptor"),
        DCs             = make_set(Computer, 10)
        by AffectedDN
    | extend
        ObjectName    = AffectedDN,
        RiskLevel   = "High - PrivGroup_ACL_Modified",
        RiskScore   = Changes * 30,
        Technique   = "T1222.001 - File/Directory Permissions"
    | project-away AffectedDN
)
| project
    ObjectName,
    RiskLevel,
    RiskScore,
    Technique,
    Changes,
    Changes_nTSD,
    FirstChange,
    LastChange,
    Actors,
    ChangedAttrs
| order by RiskScore desc

Explanation

This query is designed to detect potential security risks related to unauthorized changes in Active Directory (AD) permissions, specifically focusing on the AdminSDHolder container and privileged groups. Here's a simplified breakdown:

  1. Purpose: The query audits changes to the AdminSDHolder container and tracks permission changes that could indicate a backdoor or unauthorized access. It helps in detecting persistence mechanisms and post-compromise forensics.

  2. Time Frame: It examines events from the past 90 days.

  3. Data Source: The query uses the SecurityEvent table to gather information about changes.

  4. Key Components:

    • AdminSDHolder Changes: It identifies all attribute modifications (EventID 5136) on the AdminSDHolder object, focusing on changes to security descriptors and other attributes.
    • SDProp Objects: It identifies objects where the SDProp process propagates permissions, specifically those with adminCount=1.
    • Privileged Group ACL Changes: It tracks changes to access control lists (ACLs) for critical AD groups like Domain Admins, Enterprise Admins, etc.
  5. Analysis:

    • The query summarizes changes, identifying the number of changes, actors involved, and the types of attributes changed.
    • It assigns a risk level and score based on the nature and number of changes, with specific attention to security descriptor modifications.
    • It combines results from AdminSDHolder changes and privileged group ACL changes to provide a comprehensive view of potential security risks.
  6. Output: The results are ordered by risk score, highlighting objects with the highest potential security risk due to unauthorized changes.

In essence, this query helps security teams identify and prioritize potential security threats related to unauthorized AD modifications, focusing on persistence and privilege escalation tactics.

Details

David Alonso profile picture

David Alonso

Released: March 24, 2026

Tables

SecurityEvent

Keywords

SecurityEvent

Operators

letagowherehasextendstrcattostringcolumn_ifexistscase=~summarizemake_setcountmaxmincountif!~pack_arrayunionproject-awayprojectorder by

Actions