Skip to main content

UMDCTF Rainbet - Complete Write-Up

1) Challenge Summary

Challenge: rainbet

Prompt (paraphrased): the betting site claims a leaked RNG backend and asks if we can get enough max wins.

Target: https://rainbet.challs.umdctf.io

Goal: reach the required consecutive max-win streak (25) and receive the flag.

Final flag recovered:

UMDCTF{one_might_argue_that_gambling_is_the_best_vice_but_they_would_be_wrong}


2) What "max win" means in this challenge

The server offers two game types:

  • mines: reveal every safe tile (all non-mine tiles) in the round.
  • chicken: cross until just before the first car, then cash out at exactly the maximum safe step.

A streak increases only when the round is solved with this "perfect" play.


3) Core Vulnerability / Why this is solvable

The repository includes rainbet_gen.wasm and a local wrapper (rainbet.py) that can deterministically generate each round from:

  • session_id
  • round_idx (streak index)

That means hidden hazards are predictable offline before sending game moves.

So the game is not truly random from the attacker perspective. Once we know session_id, we can fully precompute each round:

  • all mine locations in mines
  • all car locations in chicken

With this, "max win" is straightforward automation.


4) Protocol Observations (HTTP + WebSocket + HMAC)

From dynamic testing and scripts:

  • Session secrets are exposed by GET /api/sessioninfo.
  • Gameplay is over WebSocket: wss://rainbet.challs.umdctf.io/ws.
  • Move frames are authenticated with HMAC-SHA256 using the server-provided secret.

Message signing format used by exploit:

  • Mines view string: mines:{streak}:{grid_size}:{num_mines}:{sorted_revealed_csv}
  • Chicken view string: chicken:{streak}:{steps}:{crossed}
  • Signature: HMAC_SHA256(secret_hex_bytes, view_string).hexdigest()

Outgoing action frame shape:

{
"action": "reveal|cross|cashout",
"view": "...",
"sig": "...",
"tile": 17
}

(tile is included only for mines reveal actions.)


5) File-by-file project walkthrough

The folder contains:

  • rainbet.py
  • complete_exploit.py
  • exploit.py
  • analyze_rng.py
  • test_rng.py
  • web_analysis.py
  • rainbet_gen.wasm

5.1 rainbet.py

Purpose: thin local emulator / parser around rainbet_gen.wasm.

Important functions:

  • _call_generate(session_id, round_idx)

    • Instantiates WASM runtime.
    • Writes session bytes into module memory.
    • Calls exported generate.
    • Reads generated binary game payload.
  • generate_game(session_id, round_idx)

    • Parses payload into Python dict.
    • Returns either:
      • mines game object: grid size, mine count, exact mine indexes.
      • chicken game object: risk, multipliers, exact car indexes.
  • max_safe_steps(cars)

    • Returns first car index (or all steps if no cars), i.e., max safe cashout step.
  • is_mines_max_win(...) and is_chicken_max_win(...)

    • Local validators for perfect play.

This is the heart of prediction.

5.2 complete_exploit.py

Purpose: full end-to-end attack against live server.

Flow:

  1. get_session_info() calls /api/sessioninfo and captures session_id + secret.
  2. Connects to WebSocket with same session cookies.
  3. On each round:
    • Predicts game with generate_game(session_id, streak).
    • Computes optimal move sequence.
    • Signs each move with HMAC and sends it.
  4. Repeats until streak target reached.
  5. Reads flag from completion result frame.

5.3 exploit.py

Purpose: exploratory script for endpoint probing and simulation, useful during recon.

5.4 analyze_rng.py

Purpose: demonstrate deterministic behavior across sessions/rounds and analyze patterns.

5.5 test_rng.py

Purpose: quick sanity test for generator output and chicken safe-step calculations.

5.6 web_analysis.py

Purpose: HTML/endpoint scraping helper for early recon.


6) Exploit Development Notes (including fixes)

During execution we hit two practical issues and fixed them:

  1. WebSocket compatibility check
  • Original code used self.ws.sock.closed, but installed websocket-client exposed connected instead.
  • Fix: gate send with getattr(sock, "connected", False).
  1. Frame pacing / desync resilience
  • Some sends happened while a prior frame was still in-flight (pending=True), causing stalls.
  • Fix: retry timers on failed send attempts.
  • Also removed unnecessary mines cashout after all safe reveals; server already resolves the win immediately.

These changes made the exploit stable enough to run to 25/25.


7) Final Exploit Code (working version)

#!/usr/bin/env python3

import os
import sys
import requests
import websocket
import json
import hmac
import hashlib
import time
import threading
from concurrent.futures import ThreadPoolExecutor

sys.path.append(os.path.dirname(__file__))
from rainbet import generate_game, max_safe_steps, is_chicken_max_win, is_mines_max_win

BASE_URL = "https://rainbet.challs.umdctf.io"
WS_URL = "wss://rainbet.challs.umdctf.io/ws"

class RainbetExploit:
def __init__(self):
self.session = requests.Session()
self.ws = None
self.sid = None
self.secret = None
self.streak = 0
self.target = 25
self.game = None
self.progress = {"revealed": [], "crossed": 0}
self.pending = False
self.flag = None

def get_session_info(self):
"""Get session ID and secret from API"""
try:
response = self.session.get(f"{BASE_URL}/api/sessioninfo", cookies=self.session.cookies)
data = response.json()
self.sid = data["session_id"]
self.secret = data["secret"]
print(f"Got session: {self.sid}")
return True
except Exception as e:
print(f"Error getting session info: {e}")
return False

def hmac_hex(self, secret_hex, msg):
"""Generate HMAC-SHA256 hex signature"""
key = bytes.fromhex(secret_hex)
return hmac.new(key, msg.encode(), hashlib.sha256).hexdigest()

def canon_view(self):
"""Create canonical view string for HMAC"""
if self.game["type"] == "mines":
rev = ",".join(map(str, sorted(self.progress["revealed"])))
return f"mines:{self.streak}:{self.game['grid_size']}:{self.game['num_mines']}:{rev}"
else:
return f"chicken:{self.streak}:{self.game['steps']}:{self.progress['crossed']}"

def send_move(self, action, extra=None):
"""Send a move via WebSocket"""
sock = getattr(self.ws, "sock", None) if self.ws else None
if self.pending or not self.ws or not sock or not getattr(sock, "connected", False):
return False

self.pending = True
view = self.canon_view()
sig = self.hmac_hex(self.secret, view)

message = {"action": action, "view": view, "sig": sig}
if extra:
message.update(extra)

try:
self.ws.send(json.dumps(message))
return True
except Exception as e:
print(f"Error sending move: {e}")
self.pending = False
return False

def predict_game_outcome(self, session_id, round_idx):
"""Predict game outcome using the leaked RNG"""
return generate_game(session_id, round_idx)

def calculate_optimal_moves(self, game):
"""Calculate optimal moves for max win"""
if game["type"] == "chicken":
return {"action": "cashout_at", "value": max_safe_steps(game["cars"])}
elif game["type"] == "mines":
total_cells = game["grid_size"] ** 2
safe_positions = []
for i in range(total_cells):
if i not in game["mines"]:
safe_positions.append(i)
return {"action": "reveal_sequence", "tiles": safe_positions}

def on_message(self, ws, message):
"""Handle WebSocket messages"""
try:
data = json.loads(message)
print(f"Received: {data['kind']}")

if data["kind"] == "hello":
self.streak = data["streak"]
self.target = data["target"]
if data.get("complete"):
self.flag = "already complete"
return
self.game = data["game"]
self.progress = {
"revealed": data["game"].get("revealed", []),
"crossed": data["game"].get("crossed", 0)
}
self.start_auto_play()

elif data["kind"] == "state":
self.streak = data["streak"]
if "revealed" in data:
self.progress["revealed"] = data["revealed"]
if "crossed" in data:
self.progress["crossed"] = data["crossed"]

elif data["kind"] == "result":
if data.get("session_id") and data.get("secret"):
self.sid = data["session_id"]
self.secret = data["secret"]

if data["result"] == "win" and data.get("complete"):
self.flag = data.get("flag", "FLAG_FOUND")
print(f"FLAG: {self.flag}")
return

if data["result"] == "complete":
self.flag = data.get("flag", "FLAG_FOUND")
print(f"FLAG: {self.flag}")
return

if data.get("next_game"):
delay = 1600 if data["result"] == "lose" else 600
if data["result"] == "lose":
print("Streak reset!")
threading.Timer(delay/1000.0, self.apply_next_game, [data["next_game"]]).start()

elif data["kind"] == "error":
print(f"Error: {data['error']}")

self.pending = False

except Exception as e:
print(f"Error handling message: {e}")
self.pending = False

def apply_next_game(self, next_game):
"""Apply the next game state"""
self.streak = next_game["streak"]
self.target = next_game["target"]
if next_game.get("complete"):
self.flag = "already complete"
return
self.game = next_game["game"]
self.progress = {
"revealed": next_game["game"].get("revealed", []),
"crossed": next_game["game"].get("crossed", 0)
}
self.start_auto_play()

def start_auto_play(self):
"""Start automated playing using predicted outcomes"""
print(f"Starting auto-play for {self.game['type']} game (streak: {self.streak}/{self.target})")

predicted = self.predict_game_outcome(self.sid, self.streak)
optimal = self.calculate_optimal_moves(predicted)

if predicted["type"] == "chicken":
self.play_chicken_game(optimal["value"])
elif predicted["type"] == "mines":
self.play_mines_game(optimal["tiles"])

def play_chicken_game(self, cashout_at):
"""Play chicken game automatically"""
def cross_step():
if self.progress["crossed"] < cashout_at:
print(f"Crossing step {self.progress['crossed'] + 1}/{cashout_at}")
if self.send_move("cross"):
threading.Timer(0.5, cross_step).start()
else:
threading.Timer(0.15, cross_step).start()
else:
print(f"Cashing out at step {cashout_at}")
if not self.send_move("cashout"):
threading.Timer(0.15, cross_step).start()

if cashout_at == 0:
print("Cashing out immediately (max safe steps = 0)")
self.send_move("cashout")
else:
cross_step()

def play_mines_game(self, safe_tiles):
"""Play mines game automatically"""
def reveal_next(tile_index):
if tile_index < len(safe_tiles):
tile = safe_tiles[tile_index]
print(f"Revealing tile {tile} ({tile_index + 1}/{len(safe_tiles)})")
if self.send_move("reveal", {"tile": tile}):
threading.Timer(0.3, reveal_next, [tile_index + 1]).start()
else:
threading.Timer(0.15, reveal_next, [tile_index]).start()
else:
print("All safe tiles revealed, waiting for server result")

if safe_tiles:
reveal_next(0)
else:
print("No safe tiles to reveal")
if not self.send_move("cashout"):
threading.Timer(0.15, self.play_mines_game, [safe_tiles]).start()

def run(self):
"""Run the complete exploit"""
print("Starting Rainbet exploit...")

if not self.get_session_info():
return False

ws_thread = threading.Thread(target=self.connect_websocket)
ws_thread.daemon = True
ws_thread.start()

while self.flag is None:
time.sleep(1)

print(f"Exploit complete! Flag: {self.flag}")
return True

def connect_websocket(self):
"""Connect to WebSocket"""
try:
cookie_header = "; ".join(
f"{c.name}={c.value}" for c in self.session.cookies
)
headers = [f"Cookie: {cookie_header}"] if cookie_header else None
self.ws = websocket.WebSocketApp(
WS_URL,
header=headers,
on_message=self.on_message,
on_error=lambda ws, err: print(f"WS Error: {err}"),
on_close=lambda ws, close_status_code, close_msg: print(
f"WS Closed ({close_status_code}): {close_msg}"
),
on_open=lambda ws: print("WS Connected")
)
self.ws.run_forever()
except Exception as e:
print(f"WebSocket connection error: {e}")

if __name__ == "__main__":
exploit = RainbetExploit()
exploit.run()

8) Reproduction Steps (from clean machine)

8.1 Setup

cd rainbet
python3 -m venv venv
source venv/bin/activate
./venv/bin/python -m pip install websocket-client requests wasmtime beautifulsoup4

8.2 Run exploit

./venv/bin/python complete_exploit.py

Expected end state:

  • exploit auto-plays all rounds
  • streak reaches 25/25
  • terminal prints FLAG: UMDCTF{...}

9) Flag Meaning (interpretation)

Flag text:

one_might_argue_that_gambling_is_the_best_vice_but_they_would_be_wrong

Interpretation:

  • It is a thematic joke/commentary tied to a casino challenge.
  • "Best vice" is intentionally provocative; the flag says this claim is wrong.
  • In context, it also hints that trusting gambling randomness was a mistake because deterministic RNG made it exploitable.

10) Student Glossary ("grocery" of concepts)

  • RNG (Random Number Generator): mechanism used to produce game randomness.
  • Deterministic RNG: same inputs always produce same output.
  • WASM (WebAssembly): binary format executed in a sandbox runtime.
  • Reverse engineering: understanding behavior by inspecting code/binaries/protocols.
  • Reconnaissance (recon): information gathering phase.
  • Session ID: identifier tying requests to one user session.
  • WebSocket: persistent full-duplex protocol for real-time messages.
  • HMAC: keyed hash used for message integrity/authentication.
  • Canonicalization: constructing exactly one agreed text form before signing.
  • Race/frame desync: client/server state mismatch caused by timing/order issues.
  • Streak: consecutive successful rounds.
  • Max win: perfect strategy outcome for a round.

11) Why the exploit works mathematically

Let:

  • ss = session id
  • rr = round index (streak)
  • G(s,r)G(s, r) = game state generator in leaked WASM

If server uses same deterministic generator:

\text{hidden_hazards}_{server}(s, r) = G(s, r) = \text{hidden_hazards}_{attacker}(s, r)

Then attacker can choose an optimal action sequence A(s,r)A^*(s,r) that avoids all hazards with probability 1 (ignoring transport failures).

So expected per-round success becomes near-certain, and reaching streak target is automation + reliability.


12) Security Lessons

  • Never ship or expose deterministic RNG internals that let clients reproduce hidden outcomes.
  • Never expose high-privilege secrets (/api/sessioninfo returning signing secret).
  • Signed client views are only as secure as secret handling and protocol design.
  • Real-time protocols need idempotence/retry logic or strict sequence numbering to avoid desync.

13) Full Supporting Code (for completeness)

rainbet.py

import os
from typing import Sequence

import wasmtime

CHICKEN_STEPS = 24
RISK_NAMES = ("Easy", "Medium", "Hard", "Daredevil")

CHICKEN_MULT_TABLES = (
(1.03, 1.08, 1.14, 1.21, 1.30, 1.40, 1.52, 1.66, 1.82, 2.01,
2.24, 2.52, 2.86, 3.27, 3.77, 4.40, 5.21, 6.26, 7.65, 9.52,
12.12, 15.75, 20.93, 28.52),
(1.14, 1.38, 1.67, 2.01, 2.43, 2.93, 3.54, 4.28, 5.17, 6.24,
7.54, 9.10, 10.99, 13.28, 16.04, 19.37, 23.40, 28.25, 34.11, 41.19,
49.75, 60.08, 72.55, 87.61),
(1.60, 2.74, 4.85, 8.90, 16.98, 33.97, 70.23, 150.50, 333.87, 768.41,
1835.50, 4558.50, 11815.00, 31929.00, 91101.00, 275088.00, 882501.00, 3014100.00, 11047000.00, 43692000.00,
189060000.00, 903960000.00, 4855700000.00, 30242000000.00),
(2.50, 6.85, 18.96, 52.65, 146.5, 408.0, 1139.0, 3187.0, 8932.0, 25072.0,
70450.0, 198158.0, 557930.0, 1571000.0, 4427000.0, 12482000.0, 35223000.0, 99455000.0, 281096000.0, 794796000.0,
2248571000.0, 6362800000.0, 18000000000.0, 50930000000.0),
)

_WASM_PATH = os.path.join(os.path.dirname(__file__), "rainbet_gen.wasm")
_engine = wasmtime.Engine()
_module = wasmtime.Module.from_file(_engine, _WASM_PATH)


def _instance():
store = wasmtime.Store(_engine)
inst = wasmtime.Instance(store, _module, [])
exports = inst.exports(store)
return store, exports


def _call_generate(session_id: str, round_idx: int) -> bytes:
store, exports = _instance()
memory = exports["memory"]
sid_bytes = session_id.encode("ascii")
if len(sid_bytes) > 64:
raise ValueError("session_id too long")

sid_ptr = exports["sid_buf_ptr"](store)
out_ptr = exports["out_buf_ptr"](store)
memory.write(store, sid_bytes, sid_ptr)

n = exports["generate"](store, len(sid_bytes), round_idx)
return bytes(memory.read(store, out_ptr, out_ptr + n))


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,
"max_reveals": grid_size * grid_size - num_mines,
}
if gtype == 1:
risk_idx = raw[1]
num_cars = raw[2]
cars = list(raw[3:3 + num_cars])
return {
"type": "chicken",
"steps": CHICKEN_STEPS,
"risk_idx": risk_idx,
"risk": RISK_NAMES[risk_idx],
"multipliers": list(CHICKEN_MULT_TABLES[risk_idx]),
"cars": cars,
}
raise RuntimeError(f"unknown game type {gtype}")


def max_safe_steps(cars: Sequence[int]) -> int:
car_set = set(cars)
for i in range(CHICKEN_STEPS):
if i in car_set:
return i
return CHICKEN_STEPS


def is_mines_max_win(game: dict, moves) -> bool:
if game["type"] != "mines" or not isinstance(moves, list):
return False
total = game["grid_size"] ** 2
max_reveals = total - game["num_mines"]
if len(moves) != max_reveals:
return False
mines = set(game["mines"])
seen = set()
for m in moves:
if not isinstance(m, int) or m < 0 or m >= total:
return False
if m in mines or m in seen:
return False
seen.add(m)
return True


def is_chicken_max_win(game: dict, cash_out_at) -> bool:
if game["type"] != "chicken" or not isinstance(cash_out_at, int):
return False
if cash_out_at < 0 or cash_out_at > CHICKEN_STEPS:
return False
return cash_out_at == max_safe_steps(game["cars"])

analyze_rng.py

#!/usr/bin/env python3

import os
import sys
sys.path.append(os.path.dirname(__file__))

from rainbet import generate_game


def analyze_session_patterns():
print("Analyzing different session IDs:")

session_ids = ["a", "test", "admin", "user", "flag"]

for session_id in session_ids:
print(f"\nSession ID: '{session_id}'")
for round_idx in range(3):
game = generate_game(session_id, round_idx)
if game["type"] == "chicken":
from rainbet import max_safe_steps
max_steps = max_safe_steps(game["cars"])
print(f" Round {round_idx}: risk={game['risk']}, max_safe={max_steps}, cars={game['cars']}")
else:
print(f" Round {round_idx}: mines game, mines={game['mines']}")


def test_deterministic():
print("\n" + "="*50)
print("Testing if RNG is deterministic:")

session_id = "test"
round_idx = 0

games = []
for i in range(5):
game = generate_game(session_id, round_idx)
games.append(game)

all_same = all(str(g) == str(games[0]) for g in games)
print(f"Same session+round produces identical results: {all_same}")
print(f"First game: {games[0]}")


def test_round_progression():
print("\n" + "="*50)
print("Testing round progression pattern:")

session_id = "test"
for round_idx in range(10):
game = generate_game(session_id, round_idx)
if game["type"] == "chicken":
from rainbet import max_safe_steps
max_steps = max_safe_steps(game["cars"])
print(f"Round {round_idx:2d}: risk={game['risk']:8s}, max_safe={max_steps:2d}")
else:
print(f"Round {round_idx:2d}: mines game")


if __name__ == "__main__":
analyze_session_patterns()
test_deterministic()
test_round_progression()

test_rng.py

#!/usr/bin/env python3

import os
import sys
sys.path.append(os.path.dirname(__file__))

from rainbet import generate_game


def test_rng():
session_id = "test"

print("Testing RNG generation:")
for round_idx in range(5):
game = generate_game(session_id, round_idx)
print(f"Round {round_idx}: {game}")

if game["type"] == "chicken":
from rainbet import max_safe_steps
max_steps = max_safe_steps(game["cars"])
print(f" Max safe steps: {max_steps}")
print(f" Cars at positions: {game['cars']}")

print()


if __name__ == "__main__":
test_rng()

exploit.py

#!/usr/bin/env python3

import os
import sys
import requests
import json
sys.path.append(os.path.dirname(__file__))

from rainbet import generate_game, max_safe_steps, is_chicken_max_win, is_mines_max_win

BASE_URL = "https://rainbet.challs.umdctf.io"


def test_api_endpoints():
"""Test different API endpoints to understand the interface"""

session = requests.Session()

endpoints = [
"/",
"/api/game",
"/api/generate",
"/api/session",
"/game",
"/generate",
"/session"
]

print("Testing API endpoints:")
for endpoint in endpoints:
try:
response = session.get(BASE_URL + endpoint)
print(f"{endpoint}: {response.status_code}")
if response.status_code == 200 and len(response.text) < 500:
print(f" Content: {response.text[:200]}...")
except Exception as e:
print(f"{endpoint}: Error - {e}")


def find_session_id():
"""Try to find or create a session"""

session = requests.Session()

try:
response = session.get(BASE_URL)
cookies = session.cookies
print(f"Cookies after initial request: {dict(cookies)}")

for cookie in cookies:
if 'session' in cookie.name.lower() or 'sid' in cookie.name.lower():
print(f"Found session cookie: {cookie.name} = {cookie.value}")
return cookie.value

except Exception as e:
print(f"Error getting session: {e}")

return None


def simulate_game_play():
"""Simulate playing games to understand the flow"""

session_id = "test"

print(f"\nSimulating games for session: {session_id}")

for round_idx in range(5):
game = generate_game(session_id, round_idx)
print(f"\nRound {round_idx}: {game['type']}")

if game["type"] == "chicken":
max_steps = max_safe_steps(game["cars"])
print(f" Cars: {game['cars']}")
print(f" Max safe steps: {max_steps}")
print(f" Should cash out at: {max_steps}")

is_max_win = is_chicken_max_win(game, max_steps)
print(f" Is max win: {is_max_win}")

elif game["type"] == "mines":
print(f" Grid: {game['grid_size']}x{game['grid_size']}")
print(f" Mines: {game['mines']}")
print(f" Max reveals: {game['max_reveals']}")

total_cells = game['grid_size'] ** 2
safe_positions = []
for i in range(total_cells):
if i not in game['mines']:
safe_positions.append(i)

print(f" Safe positions: {safe_positions}")
is_max_win = is_mines_max_win(game, safe_positions)
print(f" Is max win: {is_max_win}")


if __name__ == "__main__":
test_api_endpoints()
find_session_id()
simulate_game_play()

web_analysis.py

#!/usr/bin/env python3

import requests
import re
from bs4 import BeautifulSoup

BASE_URL = "https://rainbet.challs.umdctf.io"


def analyze_webpage():
"""Analyze the webpage to find API endpoints and game logic"""

try:
response = requests.get(BASE_URL)
html_content = response.text

print("=== HTML Content Analysis ===")

js_files = re.findall(r'src=["\']([^"\']*\.js)["\']', html_content)
print(f"JavaScript files found: {js_files}")

api_patterns = [
r'["\'](/[a-zA-Z0-9/_-]+)["\']',
r'fetch\s*\(\s*["\']([^"\']+)["\']',
r'\.get\s*\(\s*["\']([^"\']+)["\']',
r'\.post\s*\(\s*["\']([^"\']+)["\']',
]

potential_endpoints = set()
for pattern in api_patterns:
matches = re.findall(pattern, html_content)
for match in matches:
if match.startswith('/'):
potential_endpoints.add(match)

print(f"Potential API endpoints: {sorted(potential_endpoints)}")

session_patterns = [
r'session[_-]?id["\']?\s*[:=]\s*["\']([^"\']+)["\']',
r'sid["\']?\s*[:=]\s*["\']([^"\']+)["\']',
r'cookie\s*\(\s*["\']([^"\']+)["\']',
]

for pattern in session_patterns:
matches = re.findall(pattern, html_content)
if matches:
print(f"Session-related matches: {matches}")

soup = BeautifulSoup(html_content, 'html.parser')

forms = soup.find_all('form')
buttons = soup.find_all('button')
inputs = soup.find_all('input')

print(f"Found {len(forms)} forms, {len(buttons)} buttons, {len(inputs)} inputs")

game_elements = soup.find_all(['div', 'button'], class_=re.compile(r'game|play|bet|win'))
print(f"Game-related elements: {len(game_elements)}")

return html_content

except Exception as e:
print(f"Error analyzing webpage: {e}")
return None


def test_endpoints(html_content):
"""Test potential endpoints found in the HTML"""

if not html_content:
return

endpoints = re.findall(r'["\'](/[a-zA-Z0-9/_-]+)["\']', html_content)
endpoints = [ep for ep in set(endpoints) if len(ep) > 1 and not ep.endswith('.js')]

print(f"\n=== Testing {len(endpoints)} endpoints ===")

session = requests.Session()

for endpoint in endpoints:
if endpoint in ['/', '//', 'http://', 'https://']:
continue

try:
response = session.get(BASE_URL + endpoint, timeout=5)
print(f"GET {endpoint}: {response.status_code}")

if response.status_code == 200 and len(response.text) < 1000:
content_preview = response.text[:200].replace('\n', ' ')
print(f" Content: {content_preview}...")

except Exception as e:
print(f"GET {endpoint}: Error - {e}")


if __name__ == "__main__":
html = analyze_webpage()
test_endpoints(html)

14) Final Result

  • Required streak achieved: 25/25
  • Flag obtained successfully:

UMDCTF{one_might_argue_that_gambling_is_the_best_vice_but_they_would_be_wrong}

This challenge is a great demonstration of why predictable RNG and exposed signing secrets are fatal in game/security systems.