← Writing
ctfweb

I Hacked a Judi Online

by Gilang Windu Asmara


I Hacked a Judi Online

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

Login page

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

Home page

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

Settings page

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 .:

PartValueMeaning
PayloadeyJ1c2VybmFtZSI6ImFrdWNpbnRhcnVwaWFoIn0{"username": "akucintarupiah"}
TimestampakEZCwSerialized by itsdangerous
Signaturec-w7EwPJZ5rywXpX29W4V-hBaH0HMAC signed with the server's secret key

Flask + itsdangerous = Python backend. So we use Python's random module to simulate locally.

Banyak Omong


Confirming the Sequence

Confirming the sequence by spinning 2 times

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

Confirming the sequence by running Python locally

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 Gacor kang!!

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}

Flag captured

Takeaway

  1. random is not truly random
  2. Stop judol blokkkk!