# We Rigged a Slot Machine and Dared Hacker Summer Camp to Break It

> At DEF CON, Black Hat, and BSides Las Vegas we ran a casino slot that never pays — unless you can read the machine. Four planted web bugs, one leaderboard, and up to three months of Tandera on the line. Here is how it worked.

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.

![The 777: Jackpot or Jail entry screen](/assets/hsc/hero.jpg)

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.

![The dealer feed showing a tampered spin and the flag-1 capture](/assets/hsc/flag1-client-liar.jpg)

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.

![Submitting the recovered vault token takes flag 2](/assets/hsc/flag2-vault-cracked.jpg)

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.

![The burst dispenses the bonus several times over and hands out flag 3](/assets/hsc/flag3-race-won.jpg)

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.

![Calling the jackpot round before it happens takes flag 4](/assets/hsc/flag4-jackpot-chain.jpg)

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.

![All four flags captured, with the prize ready to claim](/assets/hsc/machine.jpg)

![The High Rollers leaderboard](/assets/hsc/leaderboard.jpg)

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

---

Canonical: https://tandera.io/blog/777-jackpot-or-jail
This page as markdown: https://tandera.io/blog/777-jackpot-or-jail.md
Index for agents: https://tandera.io/llms.txt

Every page here is also available as markdown: append `.md` to the path (e.g. `/recon.md`, `/index.md` for this homepage, `/blog/<slug>.md`), or request the canonical path with `Accept: text/markdown`.
