Detection rules › Kusto

Hunt for privilege escalation paths with high ACLs

Group by
OutgoingNodeId, SourceNodeId, SourceNodeLabel, SourceNodeName, TargetNodeLabel
Author
Robbe Van den Daele
Source
github.com/HybridBrothers/Hunting-Queries-Detection-Rules

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

TacticTechniques
Initial AccessT1078 Valid Accounts
PersistenceT1078 Valid Accounts
Privilege EscalationT1078 Valid Accounts
StealthT1078 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.

FieldKindExcluded values
SourceNodeLabeleqgroup
SourceNodeNameinDomain Admins, Enterprise Admins
SourceNodeLabelequser
SourceNodeNameeqAdministrator
IncomingNodeLabeleqgroup
IncomingNodeNameinDomain Admins, Enterprise Admins
IncomingNodeLabelequser
IncomingNodeNameeqAdministrator

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.

FieldKindValues
EdgeLabeleq
  • has role on transforms: cased
Edgesmatch
  • can authenticate as
  • can authenticate to
  • can impersonate as
  • has credentials of
  • has permissions to
  • member of
Permissionsmatch
  • genericAll
  • genericWrite
TargetNodeNamein
  • HighPermissionNodes transforms: cased

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.

FieldSource
SourceNodeIdsummarize
SourceNodeLabelsummarize
SourceNodeNamesummarize
TargetNodeLabelsummarize