Adversaries may use valid accounts to perform credential stuffing or lateral movement by generating high volumes of failed logon attempts in a short timeframe. SOC teams should proactively hunt for this behavior in Azure Sentinel to identify potential account compromise or reconnaissance efforts early.
KQL 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)
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 = mi
| Sentinel Table | Notes |
|---|---|
SecurityEvent | Ensure this data connector is enabled |
WindowsEvent | Ensure this data connector is enabled |
Scenario: Scheduled system maintenance using valid admin credentials
Description: A valid admin account is used to perform routine maintenance tasks, which may involve multiple failed logon attempts due to incorrect credentials during the setup or execution of the task.
Filter/Exclusion: Exclude logon attempts originating from known maintenance tools like PowerShell scripts, Task Scheduler, or Ansible jobs executed during scheduled maintenance windows.
Scenario: User attempting to reset password via self-service portal
Description: A user repeatedly tries to reset their password through the company’s self-service portal, which may result in multiple failed logon attempts as they enter incorrect credentials.
Filter/Exclusion: Exclude logon attempts where the source IP matches the company’s internal authentication portal (e.g., Okta, Azure AD, or LDAP servers) or where the user is known to be attempting password reset.
Scenario: Automated backup job using valid credentials
Description: A backup tool (e.g., Veeam, Commvault, or Dell EMC Data Domain) may attempt to authenticate multiple times during the backup process, leading to failed logon attempts if the credentials are temporarily invalid or the service is down.
Filter/Exclusion: Exclude logon attempts associated with backup tools or services running under known backup accounts, or during scheduled backup windows.
Scenario: User testing multi-factor authentication (MFA) setup
Description: A user is testing MFA configuration and may enter incorrect credentials multiple times while setting up or verifying their MFA method.
Filter/Exclusion: Exclude logon attempts from users who have recently initiated MFA setup or from known MFA test accounts, or where the user is known to be testing authentication flows.
Scenario: Logon attempts from a legitimate third-party service
Description: