$ cat writeup.md…
$ cat writeup.md…
umdctf
Task: a betting web game leaks both the current session identifier and the HMAC key used for websocket actions, while a bundled WebAssembly module deterministically generates the hidden board from that session state. Solution: rehost the wasm locally, predict every mines and chicken round from (session_id, round_idx), sign valid client views, and automate 25 perfect max wins.
$ cat /etc/rate-limit
Rate limit reached (20 reads/hour per IP). Showing preview only — full content returns at the next hour roll-over.
rainbet sponsored by rainbet (for legal reasons this is not true). their rng backend was leaked (for legal reasons this is also not true)! can you get enough max wins?
English summary: the service exposes a browser game over HTTP and WebSocket. To get the flag, we need 25 consecutive max wins across two game modes, but the hidden mine and car positions are actually predictable from leaked session material and a leaked WebAssembly RNG backend.
The challenge breaks because the client is trusted with too much information.
/api/sessioninfo returns both:
session_idsecretThat secret is the HMAC key used by the browser to authorize moves.
static/app.js computes:
const view = canonView(); const sig = await hmacHex(state.secret, view); ws.send(JSON.stringify({ action, view, sig, ...extra }));
So the server accepts WebSocket actions such as reveal, cross, and cashout as long as we send the correct canonical view string and HMAC.
This is not enough by itself, because the hidden board still lives on the server. The next leak removes that last uncertainty.
rainbet.py loads rainbet_gen.wasm and exposes generate_game(session_id, round_idx). The parser reveals the raw format:
def generate_game(session_id: str, round_idx: int) -> dict: raw = _call_generate(session_id, round_idx) gtype = raw[0] if gtype == 0: grid_size = raw[1] num_mines = raw[2] mines = list(raw[3:3 + num_mines]) return { "type": "mines", "grid_size": grid_size, "num_mines": num_mines, "mines": mines, } if gtype == 1: risk_idx = raw[1] num_cars = raw[2] cars = list(raw[3:3 + num_cars]) return { "type": "chicken", "risk_idx": risk_idx, "cars": cars, }
...
$ grep --similar