Detection rules › Kusto

Hunt for critical credentials on devices with non-critical accounts

Group by
SourceNodeName
Author
Robbe Van den Daele
Source
github.com/HybridBrothers/Hunting-Queries-Detection-Rules

In most organizations normal user accounts or accounts with low risk permissions have less security controls enabled. This because there are less security controls needed in order to minize the risk vectors that come with these accounts. If these accounts are used on devices where critical account credentials are also present, the critical user account can be compromised more easily when the device is accessed by an adversary via the non-critical user account. Because of this, a Privileged Access Workstation should be used which serves as a dedicated workstation for the critical accounts. By doing this, the critical user account cannot be compromised via a unhardened device.

MITRE ATT&CK coverage

TacticTechniques
Initial AccessT1078 Valid Accounts
PersistenceT1078 Valid Accounts
Privilege EscalationT1078 Valid Accounts
StealthT1078 Valid Accounts

Rule body yaml

// Search for all users and save their criticality level
let xspm_users = materialize(
    ExposureGraphNodes
    | where NodeLabel == "user"
    | extend CriticalityLevel = todynamic(NodeProperties).rawData.criticalityLevel.criticalityLevel
    | extend RuleNames = todynamic(NodeProperties).rawData.criticalityLevel.ruleNames
    | distinct NodeName, NodeId, tostring(CriticalityLevel), tostring(RuleNames)
);
// Make a list of all critical users
let critical_users = toscalar(
    xspm_users
    | where CriticalityLevel == 0
    | summarize make_set(NodeName)
);
// Make a list of all non critical users
let non_critical_users = toscalar(
    xspm_users
    | where CriticalityLevel != 0
    | summarize make_set(NodeName)
);
// Make graph for max of 3 edges, where we start from a device and end with an user
ExposureGraphEdges
| make-graph SourceNodeId --> TargetNodeId with ExposureGraphNodes on NodeId
| graph-match (SourceNode)-[anyEdge*1..3]->(TargetNode)
    where SourceNode.NodeLabel in ("device", "microsoft.compute/virtualmachines") and TargetNode.NodeLabel == "user"
    project SourceNodeName = SourceNode.NodeName,
    Edges = anyEdge.EdgeLabel,
    TargetNodeName = TargetNode.NodeName,
    TargetNodeLabel = TargetNode.NodeLabel
// Make a list of all users a device has credentials for
| summarize UserList = make_set(TargetNodeName) by SourceNodeName
// Only return devices with more than one credential
| where array_length(UserList) > 1
// Make new lists saving the critical users and non critical users per device
| extend CriticalUserList = set_intersect(UserList, critical_users),
    NonCriticalUserList = set_intersect(UserList, non_critical_users)
// Flag when a device has both critical and non critical users
| where array_length(CriticalUserList) > 0 and array_length(NonCriticalUserList) > 0

Stages and Predicates

Let binding: xspm_users

let xspm_users = materialize(
    ExposureGraphNodes
    | where NodeLabel == "user"
    | extend CriticalityLevel = todynamic(NodeProperties).rawData.criticalityLevel.criticalityLevel
    | extend RuleNames = todynamic(NodeProperties).rawData.criticalityLevel.ruleNames
    | distinct NodeName, NodeId, tostring(CriticalityLevel), tostring(RuleNames)
);

Let binding: critical_users

let critical_users = toscalar(
    xspm_users
    | where CriticalityLevel == 0
    | summarize make_set(NodeName)
);

Derived from xspm_users.

Let binding: non_critical_users

let non_critical_users = toscalar(
    xspm_users
    | where CriticalityLevel != 0
    | summarize make_set(NodeName)
);

Derived from xspm_users.

Stage 1: source

ExposureGraphEdges

Stage 2: macro

| make-graph SourceNodeId --> TargetNodeId with ExposureGraphNodes on NodeId

Stage 3: macro

| graph-match (SourceNode)-[anyEdge*1..3]->(TargetNode)
    where SourceNode.NodeLabel in ("device", "microsoft.compute/virtualmachines") and TargetNode.NodeLabel == "user"
    project SourceNodeName = SourceNode.NodeName,
    Edges = anyEdge.EdgeLabel,
    TargetNodeName = TargetNode.NodeName,
    TargetNodeLabel = TargetNode.NodeLabel

Stage 4: summarize

| summarize UserList = make_set(TargetNodeName) by SourceNodeName
Threshold
gt 1

Stage 5: where

| where array_length(UserList) > 1

Stage 6: extend

| extend CriticalUserList = set_intersect(UserList, critical_users),
    NonCriticalUserList = set_intersect(UserList, non_critical_users)

References critical_users, non_critical_users (defined above).

Stage 7: where

| where array_length(CriticalUserList) > 0 and array_length(NonCriticalUserList) > 0

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
CriticalUserListgt
  • 0 transforms: array_length
NonCriticalUserListgt
  • 0 transforms: array_length
UserListgt
  • 1 transforms: array_length

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
SourceNodeNamesummarize
UserListsummarize
CriticalUserListextend
NonCriticalUserListextend