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

Telemetry coverage

Rule body

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

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

Let binding: historic_events used in Stages 2, 9

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

Stage 1: source

let service_acc

Stage 2: source

let historic_events

Stage 3: source

DeviceLogonEvents

Stage 4: where

where Timestamp > ago(3600s)

Stage 5: where

where LogonType =~ "Network"

Stage 6: where

where ActionType =~ "LogonSuccess"

Stage 7: extend

extend AccountName

Stage 8: join

join kind=inner (service_acc) on AccountName

Stage 9: join

join kind=inner (historic_events) on AccountName

Stage 10: extend

extend HistoricLogins

Stage 11: where

where HistoricLogins !contains DeviceName

Stage 12: project-away

project-away AccountName1, AccountName2

Stage 13: where

where AccountName !~ "gsma_mdi$"

Stage 14: summarize aggregation inside the join branch

summarize by AccountName

Indicators

These rows show field, operator, and value matches.

Output fields

These fields are emitted when the rule matches.

FieldSource
AccountNamesummarize