Resumen

Durante la auditoria de una aplicacion web con autenticacion en dos factores (2FA) basada en codigos OTP enviados por email, se descubrio que el sistema de rate limiting y generacion de codigos trataba la direccion de email de forma case-sensitive, mientras que el protocolo SMTP entrega los emails de forma case-insensitive (segun RFC 5321).

Esto significaba que user@target.com, User@target.com, USER@target.com y cualquier otra variacion de case generaban codigos OTP independientes con rate limits separados, pero todos llegaban al mismo buzon de correo del atacante o la victima.

Detalles tecnicos

Comportamiento normal

El flujo de autenticacion era: login con email + password, y luego un codigo OTP de 6 digitos enviado al email. El rate limit era de 3 intentos por codigo y 1 codigo cada 60 segundos por email.

Request OTP normal
POST /api/auth/request-otp HTTP/1.1
Content-Type: application/json

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

→ 200 OK — Codigo OTP enviado (rate limit: 1/min para este email)

Bypass mediante variaciones de case

Cada variacion de case se trataba como un email distinto a nivel de aplicacion, generando un nuevo codigo OTP con su propio rate limit:

Multiples peticiones con case variations
POST /api/auth/request-otp  { "email": "victim@company.com" }    → Codigo A
POST /api/auth/request-otp  { "email": "Victim@company.com" }    → Codigo B
POST /api/auth/request-otp  { "email": "VICTIM@company.com" }    → Codigo C
POST /api/auth/request-otp  { "email": "vIctim@company.com" }    → Codigo D
POST /api/auth/request-otp  { "email": "viCtim@company.com" }    → Codigo E
...

Todos los codigos llegan al mismo buzon: victim@company.com
Cada variacion tiene su propio rate limit de 3 intentos

Impacto en el brute force

Con un email de 6 caracteres en la parte local, existen 2^6 = 64 variaciones de case posibles. Cada una genera un codigo OTP valido con 3 intentos:

Calculo de probabilidades
Codigo OTP: 6 digitos → 1.000.000 combinaciones posibles
Rate limit normal: 3 intentos / codigo = 0.0003% probabilidad

Con bypass:
- 64 variaciones de case × 1 codigo cada una = 64 codigos validos
- Cualquier codigo es valido para la misma cuenta
- 64 codigos × 3 intentos = 192 intentos totales
- Con 64 codigos validos de 6 digitos:
  P(exito) = 1 - (999936/1000000)^192 ≈ 1.2%

Repitiendo el proceso cada 60 segundos:
- En 10 minutos: P(exito) ≈ 11.5%
- En 1 hora: P(exito) ≈ 52%
Nota: Si el atacante tenia acceso al buzon de email de la victima (por ejemplo, a traves de una sesion comprometida o forwarding), podia ver todos los codigos generados y usarlos directamente sin necesidad de brute force, bypasseando completamente el 2FA.

Impacto

  • Bypass del mecanismo 2FA mediante multiplicacion de codigos validos
  • Evasion de rate limiting al generar identidades distintas para el mismo buzon
  • Acceso no autorizado a cuentas protegidas con OTP por email
  • Flood del buzon de la victima con multiples emails de codigo OTP

Remediacion

  1. Normalizar el email a minusculas antes de cualquier operacion — email = email.strip().lower()
  2. Rate limiting por email normalizado — Aplicar limites sobre la version canonicalizada del email.
  3. Limitar codigos OTP activos — Invalidar el codigo anterior al generar uno nuevo para el mismo buzon.
  4. Implementar lockout progresivo — Bloquear la cuenta temporalmente tras N intentos fallidos acumulados.
  5. Considerar TOTP — Migrar a codigos basados en tiempo (Google Authenticator) que no dependen del email.

Referencias