Event ID 4625 explained: detecting brute force, sprays & enumeration
4625 is the failed-logon record. Read it right and you spot password sprays, credential stuffing, and Kerberos abuse before they succeed.
Event ID 4625 — "An account failed to log on" — fires on the Security channel whenever an authentication attempt is rejected. It's the single most useful record for catching credential-attack activity, but only if you read the right fields.
The fields that actually matter
<Data Name="TargetUserName">administrator</Data>
<Data Name="TargetDomainName">CORP</Data>
<Data Name="Status">0xc000006d</Data>
<Data Name="SubStatus">0xc0000064</Data>
<Data Name="LogonType">3</Data>
<Data Name="WorkstationName">attacker-vm</Data>
<Data Name="IpAddress">203.0.113.7</Data>
The combination of Status and SubStatus tells you why the logon failed:
0xc0000064— account does not exist (username enumeration).0xc000006a— wrong password (the classic).0xc0000234— account locked out.0xc0000072— account disabled.0xc0000071— password expired.0xc0000133— clock skew on a Kerberos ticket (common during AS-REP roasting).0xc000018b— wrong SID — the workstation isn't in the domain it thinks it is.
A burst of 0xc0000064 against valid and invalid usernames is reconnaissance. A burst of 0xc000006a against one account is brute force. A burst of 0xc000006a against many accounts with the same password is a password spray.
The patterns
The simplest triage queries:
- Spray detection: group 4625 records by
IpAddress(orWorkstationNameif the IP isn't recorded), count distinctTargetUserNameover 10 minutes. >5 accounts per source in that window is suspicious almost everywhere. - Brute force: group by
TargetUserName, count failures per minute. >10 per minute against one account is usually a bot. - Lockout root cause: pair 4740 (account locked) with the preceding 4625s — the
WorkstationNamefield will show which device triggered the lockout, which is critical because it's often a domain-joined server with a stale stored credential, not an attacker.
The "after" matters as much as the "before"
A 4625 burst followed by a 4624 from the same IpAddress is the actionable case — the attacker found a working credential. The parser on this page lets you filter the table to a source IP and watch the level and event-ID transitions over time in the timeline. The burst-then-spike pattern is unmistakable.
Sample Sigma rule — password spray
title: Password Spray via NTLM Failed Logons
id: 6d2e1f4a-1a8b-4c7c-8a5f-2c3d4e5f6a7b
status: stable
description: One source IP failing logons against many distinct accounts within a short window — the password-spray fingerprint.
references:
- https://attack.mitre.org/techniques/T1110/003/
logsource:
product: windows
service: security
detection:
selection:
EventID: 4625
Status: '0xC000006D'
SubStatus: '0xC000006A'
condition: selection | count(TargetUserName) by IpAddress > 5
timeframe: 10m
falsepositives:
- Misconfigured service account on a host hitting many endpoints
- Vulnerability scanner authentication probes (tag scanner IPs)
level: high
tags:
- attack.credential_access
- attack.t1110.003
Sample KQL — brute force against one account
SecurityEvent
| where EventID == 4625
| where Status == "0xC000006D" and SubStatus == "0xC000006A"
| summarize Failures=count(), Sources=dcount(IpAddress)
by TargetUserName, bin(TimeGenerated, 5m)
| where Failures >= 10
| order by TimeGenerated desc
Sample Splunk — enumeration before brute
index=wineventlog EventCode=4625
| eval kind=case(SubStatus="0xC0000064", "enumeration", SubStatus="0xC000006A", "wrong_password", 1==1, "other")
| stats values(kind) AS Sequence count BY IpAddress
| where mvcount(Sequence) >= 2 AND mvfind(Sequence, "enumeration") >= 0 AND mvfind(Sequence, "wrong_password") >= 0
The signal is the progression — enumeration to find valid usernames, then brute force against those.
ATT&CK mapping
- T1110.001 — Brute Force: Password Guessing: single account, many
0xC000006Afailures. - T1110.003 — Brute Force: Password Spraying: many accounts, few failures per account, one source.
- T1110.004 — Brute Force: Credential Stuffing: many accounts, one source, often
0xC0000064(account doesn't exist) for leaked-list misses interspersed with0xC000006Ahits. - T1078 — Valid Accounts: 4625 followed by 4624 success from the same source = compromise.
- T1556 — Modify Authentication Process: anomalous
LogonProcessName(anything other thanUser32,NtLmSsp,Kerberos,Advapi, orSchannel) suggests authentication tampering.
False positives that look like attacks
- Stored credentials going stale after a password change. The user's mapped drives, scheduled tasks, or service account configs keep retrying the old password. The pattern: one
TargetUserName, oneIpAddress(sometimes oneWorkstationName), steady0xC000006Acadence. Hunt for the host with the stale credential and fix it. - Misconfigured automation: a script with a wrong password retrying in a loop. Same shape as brute force; talk to the owner before alerting.
- Vulnerability scanners during authenticated scans produce dense 4625 traffic. Tag scanner IPs.
- Lockout-policy misconfiguration: helpdesk procedures that unlock too aggressively can produce repeating 4625 → 4740 → 4624 cycles.
What you don't see in 4625
NTLMv2 and Kerberos failures coming from a domain controller don't always carry a useful IpAddress — the field can be empty or -. For those you need the matching DC events (4768/4771 for Kerberos pre-auth failures) or network-level data. Don't conclude "no source IP, no investigation" — pivot to the DC channel.
The LogonProcessName and AuthenticationPackageName fields tell you which auth stack handled the attempt. Most useful are NtLmSsp (NTLM), Kerberos, and Negotiate (which picks one of the two). User32 is local console; Schannel is TLS-based.