⚠️ AUTHORIZED PENETRATION TESTING USE ONLY Unauthorized access to computer systems is illegal. Ensure you have written authorization before testing.
A Python-based penetration testing tool that bypasses Anti-CSRF token mechanisms to perform brute-force authentication attacks. Built for DVWA (Damn Vulnerable Web Application) with full security-level awareness, live detection (no static per-level assumptions), and WAF/Bot-Management-aware response analysis.
| Framework | Mapping |
|---|---|
| MITRE ATT&CK | T1110.001 (Brute Force), T1090.002 (Proxy Rotation), T1036.005 (UA Masquerading) |
| Cyber Kill Chain | Phase 5 — Exploitation |
| PTES | Exploitation Phase |
Nothing about credentials, HTTP method, or WAF markers is hardcoded in source anymore. Everything is either live-detected against the actual target or supplied explicitly via CLI flags.
| Area | v3.0 behavior | v3.1 / v3.2 behavior |
|---|---|---|
| Login credentials | dvwa_auto_login() defaulted to admin/password |
No default at all. --login-user/--login-pass required (or --cookie, or --no-auto-login) |
| Auto-login failure | Logged, then silently continued the attack against an unauthenticated session | Fail-fast (SystemExit) by default; --continue-on-auth-fail to opt back into the old behavior |
| CSRF token presence | Assumed from the -s preset table (Low/Medium = no token, High/Impossible = token) |
Always probed live from the actual page every attempt |
| HTTP method | Assumed from the -s preset table |
Detected live from <form method=...> every attempt; -m still forces an override |
| WAF/Bot block pages | Not detected at all (counted as a wrong password) | Detected via HTTP status (--waf-block-status) and body markers loaded from an operator-supplied --waf-markers file — no markers baked into source |
| Connection refused/reset | Treated as a generic network error, retried forever with a flat 5s sleep | Recognized as a likely WAF/Bot-Management IP quarantine signal; growing backoff, aborts after --conn-block-retries |
| "Every attempt looks identical" | No detection | --stale-session-threshold consecutive no-match responses trigger an explicit warning instead of silently grinding through the whole wordlist |
The -s/--security preset table survives only as reference/fallback: it prints the documented Defense/Bypass text for that level, and supplies a fallback value only if a live probe genuinely can't find a <form> at all. It no longer decides what request the script actually sends.
Level Matrix (informational — method/token below are what a stock DVWA install uses; the script detects the real ones live, they can differ on your instance)
| Level | HTTP Method | CSRF Token | Server Delay | Lockout | Brute-Force Viable? |
|---|---|---|---|---|---|
| Low | GET |
❌ None | None | None | ✅ Yes — trivial |
| Medium | GET |
❌ None | sleep(2) |
None | ✅ Yes — just slower |
| High | GET |
✅ user_token |
sleep(0-3) random |
None | ✅ Yes — token is trivially automated |
| Impossible | POST |
✅ user_token |
None | 3 failures → 15 min lock | ❌ No — wrong attack vector |
- Defense: None
- What to learn: Basic brute-force mechanics — wordlist iteration, HTTP request construction, success/failure detection
- Bypass: Direct GET request with
username+password+Loginparameters
- Defense:
sleep(2)on every failed login attempt (server-side) - What to learn: How speed/rate-based defenses work. The delay is server-side and cannot be bypassed from the client. Conceptually, parallel sessions from different IPs could help.
- Bypass: Identical to Low, just 2 seconds slower per attempt. With 14M passwords (rockyou.txt), this turns a days-long attack into a months-long one.
- Defense: Anti-CSRF token (
user_token) embedded as a hidden form field +sleep(rand(0,3)) - What to learn: The CSRF token is NOT a real brute-force barrier. It's a defense against Cross-Site Request Forgery (a different attack), not against direct brute-forcing. Since the attacker can simply GET the page, read the token, and include it in their request, it's trivially automated.
- Bypass: Fetch fresh token per request (GET → regex extract → inject into payload). This is the level best suited to the "penetration" scenario — a genuine vulnerability exists behind a false sense of security.
- Defense: Account lockout after 3 failed attempts (15-minute cooldown), CSRF token, POST method, PDO prepared statements (no SQLi)
- What to learn: This is a properly defended brute-force mechanism. The lockout is tied to the account, not just the source IP — proxy rotation can't bypass it.
- Why brute-force is wrong here: Even with 1000 proxies, the account itself locks after 3 failures. You'd need to wait 15 minutes between every 3 attempts. With rockyou.txt that's ~70 years.
- Better approach: Analyze for DoS potential — can an attacker intentionally lock a legitimate user's account by sending 3 bad attempts? That's a denial-of-service vulnerability in the lockout mechanism itself.
--login-user/--login-pass authenticate the script's own session. They are always required unless you pass --cookie (inject an existing session) or --no-auto-login (target needs no login at all). This is deliberately separate from -u/--username, which is the brute-force target — the account whose password you're actually trying to crack.
python3 csrf_brute.py \
-t http://127.0.0.1:4280/vulnerabilities/brute/ \
-u gordonb \
-w wordlists/sample.txt \
-s low \
--login-user admin --login-pass '<known-admin-password>'# Same as Low but expect ~2s per failed attempt (server-side delay)
python3 csrf_brute.py \
-t http://127.0.0.1:4280/vulnerabilities/brute/ \
-u gordonb \
-w wordlists/sample.txt \
-s medium \
--login-user admin --login-pass '<known-admin-password>'# Token is auto-extracted and injected per request — same command works
# whether or not the target actually has a token, method/token are
# detected live either way
python3 csrf_brute.py \
-t http://127.0.0.1:4280/vulnerabilities/brute/ \
-u gordonb \
-w wordlists/sample.txt \
-s high \
--login-user admin --login-pass '<known-admin-password>' -v# ⚠ Brute-force is the WRONG attack vector here
python3 csrf_brute.py \
-t http://127.0.0.1:4280/vulnerabilities/brute/ \
-u gordonb \
-w wordlists/sample.txt \
-s impossible \
--login-user admin --login-pass '<known-admin-password>'# Copy cookies from DevTools → Application → Cookies
python3 csrf_brute.py \
-t http://127.0.0.1:4280/vulnerabilities/brute/ \
-u gordonb -w wordlists/sample.txt \
--cookie "PHPSESSID=abc123; security=high" \
--no-auto-login# waf_markers.txt is a starting template — populate it from a real observed
# block page for your WAF before relying on it (see comments in the file)
python3 csrf_brute.py \
-t http://target/vulnerabilities/brute/ \
-u gordonb -w wordlists/sample.txt \
-s high --login-user admin --login-pass '<known-admin-password>' \
--waf-markers waf_markers.txt \
--waf-block-status 403,999 \
--conn-block-retries 3 --conn-block-backoff 45 \
-vpython3 csrf_brute.py \
-t http://127.0.0.1:4280/vulnerabilities/brute/ \
-u gordonb \
-w /usr/share/wordlists/rockyou.txt \
-s high --login-user admin --login-pass '<known-admin-password>' \
--jitter-min 0.1 --jitter-max 0.3python3 csrf_brute.py \
-t http://127.0.0.1:4280/vulnerabilities/brute/ \
-u gordonb -w wordlists/sample.txt \
-s high --login-user admin --login-pass '<known-admin-password>' \
--proxy http://127.0.0.1:8080 -vpython3 csrf_brute.py \
-t http://target.local/auth/login \
-u admin -w rockyou.txt \
--login-user admin --login-pass '<known-password>' \
-m POST \
--token-name csrf_token \
--failure "Invalid credentials" \
--success "Dashboard"python3 csrf_brute.py \
-t http://127.0.0.1:4280/vulnerabilities/brute/ \
-u gordonb -w wordlists/sample.txt -s low \
--login-user admin --login-pass '<known-admin-password>' \
-o cracked.txt# A previous run against a 14M-line wordlist got killed around line 1400 —
# don't redo everything from the top, pick up where it left off. The line
# number is a RAW file line number (same as `wc -l`/a text editor), not a
# count of attempts made (comment/blank lines in the file count too).
python3 csrf_brute.py \
-t http://127.0.0.1:4280/vulnerabilities/brute/ \
-u admin -w /usr/share/wordlists/rockyou.txt \
-s low --login-user gordonb --login-pass abc123 \
--resume-from 1400pip install requests| Flag | Description | Default |
|---|---|---|
-t, --target |
Full target URL | required |
-u, --username |
Brute-force target username | required |
-w, --wordlist |
Password wordlist | required |
--resume-from |
1-indexed raw line number in the wordlist to start from (same numbering as wc -l) — skip lines already tried in a killed/interrupted run |
1 (no skip) |
-s, --security |
DVWA level: low medium high impossible — informational only, method/token are always live-detected |
None |
-m, --method |
Force HTTP method, overriding live <form method=...> detection |
Auto-detected live |
--login-user |
Username to authenticate the script's own session (auto-login) | required unless --cookie/--no-auto-login |
--login-pass |
Password for --login-user |
required unless --cookie/--no-auto-login |
--no-auto-login |
Skip the DVWA auto-login step entirely | off |
--continue-on-auth-fail |
Don't abort when auto-login fails; continue anyway | off (fail-fast by default) |
-c, --cookie |
Inject browser cookies instead of auto-login | None |
--no-token |
Skip CSRF token extraction entirely | off (always probed live) |
--token-name |
CSRF token field name | user_token |
--failure |
Failure string in response | Username and/or password incorrect |
--success |
Success string in response | Welcome to the password protected area |
--waf-markers |
Path to a file of WAF/Bot block-page substrings (one per line, # comments allowed) |
None (body-content WAF detection disabled) |
--waf-block-status |
Comma-separated HTTP status codes treated as a WAF/Bot block | 403,999 |
--stale-session-threshold |
Warn after this many consecutive no-match responses (likely dead session) | 5 |
--conn-block-retries |
Abort after this many consecutive connection-refused/reset errors | 3 |
--conn-block-backoff |
Seconds to back off after a connection error, × consecutive-failure count | 30.0 |
--jitter-min |
Min delay between requests (sec) | 0.5 |
--jitter-max |
Max delay between requests (sec) | 2.0 |
--no-ua-rotate |
Disable User-Agent rotation | off |
--proxy |
Single HTTP proxy | None |
--proxy-list |
Proxy list file for rotation | None |
--lockout-after |
Rotate proxy after N attempts | 2 |
-o, --output |
File to append cracked credentials | None |
-v, --verbose |
Show step-level debug output | off |
┌───────────────────────────────────────────────────────────────┐
│ STEP 0: DVWA Auth (--login-user/--login-pass, or --cookie) │
│ Auto-login failure → fail-fast SystemExit by default │
│ ↓ │
│ STEP 1: Session Init + security cookie override │
│ ↓ │
│ STEP 2: GET page (fetch live HTML) ←──┐ │
│ ↓ │ │
│ STEP 3: Extract CSRF token (live probe) + detect method │ │
│ from live <form method=...> — no static assumption │ │
│ ↓ │ │
│ STEP 4: Build payload (creds + token + Login) │ │
│ ↓ │ │
│ STEP 5: Execute GET/POST + evasion (jitter, UA rotation) │ │
│ ↓ │ │
│ STEP 6: Analyze response │ │
│ → success / failure / WAF-block / unmatched │ │
│ → N consecutive "unmatched" triggers a warning │ │
│ ↓ │ │
│ STEP 7: Reset token → Loop back to Step 2 ──────────────────┘ │
│ │
│ Connection refused/reset at any point → NOT a generic error, │
│ treated as a possible WAF/Bot IP quarantine: growing backoff,│
│ abort after --conn-block-retries │
└───────────────────────────────────────────────────────────────┘
The CSRF token is 100% dynamic and automated. Every iteration:
- GET → Fresh HTML fetched from server
- Regex →
user_tokennonce extracted from<input type="hidden">, and the<form method=...>attribute is read in the same pass - Inject → Token placed into GET/POST payload
- Consume → Server validates and invalidates the token
- Discard → Old token deleted, loop restarts
If no token is found on the live page, the script proceeds without one — it never assumes a level "doesn't have a token" from a static table.
Root cause: DVWA's navigation sidebar contains a Logout link on every authenticated page. The old heuristic matched "logout" in the body → 100% false success rate.
Fix: Strict matching — only the verified success string triggers success. No more heuristic fallbacks.
Root cause: DVWA sets security=impossible as a server cookie on initial page load. Python's session.cookies.set("security", "low") created a duplicate cookie instead of overwriting. The server's impossible value always took precedence.
Fix: Clear all existing security cookies from the jar before setting the desired level.
Root cause: DVWA's brute force page uses <form method="GET"> but the script was sending POST. Server ignored the POST body entirely.
Fix: Level-aware method selection (Low/Medium/High → GET, Impossible → POST). Superseded in v3.2 by live <form> detection, since the level-aware table itself turned out to be an assumption that doesn't hold on every DVWA install.
Root cause: Some DVWA installs (e.g. certain digininja/DVWA manual setups) embed user_token on every security level, not just High/Impossible. Trusting DVWA_PRESETS["low"]["has_token"] = False meant the script never sent a token Low/Medium actually required, and the server silently rejected every attempt.
Fix: Token presence is now probed live on every attempt (required=False) instead of trusted from the table.
Root cause: The script sends the password as a GET query parameter; a wordlist entry resembling an attack payload (' OR 1=1--, <script>, ../, ;id) can trip a WAF's signature rules. The response was indistinguishable from a normal "wrong password" failure.
Fix: analyze_response() checks HTTP status (--waf-block-status) and operator-supplied body markers (--waf-markers) before falling through to the normal success/failure check.
Root cause: dvwa_auto_login() defaulted to admin/password. When that credential was wrong for the target instance, the login failure was logged but its return value was never checked — init_session() returned a session that had never actually authenticated, and the attack loop ran anyway, producing an identical byte-for-byte response (the login page, redirected to) for every single wordlist entry.
Fix: No default credentials — --login-user/--login-pass required explicitly. init_session() now checks the login result and aborts (SystemExit) by default on failure. A running counter of consecutive "no known string matched" responses also triggers an explicit warning (--stale-session-threshold), so a dead session is caught even if it happens mid-run rather than at startup.
Root cause: Observed live against a NetScaler ADC in front of a WAF/Bot-Management-protected DVWA instance — after a burst of non-browser-shaped requests, the ADC stopped completing the TCP handshake from the source IP entirely (ConnectionError, no HTTP response, no block page). The old code caught this as a generic requests.RequestException, slept a flat 5 seconds, and retried indefinitely — likely extending the quarantine.
Fix: ConnectionError is now caught distinctly from other request exceptions, with a growing backoff (--conn-block-backoff) and a hard abort after --conn-block-retries consecutive failures, with an explicit message pointing at the likely cause.
Root cause: login_url = target_url.rstrip("/") stripped the trailing slash every DVWA doc/example uses (.../vulnerabilities/brute/ → .../vulnerabilities/brute). Found live against dvwa.local: the no-slash path doesn't resolve to the vulnerable page on that install — it redirects straight to the DVWA "Welcome" home page (no <form> at all). Every attempt then hit that same redirected page and produced an identical response — the exact same symptom as the silent-auth-failure bug above, but with a completely unrelated root cause. This one is easy to miss because the login itself succeeds and nothing errors; it only shows up as "every attempt looks the same" once you're actually looking at the response body (-v, or the --stale-session-threshold warning).
Fix: The trailing slash is no longer stripped — target_url is only whitespace-trimmed and given a http:// prefix if missing.
akin/
├── csrf_brute.py # Main engine v3.2
├── waf_markers.txt # WAF/Bot block-page marker template (--waf-markers) — populate from a real block page
├── proxies.txt # Proxy list template (for lockout bypass)
├── wordlists/
│ └── sample.txt # Sample wordlist (20 passwords, includes "password")
├── checklist # Broader auth-testing checklist (rate limiting, lockout, CSRF, enumeration, MFA)
├── GITCOMMITMESSAGES.md # Suggested commit messages for pending changes (nothing auto-committed)
└── README.md # This file
This tool is provided for educational and authorized security testing purposes only.