Lab BombThreat : SQLi + MFA Leak + JWT alg=none pour Désamorcer une Bombe Fictive
Pentest d'un portail de contrôle à accès restreint : contournement du login via injection SQL, récupération du code MFA directement dans la réponse JSON du serveur, élévation du niveau de clearance de level1 à level5 via une attaque JWT alg=none, et déclenchement de l'endpoint de désamorçage.
Contexte et Objectifs
| Champ | Détail |
|---|---|
| Plateforme | Webverse |
| Lien | Accéder au Lab |
| Cible | bombthreat.local |
| OS | Linux (nginx 1.27.5) |
| Vuln. clés | SQL Injection, MFA Leak, JWT alg=none |
| Difficulté | Medium |
Scénario : Un groupe anonyme appelé NEXUS a placé un dispositif de 15 kilotonnes sous Central London et exige 10 millions de dollars sous 24 heures. Le détonateur est relié à un panneau de contrôle distant appelé NEXUS Control. La mission : s'introduire dans le portail, escalader le niveau de clearance, et déclencher l'endpoint /api/bomb/deactivate avec les bons droits.
Kill chain :
Page de login unique sur port 80
│
▼
Injection SQL → contournement authentification
│
▼
Code MFA retourné en clair dans la réponse JSON
│
▼
Accès dashboard → niveau clearance level1, désamorçage exige level5
│
▼
Token JWT décodé → clearance encodée dans le payload
│
▼
Modification directe échoue (signature invalide)
│
▼
JWT alg=none → suppression de la vérification de signature
│
▼
clearance=level5 → désamorçage → flag
1. Reconnaissance
rustscan -b 500 -a bombthreat.local -- -sC -sV -Pn
Output :
PORT STATE SERVICE REASON VERSION
80/tcp open http syn-ack ttl 63 nginx 1.27.5
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-title: NEXUS Control -Detonation Portal
|_http-server-header: nginx/1.27.5
Un seul port ouvert : 80/tcp avec nginx 1.27.5. Le TTL de 63 confirme une machine Linux. L'intégralité de la surface d'attaque se résume à une application web.

Une page de login. Rien d'autre visible.
2. Enumération Web
VHOST Fuzzing
ffuf -u "http://bombthreat.local" \
-H "Host: FUZZ.bombthreat.local" \
-w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-5000.txt \
-fs 169
Aucun résultat. Pas de sous-domaine caché.
Content Discovery
feroxbuster -u "http://bombthreat.local" \
-w /usr/share/wordlists/seclists/Discovery/Web-Content/raft-medium-directories.txt
Aucun répertoire découvert.
Conclusion : La surface d'attaque est entièrement concentrée sur la page de login. Pas de panel admin sur un autre sous-domaine, pas de fichiers cachés. Tout se joue sur le formulaire d'authentification et l'API exposée.
Analyse du Code Source JavaScript
En inspectant la page (Ctrl+U), on trouve le script complet de l'application. C'est une SPA (Single Page Application) entièrement côté client qui consomme une API REST.
Les endpoints identifiés dans le code :
| Endpoint | Méthode | Rôle |
|---|---|---|
/api/auth/login | POST | Authentification username/password |
/api/auth/verify-mfa | POST | Vérification du code OTP à 6 chiffres |
/api/auth/logout | POST | Déconnexion |
/api/device | GET | Récupération des infos du dispositif (dashboard) |
/api/bomb/deactivate | POST | Désamorçage (requiert token JWT dans le header Authorization) |
Deux observations critiques dans le code :
1. Après le MFA validé, le token JWT est sauvegardé dans le sessionStorage :
sessionStorage.setItem("nexus_token", data.access_token);
2. L'appel de désamorçage envoie ce token dans le header Authorization :
const data = await api("POST", "/api/bomb/deactivate", null, {
Authorization: `Bearer ${_sessionToken}`,
});
Le token JWT contrôle donc les droits d'accès à l'endpoint critique. Si on peut modifier ce token, on peut élever nos droits.
3. SQL Injection : Contournement du Login
Références : Vaadata : SQL Injections / PayloadsAllTheThings : SQL Injection
Détection
On commence par tester la robustesse du formulaire avec une apostrophe simple :
username: 1'
password: test

Le serveur retourne Database Error.
Cette erreur est une information précieuse. Elle révèle deux choses :
- Le backend utilise une base de données SQL.
- L'entrée utilisateur est insérée directement dans la requête SQL sans nettoyage. L'apostrophe a cassé la syntaxe de la requête, provoquant une erreur côté base de données qui remonte jusqu'au client.
La requête côté serveur ressemble probablement à quelque chose de ce type :
SELECT * FROM users WHERE username = '1'' AND password = 'test'
↑
apostrophe non échappée → syntaxe invalide → erreur
Exploitation via Burp Suite
On intercepte la requête de login avec Burp Suite et on l'envoie au Repeater pour crafter notre payload.
Le payload classique de contournement d'authentification SQL :
username: ' OR '1'='1'--
password: anything
Comment ce payload fonctionne :
-- Requête originale
SELECT * FROM users WHERE username = '[INPUT]' AND password = '[INPUT]'
-- Avec notre payload dans username
SELECT * FROM users WHERE username = '' OR '1'='1'--' AND password = 'anything'
│ │
│ └─ -- commente le reste
└─ '1'='1' est toujours vrai
Le -- commente tout ce qui suit, y compris la condition sur le mot de passe. La condition '1'='1' est toujours vraie, donc la requête retourne le premier utilisateur de la table.

Réponse du serveur :

Deux surprises dans cette réponse :
- L'injection a fonctionné : on est authentifié en tant que
bgoldstein(premier compte dans la table). - Le code MFA est retourné directement dans la réponse JSON. Le serveur envoie le code OTP au client qui vient de se connecter, ce qui annule totalement l'intérêt du second facteur d'authentification.
Passage du MFA
Pour l'authentification finale, nous appliquons ce même payload directement depuis l'interface de l'application:

On récupère le code MFA depuis l'onglet Network des DevTools (ou depuis la réponse Burp) et on le saisit dans l'interface :


On est connecté au dashboard en tant que bgoldstein.
4. Exploration du Dashboard
Le dashboard est une single page. En scrollant, on atteint le bouton de désamorçage :

Clearance required: Level 5
Your clearance: Level 1
"The token encodes level 1"
Le message est explicite : notre token JWT encode level1 et le désamorçage requiert level5. Si on modifie le payload du token pour y mettre level5, on devrait passer.
5. JWT Forgery : alg=none
Références : PortSwigger JWT alg=none / jwt.io
Comprendre la Structure d'un JWT
Un JWT (JSON Web Token) est composé de trois parties séparées par des points, chacune encodée en Base64url :
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiZ29sZHN0ZWluIiwiY2xlYXJhbmNlIjoibGV2ZWwxIiwiaWF0IjoxNzgxNjg5MTg5fQ.f8kVA_V1rpOjrOE9qnn8qxC9-blHtIJn2kiuU6GFEp8
│ │ │
└── Header └── Payload └── Signature
Décodé avec jwt.io :
Header :
{
"alg": "HS256",
"typ": "JWT"
}
Payload :
{
"sub": "bgoldstein",
"clearance": "level1",
"iat": 1781689189
}
La signature est calculée ainsi :
HMAC-SHA256(
base64url(header) + "." + base64url(payload),
SECRET_KEY
)
Le problème si on modifie le payload directement :
Si on change level1 en level5 dans le payload et qu'on conserve la signature originale, le serveur recalcule la signature avec le nouveau payload et la compare à l'ancienne. Elles ne correspondent plus. Le token est rejeté.
C'est exactement ce qu'on a observé lors de la première tentative de modification.
L'Attaque alg=none
La RFC JWT spécifie qu'une valeur none pour l'algorithme signifie que le token n'est pas signé. Dans cette configuration, aucune signature n'est présente ni vérifiée.
La vulnérabilité : si le serveur ne valide pas rigoureusement la valeur du champ alg dans le header, un attaquant peut :
- Modifier le payload librement (changer
level1enlevel5) - Changer l'algorithme dans le header de
HS256ànone - Supprimer la signature (laisser la troisième partie vide)
- Soumettre ce token forgé
Le serveur, voyant alg=none, tente de "vérifier" un token sans signature, ce qui revient à ne rien vérifier du tout.
Token légitime :
header.payload.signature
Token alg=none forgé :
header_modifié.payload_modifié.
↑ signature vide, point final obligatoire
Construction du Token Forgé
Nouveau header (alg=none) :
{ "alg": "none", "typ": "JWT" }
Encodé en Base64url : eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0
Nouveau payload (clearance=level5) :
{ "sub": "bgoldstein", "clearance": "level5", "iat": 1781689189 }
Encodé en Base64url : eyJzdWIiOiJiZ29sZHN0ZWluIiwiY2xlYXJhbmNlIjoibGV2ZWw1IiwiaWF0IjoxNzgxNjg5MTg5fQ
Token final forgé :
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJiZ29sZHN0ZWluIiwiY2xlYXJhbmNlIjoibGV2ZWw1IiwiaWF0IjoxNzgxNjg5MTg5fQ.
Le point final sans rien après représente la signature vide, obligatoire pour respecter la structure JWT en trois parties.
Injection du Token Forgé
On ouvre les DevTools (F12), onglet Application, puis Session Storage. On remplace la valeur de nexus_token par notre token forgé :

On recharge la page. L'application lit le token depuis le sessionStorage via restoreSession(), l'envoie à /api/device pour charger le dashboard, et utilise ce même token pour l'appel à /api/bomb/deactivate.

Le serveur accepte le token forgé. Notre clearance est maintenant level5.
On clique sur le bouton de désamorçage.
6. Flag

Londres est sauvée.
7. Conclusion et Remédiation
Kill Chain Complète
1. RustScan → Port 80 uniquement, nginx, SPA
2. Code source JS → Endpoints API identifiés, JWT dans sessionStorage repéré
3. SQLi → ' OR '1'='1'-- → contournement login, bgoldstein
4. MFA Leak → Code OTP retourné dans la réponse JSON du login
5. Dashboard → clearance level1, désamorçage exige level5
6. JWT décodé → clearance encodée dans le payload
7. Modification directe → échoue (signature HMAC invalide)
8. alg=none → token forgé sans signature accepté par le serveur
9. clearance=level5 → /api/bomb/deactivate → flag
Remédiation
| Vecteur | Problème | Remédiation |
|---|---|---|
| SQL Injection | Entrées utilisateur insérées directement dans les requêtes SQL | Utiliser des requêtes préparées (prepared statements) ou un ORM. Ne jamais construire des requêtes SQL par concaténation de chaînes |
| MFA Leak | Le code OTP est retourné dans la réponse JSON du login | Le serveur ne doit jamais retourner le code MFA au client. Le code doit être envoyé uniquement via le canal secondaire (email, SMS, application TOTP) |
| JWT alg=none | Le serveur accepte des tokens sans signature | Valider explicitement que l'algorithme du token correspond à celui attendu côté serveur. Rejeter tout token avec alg=none ou un algorithme non autorisé |
| JWT clearance côté client | Le niveau de clearance est encodé dans le token JWT modifiable par le client | Les vérifications d'autorisation doivent toujours se faire côté serveur en base de données, jamais en faisant confiance à une valeur encodée dans un token client. Le token doit identifier l'utilisateur, le serveur récupère ses droits en base |
| Signature JWT faible | Le secret de signature HMAC n'est pas connu mais l'attaque alg=none contourne la vérification | Implémenter une vérification stricte des algorithmes côté serveur et utiliser une bibliothèque JWT maintenue avec des options de sécurité par défaut (ex: interdire none explicitement) |
[!NOTE] Ce lab illustre un chaînage de vulnérabilités où chaque étape débloque la suivante. La SQLi donne un accès partial, le MFA Leak lève le second facteur, et le JWT alg=none élève les droits. Aucune de ces trois vulnérabilités seule n'aurait suffi à atteindre l'objectif. C'est leur combinaison qui permet la compromission complète. En sécurité, l'évaluation d'une vulnérabilité isolée ne suffit pas : il faut toujours analyser ce qu'elle permet d'atteindre en chaîne.
Writeup rédigé par Zcook
