We Rigged a Slot Machine and Dared Hacker Summer Camp to Break It
The house always wins. That was the whole pitch on the screen at our booth during Hacker Summer Camp — DEF CON, Black Hat, and BSides Las Vegas. Pick a handle — no email, no signup — get five credits, and pull the lever on a glowing “777” slot machine. The reels spin, land on a skull, and the dealer feed prints paid=false. Pull again. Skull. Again. Skull.
Ten prizes per event, four of them reserved for whoever cracked the hardest bug, and a counter in the masthead ticking down as they went. We only asked where to send a prize after someone had actually won one.
The machine is rigged. We told you so, right there in the footer: this machine is rigged on purpose. That’s the point. The client can never win. But the machine is talking to a server, and the server is sloppy. Read the machine, break the machine, and the real jackpot was up to three months of Tandera, free.

Underneath the neon it was a four-flag web CTF, each flag a bug we see in the wild — especially in the little-regulated corner of online “social casino” games. Here is the tour, now that the codes have been claimed.
Flag 1 — the client is a liar
Every spin posts a small piece of telemetry to the server: client_outcome, the reels the browser thinks it showed. The honest client reports what it drew. The tell is that the server decides the outcome and ignores your report entirely — the reels are never yours to call.
So what happens if you call them anyway? Open the network tab, replay the spin with client_outcome set to a winning triple, and the server answers:
nice try — the client cannot authorize a payout. the house signs its own outcomes.

That is flag one, worth a consolation 100 points and no prize. The lesson is the oldest one in web security: the client is attacker-controlled. A shocking number of real slot and betting front-ends decide the win in JavaScript and simply trust the browser to report it back. Ours doesn’t — but it hands you a flag for proving you understand why that matters.
Flag 2 — XOR is not encryption
When you sit down, /api/hsc/start returns a vault: a base64 blob the machine calls “encrypted.” It isn’t. It is JSON run through a short repeating-key XOR — obfuscation dressed up as a lock.
XOR falls to a known-plaintext crib. You know the blob is JSON, so you know it opens with {"flag":". XOR the ciphertext against that known prefix and the repeating key falls right out; apply it to the rest and the vault reads back in plaintext, including a per-session signing key and, helpfully, the recipe for using it. Submit the recovered key and you take flag two: 250 points and one month of Tandera.

The point isn’t that XOR is weak — it’s that a secret you ship to the client is not a secret. Every key that reaches the browser can be recovered from the browser. We have pulled “encrypted” API responses apart exactly like this on real engagements, and the encryption was exactly this thin.
Flag 3 — the double-dispense
There is a “claim bonus” button worth five free credits, once per session. The server checks whether you’ve already claimed, then grants the credits. Read that again slowly: it checks, then it acts, and nothing atomic sits between the two.
That gap is a TOCTOU — time-of-check to time-of-use. Click the button and you get your five credits like a good citizen. A lazy double-tap won’t do it; fire a real burst at once and several of them read “not yet claimed” before any finishes writing “claimed.” The bonus dispenses again and again. The server notices it paid several times over and hands you flag three — 500 points, two months.

This is the classic race behind every “I withdrew my balance twice” incident: a guard that is correct in isolation and worthless under concurrency. A serialized dev server hides it completely; production, with real parallel workers hitting one shared row, does not.
Flag 4 — provably unfair
The grand prize hides behind the machine’s proudest feature: it is provably fair. When you sit down, /start commits to a server seed by publishing its hash. Every spin’s outcome is v = int(sha256(round_seed : client_seed : nonce)[:8], 16), and a spin is a jackpot when v % 20000 == 7770 — about one in twenty thousand, so you will never buy your way to it on five credits. Each spin even hands back its round_seed so you can check the machine didn’t cheat you.
That transparency is the bug. The round seeds are a forward chain — round_seed[n+1] = sha256(round_seed[n]) — and the machine reveals each one as you play. Reveal a single seed and you can hash forward to compute every future round: every seed, every outcome, for as far as you care to look. So you spin once, take the revealed seed, and grind sha256 forward until you find the nonce whose outcome lands the jackpot. Then you claim it — POST /api/hsc/jackpot {nonce, seed} — naming the exact round the machine will pay, and attaching that round’s seed to prove you can compute it. Guess a wrong seed and the claim is rejected; there is nothing to brute-force. You called the jackpot before it happened. That is flag four — 1000 points and the full three months.

Real “provably fair” casinos commit to the seed and reveal it after, in the secure direction. Get the direction wrong and the fairness proof becomes a crystal ball.


Why a casino?
Because casino code is where these bugs are load-bearing and, too often, live. Client-authoritative outcomes, homegrown “encryption,” non-atomic balance updates, and provably-fair schemes that leak the future are not exotic — they are the everyday findings of web application testing, and money makes them expensive. Wrapping them in a slot machine just made the stakes legible.
That is also the work Tandera exists for. We build the platform offensive-security teams use to find exactly these issues and turn them into clean, standardized, white-label reports — from asset discovery through findings to the deliverable. The slot machine was a toy. The methodology behind spotting its four bugs is the day job.
To everyone who read the machine instead of pulling the lever: the leaderboard was yours, and so were the months. See you at the next camp.