Detection rules › Kusto

Detect service account login on new device

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

This detection rule tries to flag suspicious logins on devices from service accounts, for which these service accounts did not login into those devices for the last 14 days. This might indicate that the service account is compromised and is being used for lateral movement into the environment. Most service accounts have a fairly static set of devices they authenticate to. Because of this, it is easier to flag deviations for service accounts compared to user accounts. However, some service accounts are known to dynamically log into devices based on observed events (susch as the MDI service accounts). Because of this some environment specific finetuning might be needed to reduce BP detections.

MITRE ATT&CK coverage

Event coverage

Rule body yaml

// Get all enabled service accounts
let service_acc = (
    IdentityInfo
    | where Timestamp > ago(7d)
    | where Type == "ServiceAccount" and IsAccountEnabled == 1
    | distinct AccountName = tolower(AccountName)
);
// Get the history service account logins
let historic_events = (
    DeviceLogonEvents
    | where Timestamp between (ago(14d) .. ago(1h))
    | where ActionType == "LogonSuccess"
    | extend AccountName = tolower(AccountName)
    | join kind=inner service_acc on AccountName
    | summarize HistoricLogins = make_set(DeviceName) by AccountName
);
// Get the account logins done over Network
DeviceLogonEvents
| where Timestamp > ago(1h)
| where LogonType == "Network"
| where ActionType == "LogonSuccess"
| extend AccountName = tolower(AccountName)
// Join inner to only get known service account logins
| join kind=inner service_acc on AccountName
// Join inner to get a list of the historic device logins for the service accounts
| join kind=inner historic_events on AccountName
// Only get sign-ins where Device is not in the history logins
| extend HistoricLogins = tostring(HistoricLogins)
| where HistoricLogins !contains DeviceName
// Make output better
| project-away AccountName1, AccountName2
// Exclude MDI Service Account - CHANGE IF DIFFERENT FOR YOUR ORG
| where AccountName != "gsma_mdi$"
// Environment specific finetuning - begin
// Environment specific finetuning - end

// Get all enabled service accounts
let service_acc = (
    IdentityInfo
    | where TimeGenerated > ago(7d)
    | where Type == "ServiceAccount" and IsAccountEnabled == 1
    | distinct AccountName = tolower(AccountName)
);
// Get the history service account logins
let historic_events = (
    DeviceLogonEvents
    | where TimeGenerated between (ago(14d) .. ago(1h))
    | where ActionType == "LogonSuccess"
    | extend AccountName = tolower(AccountName)
    | join kind=inner service_acc on AccountName
    | summarize HistoricLogins = make_set(DeviceName) by AccountName
);
// Get the account logins done over Network
DeviceLogonEvents
| where TimeGenerated > ago(1h)
| where LogonType == "Network"
| where ActionType == "LogonSuccess"
| extend AccountName = tolower(AccountName)
// Join inner to only get known service account logins
| join kind=inner service_acc on AccountName
// Join inner to get a list of the historic device logins for the service accounts
| join kind=inner historic_events on AccountName
// Only get sign-ins where Device is not in the history logins
| extend HistoricLogins = tostring(HistoricLogins)
| where HistoricLogins !contains DeviceName
// Make output better
| project-away AccountName1, AccountName2
// Exclude MDI Service Account - CHANGE IF DIFFERENT FOR YOUR ORG
| where AccountName != "gsma_mdi$"
// Environment specific finetuning - begin
// Environment specific finetuning - end

Stages and Predicates

Let binding: service_acc

let service_acc = (
    IdentityInfo
    | where Timestamp > ago(7d)
    | where Type == "ServiceAccount" and IsAccountEnabled == 1
    | distinct AccountName = tolower(AccountName)
);

Let binding: historic_events

let historic_events = (
    DeviceLogonEvents
    | where Timestamp between (ago(14d) .. ago(1h))
    | where ActionType == "LogonSuccess"
    | extend AccountName = tolower(AccountName)
    | join kind=inner service_acc on AccountName
    | summarize HistoricLogins = make_set(DeviceName) by AccountName
);

Derived from service_acc.

Stage 1: source

DeviceLogonEvents

Stage 2: where

| where Timestamp > ago(1h)

Stage 3: where

| where LogonType == "Network"

Stage 4: where

| where ActionType == "LogonSuccess"

Stage 5: extend

| extend AccountName = tolower(AccountName)

Stage 6: join

| join kind=inner service_acc on AccountName

Stage 7: join

| join kind=inner historic_events on AccountName

Stage 8: extend

| extend HistoricLogins = tostring(HistoricLogins)

Stage 9: where

| where HistoricLogins !contains DeviceName

Stage 10: project-away

| project-away AccountName1, AccountName2

Stage 11: where

| where AccountName != "gsma_mdi$"

Stage 12: summarize

summarize by AccountName

Exclusions

Top-level NOT(...) conjuncts: predicates this rule actively suppresses.

FieldKindExcluded values
HistoricLoginscontainsDeviceName

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
AccountNamene
  • gsma_mdi$ transforms: cased
ActionTypeeq
  • LogonSuccess transforms: cased corpus 5 (kusto 5)
IsAccountEnabledeq
  • 1 transforms: cased
LogonTypeeq
  • Network transforms: cased corpus 40 (splunk 13, sigma 12, elastic 9, kusto 6)
Typeeq
  • ServiceAccount 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
AccountNamesummarize