Detection rules › Kusto
Password Spraying
This query detects a password spraying attack, where a single machine has performed a large number of failed login attempts, with a large number of different accounts. For each account, the attacker uses just a few attempts to prevent account lockout. This query uses the DeviceLogonEvents per machine to detect a password spraying attacks. The machine against which the password spraying is performed (can be DC, a server or even an endpoint) needs to be enrolled in Microsoft Defender for Endpoint.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Credential Access | T1110.003 Brute Force: Password Spraying |
Event coverage
| Provider | Event/ActionType | Title |
|---|---|---|
| Security-Auditing | Event ID 4624 | An account was successfully logged on. |
| Security-Auditing | Event ID 4625 | An account failed to log on. |
| Defender-DeviceLogonEvents | LogonSuccess | Logon succeeded |
| Defender-DeviceLogonEvents | LogonFailed | Logon failed |
Rule body kusto
id: e00f72ab-fea1-4a31-9ecc-eea6397cd38d
name: Password Spraying
description: |
This query detects a password spraying attack, where a single machine has performed a large number of failed login attempts, with a large number of different accounts.
For each account, the attacker uses just a few attempts to prevent account lockout. This query uses the DeviceLogonEvents per machine to detect a password spraying attacks.
The machine against which the password spraying is performed (can be DC, a server or even an endpoint) needs to be enrolled in Microsoft Defender for Endpoint.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: MicrosoftThreatProtection
dataTypes:
- DeviceLogonEvents
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- CredentialAccess
relevantTechniques:
- T1110.003
query: |
let thresholdForUniqueFailedAccounts = 20;
let upperBoundOfFailedLogonsPerAccount = 10;
let ratioSuccessFailedLogons = 0.5;
let timeframe = 1d;
DeviceLogonEvents
| where Timestamp >= ago(timeframe)
| where LogonType != "Unlock" and ActionType in ("LogonSuccess", "LogonFailed")
| where not(isempty( RemoteIP) and isempty( RemoteDeviceName))
| extend LocalLogon=parse_json(AdditionalFields)
| where RemoteIPType != "Loopback"
| summarize SuccessLogonCount = countif(ActionType == "LogonSuccess"), FailedLogonCount = countif(ActionType == "LogonFailed"),
UniqueAccountFailedLogons=dcountif(AccountName, ActionType == "LogonFailed"), FirstFailed=minif(Timestamp, ActionType == "LogonFailed"),
LastFailed=maxif(Timestamp, ActionType == "LogonFailed"), LastTimestamp=arg_max(Timestamp, tostring(ReportId)) by RemoteIP, DeviceName // RemoteIP is the source of the logon attempt.
| project-rename IPAddress=RemoteIP
| where UniqueAccountFailedLogons > thresholdForUniqueFailedAccounts and SuccessLogonCount*ratioSuccessFailedLogons < FailedLogonCount and UniqueAccountFailedLogons*upperBoundOfFailedLogonsPerAccount > FailedLogonCount
entityMappings:
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: DeviceName
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPAddress
version: 1.0.1
kind: Scheduled
Stages and Predicates
Parameters
let thresholdForUniqueFailedAccounts = 20;
let upperBoundOfFailedLogonsPerAccount = 10;
let ratioSuccessFailedLogons = 0.5;
let timeframe = 1d;
Stage 1: source
DeviceLogonEvents
Stage 2: where
| where Timestamp >= ago(timeframe)
Stage 3: where
| where LogonType != "Unlock" and ActionType in ("LogonSuccess", "LogonFailed")
Stage 4: where
| where not(isempty( RemoteIP) and isempty( RemoteDeviceName))
Stage 5: extend
| extend LocalLogon=parse_json(AdditionalFields)
Stage 6: where
| where RemoteIPType != "Loopback"
Stage 7: summarize
| summarize SuccessLogonCount = countif(ActionType == "LogonSuccess"), FailedLogonCount = countif(ActionType == "LogonFailed"),
UniqueAccountFailedLogons=dcountif(AccountName, ActionType == "LogonFailed"), FirstFailed=minif(Timestamp, ActionType == "LogonFailed"),
LastFailed=maxif(Timestamp, ActionType == "LogonFailed"), LastTimestamp=arg_max(Timestamp, tostring(ReportId)) by RemoteIP, DeviceName
Stage 8: project-rename
| project-rename IPAddress=RemoteIP
Stage 9: where
| where UniqueAccountFailedLogons > thresholdForUniqueFailedAccounts and SuccessLogonCount*ratioSuccessFailedLogons < FailedLogonCount and UniqueAccountFailedLogons*upperBoundOfFailedLogonsPerAccount > FailedLogonCount
Exclusions
Top-level NOT(...) conjuncts: predicates this rule actively suppresses.
| Field | Kind | Excluded values |
|---|---|---|
RemoteDeviceName | is_null | |
RemoteIP | is_null |
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.
| Field | Kind | Values |
|---|---|---|
ActionType | in |
|
LogonType | ne |
|
RemoteIPType | ne |
|
SuccessLogonCount | cross_field_compare |
|
UniqueAccountFailedLogons | cross_field_compare |
|
UniqueAccountFailedLogons | gt |
|
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.
| Field | Source |
|---|---|
DeviceName | summarize |
FailedLogonCount | summarize |
FirstFailed | summarize |
LastFailed | summarize |
LastTimestamp | summarize |
RemoteIP | summarize |
SuccessLogonCount | summarize |
UniqueAccountFailedLogons | summarize |
IPAddress | project-rename |