ADPWD-013: Custom Dictionary Password Check

Platform
Active Directory
Category
AD Password & Lockout Policies
Severity
Medium
Zero Trust pillar
Identity (weight 3)
Golden fixtures
3
Branch coverage
Observed: fixtures prove the verdicts they exercise
Provenance
baseline

What it checks

The check operates on a domain-replicated copy of the NTDS.dit hash store and compares NT hashes against a generated custom dictionary. Guerrilla: - Pulls NT hashes via DSInternals (Get-ADReplAccount), which streams DRSUAPI GetNCChanges output without writing the full ntds.dit to disk. Requires DS-Replication-Get-Changes-All on the domain. - Builds the custom dictionary in memory from: the NetBIOS and DNS domain name, the configured organization name (CN of the domain), every published OU name above the user, seasons (Spring, Summer, Fall, Autumn, Winter), the current and previous two years, the months, and a caller-supplied corpus file (product names, building codes, mascot, ticker symbol, city, ZIP). - Mangles each base word with the common patterns seen in offline cracking corpora: capitalize first letter, append 1 through 9, append ! @ # $, append two-digit and four-digit years, append a season-and-year concatenation, l33t substitution (a to @, e to 3, i to 1, o to 0, s to $), and short keyboard-walk suffixes (qwerty, asdf, 123, 1qaz). - Hashes every candidate to NT (MD4 of UTF-16LE) and compares against the replicated hash table. A match means an account is using a password that is in the custom dictionary, regardless of whether the password satisfies length and complexity policy. - Cross-references the matched samAccountName against userAccountControl to flag privileged matches (Domain Admins, Enterprise Admins, krbtgt, service accounts with SPNs) for immediate attention. The check does not crack arbitrary hashes; it only confirms membership in a targeted, organization-specific wordlist. That makes it cheap (seconds per thousand users) and safe to run regularly.

Why it matters

Microsoft complexity is a per-character category test, not a strength test. Welcome01 has an uppercase letter, a lowercase letter, and a digit, satisfies complexity, satisfies an 8-character minimum, and is the single most-sprayed Windows password on the internet. Contoso2026!, Summer2025!, and Q1-Q4 quarter passwords pass every default control yet show up at the top of every cracked-hash dump. The exposure has three layers: 1. External password spraying: an attacker with a username list (collected from LinkedIn, MX records, or unauthenticated LDAP via ADIDNS) sprays a small custom wordlist against Entra ID, ADFS, OWA, or VPN endpoints. One match yields a domain-authenticated foothold. MITRE T1110.003. 2. Internal offline cracking: an attacker who reaches a Domain Controller or backup of NTDS.dit (T1003.003) cracks hashes offline. Organization-specific patterns are the first wordlist tried by every offensive operator because they recover the most hashes per CPU-second. 3. Privileged-account spray: service accounts, helpdesk accounts, and break-glass admins are frequently set with predictable passwords (Service2025!, Helpdesk!) and exempted from password expiry. A single match on a Tier-0 account is forest-takeover. Closing the gap requires a banned-list filter at password-set time (Azure AD Password Protection on-premises agent, or a third-party filter DLL). ADPWD-013 is the audit that proves the filter is needed and measures its effectiveness after deployment.

Attack path

1. Username enumeration: the attacker harvests samAccountName values from LinkedIn scraping, breached-credential databases, MX-record probing, or, on poorly hardened tenants, unauthenticated Entra ID enumeration (AADInternals Invoke-AADIntUserEnumerationAsOutsider). 2. Custom dictionary build: the attacker scrapes the targets website, annual report, and job postings for product names, office locations, mascot, and CEO surname. A small wordlist (50 to 500 base words) is generated and mangled with the same rules Guerrilla uses internally (capitalize, append year, append special character, season-year). 3. Low-and-slow spray: the attacker sprays one to three passwords per account per lockout window using MSOLSpray, TrevorSpray, or Kerbrute against Entra ID, ADFS, or KDC. Lockout thresholds are respected to avoid alarming the SOC. 4. Foothold: one in 50 to one in 200 accounts typically matches on the first round (industry baseline from SpecterOps, Mandiant, and Verizon DBIR). The attacker authenticates and enumerates the directory. 5. Privilege escalation: if a matched account is a Tier-1 admin or a service account with constrained delegation, the attacker pivots directly. Otherwise the attacker uses the foothold to dump NTDS.dit (via DCSync if the account has replication rights, or via a DC compromise chain) and runs the same custom dictionary offline against every NT hash. Hashcat with -a 0 -m 1000 against a 500-word custom dictionary cracks tens of thousands of hashes in minutes on a single GPU. 6. Persistence: every cracked Tier-0 hash becomes a long-term credential because password rotation alone does not address the underlying pattern.

How Guerrilla assesses it

Guerrilla executes Get-ADReplAccount -All -Server <DC> -NamingContext (Get-ADRootDSE).defaultNamingContext via the DSInternals module to stream NT hashes from a writable Domain Controller. The replication call requires the running principal to hold DS-Replication-Get-Changes and DS-Replication-Get-Changes-All on the domain head; the check refuses to run otherwise. The custom dictionary is assembled from (Get-ADDomain).NetBIOSName, (Get-ADDomain).DNSRoot, the optional -CustomWordList file passed by the caller, plus the built-in seasons, months, and year corpus. Each base word is mangled with the SetTransform pipeline in DSInternals (Test-PasswordQuality -WeakPasswordsFile) and the resulting NT-hash candidates are compared in-memory against the replicated hash set. The finding records samAccountName, the matched dictionary entry, the mangling rule that produced the hit, the accounts userAccountControl flags, group memberships (Domain Admins, Enterprise Admins, Schema Admins, Account Operators), pwdLastSet, and lastLogonTimestamp. Plaintext passwords are never written to the report; only the dictionary base word and the rule name are recorded so the operator can confirm the pattern and the user can be re-trained.

Recommended value

No accounts using passwords containing organization-specific terms, common patterns (Season+Year), or keyboard walks

Remediation

Build a custom dictionary including company names, product names, location names, seasons, and common patterns. Test password hashes against this dictionary using DSInternals or similar tools. Force password changes on matching accounts. Deploy custom password filters or Azure AD Password Protection custom banned password list

Fixture-proven verdicts

Every verdict below is proven by a golden fixture in the module's gating test suite. This table derives from the last green run; it cannot be edited by hand.

Verdict scenarios for ADPWD-013
ScenarioExpected verdict
cleanPASS
known-badFAIL
no-dataNot Assessed

Framework mappings

NIST SP 800-53
IA-5(1)
CIS AD Benchmark
5.5.4
MITRE ATT&CK
T1110.001, T1110.003