No rate‑limit on OTP verification leads to 10k bounty
Disclaimer — redactions
This write-up describes a real vulnerability I discovered in a bug‑bounty program. The target is intentionally redacted (hosts, cookies, exact endpoints and company identifiers removed) so this public post does not disclose sensitive data or enable malicious reuse. All examples, PoC requests and identifiers below have been sanitized.
Summary
A missing or ineffective rate limit on an OTP/verification endpoint allowed an attacker to repeatedly submit one‑time codes and obtain a valid authentication token for any account (by only knowing the victim’s email). By automating requests and combining modest parallelism with simple IP rotation, I was able to send large numbers of verification attempts in a short time and obtain access tokens.
Impact: Mass account takeover — full account compromise for any user whose email address is known.
Timeline (abridged)
- I found a flow on a subdomain where a verification code is submitted/checked.
- The endpoint didn’t enforce practical limits. But cloudflare would stop you if you send a lot of requests.
- Initial PoC: Burp Intruder with a small number of threads (10) and payload lists—differences in responses hinted at success.
- Later testing: scaled to thousands/100k attempts using a combination of IP rotation and parallel threads. I could hit 100k requests in minutes in my controlled tests.
- Reported to the vendor via HackerOne; triaged and fixed after validation.
How it works (technical details)
Typical OTP flows:
- User provides email -> server sends a one‑time code to email (or via other channel).
- User posts the code to
/graphql
(or similar) along with an identifier. - Server validates code, creates a session / issues an access token.
The vulnerable implementation failed to enforce any of the following effectively:
- per‑user attempt counter (e.g. block after X failed attempts)
- progressively increasing delays / captchas / blocking on suspicious activity
- global throttling or anomaly detection for the verification endpoint
Because the endpoint accepted arbitrary numbers of attempts, an attacker who knows a target email can attempt the full codespace until a match is found. Cloudflare would start giving me code 429 after sending a couple of thousands of requests… but that is easily bypassed.
Codespace math (quick reminder)
If the verification code is numeric and N
digits long, the number of possible codes is 10^N
. For example:
- 4 digits → 10,000 possibilities
- 6 digits → 1,000,000 possibilities
- 8 digits → 100,000,000 possibilities
The practicality of brute‑forcing depends on code length, token expiry window, and detection/defenses. In this case the application’s other protections were absent, making brute forcing feasible.
In this case it was a 6 digit code so -> 1,000,000 possibilities
My methodology and PoC notes
- Burp Suite (Intruder) for high‑speed requests and payload injection.
- IP rotation using the burp extension that redirects your traffic through AWS in selected regions
I started small (10 threads, Burp Intruder) to confirm the endpoint accepted rapid attempts. The WAF would trigger code 429 when sending multiple requests, but chaging IP would fix the issue. With expanded parallelism and IP diversity, I scaled up to ~100k attempts in <= 8 minutes.
Redacted HTTP request (sanitized)
Below is the redacted raw request captured while testing. Sensitive fields (host, cookies, some headers) have been replaced with REDACTED
tokens.
POST /graphql HTTP/2
Host: redacted.target.example.com
Connection: keep-alive
Accept: application/json, text/plain, */*
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...
Content-Type: application/json
Origin: https://redacted.target.example.com
Referer: https://redacted.target.example.com/
Cookie: session=REDACTED; other_cookie=REDACTED
Content-Length: 210
{
"operationId":"CreateSessionForVerificationCode",
"variables": {
"identifier": "victim@example.com",
"identifier_type": "email",
"verification_code": "123456"
},
"extensions": { "persistedQuery": { "version": 1, "sha256Hash": "REDACTED" } }
}
Notes on the example above
- The
verification_code
is shown as a 6‑digit numeric value (used here as an example). - The endpoint accepted JSON and returned an authentication/session token on success.
- Cookies and hostnames are redacted to avoid revealing sensitive infrastructure.
- ~100k requests over a short period (minutes) with IP rotation — brute forcing at scale was feasible. It was possible to actually do the 1M request in less than 10 minutes (the code would expire in 10 minutes), but I didn’t want to bombard the application with that amount of requests.
Why this is critical
- Knowing only an email address, an attacker can eventually obtain a valid code and log in as that user.
- Many sites reuse email addresses for account lookup; high‑privilege accounts or accounts with payment details are at risk.
- Automation + ephemeral proxies make large‑scale attacks readily available to motivated attackers.
- This can lead to mass account takeover, data theft, impersonation, and further post‑exploitation.
Responsible disclosure and outcome
I reported the issue via HackerOne with PoC videos and logs. The report was triaged as high by the Hackerone triager, but fortunately for me the company paid the Max-bounty!
Final notes / lessons learned
- The company finally fixed the issue by making the OTP code invalid after a few attempts, I tried bypassing the fix with different techniques but all were hopeless
- The issue is in “Pending disclosure” for over a year (and fixed since 2021). The company doesn’t want to disclose the issue, so I decided to redact all details and disclose the vulnerability
Follow me on twitter https://twitter.com/zonduu1 so you can see where I publish new write-up in my own website,
– zonduu