Vulnerability
[FAIL] OTP Bruteforce Protection Bypass - Authentication - Password Reset
Description
Reset OTP verification (/v3/check-otp) locks after 9 attempts per session, BUT this limit is reset every time /forget-password is called, and that endpoint itself has no rate limit or CAPTCHA. The brute-force protection can therefore be bypassed with a repeating cycle.
Impact
Confidentiality & Integrity: account takeover (ATO) of any user whose email is known, by brute-forcing the 4-digit OTP (10,000 combinations) with no effective obstacle.
CWE
CWE-307: Improper Restriction of Excessive Authentication Attempts
OWASP
API2:2023 - Broken Authentication
Scenario
Normal: request an OTP and make one attempt (Invalid). Exploitation: repeating forget-password without limit resets the lockout -> brute-force the 4-digit OTP -> ATO -> CIA (Confidentiality + Integrity).
Endpoint
POST http://127.0.0.1:8888/identity/api/auth/v3/check-otp
POST http://127.0.0.1:8888/identity/api/auth/forget-password
Affected feature
Password Reset via OTP (crapi-identity)
Remediation
- Apply PERSISTENT per-account rate limiting + lockout (not reset by a new OTP request).
- Throttle and add CAPTCHA to forget-password.
- Use OTPs of >=6 digits with a short expiry.
Proof of concept
- (Screenshot 1) NORMAL: POST forget-password {victim email} -> 200, OTP sent.
- (Screenshot 2) NORMAL: POST v3/check-otp with one wrong attempt -> Invalid OTP.
- (Screenshot 3) EXPLOITATION + CIA: after the lockout, call forget-password again (200, no throttling) -> the counter is reset and a new OTP is issued; repeat the cycle [reset -> 9 guesses] until the OTP is guessed -> victim password reset (ATO).
The complete record
LAB All targets are lab systems. Nothing here is a client engagement. Evidence is shown as recorded; secrets are blacked out.
Normal: request a reset OTP
Normal flow: the forgot-password flow sends a 4-digit OTP to the victim's email.
HTTP request
POST /identity/api/auth/forget-password HTTP/1.1
Host: 127.0.0.1:8888
Content-Type: application/json
{"email":"[REDACTED-EMAIL]"}
HTTP response
HTTP/1.1 200
Server: openresty/1.27.1.2
Content-Type: application/json
Content-Length: 75
{
"message": "OTP Sent on the provided email, [REDACTED-EMAIL]",
"status": 200
}
Normal: one OTP attempt
Normal flow: one incorrect OTP attempt returns Invalid OTP.
HTTP request
POST /identity/api/auth/v3/check-otp HTTP/1.1
Host: 127.0.0.1:8888
Content-Type: application/json
{"email":"[REDACTED-EMAIL]","otp":"1234","password":"[REDACTED-LAB-VALUE]"}
HTTP response
HTTP/1.1 500
Server: openresty/1.27.1.2
Content-Type: application/json
Content-Length: 58
{
"message": "Invalid OTP! Please try again..",
"status": 500
}
Impact: forget-password resets the lockout
Impact: forget-password has no request limit and resets the lockout. Repeating [reset -> 9 guesses] defeats the 4-digit OTP and lets the attacker reset the victim's password. The recorded account takeover (ATO) compromises confidentiality and integrity.
HTTP request
POST /identity/api/auth/forget-password HTTP/1.1
Host: 127.0.0.1:8888
Content-Type: application/json
{"email":"[REDACTED-EMAIL]"}
# forget-password DIPANGGIL ULANG (tanpa rate-limit) => counter lockout ter-RESET & OTP baru terbit
HTTP response
HTTP/1.1 200
Server: openresty/1.27.1.2
Content-Type: application/json
Content-Length: 75
{
"message": "OTP Sent on the provided email, [REDACTED-EMAIL]",
"status": 200
}
← Previous case: OTP Bruteforce Protection - Authentication - Password Reset (Per-Session Lockout) Next case: Server-Side Request Forgery - Workshop - Contact Mechanic →