Password policy inference
Before brute-forcing, learn the policy. Filtering rockyou.txt from 14M lines to ~400k policy-compliant passwords is the difference between a feasible attack and a futile one.
# Probe policy by registering accounts with progressively-weaker passwordsQwertyuiop123!@# → accepted: policy is satisfiedQwertyuiop123 → "needs special character"Qwertyiop!@# → "needs digit"qwertyuiop123!@# → "needs uppercase"Qwerty1! → "minimum 12 characters"
# Filter a wordlist to compliant passwords only (12+ chars, mixed case, digit, special)grep -E '^.{12,}$' rockyou.txt | grep '[[:upper:]]' | grep '[[:lower:]]' \ | grep '[0-9]' | grep '[[:punct:]]'Success indicator: a wordlist sized to the actual policy, not the universe of all possible passwords.
Why this matters
Section titled “Why this matters”A standard wordlist (rockyou.txt) is 14 million entries. Brute-forcing against any modern login form - even with rate-limit bypasses - at one attempt per second takes 162 days. Filter to “must have uppercase, lowercase, digit, special, 12+ characters” and you’re at maybe 100k entries, plus most of those won’t be plausible candidates. With targeted filtering, brute-force becomes a 12-hour job instead of a half-year job.
The policy is also a hint about what real users in this organization probably picked - see the Password attacks section below.
Where the policy lives
Section titled “Where the policy lives”Every form that creates or changes a password reveals the policy:
- Registration form - fastest path; just try registering with weak passwords until one’s accepted
- Password change page (post-login) - needs an account first
- Password reset flow - sometimes leaks policy when setting the new password
- API endpoints - JSON validation errors are often verbose
- Embedded policy hints in HTML -
<small>Password must be 12+ chars, mixed case, digit, special</small>is gift-wrapped
Try the registration page first. If self-registration isn’t open, the reset flow or password-change form (after compromising any account) is the next stop.
Policy axes
Section titled “Policy axes”Most policies are combinations of these axes:
| Axis | Common values |
|---|---|
| Minimum length | 6, 8, 10, 12, 14, 16 |
| Maximum length | usually unlimited; sometimes 20, 32, 64 |
| Character classes required | lowercase, uppercase, digit, special (any subset of the four) |
| Number of classes required | ”at least 3 of the 4” patterns are common |
| Repeated characters allowed | usually yes, sometimes “no three in a row” |
| Dictionary check | sometimes - rejects common passwords from a known list |
| Username must not appear | sometimes - admin can’t pick Admin2024! |
| Previous passwords | password-change-only - can’t reuse last 5/10/24 |
The first four are what you’ll see most often. The dictionary check is the surprising one - modern apps (and NIST 800-63B recommendations) push toward “check against a leaked-password list” rather than complexity rules, which both improves security and breaks some brute-force assumptions.
Inferring the policy
Section titled “Inferring the policy”Start from the strongest plausible password, weaken one axis at a time:
1. Qwertyuiop123!@#$% 20 chars, all 4 classes Accepted? → policy is at most this strong2. Qwertyuiop123!@# 15 chars, all 4 classes Accepted? → length max ≥ 153. Qwertyuiop123 13 chars, no specials Rejected → special required4. Qwerty12! 9 chars, all 4 classes Accepted? → min ≤ 95. Qwerty1! 8 chars, all 4 classes Accepted? → min ≤ 86. Qwerty! 7 chars, no digit Rejected → digit required AND min > 77. qwerty12! 8 chars, no uppercase Rejected → uppercase required8. QWERTY12! 8 chars, no lowercase Rejected → lowercase requiredAfter 8 attempts, you know: min length 8, all 4 character classes required, max length ≥ 15.
Keep a matrix
Section titled “Keep a matrix”This goes wrong without a tracking grid. Use this:
| Tried | Password | Lower | Upper | Digit | Special | ≥8 | ≥12 | Accepted |
|---|---|---|---|---|---|---|---|---|
| Yes | qwerty | X | No | |||||
| Yes | Qwerty | X | X | No | ||||
| Yes | Qwerty1 | X | X | X | No | |||
| Yes | Qwerty1! | X | X | X | X | X | ? | |
| Yes | Qwertyuiop1! | X | X | X | X | X | X | ? |
| Yes | QWERTY1! | X | X | X | X | ? | ||
| Yes | qwerty1! | X | X | X | X | ? |
Five to ten well-chosen attempts pin the policy. Note that some apps reveal the full policy in the first error message (“password must be 8+ characters with mixed case, digit, and special”) - read every error response carefully before assuming you need to infer.
Generic error responses
Section titled “Generic error responses”If the response is generic (“password does not meet requirements”) without naming which requirement, infer by elimination. Try password variants that each violate exactly one axis and see which produce errors.
Multi-step disclosure
Section titled “Multi-step disclosure”Some apps reveal one requirement at a time (“password must be at least 8 characters” → fix that, retry → “password must contain a digit”). Walk through them one error at a time until the password is accepted.
Filtering a wordlist to policy
Section titled “Filtering a wordlist to policy”Once the policy is known, filter the base wordlist with grep:
# Minimum 12 charsgrep -E '^.{12,}$' rockyou.txt > /tmp/12plus.txt
# Plus uppercase requiredgrep '[[:upper:]]' /tmp/12plus.txt > /tmp/12plus-upper.txt
# Plus lowercase requiredgrep '[[:lower:]]' /tmp/12plus-upper.txt > /tmp/12plus-mixed.txt
# Plus digit requiredgrep '[0-9]' /tmp/12plus-mixed.txt > /tmp/12plus-mixed-digit.txt
# Plus special requiredgrep -E '[!@#$%^&*()_+={};:,<.>/?\\|`~"-]' /tmp/12plus-mixed-digit.txt \ > /tmp/policy-compliant.txt
wc -l /tmp/policy-compliant.txtA typical chain - starting from rockyou.txt’s 14M entries and filtering down to a strict 12+ chars, mixed-case, digit, special policy - produces about 100-500k entries.
Single-pipeline form
Section titled “Single-pipeline form”grep -E '^.{12,}$' rockyou.txt \ | grep '[[:upper:]]' \ | grep '[[:lower:]]' \ | grep '[0-9]' \ | grep -E '[!@#$%^&*()_+={};:,<.>/?\\|`~"-]' \ > policy-compliant.txtLength ranges
Section titled “Length ranges”The policy might have a max length too - many apps cap at 20 or 32:
# 12-20 charsgrep -E '^.{12,20}$' rockyou.txt | grep '[[:upper:]]' | ...Excluding the username
Section titled “Excluding the username”If the policy bans the username, post-filter:
# Exclude passwords containing the username (case-insensitive)grep -viE '(^|[^a-z])admin([^a-z]|$)' policy-compliant.txt > final.txtPolicy-aware wordlists
Section titled “Policy-aware wordlists”Knowing the policy also tells you what users probably picked. The policy is the lower bound of complexity; real passwords cluster just above it.
If the policy is “8 chars, mixed case, digit, special,” common real-world patterns include:
<CommonWord>1! Spring1!, Summer1!, Winter1!, Autumn1!<CommonWord>123! Password123!, Welcome123!<CompanyName>1! Acmecorp1!, Initech1!<Year>! 2024!, 2025!<Season><Year>! Spring2024!, Summer2024!P@ssw0rd! the granddaddy of weak compliant passwordsWelcome1! onboarding default that "got changed" - to itselfIf the policy is “12 chars” the patterns lengthen but follow the same form:
Password1234!Welcome2024!!CompanyName2024!A bespoke wordlist of 100 entries following these patterns often outperforms a 500k-line filtered rockyou.
Generating organization-specific patterns
Section titled “Generating organization-specific patterns”# Common patterns scoped to "acme corp"cat > /tmp/acme-patterns.txt << 'EOF'Acme2024!Acme2025!Acme1234!ACME2024!Acmecorp1!Acmecorp2024!AcmeCorp1!AcmeCorp2024!Welcome2024!Spring2024!Summer2024!Autumn2024!Winter2024!EOFMix these with cewl output (a wordlist generator that crawls a target site for terms) for an even more targeted set.
NIST 800-63B context
Section titled “NIST 800-63B context”NIST’s modern guidance flipped the conventional wisdom:
- Don’t require composition rules (mixed case, special chars)
- Don’t require periodic rotation
- Do check against a known-leaked-passwords list
- Do encourage long passphrases (length > complexity)
Applications following the new guidance often don’t have a complexity policy at all - they just reject passwords from a leaked-password list. The brute-force implications:
- Wordlist filtering by complexity is useless (no complexity policy to filter against)
- Most rockyou.txt entries are rejected outright as “known leaked”
- New passwords cluster around long-but-unique strings - much harder to brute-force
If you find an application with no apparent complexity policy, test by submitting a known-leaked password like password123 during registration. If it’s rejected, you’re against a modern policy and brute-forcing through a leaked-password list is closed.
Inferring without registering
Section titled “Inferring without registering”When self-registration isn’t available, the password change page (post-login on any account, including your test account if you can create one) reveals the same policy. Same matrix-driven approach.
For applications where you can’t get even one account:
# Sometimes the password policy is in robots.txt, README, or API docscurl -s https://<TARGET>/robots.txtcurl -s https://<TARGET>/api/auth/policy 2>/dev/nullcurl -s https://<TARGET>/help/security-faq
# Or in client-side validation JavaScriptcurl -s https://<TARGET>/login | grep -iE 'password.*length|password.*regex'curl -s https://<TARGET>/js/*.js 2>/dev/null | grep -iE 'password.*length'Modern apps often validate client-side first using a regex visible in the page source. That regex is the policy.
Brute-forcing with the filtered list
Section titled “Brute-forcing with the filtered list”Once you have a compliant wordlist, brute-force with awareness of bruteforce protections:
# Standard form brute-force with CSRF token re-fetch and rate-limit-aware throttlingffuf -w policy-compliant.txt:PASS \ -u https://<TARGET>/login \ -X POST -d "user=admin&pass=PASS" \ -t 1 -p 7 \ -fr "Invalid credentials"Detection-only signals
Section titled “Detection-only signals”# Submit a definitely-invalid password and read every error in the responsecurl -s -X POST -d "user=test&pass=a" \ https://<TARGET>/register | grep -iE '(password|requirement|character|length)'A verbose response page that lists all the requirements is the cheapest possible policy disclosure. Always read the response carefully before inferring through trial-and-error.
- Document the policy. Once inferred, write it down. Engagements that span multiple days waste effort re-deriving the same policy after a break.
- Length bounds matter on both ends. A few apps cap the maximum password length surprisingly low (16 characters) - passwords longer than the max are accepted at registration but silently truncated, leading to weird login failures during testing.
- The “complexity” axis isn’t the only one. Some apps reject passwords containing the username, dictionary words, sequential characters (
abcd,1234), or repeated characters. Each of these requires its own grep filter. - Server-side vs. client-side policy. Sometimes the policy enforced in JavaScript is stricter than the server actually enforces. Bypass the JS and submit a weaker password directly to the endpoint - if it’s accepted, your wordlist filter can be looser.