- Published on
How to Find Users With Bad Password Attempts in Active Directory (PowerShell Script)
A spike in failed logon attempts is one of the earliest signals of either a brute-force attempt or something much more mundane — a saved credential somewhere still retrying an old password. Either way, catching it before it turns into a full account lockout (or a real compromise) means checking BadPwdCount regularly, not just after someone already complains they're locked out.
The quick answer
Get-ADUser -Filter {BadPwdCount -ge 1} -Properties BadPwdCount, LastBadPasswordAttempt, LockedOut
A more useful reporting script
#requires -Modules ActiveDirectory
<#
.SYNOPSIS
Lists Active Directory users with recent bad password attempts.
#>
[CmdletBinding()]
param(
[int]$Threshold = 1,
[string]$SearchBase,
[string]$OutputCsv
)
Import-Module ActiveDirectory -ErrorAction Stop
$params = @{
Filter = { Enabled -eq $true -and BadPwdCount -ge $Threshold }
Properties = @('BadPwdCount', 'LastBadPasswordAttempt', 'LockedOut', 'DistinguishedName')
}
if ($SearchBase) { $params['SearchBase'] = $SearchBase }
$results = Get-ADUser @params |
Select-Object Name, SamAccountName, BadPwdCount, LastBadPasswordAttempt, LockedOut, DistinguishedName |
Sort-Object BadPwdCount -Descending
if (-not $results) {
Write-Host "No users found with $Threshold or more bad password attempts." -ForegroundColor Yellow
return
}
Write-Host "Found $(@($results).Count) user(s) with $Threshold or more bad password attempts." -ForegroundColor Cyan
$results | Format-Table Name, SamAccountName, BadPwdCount, LastBadPasswordAttempt, LockedOut -AutoSize
$lockedCount = @($results | Where-Object LockedOut).Count
if ($lockedCount -gt 0) {
Write-Host "$lockedCount of these account(s) are currently locked out." -ForegroundColor Red
}
if ($OutputCsv) {
$results | Export-Csv -Path $OutputCsv -NoTypeInformation
Write-Host "Exported results to $OutputCsv" -ForegroundColor Green
}
-Threshold 5 raises the bar to only surface accounts with meaningfully repeated failures, cutting the noise from the occasional single mistyped password everyone generates day to day.
Why BadPwdCount isn't a perfect forensic total
Like LastLogon, BadPwdCount is tracked per-DC and not replicated — the value you get back reflects whichever DC (usually the PDC emulator, since that's who most clients validate failed attempts against) answered the query. It's a strong, fast signal for "something's going on with this account," but if you need an exact domain-wide count for an incident writeup, you'd want to check each DC individually the way Get-ADUserLastLogonAllDCs.ps1 does for logon time.
If an account is already locked out
This script tells you who has bad attempts; if you need to know which computer is generating them, Get-ADAccountLockoutSource.ps1 in the same repo checks the Security log across every DC for the source machine.
Where the manual approach runs out of road
A one-off script is fine for a single check. It starts to hurt once you actually need to run this regularly:
- No scheduling. Cron/Task Scheduler can run the script, but now you own the scheduling, the credentials it runs as, and what happens when it silently fails.
- No history. A CSV export is a snapshot. Was this the same five accounts as last month, or a growing list? A single export can't tell you.
- No distribution. Getting the report to the right people (security, IT ops, compliance) on a schedule means building that plumbing yourself.
- Multi-domain/multi-forest pain. Run it once per domain, reconcile the results yourself, and hope naming/OU conventions are consistent across all of them.
- No alerting. If something changes unexpectedly between runs — an account re-enabled, a privileged group gaining a member — nothing tells you until you happen to run the script again.
None of that is a PowerShell problem. It's what turns a script into a product.
What SysFlint AD does instead
Our SysFlint AD runs the same kind of discovery shown above automatically, on a schedule, entirely inside your own network. No agents on domain controllers, no data leaving your environment. It's free, forever.
- Stale and inactive account detection — Enabled accounts that nobody has logged into for 30, 60, or 90+ days — the ones that are still a live attack surface.
- Scheduled reports by email — Daily, weekly, or monthly runs delivered to the right inbox automatically. No Task Scheduler job for you to babysit.
- Multi-domain and forest coverage — Enumerate every domain in the forest in one pass and get one consolidated report instead of one per domain.
If you're currently doing this with a script on a scheduled task, here's the honest comparison between the two, or go straight to the download page.
Get this script, and the rest of them
The script above is part of awesome-it-scripts, SysFlint's free, open-source library of PowerShell scripts for Active Directory and other IT-admin tasks — no signup, no catch, MIT licensed. Grab this one directly from active-directory/Find-ADUsersBadPasswordAttempts.ps1, or browse the whole thing.
Find every disabled/locked-out/stale/privileged-account script we've published (plus the FAQ that goes with each one) in the repo's active-directory folder. Star it if it's useful — new scripts land there regularly.