Enumeración de usuarios

Los mecanismos de autenticación y autorización son uno de los elementos que más atención deben requerir en un sistema, sin embargo, muchas veces nos encontramos con sistemas con graves fallas de seguridad o malas configuraciones que nos permiten sortear las medidas de protección.

El primer paso que debe realizar un atacante es encontrar un usuaro válido en el sistema con el que poder autenticarse, y si es posible, con privilegios elevados para poder tener mayor poder de acción en el sistema.

1. Mensajes de respuesta diferentes

Imagina que nuestra aplicación web cuenta con un formulario de autenticación que muestra el mensaje Invalid Username cuando el usuario introducido no existe, y el mensaje Invalid Password cuando la contraseña es incorrecta. Un atacante podría usar una wordlist de nombres de usuario y filtrar los resultados devolviendo solo aquellos que incluyesen la cadena Invalid Password. Esto revelaría una lista de usuarios válidos en el sistema. El siguiente código es un ejemplo de explotación.

ffuf -X POST -u http://10.10.10.10 \
  -d "username=FUZZ&password=foo" \
  -mr "Invalid Password" \
  -w /usr/share/wordlists/SecLists/Usernames/xato-net-10-million-usernames.txt

2. Diferencias sutiles en los mensajes de respuesta

Aunque a primera vista pueda parecer que el mensaje de respuesta es el mismo cuando enviamos un usuario incorrecto que cuando enviamos una contraseña incorrecta con un usuario correcto, merece la pena ser observadores. A veces, los desarrolladores escriben el mensaje de error repetidas veces en el código en lugar de almacenarlo en variables y reutilizarlo. Esto puede derivar en que los mensajes de respuesta sean ligeramente distintos, como un espacio adicional, o un punto al final. No es lo mismo Invalid username or password que Invalid username or password. ¿Notas el punto adicional al final? A veces es un espacio, otras un punto o una coma, el caso es que este simple error de tipografía puede permitir a un atacante realizar el escaneo anterior con la misma efectividad.

ffuf -X POST -u http://10.10.10.10 \
  -d "username=FUZZ&password=foo" \
  -mr "Invalid username or password." \
  -w /usr/share/wordlists/SecLists/Usernames/xato-net-10-million-usernames.txt

3. Diferencias en el tiempo de respuesta

Cuando se programa la autenticación en una aplicación web, es un error muy común comprobar en dos fases si el usuario existe y si la contraseña es correcta para ese usuario. Observa el siguiente ejemplo:

function auth(username: string, password: string): boolean {
  User user = db.findUserByName(username);
  if (!user) return false;
  return bcrypt.verify(user.password, password);
}

Lo que está ocurriendo es que si el usuario no existe en la base de datos, la función termina inmediatamente, en cambio, si el usuario existe, entonces se comprueba la contraseña.

Como sabrás, las contraseñas se almacenan cifradas en la base de datos y el algoritmo más habitual para encriptarlas es Bcrypt. Este algoritmo tiene la característica de que tiene un coste de descifrado alto para mitigar ataques de fuerza bruta. El problema, es que cuanto más larga es la contraseña, más tardará el algoritmo en desencriptarla. Esto significa que si mandamos una contraseña lo suficientemente larga, podría tardar algunos segundos en verificarla.

¿Ves ya el problema? No te preocupes, es muy sencillo, el problema es que si hacemos fuerza bruta con una wordlist de nombres de usuario pasando una contraseña muy larga, cuando el usuario no exista la respuesta será inmediata, pero cuando sí exista, la respuesta tardará algunos segundos. Entonces podemos filtrar el resultado por tiempo de respuesta, por ejemplo las que tarden más de 2 segundos, pudiendo así enumerar usuarios de forma efectiva.

ffuf -X POST -u http://10.10.10.10 \
  -d "username=FUZZ&password=qwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiop" \
  -mt >3000 \
  -w /usr/share/wordlists/SecLists/Usernames/xato-net-10-million-usernames.txt

NOTA: Debes adaptar el filtrado del tiempo de respuesta a tu escenario, ya que esto varía mucho en función de la latencia. Puedes hacer algunas pruebas para comprobar cuál es el tiempo medio de respuesta cuando el usuario es incorrecto y en función de esto establecer el umbral más adecuado.

4. Enumeración por medio de bloqueo de cuenta

Otro mecanismo que tienen los atacantes para enumerar usuarios consiste en forzar el bloqueo de las cuentas para obtener mensajes de error que delaten la existencia del usuario.

Imagina que tenemos un fomulario de autenticación que siempre devuelve el mensaje Invalid Username or Password independientemente de si el usuario existe o no. Esto es un inconveniente para el atacante porque dificulta la enumeración de usuarios. Sin embargo, resulta que una vez que el usuario se autentica 3 veces de forma incorrecta la cuenta se bloquea durante 1 minuto con el fin de evitar ataques de fuerza bruta y el sistema lo notifica con el mensaje User blocked temporally. En este caso estamos en un escenario similar a los anteriores. El atacante solo debe lanzar el ataque 4 veces y en esta última recibirá los resultados.

for i in {1..4}; do \
ffuf -X POST -u http://10.10.10.10 \
-d "username=FUZZ&password=foo" \
-mr "User blocked temporally" \
-w /usr/share/wordlists/SecLists/Usernames/xato-net-10-million-usernames.txt \
; done

5. Medidas preventivas

  1. Devolver siempre el mismo mensaje genérico Invalid Username or Password en la autenticación.
  2. Usar variables para los mensajes del error para evitar errores al escribirlo varias veces.
  3. Devolver mensajes genéricos en formularios de reseteo de contraseñas y de alta de usuarios como Si el usuario existe, se enviará a tu correo el enlace de recuperación.
  4. Comprobar usuario y contraseña al mismo tiempo, no en dos fases, ya que puede generar tiempos de respuesta distintos que pueden ser aprovechados para enumerar usuarios.
  5. Si mostramos un mensaje de usuario bloqueado por demasiados intentos de inicio de sesión fallidos, debemos mostrar también el mensaje para usuarios que no existan.
Usa el conocimiento para construir, no para destruir 🎓
Copiado al portapapeles
menu