Password spray and authentication abuse hunting¶
Three hunts for authentication abuse patterns in Windows Security event logs: source-based failure clustering that distinguishes spray from brute force, lockout bursts that confirm a spray in progress, and failure-to-success sequences that indicate a successful credential.
Data source: Windows Security Event Log on domain controllers (for domain account authentication) and on member servers or workstations (for local account authentication and RDP/network logon failures). Run these queries on domain controllers first; local account abuse appears on the targeted machine.
Source-based failure clustering¶
Hypothesis: an attacker is spraying one or a small set of passwords across many accounts from a single source, staying below the per-account lockout threshold.
# Event 4625: logon failures over the past four hours
# Change $hours to adjust the lookback window
$since = (Get-Date).AddHours(-4)
$failures = Get-WinEvent -LogName Security -FilterXPath '*[System[EventID=4625]]' -ErrorAction SilentlyContinue |
Where-Object { $_.TimeCreated -ge $since } |
ForEach-Object {
[xml]$xml = $_.ToXml()
$d = @{}
$xml.Event.EventData.Data | ForEach-Object { $d[$_.Name] = $_.'#text' }
[PSCustomObject]@{
Time = $_.TimeCreated
TargetUser = $d['TargetUserName']
IpAddress = $d['IpAddress']
Status = $d['Status']
LogonType = $d['LogonType']
}
}
# Spray indicator: one source, many distinct usernames
$failures |
Group-Object IpAddress |
Select-Object Name,
Count,
@{N='DistinctUsers'; E={($_.Group.TargetUser | Sort-Object -Unique).Count}} |
Where-Object { $_.DistinctUsers -gt 10 } |
Sort-Object DistinctUsers -Descending
# Brute force indicator: many failures against one account from multiple sources
$failures |
Group-Object TargetUser |
Select-Object Name,
Count,
@{N='DistinctSources'; E={($_.Group.IpAddress | Sort-Object -Unique).Count}} |
Where-Object { $_.Count -gt 20 } |
Sort-Object Count -Descending
Spray produces a high DistinctUsers count per source with a low per-account failure count. Brute force produces a high failure count on a single TargetUser. Credential stuffing resembles spray in structure but produces a higher success rate; the failure-to-success query below surfaces that pattern.
Account lockout bursts¶
Hypothesis: an automated spray tool has exceeded the lockout threshold on multiple accounts in a short window, generating a cluster of Event 4740.
# Event 4740: account lockout events — clustered in time indicate a spray in progress
$since = (Get-Date).AddHours(-2)
Get-WinEvent -LogName Security -FilterXPath '*[System[EventID=4740]]' -ErrorAction SilentlyContinue |
Where-Object { $_.TimeCreated -ge $since } |
ForEach-Object {
[xml]$xml = $_.ToXml()
$d = @{}
$xml.Event.EventData.Data | ForEach-Object { $d[$_.Name] = $_.'#text' }
[PSCustomObject]@{
Time = $_.TimeCreated
LockedAccount = $d['TargetUserName']
CallerMachine = $d['SubjectUserName']
}
} |
Sort-Object Time -Descending | Select-Object -First 40
A single lockout event is a normal occurrence. Several accounts locking out within minutes of each other from the same source is a spray in progress. The lockout policy (threshold and observation window) determines how visible this pattern is; a spray tuned to stay just below the threshold produces no lockouts but remains visible in the failure clustering hunt above.
Failure-to-success sequences¶
Hypothesis: a credential-stuffing or spray attack has produced at least one successful authentication following a failure burst, indicating a valid credential was found.
# Event 4624: logon successes — filter for sources that also appear in the failure set
$since = (Get-Date).AddHours(-4)
$failureSources = Get-WinEvent -LogName Security -FilterXPath '*[System[EventID=4625]]' -ErrorAction SilentlyContinue |
Where-Object { $_.TimeCreated -ge $since } |
ForEach-Object {
[xml]$xml = $_.ToXml()
$d = @{}
$xml.Event.EventData.Data | ForEach-Object { $d[$_.Name] = $_.'#text' }
$d['IpAddress']
} | Sort-Object -Unique
# Logon successes from any source that also generated failures
Get-WinEvent -LogName Security -FilterXPath '*[System[EventID=4624]]' -ErrorAction SilentlyContinue |
Where-Object { $_.TimeCreated -ge $since } |
ForEach-Object {
[xml]$xml = $_.ToXml()
$d = @{}
$xml.Event.EventData.Data | ForEach-Object { $d[$_.Name] = $_.'#text' }
[PSCustomObject]@{
Time = $_.TimeCreated
TargetUser = $d['TargetUserName']
IpAddress = $d['IpAddress']
LogonType = $d['LogonType']
}
} |
Where-Object { $_.IpAddress -in $failureSources } |
Sort-Object Time -Descending | Select-Object -First 20
A source IP that produced a failure burst followed by a successful logon from the same IP has either found a valid credential or triggered an account that does not enforce lockout. The subsequent session activity from that account is worth reviewing: Event 4769 (Kerberos TGS requests), net logon events, and any lateral movement indicators in the hours following the success.