Detection rules › Kusto
Detect service account login on new device
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
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
);
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.
| Field | Kind | Values | Search |
|---|---|---|---|
AccountName | ne |
| field:"user" kind:ne value:"gsma_mdi$" |
ActionType | eq |
| field:"ActionType" kind:eq value:"LogonSuccess" |
HistoricLogins | cross_field_compare |
| field:"HistoricLogins" kind:cross_field_compare value:"DeviceName" |
IsAccountEnabled | eq |
| field:"IsAccountEnabled" kind:eq value:"1" |
LogonType | eq |
| field:"LogonType" kind:eq value:"Network" |
Type | eq |
| field:"Type" kind:eq value:"ServiceAccount" |
Output fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountName | summarize |