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 Access
Persistence
Privilege Escalation
Stealth

References

Telemetry coverage

Rule body

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 used in Stages 1, 7

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
);

Let binding: HighPermissionNodes used in Stage 3

let HighPermissionNodes = toscalar(
    HighPermissionLinks
    | summarize SourceNodes = make_set(SourceNodeName)
);

Stage 1: source

let HighPermissionLinks

Stage 2: source

ExposureGraphEdges

Stage 3: where

where TargetNodeName =~ "HighPermissionNodes"

Stage 4: macro

macro

Stage 5: macro

macro

Stage 6: where

where (Edges contains "member of" or Edges contains "has permissions to" or Edges contains "can authenticate to" or Edges contains "can authenticate as" or Edges contains "has credentials of" or Edges contains "can impersonate as")

Stage 7: join

join kind=inner (HighPermissionLinks) on OutgoingNodeId, SourceNodeId

Stage 8: where

where not ((IncomingNodeLabel =~ "group" and IncomingNodeName in~ ("Domain Admins", "Enterprise Admins")))

Stage 9: where

where not ((IncomingNodeLabel =~ "user" and IncomingNodeName =~ "Administrator"))

Stage 10: distinct

distinct IncomingNodeLabel, IncomingNodeName, OutgoingNodeName, TargetNodeCount, TargetNodeLabel

Stage 11: summarize aggregation inside the join branch

summarize by SourceNodeName, SourceNodeLabel, TargetNodeLabel, SourceNodeId

Exclusions

The rule actively suppresses these predicates.

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EdgeLabeleq
  • has role on
field:"EdgeLabel" kind:eq value:"has role on"
Edgesmatch
  • can authenticate as transforms: term
  • can authenticate to transforms: term
  • can impersonate as transforms: term
  • has credentials of transforms: term
  • has permissions to transforms: term
  • member of transforms: term
field:"Edges" kind:match
Permissionsmatch
  • genericAll transforms: term
  • genericWrite transforms: term
field:"Permissions" kind:match
TargetNodeNamein
  • HighPermissionNodes
field:"TargetNodeName" kind:in value:"HighPermissionNodes"

Output fields

These fields are emitted when the rule matches.

FieldSource
SourceNodeIdsummarize
SourceNodeLabelsummarize
SourceNodeNamesummarize
TargetNodeLabelsummarize