DVCTF 2025 - WEB LouvreArchives


Screenshot

In this challenge, the goal is to access flag.webp as indicated in the source code.

While exploring the page, I noticed a long base64-encoded string. Once decoded and reorganized, it contained several .webp images with unusual names like 1991781613.webp, 1273827392.webp, etc.

Checking the source code:

Screenshot

We understand that filenames are generated using Python’s getrandbits(32), which produces 32 random bits via the random.getrandbits function.

The interesting point here is that this function uses the Mersenne Twister generator, a PRNG (Pseudo-Random Number Generator) that is not cryptographically secure.


Study Time: The Mersenne Twister Link to heading

The Mersenne Twister is a widely-used pseudo-random generator (for example, it’s the default in Python’s random module). It generates 32-bit integers based on an internal state composed of 624 integers of 32 bits. Once this state is known, all subsequent numbers can be accurately predicted.

However, there’s a catch: the generated values are “tempered”—that is, transformed by a function to improve their distribution. To reconstruct the internal state, you must “untemper” these values.


Exploitation Link to heading

Since we can observe more than 624 .webp files named using getrandbits(32), we have enough outputs to reconstruct the generator’s internal state.

I used this repo: 👉 https://github.com/kmyk/mersenne-twister-predictor

What it does:

  1. Takes 624 outputs (file names, in this case),
  2. Applies the “untempering” algorithm to recover the original state,
  3. Allows us to predict future values of the generator.

Using this method, we retrieve the secret key used to sign the JWT:

1991781613


Crafting a Forged JWT Link to heading

Using this key, we can create a customized JWT with flag.webp as our profile image:

import jwt, datetime

secret = '1991781613'

payload = {
    'sub': 'user_id',
    'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1),
    'iat': datetime.datetime.utcnow(),
    'profilepicture': './images/flag.webp'
}

token = jwt.encode(
    payload,
    secret,
    algorithm='HS256',
    headers={'alg':'HS256','typ':'JWT'}
)

print(token)

Retrieving the Flag Link to heading

Replace the JWT cookie in Burp:

Screenshot

And we get:

Screenshot