OTP Brute-Force Testing
Validate whether an OTP verification endpoint can be brute-forced: unlimited
attempts, no lockout, long TTL, weak entropy, or a response oracle that reveals
the correct code. Targets must be within the scope of the engagement you are
authorized to test.
When to use
- Phone/email verification flows (e.g., "add phone number to profile")
- MFA / 2FA challenge steps and login-by-OTP
- Password reset and account recovery
- Transaction confirmation (payments, withdrawals, admin actions)
- Any endpoint that accepts a 4-8 digit numeric code and returns success/failure
Core attack pattern (reference: HackerOne #3265780, CoinMate.io)
- Trigger an OTP challenge, submit any code, and intercept the verification
request (Burp proxy).
- Brute-force the OTP space (100000–999999) with Burp Intruder or the bundled
script.
- Detect the valid OTP via a response oracle — in the CoinMate case, the
response containing a valid OTP had a Content-Length of 961 bytes vs.
~150 for failures.
- Replay the original intercepted request with the recovered OTP to
complete the operation (the phone number is added to the profile).
- Chain the impact: attacker-controlled phone number on the account → the
number receives login/reset OTPs → account takeover.
Phase 0 — Recon the OTP flow
- Map all OTP-issuing and OTP-verifying endpoints; note parameter names
(
otp, code, token, verificationCode), length (4/6/8 digits), format.
- Check whether the code is ever returned in responses, logs, or debug headers.
- Check OTP entropy: sequential, timestamp-derived, or weak-PRNG codes have
a much smaller candidate space than the nominal digit range.
- Note whether the same OTP is reusable across accounts or across flows
(verify vs. reset vs. login).
Phase 1 — Baseline and response-oracle discovery
Send 10–20 wrong OTPs plus 1 valid OTP and diff the responses. Use the script's
probe mode to inspect a single attempt:
python3 scripts/otp_bruteforce.py --url https://target/api/verify-phone \
--method POST \
--headers '{"Content-Type":"application/json","Cookie":"session=abc"}' \
--body '{"phone":"+15551234567","otp":"{{OTP}}"}' \
--probe 123456
Signals to look for:
- Content-Length (e.g., valid = 961 bytes — the CoinMate oracle)
- HTTP status (200 vs 400/403)
- Body markers (
"verified": true, new session token, redirect)
- Timing (valid code may take a different code path)
- Set-Cookie / session mutation
If no oracle is obvious, run the full sweep in auto mode: the valid OTP is
statistically the outlier bucket among otherwise uniform responses.
Phase 2 — Rate-limit and lockout probing
- Fire 10–30 rapid wrong attempts. If you never see 429/403, CAPTCHA, lockout,
or escalating delay, the endpoint is a brute-force candidate.
- Test throttling scope: limits keyed to session/IP only are bypassable via
new sessions, IP rotation, or
X-Forwarded-For spoofing — retest per account.
- Check lockout semantics: per-account, per-session, or global? Does a lockout
also kill the legitimate user (DoS angle)?
- Test OTP TTL and reuse: does a code survive past expiry? Is it
single-use? Does resending invalidate the previous code?
Phase 3 — Brute-force automation
Use scripts/otp_bruteforce.py:
# Known oracle: valid responses are 961 bytes (the CoinMate signal)
python3 scripts/otp_bruteforce.py --url https://target/api/verify-phone \
--method POST \
--headers '{"Content-Type":"application/json","Cookie":"session=abc"}' \
--body '{"phone":"+15551234567","otp":"{{OTP}}"}' \
--oracle-length 961 --threads 10
# No oracle known: auto-detect the outlier response
python3 scripts/otp_bruteforce.py --url https://target/api/verify-phone \
--method POST \
--headers '{"Content-Type":"application/json","Cookie":"session=abc"}' \
--body '{"phone":"+15551234567","otp":"{{OTP}}"}' \
--range 100000-999999 --threads 20 --delay 0.05
# Route everything through Burp for visibility
python3 scripts/otp_bruteforce.py ... --proxy http://127.0.0.1:8080
Burp Intruder alternative (the original researcher's method):
- Positions: replace the OTP value with
§100000§.
- Payload type: Numbers, 100000 to 999999, step 1, min/max fraction digits =
6.
- Resource pool: 10–20 concurrent requests; watch for 429 and slow down.
- Grep-Match on the oracle signal (length column, marker, or status).
- On hit: copy the valid response, replay the original intercepted request
with that OTP (Burp Repeater) to complete the action.
Phase 4 — Confirmation and impact chaining
- Confirm the OTP on a fresh replay of the original request — this proves
the finding end-to-end (the CoinMate report's Step 4). For single-use code
flows, use
--confirm-trials 1 since the sweep may have consumed the code.
- Chain impacts and re-score:
- Phone added to profile → OTP-based password reset → ATO
- MFA bypass → session hijack on victim accounts
- Payment confirmation bypass → financial fraud
- Reset-flow brute force → mass account takeover
- If the flow targets victim accounts (attacker triggers reset on someone
else's account), interaction/privilege requirements change the score.
Detection heuristics summary
| Signal |
Meaning |
| Unique content-length bucket (e.g., 961) |
Valid-code oracle |
| Rare HTTP status (200 vs 400) |
Valid-code oracle |
| Success marker / token in body |
Valid-code oracle |
| No 429/403/lockout after 30 rapid tries |
No rate limiting |
| Lockout only per-IP/session |
Throttling bypassable |
| Old code still valid after resend/expiry |
Weak TTL/reuse |
| Constant response size for all inputs |
No oracle — needs timing or other side channel |
Reporting
- CWE-307 (Improper Restriction of Excessive Authentication Attempts) is the
primary mapping; also consider CWE-640 (weak password recovery) and
CWE-287 (improper authentication). Note: platform auto-tags can be off —
the CoinMate case was tagged "Insecure Storage of Sensitive Information".
- CVSS 3.1: base ~4.3–6.5 medium
(AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N). Escalate to 7.5+ high when it directly
yields account takeover or bypasses MFA on victim accounts.
- PoC write-up: endpoint, request template, oracle used, candidate space,
request rate, time-to-recovery, and the replay confirmation.
Remediation checklist
- Rate-limit per account + IP (e.g., 5 attempts / 15 min) with CAPTCHA
escalation; key limits on the user identifier, not just session/IP.
- Short OTP TTL (2–5 min), single-use, invalidated on resend.
- Cryptographically secure RNG for code generation; never sequential or
time-derived codes.
- Server-side attempt counter with lockout; uniform responses so no
length/status oracle leaks validity.
- Monitor and alert on verification-failure bursts; add jitter to error paths.
- Prefer TOTP/HOTP (RFC 6238/4226) with rate-limited verification windows.
Files
scripts/otp_bruteforce.py — threaded Python3 brute-forcer with explicit
and auto-detect oracles, throttling backoff, confirmation replays, and Burp
proxy support.
1---2name: otp-bruteforce-testing3description: Detect, validate, and exploit OTP (one-time password) brute-force vulnerabilities in phone/email verification, MFA, password-reset, and transaction-confirmation flows. Use when a target issues numeric one-time codes (4-8 digits via SMS/email), when OTP verification endpoints appear unthrottled, when reviewing authentication or account-recovery code, or when assessing rate limiting on verification APIs. Produces PoC scripts, response-oracle analysis, CVSS scoring, and remediation guidance.4---56# OTP Brute-Force Testing78Validate whether an OTP verification endpoint can be brute-forced: unlimited9attempts, no lockout, long TTL, weak entropy, or a response oracle that reveals10the correct code. Targets must be within the scope of the engagement you are11authorized to test.1213## When to use14- Phone/email verification flows (e.g., "add phone number to profile")15- MFA / 2FA challenge steps and login-by-OTP16- Password reset and account recovery17- Transaction confirmation (payments, withdrawals, admin actions)18- Any endpoint that accepts a 4-8 digit numeric code and returns success/failure1920## Core attack pattern (reference: HackerOne #3265780, CoinMate.io)211. Trigger an OTP challenge, submit any code, and **intercept the verification22 request** (Burp proxy).232. Brute-force the OTP space (100000–999999) with Burp Intruder or the bundled24 script.253. Detect the valid OTP via a **response oracle** — in the CoinMate case, the26 response containing a valid OTP had a **Content-Length of 961 bytes** vs.27 ~150 for failures.284. Replay the **original** intercepted request with the recovered OTP to29 complete the operation (the phone number is added to the profile).305. Chain the impact: attacker-controlled phone number on the account → the31 number receives login/reset OTPs → **account takeover**.3233## Phase 0 — Recon the OTP flow34- Map all OTP-issuing and OTP-verifying endpoints; note parameter names35 (`otp`, `code`, `token`, `verificationCode`), length (4/6/8 digits), format.36- Check whether the code is ever returned in responses, logs, or debug headers.37- Check OTP **entropy**: sequential, timestamp-derived, or weak-PRNG codes have38 a much smaller candidate space than the nominal digit range.39- Note whether the same OTP is reusable across accounts or across flows40 (verify vs. reset vs. login).4142## Phase 1 — Baseline and response-oracle discovery43Send 10–20 wrong OTPs plus 1 valid OTP and diff the responses. Use the script's44probe mode to inspect a single attempt:4546```bash47python3 scripts/otp_bruteforce.py --url https://target/api/verify-phone \48 --method POST \49 --headers '{"Content-Type":"application/json","Cookie":"session=abc"}' \50 --body '{"phone":"+15551234567","otp":"{{OTP}}"}' \51 --probe 12345652```5354Signals to look for:55- **Content-Length** (e.g., valid = 961 bytes — the CoinMate oracle)56- **HTTP status** (200 vs 400/403)57- **Body markers** (`"verified": true`, new session token, redirect)58- **Timing** (valid code may take a different code path)59- **Set-Cookie / session mutation**6061If no oracle is obvious, run the full sweep in auto mode: the valid OTP is62statistically the outlier bucket among otherwise uniform responses.6364## Phase 2 — Rate-limit and lockout probing65- Fire 10–30 rapid wrong attempts. If you never see 429/403, CAPTCHA, lockout,66 or escalating delay, the endpoint is a brute-force candidate.67- Test throttling scope: limits keyed to **session/IP only** are bypassable via68 new sessions, IP rotation, or `X-Forwarded-For` spoofing — retest per account.69- Check lockout semantics: per-account, per-session, or global? Does a lockout70 also kill the legitimate user (DoS angle)?71- Test OTP **TTL and reuse**: does a code survive past expiry? Is it72 single-use? Does resending invalidate the previous code?7374## Phase 3 — Brute-force automation75Use `scripts/otp_bruteforce.py`:7677```bash78# Known oracle: valid responses are 961 bytes (the CoinMate signal)79python3 scripts/otp_bruteforce.py --url https://target/api/verify-phone \80 --method POST \81 --headers '{"Content-Type":"application/json","Cookie":"session=abc"}' \82 --body '{"phone":"+15551234567","otp":"{{OTP}}"}' \83 --oracle-length 961 --threads 108485# No oracle known: auto-detect the outlier response86python3 scripts/otp_bruteforce.py --url https://target/api/verify-phone \87 --method POST \88 --headers '{"Content-Type":"application/json","Cookie":"session=abc"}' \89 --body '{"phone":"+15551234567","otp":"{{OTP}}"}' \90 --range 100000-999999 --threads 20 --delay 0.059192# Route everything through Burp for visibility93python3 scripts/otp_bruteforce.py ... --proxy http://127.0.0.1:808094```9596Burp Intruder alternative (the original researcher's method):97- Positions: replace the OTP value with `§100000§`.98- Payload type: Numbers, 100000 to 999999, step 1, **min/max fraction digits =99 6**.100- Resource pool: 10–20 concurrent requests; watch for 429 and slow down.101- Grep-Match on the oracle signal (length column, marker, or status).102- On hit: copy the valid response, replay the **original** intercepted request103 with that OTP (Burp Repeater) to complete the action.104105## Phase 4 — Confirmation and impact chaining106- Confirm the OTP on a **fresh replay** of the original request — this proves107 the finding end-to-end (the CoinMate report's Step 4). For single-use code108 flows, use `--confirm-trials 1` since the sweep may have consumed the code.109- Chain impacts and re-score:110 - Phone added to profile → OTP-based password reset → ATO111 - MFA bypass → session hijack on victim accounts112 - Payment confirmation bypass → financial fraud113 - Reset-flow brute force → mass account takeover114- If the flow targets *victim* accounts (attacker triggers reset on someone115 else's account), interaction/privilege requirements change the score.116117## Detection heuristics summary118| Signal | Meaning |119|---|---|120| Unique content-length bucket (e.g., 961) | Valid-code oracle |121| Rare HTTP status (200 vs 400) | Valid-code oracle |122| Success marker / token in body | Valid-code oracle |123| No 429/403/lockout after 30 rapid tries | No rate limiting |124| Lockout only per-IP/session | Throttling bypassable |125| Old code still valid after resend/expiry | Weak TTL/reuse |126| Constant response size for all inputs | No oracle — needs timing or other side channel |127128## Reporting129- **CWE-307** (Improper Restriction of Excessive Authentication Attempts) is the130 primary mapping; also consider **CWE-640** (weak password recovery) and131 **CWE-287** (improper authentication). Note: platform auto-tags can be off —132 the CoinMate case was tagged "Insecure Storage of Sensitive Information".133- **CVSS 3.1**: base ~4.3–6.5 medium134 (AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N). Escalate to 7.5+ high when it directly135 yields account takeover or bypasses MFA on victim accounts.136- PoC write-up: endpoint, request template, oracle used, candidate space,137 request rate, time-to-recovery, and the replay confirmation.138139## Remediation checklist1401. Rate-limit per **account + IP** (e.g., 5 attempts / 15 min) with CAPTCHA141 escalation; key limits on the user identifier, not just session/IP.1422. Short OTP TTL (2–5 min), **single-use**, invalidated on resend.1433. Cryptographically secure RNG for code generation; never sequential or144 time-derived codes.1454. Server-side attempt counter with lockout; uniform responses so no146 length/status oracle leaks validity.1475. Monitor and alert on verification-failure bursts; add jitter to error paths.1486. Prefer TOTP/HOTP (RFC 6238/4226) with rate-limited verification windows.149150## Files151- `scripts/otp_bruteforce.py` — threaded Python3 brute-forcer with explicit152 and auto-detect oracles, throttling backoff, confirmation replays, and Burp153 proxy support.