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

Deja de leer sobre bugs y empieza a cazarlos

Sin tarjeta · Academy gratis · cancela cuando quieras

Crear cuenta gratis

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.uario → usuario) 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.

Gratis · sin cuenta

La checklist que repaso en cada objetivo nuevo

47 comprobaciones ordenadas por coste: primero lo que te puede meter en un lío, luego lo barato, y al final lo caro — que es donde están los bounties grandes. Te la mando al correo ahora mismo.

Te das de baja en un clic, desde cualquier correo.

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

7,99 €/mes · cancela cuando quieras

Artículos relacionados

2.786

hunters entrenando

63

labs de hackeos reales

1.767

completaciones

$14.790

pagados por estos bugs

37 flags capturadas esta semana·Hackeos reales de HackerOne · YesWeHack · Bugcrowd·Sin permanencia·Academy gratis

La comunidad

Esto no es una plataforma con usuarios.Es gente rompiendo lo mismo que tú.

Dentro se pregunta, se enseña lo que se ha encontrado y se resuelve en grupo lo que atascaría a cualquiera solo. Se entra gratis: sin cuenta, sin plan y sin dejar el correo.

Entrar a la comunidad de BBLABS en Discord
Entrar al Discord

Es gratis y abierto. No hace falta tener cuenta en BBLABS para entrar.