What AD badPwdCount Triage Can Establish
AD badPwdCount triage starts with a question about evidence: which domain controller supplied this account's counter, at what time, and did the collection actually succeed? A directory snapshot can identify accounts worth investigating. It cannot, by itself, establish a dictionary attack, identify the source of failures, or show that a password is weak.
The practical outcome is a review record that connects directory state to authentication activity. Preserve the original finding, collect comparable views from the relevant domain controllers, check the calculated lockout state, and investigate the logs for the authentication path involved. Then record whether the evidence supports user error, an application retrying an outdated secret, suspicious activity, or an unresolved case.
This guide uses defensive directory reads and a documented, targeted unlock example. The commands are illustrative adaptations for an authorized administrator with the ActiveDirectory PowerShell module and appropriate permissions. They were not executed against a domain. No password spray or deliberate lockout test was performed.
What the LDAP Counter Actually Measures
Microsoft's user security attributes reference describes badPwdCount as a non-replicated attribute maintained separately on each domain controller. It counts bad-password logon attempts in that controller's view; the reference also says zero can mean an unknown value. It recommends querying every DC for a domain-wide view, rather than treating one read as the domain's total.
The Bad-Pwd-Count schema reference adds that the attribute resets on a particular DC when the user successfully logs on to that DC. Consequently, a counter is not a permanent history. A lower value in a later snapshot does not establish that all earlier failures have been investigated.
For triage, retain the separate DC observations before creating any summary. A number without its server and collection time loses the context needed to compare it with another observation. A summary of counters also does not identify distinct attempts within a chosen time interval; use event records to build that timeline.
Lockout is a separate question. Microsoft documents UF_LOCKOUT, value 0x0010, in msDS-User-Account-Control-Computed. Request that calculated attribute explicitly when reviewing current AD DS state. Do not assume a collector's boolean named LockedOut necessarily came from it.
Detection: Collect Comparable Views from Each DC
Identify the account and the collection scope
Start with the finding's account identifier and the original collector endpoint. Record the domain, account DN or GUID, collector version, collection time, requested attributes, and any collection errors. Confirm the DC list against the domain inventory; a two-server example below is not an instruction to omit additional DCs.
The Get-ADUser documentation supports selecting a user by DN, requesting extra properties, and specifying a directory server. These adapted reads use an explicit DC hostname instead of relying on automatic server selection:
Get-ADUser -Identity 'CN=Alex Smith,OU=Users,DC=contoso,DC=com' -Server 'dc01.contoso.com' -Properties badPwdCount,userAccountControl,msDS-User-Account-Control-Computed,lockoutTime -ErrorAction Stop
Get-ADUser -Identity 'CN=Alex Smith,OU=Users,DC=contoso,DC=com' -Server 'dc02.contoso.com' -Properties badPwdCount,userAccountControl,msDS-User-Account-Control-Computed,lockoutTime -ErrorAction Stop
Replace the illustrative DN and hostnames with verified values. Requesting lockoutTime preserves another raw attribute for review; do not use a nonzero value alone as the current-lockout verdict. Interpret the calculated lockout bit only when its value was returned successfully. A missing value is unknown, not false.
For a wider, authorized inventory, Get-ADUser also accepts an LDAP filter. This example retrieves enabled user objects and the relevant attributes from one explicitly selected server. It deliberately does not restrict the population to accounts already above the product threshold:
Get-ADUser -LDAPFilter '(&(objectClass=user)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))' -Server 'dc01.contoso.com' -Properties badPwdCount,userAccountControl,msDS-User-Account-Control-Computed,lockoutTime -ErrorAction Stop
Repeat the read against the other verified DCs within the approved scope. Avoid constructing an LDAP filter by concatenating an unescaped account name. For an individual case, the identity-based reads avoid that interpolation entirely.
Preserve collection quality alongside the values
Use a record with one row per account and DC. The following fields are an operational recommendation, not output that the detector automatically supplies:
| Field | What to record | Why it matters |
|---|---|---|
| Account | DN or GUID, plus the displayed account name | Keeps different server observations attached to the same identity |
| DC | Explicit endpoint and observed server identity where available | Prevents a single endpoint from masquerading as domain coverage |
| Time | Read start/end in UTC | Makes comparison with the event timeline reviewable |
| Attributes | Requested names, returned values, and missing values | Separates a measured value from an absent property |
| Collection result | Success, access failure, timeout, or other error | Stops an unsuccessful query from becoming a reassuring zero |
| Lockout evidence | Raw calculated value and its interpreted lockout bit | Keeps the state calculation separate from the collector's boolean |
If a DC is unavailable, report the gap and retain the successful reads. Do not replace the missing row with zero or silently reduce the declared scope. If the observations differ, preserve both and investigate their timing and provenance before choosing a summary. The objective is an auditable comparison, not making every cell look identical.
Detection: Match Events to the Authentication Path
The LDAP snapshot selects an investigation target; the event timeline supplies the activity context. Verify audit coverage, forwarding, retention, and the systems searched before interpreting an empty result. Choose the review interval from the case timeline and available evidence. The counter itself does not provide that interval.
| Event | Where to look | Evidence to preserve |
|---|---|---|
| 4625: failed logon | Computer where the failed logon occurred: workstation, member server, or DC | Account/domain, Status, SubStatus, LogonType, workstation, address, process, event time and emitting computer |
| 4771: Kerberos pre-authentication failure | DC Security logs | Account/SID, client address and port, failure code, pre-authentication type, time and DC |
| 4776: NTLM credential validation | Authoritative computer; for a domain account, a DC | Account, source workstation, status/error code, time and emitting computer |
Microsoft associates these events with Audit Logon/Account Lockout, Audit Kerberos Authentication Service, and Audit Credential Validation respectively. A DC-only search does not cover every 4625 location. Event 4771 is absent for accounts configured not to require Kerberos preauthentication. Event 4776 covers both successful and failed validation, and identifies the source workstation rather than the destination server or an IP address.
Read the code vocabulary appropriate to each event. For 4625, preserve both status fields rather than flattening them into a generic failure label. In 4771, 0x18 means invalid preauthentication information, with a wrong password documented as a cause. In 4776, 0xC000006A denotes a bad or mistyped password, 0xC0000064 an unknown username, and 0xC0000234 a locked account. A failure caused by lockout is different evidence from the bad-password activity that preceded it.
Build a timeline grouped by account, source, authentication path, and emitting system. Retain references to original records so another analyst can inspect the conclusion. Do not add every row from different event types into one claimed count of independent guesses: first determine which records describe the same authentication flow.
Look beyond the selected account when the sources or timing warrant it. Our password-spraying guide's multi-account and low-and-slow correlation context explains that broader investigation. Here, the first task remains qualifying the directory observation that opened the case.
Remediation: Resolve the Cause Before Closing the Finding
Preserve evidence and test competing explanations
Keep the initial LDAP values and relevant event records before changing credentials or account state. Identify the account owner and applications that use it. Ask about recent password changes, service configuration changes, scheduled jobs, remote access, and the user's reported experience. Use those answers to select systems to inspect, not as a substitute for their logs.
The following decision matrix is a proposed triage method. None of these explanations is computed from badPwdCount alone:
| Working explanation | Evidence to seek | Targeted next action |
|---|---|---|
| User error | User-confirmed activity, expected device, compatible timing and failure reason | Resolve the access problem and monitor the relevant path for recurrence |
| Outdated application secret | Identified workload owner, retries tied to a service or job, configuration/change history | Stop or correct the retrying consumer through its approved change procedure |
| Suspicious activity | Unexpected sources or timing, multiple targets, unexplained access attempts or subsequent behavior | Escalate with the timeline and preserve evidence for incident response |
| Insufficient evidence | Missing DC reads, unavailable attributes, incomplete logs, or uncertain source attribution | Record the gap, assign an owner, and keep the conclusion open |
For a workload using an outdated credential, coordinate the credential change with all known consumers. Updating only one scheduled task while leaving another retrying client unresolved is not a complete fix. Verify the workload's own configuration and subsequent authentication records before attributing the failures to that workload conclusively.
Treat unusual names as a different signal. Our account-naming review distinguishes a static naming indicator from logon activity. A generic account name does not establish what password was attempted, who submitted it, or whether this case is malicious.
Unlock only the reviewed account when appropriate
If the calculated state confirms lockout and the responsible administrator has approved restoring access after reviewing the cause, use a targeted operation. This adapts the identity and server parameters documented for Unlock-ADAccount:
Unlock-ADAccount -Identity 'CN=Alex Smith,OU=Users,DC=contoso,DC=com' -Server 'dc01.contoso.com' -Confirm
This is a state-changing example, not a command executed for this article. Confirm the identity and server before use. Do not pipe the whole inventory into a bulk unlock. An unlock does not correct an outdated application secret or resolve suspicious activity; use the evidence to choose the appropriate response.
Where the investigation establishes compromise or a continuing threat, follow the organization's incident-response procedure for containment, credential changes, and affected systems. A directory counter is not enough to select those actions automatically. Likewise, do not loosen lockout controls just to reduce findings without reviewing their security and availability consequences.
Verify the correction and document the conclusion
Repeat the same directory reads with the same account identity and explicit DCs after the approved change. Retain timestamps and collection quality again. Review the relevant logs over a stated observation period, and have the owner confirm that the affected application or user workflow operates as expected.
Close the case with a supported statement: an identified retrying application was corrected; a user access issue was resolved; activity was escalated as suspicious; or coverage remains incomplete. Record the supporting event references, excluded explanations, missing evidence, actions, owner, and follow-up date. A cleared product threshold is useful context, but the closure should explain the cause and the evidence for the decision.
How EtcSec Detects This
The etc-collector check is PASSWORD_DICT_ATTACK_RISK, category Password, severity Medium. The reviewed Detect() is public/internal/audit/detectors/ad/password/dict-attack-risk.go:24. It excludes users with Disabled=true and selects each remaining user when BadPasswordCount > 3 or LockedOut=true.
Count is the number of selected users, not the sum of failed attempts, the number of source systems, or the number of attacks. Each user contributes once even when both conditions match. The implementation returns the finding even at zero; affected entities are included when details are requested and at least one user is selected.
The provider determines what the check can actually see:
parser.go:164readsbadPwdCountthroughgetIntAttr; that helper at line 290 returns zero for an absent value and ignores errors fromstrconv.Atoi.client.go:460retrieves users from the chosen LDAP context. This path does not aggregate observations from every DC.parser.go:189derivesLockedOutfrom the0x0010bit inuserAccountControl. The default user attribute list does not requestmsDS-User-Account-Control-Computed. AlthoughlockoutTimeis collected, it is not used to calculate this boolean.
That lockout branch therefore must not be presented as a reliable calculated current-lockout assessment for modern AD DS. Independently request the computed attribute when triaging the finding. Preserve a disagreement between the collector boolean and that read rather than treating the collector's label as authoritative.
The check has no time window, authentication source, event correlation, effective-lockout-policy evaluation, or password-quality measurement. The threshold is a product predicate, not an AD DS policy value. Three or fewer recorded failures do not establish safety, and a positive count does not establish an attack.
There is a concrete reason to investigate counters without assuming their cause. The public SafeSpray repository and its main.go source use an LDAP filter containing badPwdCount=0 to select users before SMB authentication attempts. This is evidence of how one public tool uses counters, not proof it ran in your environment or that its lockout-avoidance claim is reliable. The code was read; the tool was not executed.
Explore the identity security pages that support this topic

