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

Nivel IntermedioCon cuenta

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.

Gorka El Bochi11 de mayo de 202613 min

Respuesta rápida

Password reset es el endpoint más auditado y aún así el más roto del bug bounty. La razón: cualquier "feature" del flow (case insensitivity, internationalization, alias support) crea collision space. Cuatro bypasses rentables: Puny code / IDN homograph (registrar víctima@gmail.com con cirílico → recibe el reset email), Host header injection (cambia el link del email para apuntar a tu dominio), email normalization (gmail dot/plus aliases tratados como distintos en register pero iguales en reset), token leakage (referer, response body, log). Bounties: €1.500-€12.000 con ATO chain.


El flow estándar y dónde rompe

arduino
1. User submits email → POST /auth/forgot { email }
2. Server busca user por email
3. Server genera token reset (random 32-128 chars)
4. Server guarda { userId, token, expiresAt } en DB
5. Server envía email con link: https://target.com/reset?token=XXX
6. User clica → GET /reset?token=XXX → form de nuevo password
7. POST /reset { token, newPassword } → password updated

Cada paso es atacable.


Puny code / IDN homograph attack

Unicode permite registrar victim@gmail.com con caracteres cirílicos que se ven idénticos al ojo humano pero son diferentes para el sistema. Combinado con email normalization downstream → ATO.

El truco

Las letras latinas y cirílicas comparten glifos:

LatínCirílicoPunto código
aаU+0061 vs U+0430
eеU+0065 vs U+0435
iіU+0069 vs U+0456
oоU+006F vs U+043E
pрU+0070 vs U+0440
cсU+0063 vs U+0441

Escenario de exploit

  1. Víctima tiene cuenta gorka@target.com.
  2. Atacante registra cuenta con gоrka@target.com (la о es cirílica U+043E).
  3. Para registro pasa porque la app trata strings byte-exact → es un email "diferente".
  4. Atacante solicita password reset de la VÍCTIMA gorka@target.com.
  5. Si la app normaliza Unicode en el lookup (NFKC, Punycode lookup) pero no en el storage → encuentra al user atacante → manda email al atacante.

Variante: mailbox provider acepta ambos

Algunos mail providers (Gmail) rechazan caracteres cirílicos en mailbox local-part — pero ProtonMail, Yandex, custom domain mailservers a veces sí los aceptan.

graphql
gorka@yandex.com
gоrka@yandex.com    ← cirílico, Yandex acepta ambos como mailboxes separados

Si target.com no normaliza pero el provider sí → atacante recibe correos de la víctima.

Detección

Test register con victim+a@target.com y victim+ɑ@target.com (latin alpha). ¿Acepta ambos como distintos? Si sí → posible chain.


Email normalization — Gmail dots and plus aliases

Gmail trata usuario@gmail.com == us.uario@gmail.com == usu.ar.io@gmail.com == usuario+anything@gmail.com. Casi nadie más lo hace.

Chain común

  1. Víctima registra usuario@gmail.com en target.com.
  2. Atacante registra us.uario@gmail.com en target.com (Gmail entrega ambos a la misma inbox; target.com los trata como distintos).
  3. Atacante solicita reset password de usuario@gmail.com.
  4. Email llega a usuario@gmail.com (víctima).
  5. Pero si la app normaliza al hacer reset (u.s.uariousuario) y el lookup encuentra al atacante → atacante recibe reset → ATO.

Plus aliases

scss
usuario@gmail.com
usuario+atacante@gmail.com   ← misma inbox para Gmail; misma cuenta para apps que normalizan

App que acepta plus alias en register pero lo strippea en reset → atacante registra usuario+evil@gmail.com, pide reset a usuario@gmail.com (sin alias), token va a la víctima... no, espera, va a la víctima. El bug real es al revés: registrar usuario+evil@gmail.com, pedir reset de usuario+evil@gmail.com (que está en tu inbox como usuario) → token llega a la víctima.

Más exacta: registra usuario+atacante@gmail.com → eres usuario para la app si normaliza → pides reset → email va a usuario+atacante@gmail.com (tu inbox) → ATO de la víctima usuario@gmail.com.

<!-- PAYWALL -->

Host header injection

Quizás el bypass más viejo y aún más explotable. La app construye el link del reset usando el Host header del request:

javascript
const resetLink = `https://${req.headers.host}/reset?token=${token}`;
sendEmail(user.email, resetLink);

Atacante manda:

http
POST /auth/forgot HTTP/1.1
Host: evil.tld
Content-Type: application/json

{"email": "victim@target.com"}

Si el server confía en el Host header del request → email a víctima contiene:

perl
Click here to reset: https://evil.tld/reset?token=XXX

Víctima clica → token va a evil.tld (en path o query) → atacante usa el token en target.com/reset → ATO.

Variantes

  • X-Forwarded-Host: X-Forwarded-Host: evil.tld — algunas frameworks priorizan este header.
  • Host: target.com:@evil.tld: parser confuso, algunos ven target.com (legit), email construido con evil.tld.
  • Double Host header: Host: target.com\r\nHost: evil.tld — algunos parsers leen el último, otros el primero.

Detección

Inspecciona el email que recibes con reset. ¿El dominio del link viene del input HTTP (Host header) o está hardcoded? Cambia Host y observa.


Token leakage

Via Referer

Si el link del email lleva a una página que carga assets de terceros (CDN, analytics, fonts), los terceros reciben Referer: target.com/reset?token=XXX.

Detección: visita el link de reset, captura todas las requests en Burp/DevTools → ¿alguna sale a otro dominio con Referer que incluye ?token=?

Fix correcto: Referrer-Policy: strict-origin en la página de reset.

Via response body

Algunos endpoints, en respuesta a la solicitud de reset (POST /forgot), incluyen el token:

json
{
  "message": "Email sent",
  "debug": {
    "token": "XXX"
  }
}

Endpoint dev olvidado en producción. Test con Burp Repeater.

Via logs / monitoring

Si la app loggea full URL en logs (Datadog, Sentry, custom logs) y los logs son accesibles vía vulnerable endpoint → exfil.

Via JS bundle

Algunos SPAs incluyen el token en window state al renderizar el reset page (hydration data). Si la página es indexada por crawler / cacheada / fugada por error → exfil.


Token reuse / not invalidated

Reuse tras uso

El token debería ser válido para una sola operación. Bug frecuente: tras usar el token, no se invalida. Atacante:

  1. Víctima resetea password con token X → OK.
  2. Atacante (si interceptó X via cualquier método) usa X otra vez → password reset otra vez.

Reuse tras login

Si la víctima loguea con nuevo password y luego solicita ANOTHER reset por error → el primer token sigue válido. Cualquiera con el primer token tiene acceso permanente.

Token no expira

Token válido por 24h es estándar — pero algunas apps no expiran nunca. Token leaked hace 6 meses sigue válido.


Response manipulation — el bypass tonto que funciona

Endpoint POST /reset retorna {"success": true} o {"success": false}. Frontend SPA redirige basándose en el success. Intercepta:

json
HTTP/1.1 400 Bad Request
{"success": false, "error": "Invalid token"}
                                  ← cambia a:
HTTP/1.1 200 OK
{"success": true}

→ Frontend redirige a "password changed", muestra login. Pero el password no se cambió en realidad. Bug no útil para ATO porque el password real no cambió.

Salvo que el endpoint /finalize-reset también esté roto: si reset completa la sesión + emite cookie de auth via response que también es manipulable → ATO.


Account linking / unverified email takeover

Algunas apps permiten password reset incluso si el email no está verificado (anti-pattern). Si la app permite cambiar email vía API:

  1. Atacante registra cuenta nueva.
  2. Cambia email a victim@target.com (sin verificación).
  3. Solicita password reset → email a victim@target.com.
  4. Víctima ve email "extraño" pero clica → reset el password del atacante (que ahora controla la cuenta linkeada a victim's email).

Variante: si target.com permite multiple emails por cuenta + verificación débil + reset por cualquier email asociado → reset por email atacante-controlado.


Hunting checklist

  • Host header injection: cambia Host / X-Forwarded-Host y observa email recibido.
  • Puny code: registra victim@target.com con cirílico (NIST de homographs).
  • Gmail normalization: prueba user@gmail.com vs u.ser@gmail.com vs user+x@gmail.com.
  • Token en response body del POST /forgot — endpoint dev olvidado.
  • Token en referer de assets externos en la página de reset.
  • Token reuse: usa el mismo token 2 veces, ¿ambos pasan?
  • Token expiration: deja token 24h+, ¿sigue válido?
  • Email change without verify + reset → ATO chain.
  • Response manipulation: status / body / cookie del endpoint final.
  • Rate limit en /forgot: si no hay, brute force token de 4-6 dígitos.
  • Token entropy: si el token es predecible (timestamp + userId) → forge.
  • Documenta: el chain mínimo + impact (ATO definitivo vs window de horas).

Labs relacionados

Practica Host header injection, Puny code y normalization attacks en labs de password reset.

Practica esto en un lab

Password Reset

Resolver

Sigue aprendiendo · cuenta gratis

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

Crear cuenta
Premium · 1 técnica más

Hay un payload extra al final

El bypass de password reset usando Unicode collisions en `i` cyrillic + Gmail normalization para tomar accounts ajenas sin nunca tocar su email real.

Desbloquear

5 €/mes · cancela cuando quieras

Artículos relacionados