Detection rules › Kusto
Hunt for privilege escalation paths with high ACLs
When an adversary establishes data collection of an Active Directory domain, they regularly search for interesting accounts with privilege escalation paths using the genericWrite and genericAll ACL permissions on objects. When using BloodHound, it is very easy to get a visual overview of these paths in an Active Directory domain. This query tries to establish the same using Defender XDR Exposure Management.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access | T1078 Valid Accounts |
| Persistence | T1078 Valid Accounts |
| Privilege Escalation | T1078 Valid Accounts |
| Stealth | T1078 Valid Accounts |
References
Rule body yaml
let high_permissions = dynamic(["genericWrite", "genericAll"]);
let edge_labels = dynamic(["member of", "has permissions to", "can authenticate to", "can authenticate as", "has credentials of", "can impersonate as"]);
// Get users and groups with high ACL permissions on other objects
let HighPermissionLinks = (ExposureGraphEdges
// Get edges related to roles
| where EdgeLabel == "has role on"
// Get edges containing high permission ACLs
| extend Permissions = todynamic(EdgeProperties).rawData.acl.controlTypes
| where Permissions has_any (high_permissions)
// Exclude Domain and Enterprise Administrators as source node
| where not(SourceNodeLabel == "group" and SourceNodeName in ("Domain Admins", "Enterprise Admins"))
// Exclude Built-in administrator account
| where not(SourceNodeLabel == "user" and SourceNodeName == "Administrator")
| summarize TargetNodes = make_set(TargetNodeName), TargetNodeCount = count() by SourceNodeName, SourceNodeLabel, tostring(Permissions), TargetNodeLabel, SourceNodeId
);
let HighPermissionNodes = toscalar(
HighPermissionLinks
| summarize SourceNodes = make_set(SourceNodeName)
);
// Get edges for links to the high ACL permissions
ExposureGraphEdges
| where TargetNodeName in (HighPermissionNodes)
| make-graph SourceNodeId --> TargetNodeId with ExposureGraphNodes on NodeId
// Get between one and three relations
| graph-match (SourceNode)-[anyEdge*1..3]->(TargetNode)
project IncomingNodeName = SourceNode.NodeName,
IncomingNodeLabel = SourceNode.NodeLabel,
Edges = anyEdge.EdgeLabel,
OutgoingNodeName = TargetNode.NodeName,
OutgoingNodeId = TargetNode.NodeId
// Filter for interesting edges
| where Edges has_any (edge_labels)
// Join the high permission ACLs
| join kind=inner HighPermissionLinks on $left.OutgoingNodeId == $right.SourceNodeId
// Exclude Domain and Enterprise Administrators as source node
| where not(IncomingNodeLabel == "group" and IncomingNodeName in ("Domain Admins", "Enterprise Admins"))
// Exclude Built-in administrator account
| where not(IncomingNodeLabel == "user" and IncomingNodeName == "Administrator")
| distinct IncomingNodeName, IncomingNodeLabel, tostring(Edges), OutgoingNodeName, OutgoingNodeLabel = SourceNodeLabel, tostring(Permissions), TargetNodeLabel, tostring(TargetNodes), TargetNodeCount
Stages and Predicates
Parameters
let high_permissions = dynamic(["genericWrite", "genericAll"]);
let edge_labels = dynamic(["member of", "has permissions to", "can authenticate to", "can authenticate as", "has credentials of", "can impersonate as"]);
Let binding: HighPermissionLinks
let HighPermissionLinks = (ExposureGraphEdges
| where EdgeLabel == "has role on"
| extend Permissions = todynamic(EdgeProperties).rawData.acl.controlTypes
| where Permissions has_any (high_permissions)
| where not(SourceNodeLabel == "group" and SourceNodeName in ("Domain Admins", "Enterprise Admins"))
| where not(SourceNodeLabel == "user" and SourceNodeName == "Administrator")
| summarize TargetNodes = make_set(TargetNodeName), TargetNodeCount = count() by SourceNodeName, SourceNodeLabel, tostring(Permissions), TargetNodeLabel, SourceNodeId
);
Derived from high_permissions.
Let binding: HighPermissionNodes
let HighPermissionNodes = toscalar(
HighPermissionLinks
| summarize SourceNodes = make_set(SourceNodeName)
);
Derived from HighPermissionLinks.
Stage 1: source
ExposureGraphEdges
Stage 2: where
| where TargetNodeName in (HighPermissionNodes)
References HighPermissionNodes (defined above).
Stage 3: macro
| make-graph SourceNodeId --> TargetNodeId with ExposureGraphNodes on NodeId
Stage 4: macro
| graph-match (SourceNode)-[anyEdge*1..3]->(TargetNode)
project IncomingNodeName = SourceNode.NodeName,
IncomingNodeLabel = SourceNode.NodeLabel,
Edges = anyEdge.EdgeLabel,
OutgoingNodeName = TargetNode.NodeName,
OutgoingNodeId = TargetNode.NodeId
Stage 5: where
| where Edges has_any (edge_labels)
Stage 6: join
| join kind=inner HighPermissionLinks on $left.OutgoingNodeId == $right.SourceNodeId
Stage 7: where
| where not(IncomingNodeLabel == "group" and IncomingNodeName in ("Domain Admins", "Enterprise Admins"))
Stage 8: where
| where not(IncomingNodeLabel == "user" and IncomingNodeName == "Administrator")
Stage 9: distinct
| distinct IncomingNodeName, IncomingNodeLabel, tostring(Edges), OutgoingNodeName, OutgoingNodeLabel = SourceNodeLabel, tostring(Permissions), TargetNodeLabel, tostring(TargetNodes), TargetNodeCount
Stage 10: summarize
summarize by SourceNodeName, SourceNodeLabel, TargetNodeLabel, SourceNodeId
Exclusions
Top-level NOT(...) conjuncts: predicates this rule actively suppresses.
| Field | Kind | Excluded values |
|---|---|---|
SourceNodeLabel | eq | group |
SourceNodeName | in | Domain Admins, Enterprise Admins |
SourceNodeLabel | eq | user |
SourceNodeName | eq | Administrator |
IncomingNodeLabel | eq | group |
IncomingNodeName | in | Domain Admins, Enterprise Admins |
IncomingNodeLabel | eq | user |
IncomingNodeName | eq | Administrator |
Indicators
Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.
| Field | Kind | Values |
|---|---|---|
EdgeLabel | eq |
|
Edges | match |
|
Permissions | match |
|
TargetNodeName | in |
|
Output fields
Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.
| Field | Source |
|---|---|
SourceNodeId | summarize |
SourceNodeLabel | summarize |
SourceNodeName | summarize |
TargetNodeLabel | summarize |