Attaques JWT¶
Les jetons web JSON (JSON Web Tokens, JWT) sont un format standardisé pour transmettre des données JSON signées cryptographiquement entre systèmes. Ils peuvent en théorie contenir n'importe quelle donnée, mais servent surtout à véhiculer des informations sur l'utilisateur — les claims, ou revendications — dans le cadre de l'authentification, de la gestion de session et du contrôle d'accès. Contrairement aux jetons de session classiques, toutes les données nécessaires au serveur sont stockées côté client, dans le jeton lui-même, ce qui en fait un choix prisé des architectures distribuées.
Format d'un JWT¶
Un JWT comporte trois parties séparées par des points : un en-tête, une charge utile et une signature. L'en-tête et la charge utile sont de simples objets JSON encodés en base64url ; l'en-tête contient des métadonnées sur le jeton, la charge utile les revendications sur l'utilisateur :
Ces données sont lisibles et modifiables par quiconque possède le jeton. Toute la sécurité repose donc sur la signature. Le serveur émetteur la génère en hachant l'en-tête et la charge utile à l'aide d'une clé secrète : la moindre modification d'un octet rend la signature invalide, et sans la clé, il ne devrait pas être possible de produire une signature correcte.
JWT, JWS, JWE
La spécification JWT ne définit qu'un format de revendications. En pratique, elle est étendue par JWS (JSON Web Signature, contenu signé) et JWE (JSON Web Encryption, contenu chiffré). Lorsqu'on parle de « JWT », on désigne presque toujours un jeton JWS ; c'est le cas dans toute cette section.
Les attaques JWT consistent à envoyer au serveur des jetons modifiés dans le but de contourner l'authentification et le contrôle d'accès, généralement en usurpant l'identité d'un autre utilisateur. L'impact est typiquement sévère : forger des jetons valides permet l'élévation de privilèges ou la prise de contrôle de comptes.
Failles de vérification de la signature¶
Le serveur ne conservant aucune trace des jetons qu'il émet, il ignore tout du contenu original : si la signature n'est pas correctement vérifiée, rien n'empêche de modifier la charge utile. Pour un jeton contenant {"username":"utilisateur","isAdmin":false}, passer isAdmin à true ouvre la voie à une élévation de privilèges.
Deux erreurs d'implémentation reviennent souvent. La première : les bibliothèques exposent généralement une méthode pour vérifier et une autre pour décoder. Un développeur qui confond les deux et n'utilise que la méthode de décodage ne vérifie en réalité jamais la signature.
La seconde concerne le paramètre alg de l'en-tête, qui indique l'algorithme de signature. Un JWT peut être non signé, auquel cas alg vaut none. Les serveurs rejettent normalement ces jetons, mais comme ce filtrage repose sur une comparaison de chaînes, on peut parfois le contourner par des techniques d'obfuscation — casse mélangée, encodages inattendus :
Le point final
Même non signé, le jeton doit conserver son point final : la partie signature est vide, mais le séparateur reste présent.
Brute force de la clé secrète¶
Les algorithmes comme HS256 (HMAC + SHA-256) utilisent une clé symétrique : une chaîne secrète qui sert à la fois à signer et à vérifier. Si cette clé est faible ou laissée à une valeur par défaut — un secret d'exemple copié-collé et jamais changé — elle peut être retrouvée par force brute hors ligne.
L'outil recommandé est hashcat. Avec un seul jeton valide et une liste de secrets connus, on lance :
Hashcat signe l'en-tête et la charge utile avec chaque secret de la liste et compare à la signature cible. Comme l'opération est entièrement locale, elle est extrêmement rapide, même sur une grande liste. Une fois le secret trouvé, on peut signer n'importe quel jeton : il suffit d'encoder le secret en Base64, de générer une clé symétrique dans l'extension JWT Editor de Burp en plaçant cette valeur dans le paramètre k, puis de modifier la charge utile et de resigner. Le dépôt wallarm/jwt-secrets fournit une bonne liste de secrets connus.
Injections via les paramètres d'en-tête¶
La spécification JWS définit plusieurs paramètres d'en-tête optionnels (les en-têtes JOSE) qui indiquent au serveur quelle clé utiliser pour vérifier la signature. Mal gérés, ils permettent d'injecter un jeton signé avec sa propre clé. Trois sont particulièrement intéressants : jwk (une clé publique intégrée), jku (l'URL d'un jeu de clés) et kid (un identifiant de clé).
Paramètre jwk¶
Le paramètre jwk permet d'intégrer une clé publique directement dans l'en-tête, au format JWK. Idéalement, le serveur ne devrait accepter qu'une liste blanche de clés ; un serveur mal configuré utilise n'importe quelle clé présente dans jwk. On exploite cela en signant le jeton avec sa propre clé privée RSA et en intégrant la clé publique correspondante dans l'en-tête.
L'extension JWT Editor automatise l'attaque : on génère une clé RSA, on modifie la charge utile dans l'onglet du jeton, puis on choisit l'attaque Embedded JWK en sélectionnant cette clé. Manuellement, il faudrait aussi aligner le paramètre kid de l'en-tête sur celui de la clé intégrée — ce que l'attaque automatisée fait pour nous.
Paramètre jku¶
Plutôt que d'intégrer la clé, certains serveurs récupèrent le jeu de clés à une URL indiquée par le paramètre jku. Les serveurs sûrs restreignent cette récupération à des domaines de confiance, mais on peut parfois exploiter des différences d'analyse d'URL pour contourner ce filtrage (voir les techniques de la section SSRF).
L'attaque consiste à héberger, sur un serveur que l'on contrôle, un jeu de clés contenant sa propre clé RSA, puis à modifier le jeton : aligner kid sur celui de notre clé, et ajouter un paramètre jku pointant vers notre jeu de clés.
{
"keys": [
{
"kty": "RSA",
"e": "AQAB",
"kid": "893d8f0b-061f-42c2-a4aa-5056e12b8ae7",
"n": "..."
}
]
}
Paramètre kid¶
Le kid identifie la clé à utiliser ; sa structure est une chaîne arbitraire choisie par le développeur, qui peut désigner une entrée en base ou un nom de fichier. S'il est vulnérable à la traversée de répertoires, on peut forcer le serveur à utiliser un fichier arbitraire comme clé de vérification.
C'est particulièrement dangereux si le serveur accepte les algorithmes symétriques : on fait alors pointer kid vers un fichier au contenu prévisible, et on signe le jeton avec un secret correspondant à ce contenu. La cible classique est /dev/null : ce fichier vide renvoie une chaîne vide, donc signer le jeton avec une chaîne vide produit une signature valide.
En pratique, on crée dans JWT Editor une clé symétrique dont le paramètre k vaut la version Base64 d'un octet nul (AA==), on modifie le kid comme ci-dessus, puis on signe sans toucher à l'en-tête. Si les clés sont stockées en base, le paramètre kid devient par ailleurs un vecteur d'injection SQL.
Autres paramètres
Le paramètre cty (type de contenu) peut, en cas de contournement de la signature, être détourné vers text/xml ou application/x-java-serialized-object pour ouvrir la voie à des attaques XXE ou de désérialisation. Le paramètre x5c (chaîne de certificats X.509) permet d'injecter des certificats auto-signés, à la manière de l'attaque jwk.
Attaques par confusion d'algorithme¶
Même avec un secret robuste, on peut forger des jetons valides en signant avec un algorithme différent de celui prévu par les développeurs. C'est l'attaque par confusion d'algorithme (ou confusion de clé).
Le principe repose sur la différence entre algorithmes symétriques et asymétriques. HS256 est symétrique : une seule clé signe et vérifie, elle doit donc rester secrète. RS256 est asymétrique : une clé privée signe, une clé publique — souvent partagée — vérifie. La faille vient des bibliothèques qui exposent une méthode de vérification unique, indépendante de l'algorithme, qui se fie au paramètre alg du jeton :
function verify(token, secretOrPublicKey){
algorithm = token.getAlgHeader();
if(algorithm == "RS256"){
// utilise la clé comme clé publique RSA
} else if (algorithm == "HS256"){
// utilise la clé comme secret HMAC
}
}
Le problème surgit quand le développeur, supposant que seul RS256 sera utilisé, passe systématiquement la clé publique à cette méthode. Si l'attaquant envoie un jeton signé en HS256, la bibliothèque traite alors la clé publique — qui est connue — comme un secret HMAC. L'attaquant peut donc signer son jeton en HS256 en utilisant la clé publique du serveur comme secret, et le serveur le validera.
L'attaque se déroule en quatre temps :
1. Obtenir la clé publique. Les serveurs l'exposent souvent comme objet JWK via un point de terminaison standard tel que /jwks.json ou /.well-known/jwks.json. Si elle n'est pas exposée, on peut la dériver d'une paire de jetons existants (voir plus bas).
2. Convertir la clé au bon format. Le serveur utilise sa propre copie locale, souvent dans un autre format (X.509 PEM, par exemple). La clé utilisée pour signer doit lui être identique octet pour octet, caractères non imprimables compris. Avec JWT Editor : créer une clé RSA à partir du JWK obtenu, copier sa version PEM, l'encoder en Base64, puis créer une clé symétrique dont le paramètre k reçoit cette chaîne encodée.
3. Modifier le jeton, en s'assurant que alg est bien réglé sur HS256.
4. Signer le jeton avec HS256 en utilisant la clé publique RSA comme secret.
Dériver la clé publique de jetons existants¶
Quand la clé publique n'est pas accessible, on peut la reconstituer à partir de deux jetons émis par le serveur. L'outil rsa_sign2n calcule les valeurs possibles de n, et PortSwigger en propose une version conteneurisée exécutable en une commande :
Le script affiche, pour chaque valeur candidate, une clé PEM encodée en Base64 et un jeton forgé signé avec elle. On envoie chaque jeton via Burp Repeater : un seul est accepté par le serveur, ce qui révèle la bonne clé. On crée alors une clé symétrique avec cette valeur de k, et on resigne sa requête sans modifier l'en-tête — en veillant à passer alg en HS256.
Aide-mémoire¶
| Faille | Approche |
|---|---|
| Signature non vérifiée | Méthode de décodage utilisée au lieu de la vérification |
| Jeton non signé accepté | alg à none, obfusqué (nOnE) ; conserver le point final |
| Secret faible (HS256) | Brute force hors ligne avec hashcat (-m 16500) |
| Clé intégrée acceptée | Paramètre jwk + clé RSA personnelle (Embedded JWK) |
| Clé récupérée à distance | Paramètre jku pointant vers son propre jeu de clés |
| Clé désignée par fichier | Paramètre kid en traversée vers /dev/null |
| Secret robuste mais clé publique connue | Confusion d'algorithme RS256 → HS256 |
| Clé publique indisponible | Dérivation depuis deux jetons (rsa_sign2n / sig2n) |
L'extension JWT Editor de Burp Suite couvre la quasi-totalité de ces manipulations (génération de clés, signature, attaques intégrées) et constitue l'outil de référence pour tester les JWT.