Detection rules › Kusto

Detect non-admin requesting token for admin applications

Group by
AccountObjectId, AccountUPN, AppDisplayName, IPAddress, ResultType, UserId, UserPrincipalName
Author
Robbe Van den Daele
Source
github.com/HybridBrothers/Hunting-Queries-Detection-Rules

This rule detects sign-in attempts from non-admin users to admin applications in Entra ID.

MITRE ATT&CK coverage

TacticTechniques
Execution

References

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Rule body

let ITAccounts=(_GetWatchlist('ITAccounts') | summarize make_set(ITAccounts));
// Materialize Dataset
let DataSetMat= materialize (SigninLogs
| where TimeGenerated > ago(1h)
| where AppDisplayName has_any ("PowerShell", "CLI", "Command Line", "Management Shell")
// Get successful and failed due to no assignment logins
| where ResultType in ("0", "50105")
| summarize max(TimeGenerated) by UserPrincipalName, AppDisplayName, IPAddress, UserId, ResultType
// join IdentityInfo to get more information
| join kind=leftouter (IdentityInfo | where TimeGenerated > ago(14d) | summarize arg_max(TimeGenerated, *) by AccountObjectId ) on $left.UserId == $right.AccountObjectId
// exclude Accounts with Assigned Roles
| where array_length(AssignedRoles) == 0
// exclude known IT personnel Departments
| where Department !has "it" and Department !has "ict" and Department !has "operations"
// exclude service accounts
| where JobTitle != "Service Account");
// exclude IT accounts
let FIL= (DataSetMat
| extend ITAccounts= toscalar(ITAccounts)
| mv-expand ITAccounts
| where AccountUPN contains ITAccounts or AccountDisplayName contains ITAccounts);
DataSetMat
// exclude service accounts
| join kind=leftanti FIL on AccountUPN
| distinct  max_TimeGenerated, UserPrincipalName, AppDisplayName, IPAddress, JobTitle, Department, UserId, ResultType

Stages and Predicates

let DataSetMat is inlined into the numbered stages below.

Let binding: ITAccounts

let ITAccounts = (_GetWatchlist('ITAccounts') | summarize make_set(ITAccounts));

Let binding: FIL used in Stage 11

let FIL = (DataSetMat
| extend ITAccounts= toscalar(ITAccounts)
| mv-expand ITAccounts
| where AccountUPN contains ITAccounts or AccountDisplayName contains ITAccounts);

Stage 1: source

let DataSetMat

Stage 2: source

SigninLogs

Stage 3: where

where TimeGenerated > ago(3600s)

Stage 4: where

where (AppDisplayName contains "PowerShell" or AppDisplayName contains "CLI" or AppDisplayName contains "Command Line" or AppDisplayName contains "Management Shell")

Stage 5: where

where ResultType in~ (0, 50105)

Stage 6: summarize

summarize by UserPrincipalName, AppDisplayName, IPAddress, UserId, ResultType

Stage 7: join

join kind=leftouter (IdentityInfo) on UserId, AccountObjectId

Stage 8: where

where AssignedRoles == 0

Stage 9: where

where not (Department contains "ict") and not (Department contains "it") and not (Department contains "operations")

Stage 10: where

where JobTitle !~ "Service Account"

Stage 11: join (negated)

join kind=leftanti (FIL) on AccountUPN

Stage 12: distinct

distinct AppDisplayName, Department, IPAddress, JobTitle, ResultType, UserId, UserPrincipalName, max_TimeGenerated

Exclusions

The rule actively suppresses these predicates.

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
AppDisplayNamematch
  • CLI transforms: term
  • Command Line transforms: term
  • Management Shell transforms: term
  • PowerShell transforms: term
field:"Application" kind:match
Departmentmatch
  • ict transforms: term
  • it transforms: term
  • operations transforms: term
field:"Department" kind:match
JobTitlene
  • Service Account
field:"JobTitle" kind:ne value:"Service Account"
ResultTypein
  • 0
  • 50105
field:"ResultType" kind:in

Output fields

These fields are emitted when the rule matches.

FieldSource
AppDisplayNamesummarize
IPAddresssummarize
ResultTypesummarize
UserIdsummarize
UserPrincipalNamesummarize