Web
Medium
250 points

BP88 Level 2

Recuite 2025 - HCMUS
6 tháng 10, 2025
PRNG
Dynamic Seed
Timestamp
Socket.IO
Recuite 2025 - HCMUS
Web

BP88 Level 2 - Write-up

Challenge Information

  • Category: Web
  • Difficulty: Medium
  • Vulnerability: Predictable PRNG (Dynamic Seed - Exposed)
  • Requirement: Complete Level 1 (50 coins)

Overview

Level 2 increases difficulty by using a dynamic seed based on time.time(). However, the server broadcasts the seed value via Socket.IO event gameData, allowing attackers to predict dice results.

Game Mechanics

Win Condition

  • Level 2: Reach 1,000 coins to unlock Level 3
  • Start from 50 coins (from Level 1)

Vulnerability: Exposed Dynamic Seed

Source Code Analysis

def prepare_seed(level):
    """ Create seed for `random` module """
    if level == 1:
        random.seed(13371337)
    elif level == 2:
        random.seed(round(time.time()))  # ⚠️ Seed based on timestamp
    else:
        random.seed()

Server broadcasts seed via Socket.IO:

// Client-side event
socket.on('gameData', function(data) {
    # data.id_level2 = round(time.time())
    console.log('Seed for Level 2:', data.id_level2);
});

The Problem:

  • Seed value is exposed via WebSocket events
  • Attacker can use seed to predict dice results
  • Server-side and client-side share the same seed value

Exploitation

Step 1: Capture Seed Value

Method 1: Browser DevTools

  1. Open DevTools (F12) → Console
  2. Listen for Socket.IO events:
// Intercept gameData event
let level2_seed = null;

socket.on('gameData', function(data) {
    console.log('Game Data:', data);
    if (data.id_level2) {
        level2_seed = data.id_level2;
        console.log('🎯 Seed Level 2:', level2_seed);
    }
});
  1. Seed will be displayed on page (under countdown timer) or in console

Method 2: Network Tab

  1. DevTools → Network → WS (WebSocket)
  2. Click on Socket.IO connection
  3. View Messages tab
  4. Find gameData event with id_level2 field

Step 2: Predict Result

import random

def predict_result_from_seed(seed):
    random.seed(seed)
    dice = [random.randint(1, 6) for _ in range(3)]
    total = sum(dice)
    result = 'tai' if total >= 11 else 'xiu'
    return result, dice, total

# Example: Seed captured from gameData
seed_level2 = 1760086470  # From Socket.IO event

result, dice, total = predict_result_from_seed(seed_level2)
print(f"Bet: {result}")
print(f"Dice: {dice}, Total: {total}")

Step 3: Automated Exploitation

import socketio
import random
import requests
import re
import time

class Level2Solver:
    def __init__(self, base_url):
        self.base_url = base_url
        self.http = requests.Session()
        self.sio = socketio.Client()
        self.latest_seed = None
        
        # Setup event handlers
        self.sio.on('gameData', self.on_game_data)
        self.sio.on('gameStart', self.on_game_start)
        self.sio.on('gameOver', self.on_game_over)
    
    def on_game_data(self, data):
        print(f"[*] gameData: {data}")
        if 'id_level2' in data:
            self.latest_seed = data['id_level2']
            print(f"[+] Captured seed: {self.latest_seed}")
    
    def on_game_start(self, data):
        print("[*] Round starting...")
        self.place_bet()
    
    def on_game_over(self, data):
        print(f"[*] Round over: {data}")
    
    def predict_result(self, seed):
        random.seed(seed)
        dice = [random.randint(1, 6) for _ in range(3)]
        return 'tai' if sum(dice) >= 11 else 'xiu'
    
    def place_bet(self):
        if not self.latest_seed:
            print("[!] Seed not captured yet")
            return
        
        # Predict result
        prediction = self.predict_result(self.latest_seed)
        print(f"[+] Prediction: {prediction}")
        
        # Get current money
        money = self.get_current_money()
        bet_amount = min(money, 50)  # Bet 50 or all-in
        
        # Place bet via Socket.IO
        self.sio.emit('pull', {
            'id': self.sio.sid,
            'dice': prediction,
            'level': 'level2',
            'name': self.username,
            'money': bet_amount
        })
        print(f"[+] Bet {bet_amount} on {prediction}")
    
    def get_current_money(self):
        resp = self.http.get(f"{self.base_url}/")
        match = re.search(r"You have\s+(\d+)\s+coins", resp.text)
        return int(match.group(1)) if match else 0
    
    def solve(self):
        # Login
        self.http.post(f"{self.base_url}/signup", data={
            'username': 'solver2',
            'password': 'pass123'
        })
        
        # Connect Socket.IO
        cookies = self.http.cookies.get_dict()
        cookie_str = '; '.join(f"{k}={v}" for k, v in cookies.items())
        self.sio.connect(self.base_url, headers={'Cookie': cookie_str})
        
        # Navigate to level2
        self.http.get(f"{self.base_url}/level2")
        
        # Wait and let auto-betting work
        while self.get_current_money() < 1000:
            time.sleep(2)
        
        print("[+] Finished Level 2!")
        print(f"[+] Final Balance: {self.get_current_money()}")

# Run solver
solver = Level2Solver("http://target.com")
solver.solve()

Real-Time Exploitation

Quick Manual Method

  1. Open Level 2 page
  2. Open DevTools Console
  3. Paste this code:
// Auto-predict and display
socket.on('gameData', function(data) {
    if (!data.id_level2) return;
    
    # Simulate Python's random
    let seed = data.id_level2;
    console.log('Seed:', seed);
    
    # You need to run separate Python script to get prediction
    # Or implement Mersenne Twister in JS
    console.log('🎲 Use this seed in Python script!');
});
  1. Run Python prediction:
# In terminal
python3 -c "
import random
seed = int(input('Enter seed from browser: '))
random.seed(seed)
dice = [random.randint(1,6) for _ in range(3)]
result = 'tai' if sum(dice) >= 11 else 'xiu'
print(f'Bet on: {result.upper()}')
"
  1. Place bet on predicted result

Understanding Timing

Seed Generation

# Server creates seed when round starts
current_time = round(time.time())  # Unix timestamp (seconds)
random.seed(current_time)

# Example timestamps:
# 2025-10-12 09:30:00 → 1760086200
# 2025-10-12 09:30:01 → 1760086201
# 2025-10-12 09:30:02 → 1760086202

Race Condition?

Q: Can I bet before receiving the seed?

A: No! Server broadcasts seed before accepting bets:

1. Round starts → Create seed
2. Broadcast gameData event (include seed)
3. Accept bets (5-10 second window)
4. Round ends → Show dice

Timing Attack (Not needed here)

If seed wasn't broadcasted, you could brute-force timestamp:

import time
import random

def brute_force_seed():
    current_time = round(time.time())
    
    # Try range ±5 seconds
    for offset in range(-5, 6):
        seed = current_time + offset
        random.seed(seed)
        dice = [random.randint(1, 6) for _ in range(3)]
        result = 'tai' if sum(dice) >= 11 else 'xiu'
        print(f"Seed {seed}: {result} {dice}")

Mitigation

❌ Vulnerable Code

# Seed exposed to client
seed = round(time.time())
random.seed(seed)
socket.emit('gameData', {'id_level2': seed})  # ⚠️ Leaked!

✅ Secure Code

import secrets

# DO NOT expose seed to client
def generate_secure_round():
    # Use cryptographically secure random
    dice = [secrets.randbelow(6) + 1 for _ in range(3)]
    
    # Optional: Commitment scheme for provably fair
    import hashlib
    server_seed = secrets.token_hex(32)
    client_seed = request.get('client_seed', '')
    
    # Hash commitment
    combined = f"{server_seed}{client_seed}"
    commitment = hashlib.sha256(combined.encode()).hexdigest()
    
    # Reveal after betting
    return dice, commitment, server_seed

Implementing Provably Fair

def provably_fair_dice(server_seed_hash, client_seed, nonce):
    # Client can verify after round
    import hashlib
    
    # Combine seeds
    combined = f"{server_seed_hash}{client_seed}{nonce}"
    hash_output = hashlib.sha256(combined.encode()).hexdigest()
    
    # Create dice from hash
    dice = []
    for i in range(3):
        byte_val = int(hash_output[i*2:(i+1)*2], 16)
        dice_val = (byte_val % 6) + 1
        dice.append(dice_val)
    
    return dice

Key Takeaways

  1. Never expose PRNG seeds to clients
  2. Time-based seeds can be predictable even if not exposed
  3. Socket.IO events can leak sensitive data
  4. Use secure random (secrets module) for games
  5. Implement provably fair if transparency is needed

Tools Used

  • Browser DevTools - Capture seed
  • Python socketio - Automated exploitation
  • Python random - Predict PRNG

Flag

HCMUS{t1m3st4mp_s33d_3xp0s3d}

References

250
Points
Medium
Difficulty
Web
Category