3st stage MetaRed CTF 2023 - Master JWT (web)

With just two requests you can get the flag. Read the code :) Don’t brute-force paths, you have the source code uploaded.

  • [48 solves / 401 points]

I was toooo happy to solve this web challenge!!

Analysis

This is the source code provided.

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)

The server gets the Authorization value from the header of the request. If it doesn’t exist, the server creates and send to response JWT token using app.secret_key and HS256 algorithm.

If Authorization value is exit, the server will try to decode it using app.secret_key and HS256 algorithm and obtain the user value. If the user value is admin, you can get the flag!

An structure of a JWT is shown below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
xxxx.yyyy.zzzz

- xxxx: header
{
"alg": "HS256",
"typ": "JWT"
}
- yyyy: payload
{
"user": "guest"
}
- zzzz: signature
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret_key
)

I had doubts. Can I guess the input(JWT token) from the response? Can I know the app.secret_key?

So, I tried below things for my doubts.

  1. using None algorithm instead of HS256 when sending jwt token.
    • It was wrong. Because it’s fixed the value of algorithms=['HS256'] when decoding.
    • I could have tried this if the algorithm wasn’t fixed.
  2. try to find app.secret_key.
    1
    2
    3
    # app.py
    random.seed(f"Sup3S4f3{randint(0, 1000)}")
    app.secret_key = f"Sup3S4f3{randint(0, 1000000000000)}"
    • If I know the seed, I can know the random value. It’s not completely random. The number of cases is only 1001.
    • I used the hashcat to find the key of jwt token created using the HS256 algorithm. For this, I needed to write a script to create the key candidate list.
      1
      2
      3
      4
      5
      6
      import random
      from random import randint

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

Finally I got the list for hashcat!

  • https://github.com/hashcat/hashcat
    1
    hashcat -m 16500 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoibm9uIHByaXZpbGVnZWQifQ.SmQA4Ic4hOGFRD4hdRuuact9gwKRZZwI3zpEgBnjocw ./list

The -m means hash modes, 16500 is JWT (JSON Web Token). Now I know the app.secret_key from the result. It was Sup3S4f3472520232207.

I used this site for create jwt token.

Solve

  1. Just connect and receive a jwt token in response.
  2. Infer the key used for encoding using the obtained jwt token and hashcat.
  3. Change the user value to admin, create a new jwt token, and send it.