🏢Active DirectoryPasswordMonitoring

AD badPwdCount triage: qualify counters before an attack verdict

Qualify AD badPwdCount snapshots by DC, collection quality and authentication logs before deciding whether failures indicate user error or suspicious activity.

Younes AZABARBy Younes AZABAR11 min read
AD badPwdCount triage: qualify counters before an attack verdict

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:

FieldWhat to recordWhy it matters
AccountDN or GUID, plus the displayed account nameKeeps different server observations attached to the same identity
DCExplicit endpoint and observed server identity where availablePrevents a single endpoint from masquerading as domain coverage
TimeRead start/end in UTCMakes comparison with the event timeline reviewable
AttributesRequested names, returned values, and missing valuesSeparates a measured value from an absent property
Collection resultSuccess, access failure, timeout, or other errorStops an unsuccessful query from becoming a reassuring zero
Lockout evidenceRaw calculated value and its interpreted lockout bitKeeps 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.

EventWhere to lookEvidence to preserve
4625: failed logonComputer where the failed logon occurred: workstation, member server, or DCAccount/domain, Status, SubStatus, LogonType, workstation, address, process, event time and emitting computer
4771: Kerberos pre-authentication failureDC Security logsAccount/SID, client address and port, failure code, pre-authentication type, time and DC
4776: NTLM credential validationAuthoritative computer; for a domain account, a DCAccount, 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 explanationEvidence to seekTargeted next action
User errorUser-confirmed activity, expected device, compatible timing and failure reasonResolve the access problem and monitor the relevant path for recurrence
Outdated application secretIdentified workload owner, retries tied to a service or job, configuration/change historyStop or correct the retrying consumer through its approved change procedure
Suspicious activityUnexpected sources or timing, multiple targets, unexplained access attempts or subsequent behaviorEscalate with the timeline and preserve evidence for incident response
Insufficient evidenceMissing DC reads, unavailable attributes, incomplete logs, or uncertain source attributionRecord 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:164 reads badPwdCount through getIntAttr; that helper at line 290 returns zero for an absent value and ignores errors from strconv.Atoi.
  • client.go:460 retrieves users from the chosen LDAP context. This path does not aggregate observations from every DC.
  • parser.go:189 derives LockedOut from the 0x0010 bit in userAccountControl. The default user attribute list does not request msDS-User-Account-Control-Computed. Although lockoutTime is 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