Detection rules › Kusto
Failed logon attempts by valid accounts within 10 mins
Identifies when failed logon attempts are 20 or higher during a 10 minute period (2 failed logons per minute minimum) from valid account.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Credential Access | T1110 Brute Force |
Event coverage
| Provider | Event | Title |
|---|---|---|
| Security-Auditing | Event ID 4625 | An account failed to log on. |
Rule body kusto
id: 0777f138-e5d8-4eab-bec1-e11ddfbc2be2
name: Failed logon attempts by valid accounts within 10 mins
description: |
'Identifies when failed logon attempts are 20 or higher during a 10 minute period (2 failed logons per minute minimum) from valid account.'
severity: Low
requiredDataConnectors:
- connectorId: SecurityEvents
dataTypes:
- SecurityEvent
- connectorId: WindowsSecurityEvents
dataTypes:
- SecurityEvent
- connectorId: WindowsForwardedEvents
dataTypes:
- WindowsEvent
queryFrequency: 10m
queryPeriod: 10m
triggerOperator: gt
triggerThreshold: 0
tactics:
- CredentialAccess
relevantTechniques:
- T1110
query: |
let threshold = 20;
let ReasontoSubStatus = datatable(SubStatus: string, Reason: string) [
"0xc000005e", "There are currently no logon servers available to service the logon request.",
"0xc0000064", "User logon with misspelled or bad user account",
"0xc000006a", "User logon with misspelled or bad password",
"0xc000006d", "Bad user name or password",
"0xc000006e", "Unknown user name or bad password",
"0xc000006f", "User logon outside authorized hours",
"0xc0000070", "User logon from unauthorized workstation",
"0xc0000071", "User logon with expired password",
"0xc0000072", "User logon to account disabled by administrator",
"0xc00000dc", "Indicates the Sam Server was in the wrong state to perform the desired operation",
"0xc0000133", "Clocks between DC and other computer too far out of sync",
"0xc000015b", "The user has not been granted the requested logon type (aka logon right) at this machine",
"0xc000018c", "The logon request failed because the trust relationship between the primary domain and the trusted domain failed",
"0xc0000192", "An attempt was made to logon, but the Netlogon service was not started",
"0xc0000193", "User logon with expired account",
"0xc0000224", "User is required to change password at next logon",
"0xc0000225", "Evidently a bug in Windows and not a risk",
"0xc0000234", "User logon with account locked",
"0xc00002ee", "Failure Reason: An Error occurred during Logon",
"0xc0000413", "Logon Failure: The machine you are logging onto is protected by an authentication firewall. The specified account is not allowed to authenticate to the machine"
];
(union isfuzzy=true
(SecurityEvent
| where EventID == 4625
| where AccountType =~ "User"
| where SubStatus !~ '0xc0000064' and Account !in ('\\', '-\\-')
// SubStatus '0xc0000064' signifies 'Account name does not exist'
| extend
ResourceId = column_ifexists("_ResourceId", _ResourceId),
SourceComputerId = column_ifexists("SourceComputerId", SourceComputerId),
SubStatus = tolower(SubStatus)
| lookup ReasontoSubStatus on SubStatus
| extend coalesce(Reason, strcat('Unknown reason substatus: ', SubStatus))
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), FailedLogonCount = count() by bin(TimeGenerated,10m), EventID,
Activity, Computer, Account, TargetAccount, TargetUserName, TargetDomainName,
LogonType, LogonTypeName, LogonProcessName, Status, SubStatus, Reason, ResourceId, SourceComputerId, WorkstationName, IpAddress
| where FailedLogonCount >= threshold
),
(
(WindowsEvent
| where EventID == 4625 and not(EventData has '0xc0000064')
| extend TargetAccount = strcat(tostring(EventData.TargetDomainName), "\\", tostring(EventData.TargetUserName))
| extend TargetUserSid = tostring(EventData.TargetUserSid)
| extend AccountType=case(EventData.TargetUserName endswith "$" or TargetUserSid in ("S-1-5-18", "S-1-5-19", "S-1-5-20"), "Machine", isempty(TargetUserSid), "", "User")
| where AccountType =~ "User"
| extend SubStatus = tostring(EventData.SubStatus)
| where SubStatus !~ '0xc0000064' and TargetAccount !in ('\\', '-\\-')
// SubStatus '0xc0000064' signifies 'Account name does not exist'
| extend
ResourceId = column_ifexists("_ResourceId", _ResourceId),
SourceComputerId = column_ifexists("SourceComputerId", ""),
SubStatus = tolower(SubStatus)
| lookup ReasontoSubStatus on SubStatus
| extend coalesce(Reason, strcat('Unknown reason substatus: ', SubStatus))
| extend Activity="4625 - An account failed to log on."
| extend TargetUserName = tostring(EventData.TargetUserName)
| extend TargetDomainName = tostring(EventData.TargetDomainName)
| extend LogonType = tostring(EventData.LogonType)
| extend Status= tostring(EventData.Status)
| extend LogonProcessName = tostring(EventData.LogonProcessName)
| extend WorkstationName = tostring(EventData.WorkstationName)
| extend IpAddress = tostring(EventData.IpAddress)
| extend LogonTypeName=case(
LogonType == 2, "2 - Interactive",
LogonType == 3, "3 - Network",
LogonType == 4, "4 - Batch",
LogonType == 5, "5 - Service",
LogonType == 7, "7 - Unlock",
LogonType == 8, "8 - NetworkCleartext",
LogonType == 9, "9 - NewCredentials",
LogonType == 10, "10 - RemoteInteractive",
LogonType == 11, "11 - CachedInteractive",
tostring(LogonType)
)
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), FailedLogonCount = count() by bin(TimeGenerated,10m), EventID,
Activity, Computer, TargetAccount, TargetUserName, TargetDomainName,
LogonType, LogonTypeName, LogonProcessName, Status, SubStatus, Reason, ResourceId, SourceComputerId, WorkstationName, IpAddress
| where FailedLogonCount >= threshold
)))
| summarize arg_max(TimeGenerated, *) by Computer, TargetAccount, TargetUserName, TargetDomainName
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: TargetAccount
- identifier: Name
columnName: TargetUserName
- identifier: NTDomain
columnName: TargetDomainName
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: Computer
- identifier: HostName
columnName: HostName
- identifier: NTDomain
columnName: HostNameDomain
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IpAddress
version: 1.2.5
kind: Scheduled
metadata:
source:
kind: Community
author:
name: Microsoft Security Research
support:
tier: Community
categories:
domains: [ "Security - Others", "Identity" ]
Stages and Predicates
Stage 0: let
let threshold = 20;
let ReasontoSubStatus = datatable(SubStatus: string, Reason: string) [
"0xc000005e", "There are currently no logon servers available to service the logon request.",
"0xc0000064", "User logon with misspelled or bad user account",
"0xc000006a", "User logon with misspelled or bad password",
"0xc000006d", "Bad user name or password",
"0xc000006e", "Unknown user name or bad password",
"0xc000006f", "User logon outside authorized hours",
"0xc0000070", "User logon from unauthorized workstation",
"0xc0000071", "User logon with expired password",
"0xc0000072", "User logon to account disabled by administrator",
"0xc00000dc", "Indicates the Sam Server was in the wrong state to perform the desired operation",
"0xc0000133", "Clocks between DC and other computer too far out of sync",
"0xc000015b", "The user has not been granted the requested logon type (aka logon right) at this machine",
"0xc000018c", "The logon request failed because the trust relationship between the primary domain and the trusted domain failed",
"0xc0000192", "An attempt was made to logon, but the Netlogon service was not started",
"0xc0000193", "User logon with expired account",
"0xc0000224", "User is required to change password at next logon",
"0xc0000225", "Evidently a bug in Windows and not a risk",
"0xc0000234", "User logon with account locked",
"0xc00002ee", "Failure Reason: An Error occurred during Logon",
"0xc0000413", "Logon Failure: The machine you are logging onto is protected by an authentication firewall. The specified account is not allowed to authenticate to the machine"
];
Stage 1: union
union isfuzzy=true
Stage 2: source
SecurityEvent
Stage 3: where
| where EventID == 4625
Stage 4: where
| where AccountType =~ "User"
Stage 5: where
| where SubStatus !~ '0xc0000064' and Account !in ('\\', '-\\-')
Stage 6: extend
| extend
ResourceId = column_ifexists("_ResourceId", _ResourceId),
SourceComputerId = column_ifexists("SourceComputerId", SourceComputerId),
SubStatus = tolower(SubStatus)
Stage 7: kusto:lookup
| lookup ReasontoSubStatus on SubStatus
Stage 8: extend
| extend coalesce(Reason, strcat('Unknown reason substatus: ', SubStatus))
Stage 9: summarize time_window=600s
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), FailedLogonCount = count() by bin(TimeGenerated,10m), EventID,
Activity, Computer, Account, TargetAccount, TargetUserName, TargetDomainName,
LogonType, LogonTypeName, LogonProcessName, Status, SubStatus, Reason, ResourceId, SourceComputerId, WorkstationName, IpAddress
Stage 10: where
| where FailedLogonCount >= threshold
Stage 11: source
WindowsEvent
Stage 12: where
| where EventID == 4625 and not(EventData has '0xc0000064')
Stage 13: extend
| extend TargetAccount = strcat(tostring(EventData.TargetDomainName), "\\", tostring(EventData.TargetUserName))
Stage 14: extend
| extend TargetUserSid = tostring(EventData.TargetUserSid)
Stage 15: extend
| extend AccountType=case(EventData.TargetUserName endswith "$" or TargetUserSid in ("S-1-5-18", "S-1-5-19", "S-1-5-20"), "Machine", isempty(TargetUserSid), "", "User")
Stage 16: where
| where AccountType =~ "User"
Stage 17: extend
| extend SubStatus = tostring(EventData.SubStatus)
Stage 18: where
| where SubStatus !~ '0xc0000064' and TargetAccount !in ('\\', '-\\-')
Stage 19: extend
| extend
ResourceId = column_ifexists("_ResourceId", _ResourceId),
SourceComputerId = column_ifexists("SourceComputerId", ""),
SubStatus = tolower(SubStatus)
Stage 20: kusto:lookup
| lookup ReasontoSubStatus on SubStatus
Stage 21: extend
| extend coalesce(Reason, strcat('Unknown reason substatus: ', SubStatus))
Stage 22: extend
| extend Activity="4625 - An account failed to log on."
Stage 23: extend
| extend TargetUserName = tostring(EventData.TargetUserName)
Stage 24: extend
| extend TargetDomainName = tostring(EventData.TargetDomainName)
Stage 25: extend
| extend LogonType = tostring(EventData.LogonType)
Stage 26: extend
| extend Status= tostring(EventData.Status)
Stage 27: extend
| extend LogonProcessName = tostring(EventData.LogonProcessName)
Stage 28: extend
| extend WorkstationName = tostring(EventData.WorkstationName)
Stage 29: extend
| extend IpAddress = tostring(EventData.IpAddress)
Stage 30: extend
| extend LogonTypeName=case(
LogonType == 2, "2 - Interactive",
LogonType == 3, "3 - Network",
LogonType == 4, "4 - Batch",
LogonType == 5, "5 - Service",
LogonType == 7, "7 - Unlock",
LogonType == 8, "8 - NetworkCleartext",
LogonType == 9, "9 - NewCredentials",
LogonType == 10, "10 - RemoteInteractive",
LogonType == 11, "11 - CachedInteractive",
tostring(LogonType)
)
Stage 31: summarize time_window=600s
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), FailedLogonCount = count() by bin(TimeGenerated,10m), EventID,
Activity, Computer, TargetAccount, TargetUserName, TargetDomainName,
LogonType, LogonTypeName, LogonProcessName, Status, SubStatus, Reason, ResourceId, SourceComputerId, WorkstationName, IpAddress
Stage 32: where
| where FailedLogonCount >= threshold
Stage 33: summarize
| summarize arg_max(TimeGenerated, *) by Computer, TargetAccount, TargetUserName, TargetDomainName
Stage 34: extend
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
Stage 35: extend
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
Exclusions
Top-level NOT(...) conjuncts: predicates this rule actively suppresses.
| Stage | Field | Kind | Excluded values |
|---|---|---|---|
| 5 | Account | in | -\\-, \\ |
| 12 | EventData | match | 0xc0000064 |
| 18 | TargetAccount | in | -\\-, \\ |
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 |
|---|---|---|
AccountType | eq |
|
EventID | eq |
|
FailedLogonCount | ge |
|
SubStatus | ne |
|
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 |
|---|---|
Computer | summarize |
TargetAccount | summarize |
TargetDomainName | summarize |
TargetUserName | summarize |
DomainIndex | extend |
HostName | extend |
HostNameDomain | extend |