Account lockout mechanisms are designed to protect against brute-force password attacks by temporarily or permanently locking accounts after a certain number of failed login attempts. This test evaluates whether the lockout mechanism is properly implemented and cannot be bypassed. Weak or missing lockout mechanisms allow attackers to perform unlimited password guessing attempts.
What to Check
Lockout Implementation
Lockout threshold exists
Lockout duration appropriate
Lockout applies to all authentication methods
Lockout cannot be bypassed
Account enumeration via lockout
Denial of service via lockout
Bypass Techniques
Technique
Description
IP rotation
Different IPs reset counter
Session rotation
New sessions reset counter
Username variation
Case changes bypass counter
Simultaneous attacks
Race conditions
Alternative endpoints
Different login paths
How to Test
Step 1: Determine Lockout Threshold
#!/bin/bash
TARGET="https://target.com/api/login"
USERNAME="testuser"echo"Testing lockout threshold..."for i in {1..20}; do
response=$(curl -s -X POST "$TARGET" \
-H "Content-Type: application/json" \
-d "{\"username\":\"$USERNAME\",\"password\":\"wrongpass\"}")
| grep -qi ;
$i
echo
"Attempt $i: $response"
# Check for lockout indicators
if
echo
"$response"
"locked\|blocked\|too many"
then
echo
"[+] Account locked after $i attempts"
break
fi
done
Step 2: Test Lockout Duration
#!/bin/bash# After account is locked, test how long until it unlocks
TARGET="https://target.com/api/login"
USERNAME="testuser"
CORRECT_PASSWORD="correctpassword"# First, lock the accountfor i in {1..10}; do
curl -s -X POST "$TARGET" \
-H "Content-Type: application/json" \
-d "{\"username\":\"$USERNAME\",\"password\":\"wrong\"}" > /dev/null
doneecho"Account should be locked. Testing unlock timing..."# Test every minutefor minute in {1..30}; dosleep 60
response=$(curl -s -X POST "$TARGET" \
-H "Content-Type: application/json" \
-d "{\"username\":\"$USERNAME\",\"password\":\"$CORRECT_PASSWORD\"}")
ifecho"$response" | grep -qi "success\|token"; thenecho"[+] Account unlocked after $minute minutes"breakelseecho"Minute $minute: Still locked"fidone
Step 3: Test IP-Based Bypass
#!/bin/bash# Test if lockout is per-IP or per-account
TARGET="https://target.com/api/login"
USERNAME="testuser"# Use different X-Forwarded-For headers
ips=("1.1.1.1""2.2.2.2""3.3.3.3""4.4.4.4""5.5.5.5")
for ip in"${ips[@]}"; dofor i in {1..10}; do
curl -s -X POST "$TARGET" \
-H "Content-Type: application/json" \
-H "X-Forwarded-For: $ip" \
-d "{\"username\":\"$USERNAME\",\"password\":\"wrong$i\"}" > /dev/null
doneecho"Sent 10 attempts from $ip"done# Now test if account is locked
response=$(curl -s -X POST "$TARGET" \
-H "Content-Type: application/json" \
-d "{\"username\":\"$USERNAME\",\"password\":\"wrongtest\"}")
ifecho"$response" | grep -qi "locked"; thenecho"[+] Account is locked (proper implementation)"elseecho"[!] Account not locked - IP-based lockout may be bypassable"fi
Step 4: Test Session-Based Bypass
#!/bin/bash
TARGET="https://target.com/login"
USERNAME="testuser"# Try multiple sessionsfor session in {1..5}; doecho"Session $session..."for i in {1..5}; do
curl -s -X POST "$TARGET" \
-H "Content-Type: application/x-www-form-urlencoded" \
-c cookies_$session.txt \
-b cookies_$session.txt \
-d "username=$USERNAME&password=wrong$i" > /dev/null
donerm cookies_$session.txt
done# Test if still locked
response=$(curl -s -X POST "$TARGET" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=$USERNAME&password=test")
echo"Final response: $response"
Step 5: Test Username Variation Bypass
#!/bin/bash
TARGET="https://target.com/api/login"# Test username case sensitivity
usernames=("admin""Admin""ADMIN""aDmIn"" admin""admin ")
for username in"${usernames[@]}"; dofor i in {1..5}; do
curl -s -X POST "$TARGET" \
-H "Content-Type: application/json" \
-d "{\"username\":\"$username\",\"password\":\"wrong$i\"}" > /dev/null
doneecho"Sent 5 attempts for: '$username'"done# Total: 30 attempts if all treated as same user# Check if any version is lockedfor username in"${usernames[@]}"; do
response=$(curl -s -X POST "$TARGET" \
-H "Content-Type: application/json" \
-d "{\"username\":\"$username\",\"password\":\"test\"}")
echo"'$username': $response" | head -c 100
echo""done
Step 6: Test Alternative Login Endpoints
# Test if lockout applies to all login methods
endpoints=(
"/login""/api/login""/api/v1/login""/api/v2/auth""/oauth/token""/auth/login""/mobile/login"
)
USERNAME="testuser"# Lock account on main endpointfor i in {1..10}; do
curl -s -X POST "https://target.com/login" \
-d "username=$USERNAME&password=wrong" > /dev/null
done# Try other endpointsfor endpoint in"${endpoints[@]}"; do
response=$(curl -s -X POST "https://target.com$endpoint" \
-H "Content-Type: application/json" \
-d "{\"username\":\"$USERNAME\",\"password\":\"test\"}" \
-w "\n%{http_code}")
echo"$endpoint: $response" | tail -2
done
Step 7: Test Account Lockout Enumeration
#!/bin/bash# Test if lockout behavior reveals valid accounts
TARGET="https://target.com/api/login"# Test with valid usernameecho"Testing with potentially valid username..."for i in {1..10}; do
curl -s -X POST "$TARGET" \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"wrong"}' > /dev/null
done
valid_response=$(curl -s -X POST "$TARGET" \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"wrong"}')
# Test with invalid usernameecho"Testing with invalid username..."for i in {1..10}; do
curl -s -X POST "$TARGET" \
-H "Content-Type: application/json" \
-d '{"username":"definitelynotauser12345","password":"wrong"}' > /dev/null
done
invalid_response=$(curl -s -X POST "$TARGET" \
-H "Content-Type: application/json" \
-d '{"username":"definitelynotauser12345","password":"wrong"}')
echo"Valid username response: $valid_response"echo"Invalid username response: $invalid_response"# If responses are different, accounts can be enumerated
Tools
Brute Force Testing
Tool
Description
Usage
Hydra
Fast brute-forcer
hydra -l admin -P wordlist.txt target http-post
Burp Intruder
Request repeater
Test lockout behavior
Custom scripts
Targeted testing
Control timing and sources
Analysis
Tool
Description
Burp Suite
Response comparison
OWASP ZAP
Automated scanning
Example Commands/Payloads
Lockout Testing Script
#!/usr/bin/env python3import requests
import time
from concurrent.futures import ThreadPoolExecutor
classLockoutTester:
def__init__(self, login_url, username):
self.url = login_url
self.username = username
self.results = {}
defattempt_login(self, password, headers=None):
"""Single login attempt"""try:
response = requests.post(
self.url,
json={"username": self.username, "password": password},
headers=headers or {},
timeout=10
)
return response.status_code, response.text
except Exception as e:
returnNone, str(e)
deffind_lockout_threshold(self, max_attempts=30):
"""Find number of attempts before lockout"""print(f"Finding lockout threshold for {self.username}...")
for i inrange(1, max_attempts + 1):
status, text = self.attempt_login(f"wrongpassword{i}")
if"locked"in text.lower() or"too many"in text.lower():
self.results["threshold"] = i
print(f"[+] Lockout after {i} attempts")
return i
time.sleep(0.5) # Small delayself.results["threshold"] = f">{max_attempts}"print(f"[!] No lockout after {max_attempts} attempts")
returnNonedeftest_ip_bypass(self, attempts_per_ip=5):
"""Test if X-Forwarded-For bypasses lockout"""print("Testing IP-based bypass...")
fake_ips = [f"10.0.0.{i}"for i inrange(1, 11)]
for ip in fake_ips:
for i inrange(attempts_per_ip):
self.attempt_login(
f"wrong{i}",
headers={"X-Forwarded-For": ip}
)
# Test if account is now locked
status, text = self.attempt_login("test")
if"locked"in text.lower():
self.results["ip_bypass"] = Falseprint("[+] IP bypass NOT possible - lockout is per-account")
else:
self.results["ip_bypass"] = Trueprint("[!] IP bypass MAY be possible")
deftest_concurrent_bypass(self, num_threads=10):
"""Test race condition in lockout"""print("Testing concurrent request bypass...")
defattempt():
returnself.attempt_login("wrongpassword")
with ThreadPoolExecutor(max_workers=num_threads) as executor:
futures = [executor.submit(attempt) for _ inrange(50)]
results = [f.result() for f in futures]
# Check final state
success_count = sum(1for s, t in results if s == 200and"locked"notin t.lower())
self.results["concurrent_bypass"] = success_count > 10print(f"Successful attempts during race: {success_count}")
deftest_username_variation(self):
"""Test if username case affects lockout"""print("Testing username variation bypass...")
variations = [
self.username,
self.username.upper(),
self.username.lower(),
self.username.capitalize(),
f" {self.username}",
f"{self.username} "
]
for var in variations:
for i inrange(3):
requests.post(
self.url,
json={"username": var, "password": f"wrong{i}"},
timeout=10
)
# Test original username
status, text = self.attempt_login("test")
if"locked"in text.lower():
self.results["username_bypass"] = Falseprint("[+] Username normalization working correctly")
else:
self.results["username_bypass"] = Trueprint("[!] Username variation may bypass lockout")
defgenerate_report(self):
"""Generate test report"""print("\n=== LOCKOUT MECHANISM TEST REPORT ===\n")
for test, result inself.results.items():
status = "[VULN]"if result isTrueor result == ">30"else"[OK]"print(f"{status}{test}: {result}")
# Usage
tester = LockoutTester("https://target.com/api/login", "testuser")
tester.find_lockout_threshold()
tester.test_ip_bypass()
tester.test_concurrent_bypass()
tester.test_username_variation()
tester.generate_report()
deflogin():
username = request.json.get('username', '')
password = request.json.get('password', '')
# Always check lockout, even for non-existent usersif lockout.is_locked(username):
# Generic message - don't reveal account existencereturn jsonify({
"error": "Account temporarily locked",
"retry_after": lockout.LOCKOUT_DURATION
}), 429
user = authenticate(username, password)
ifnot user:
# Record failure even for non-existent users# Prevents enumeration via different lockout behavior
lockout.record_failure(username)
# Generic error messagereturn jsonify({"error": "Invalid credentials"}), 401return jsonify({"token": generate_token(user)})
3. Prevent DoS via Lockout
classSmartLockout:
"""Prevent attackers from locking out legitimate users"""defrecord_failure(self, username, ip_address):
# Track both account and IP
account_attempts = self.get_account_attempts(username)
ip_attempts = self.get_ip_attempts(ip_address)
# If single IP causing all attempts, don't lock accountif ip_attempts >= 10and account_attempts < 3:
# Block IP, not accountself.block_ip(ip_address)
return"ip_blocked"# Normal lockoutif account_attempts >= 5:
self.lock_account(username)
return"account_locked"return"attempt_recorded"
Risk Assessment
CVSS Score
Finding
CVSS
Severity
No account lockout
7.5
High
Lockout bypass via IP rotation
6.5
Medium
Lockout bypass via username variation
6.5
Medium
Account enumeration via lockout
5.3
Medium
Very high lockout threshold (>20)
5.3
Medium
CWE Categories
CWE ID
Title
Description
CWE-307
Improper Restriction of Excessive Authentication Attempts