CTF: TraceBash CTF
TraceBash CTF 2026 is organized by Team TraceBash, a globally ranked cybersecurity team focused on fostering learning, collaboration, and competition among security enthusiasts worldwide.
This challenge gave us a web app with a spin wheel. To win, I needed to score 85+ across 10 spins.
Challenge Description
Random Cheese
Jerry's finally opened his dream cheese shop, and Tom is furious! Every customer gets a lucky draw — spin the wheel 10 times and score 85+ to win the grand meal. But Tom rigged the system to make sure nobody ever gets THAT lucky... or did he?
URL: https://web-random-cheese.tracebash.xyz/ Author: DeadDroid
Recon

Started with the login page. Created a new account and logged in.

After logging in, I get a classic judol ahh game with a game progress tracker. Both attempts resulted in exactly 60 points, which felt a little too consistent to be pure luck.
Its unwinnable. Clearly rigged by the judol admin, but my gambling addicted bro, however, would've already deposited his entire life saving trying to recover the losses (100% mentalitas pemain slot). Anywayyyy, lets dig deeper.
The Spin Request
POST /spin HTTP/2
Host: web-random-cheese.tracebash.xyz
Cookie: session=eyJ1c2VybmFtZSI6ImFrdWNpbnRhcnVwaWFoIn0.akES2g.3f6JKT1cYli9atdGuZYg4uwBb2A
Response:
{"image":"cake-svgrepo-com.svg","score":9,"spins":1,"success":true,"val":9}
No payload in the request body, it just hits /spin bare. The score per spin looks random.
Finding the Lucky Number

I notice that there's a Settings page with a form to set a "Lucky Number" (range: 1–1000).
POST /update_lucky HTTP/2
Host: web-random-cheese.tracebash.xyz
Cookie: session=...
lucky_number=1
Looks like it seeds the spin wheel's RNG. I reset the lucky number to 1 and spun the wheel 10 times:
3, 10, 2, 5, 2, 8, 8, 8, 7, 4 → total: 57
Different seeds produce different sequences, but the same seed always produces the same sequence (ngomong apaan gw jir). Lucky number = RNG seed. Confirmed.
I thought I could simply brute force t seeds 1–1000 until we find one that totals 85+.
But /update_lucky has rate limiting:
HTTP/2 429 Too Many Requests
Rate Limit Exceeded: You are sending requests too fast.
Please do not brute force the lucky number. Try again in a minute.
No problem, i don't need to hit the server at all.
Why "Random" Isn't Really Random
Did you know that computers can't generate truly random numbers on their own?. Instead, they use Pseudorandom Number Generators (PRNGs) algorithms that produce a deterministic sequence of numbers from a starting value called a seed. Given the same seed, the same algorithm will always produce the same sequence. Every time.
This is why seeded RNGs are predictable and exploitable in challenges like this.
Different languages use different PRNG algorithms (Mersenne Twister, PCG, xoshiro, etc.) and may also handle seed initialization differently, so I need to confirm which language (and runtime) the backend uses before simulating locally.
Identifying the Backend Language
Response headers often leak the tech stack. Headers to look for: X-Powered-By, X-Generator, Server, Via, etc.
In this case the Server header reveals nginx/1.28.3 just the web server, not the language.
The real clue is the session cookie:
Set-Cookie: session=eyJ1c2VybmFtZSI6ImFrdWNpbnRhcnVwaWFoIn0.akEZCw.c-w7EwPJZ5rywXpX29W4V-hBaH0; HttpOnly; Path=/
This is a typical Flask session cookie. It has three Base64-encoded parts separated by .:
| Part | Value | Meaning |
|---|---|---|
| Payload | eyJ1c2VybmFtZSI6ImFrdWNpbnRhcnVwaWFoIn0 | {"username": "akucintarupiah"} |
| Timestamp | akEZCw | Serialized by itsdangerous |
| Signature | c-w7EwPJZ5rywXpX29W4V-hBaH0 | HMAC signed with the server's secret key |
Flask + itsdangerous = Python backend. So we use Python's random module to simulate locally.

Confirming the Sequence

Set lucky number to 1, spun twice got 3 then 10.
Now verify locally:
>>> import random
>>> random.seed(1)
>>> random.randint(1, 10)
3
>>> random.randint(1, 10)
10

Yep, we're cooking. I can now fully simulate the server's RNG offline by building my own casino.
Brute Force the Seed
import random
for seed in range(1, 1001):
random.seed(seed)
total = sum(random.randint(1,10) for _ in range(10))
if total >= 85:
random.seed(seed)
values = [random.randint(1,10) for _ in range(10)]
print(f"Gacorrr!!! Seed {seed}, values: {values}, total: {total}")
C:\projects\ctf>python judol.py
Gacorrr!!! Seed 854, values: [6, 6, 9, 9, 10, 8, 10, 9, 10, 10], total: 87
Gacorrr!, the seeder is 854. now lets put it on lucky number

Getting the Flag
Set the winning seed in Settings, spin the wheel 10 times, score 85+, and the flag appears.
Flag:
TBCTF{t0m_4nd_j3rry_l0v3s_ch33s3_4nd_r4nd0mness}
Takeaway
- random is not truly random
- Stop judol blokkkk!

