Description Can you guess the exact token and unlock the hidden flag? Our school relies on tokens to authenticate students. Unfortunately, someone leaked an important file for token generation. Guess the token to get the flag. Additional details will be available after launching your challenge instance.

Security bugs are often not about complex cryptography, but about wrong assumptions. One of the most common — and dangerous — assumptions is that the current time is random enough for secrets.

In this write-up, we'll analyze the Chronohack challenge from picoCTF and show how a seemingly large token space can be cracked through clever timing and knowledge of Python's pseudorandom number generator.

Understanding the Challenge

When launched, Chronohack presents a remote service asking you to guess a token:

  • Token length: 20 characters
  • Alphabet: 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz
  • You get up to 50 guesses per connection
  • If you guess correctly, the server prints the flag

The service leaks the token generation code, and that's where the vulnerability lies.

The Token Generator

The server generates the token using this logic:

random.seed(int(time.time() * 1000))
token = ''.join(random.choice(ALPHABET) for _ in range(20))

So:

  • It uses Python's random module
  • It seeds with the current time in milliseconds
  • The entire token is a deterministic function of that seed

Breaking the Generator

1) Python's random is Predictable

The Python standard library's random the module uses a deterministic Mersenne Twister PRNG. If you know the seed, you can reproduce the same sequence of random choices.

That means perfectly duplicating the server's token generation is possible -if you can guess the seed.

2) The Seed Is Time-Based

The server seeds the RNG with:

int(time.time() * 1000)

This gives the current Unix time in milliseconds.

While we don't know the exact server timestamp, we do know it is close to our own clock plus network delay. This drastically limits the brute-force window: instead of 62²⁰possibilities, we need only to search for a few hundred milliseconds.

3) Multiple Attempts Per Connection

Critically, the server keeps the token fixed for up to 50 guesses. This allows us to repeatedly try predicted tokens on a single connection without reconnecting each time.

If the token were regenerated every guess, this attack wouldn't work.

Exploit Strategy

The strategy is to:

  1. Open a connection and note the local time in milliseconds
  2. Account for network latency by trying a range of nearby seed values
  3. For each seed and each guess index (0–49), generate a candidate token
  4. Submit these tokens until one matches

Instead of trying enormous token combinations, we try seeds like:

possible_seed ≈ int(local_time_ms) + offset + guess_index

Where offset compensates for differences between local and server clocks.

The Working Exploit

Here's the complete exploitation script that demonstrates this in practice:

HOST = "verbal-sleep.picoctf.net"
PORT = 64083
ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"

def gen(seed):
    random.seed(seed)
    return "".join(random.choice(ALPHABET) for _ in range(20))


for adjust in range(-50, 1000, 40):
    with socket.create_connection((HOST, PORT)) as s:
        start = int(time.time() * 1000)
        s.recv(1024)
        for i in range(50):
            seed = start + i + adjust
            token = gen(seed)
            s.sendall(token.encode() + b"\n")
            resp = s.recv(1024)
            if b"Sorry" not in resp:
                print("FLAG FOUND")
                print(resp.decode())
                print(s.recv(1024).decode())
                exit()

Why This Works

Time Is Predictable

Time is not random — it's ever increasing. Since the server seeds with a current timestamp, an attacker can approximate the seed to within a few milliseconds.

random Is Not Cryptographically Secure

Python's random module is not intended for security. It's deterministic, hence predictable.

The Token Is Reused

Had the server changed the token after each failed guess, this technique would fail.

How to Fix This

If you're implementing real token generation, never use:

random.seed(time.time())

Instead, use a cryptographically secure random source, such as:

import secrets
token = secrets.token_urlsafe(32)

Or:

token = secrets.choice(ALPHABET) for _ in range(20)

Python's secrets the module is designed for exactly this use case.

Conclusion

The Chronohack challenge beautifully illustrates how a tiny mistake — using predictable time-based seeding — can reduce an intractable problem to a small brute-force search.

From an apparent 62²⁰ search space, the flaw turns the problem into guessing a 200–1000 ms range of timestamps.