Web
Medium
300 points

BP88 Level 3

Recuite 2025 - HCMUS
6 tháng 10, 2025
PRNG
Hidden Seed
DevTools
Browser Exploitation
Recuite 2025 - HCMUS
Web

BP88 Level 3 - Write-up

Challenge Information

  • Category: Web
  • Difficulty: Medium-Hard
  • Vulnerability: Predictable PRNG (Dynamic Seed - Hidden)
  • Requirement: Complete Level 2 (1,000 coins)

Overview

Level 3 is the final challenge of BP88. The seed is still a dynamic timestamp like Level 2, but it is NOT broadcasted in the gameData event. Players must find a way to extract the hidden seed value via browser DevTools and inspecting Socket.IO events.

Game Mechanics

Win Condition

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

Vulnerability: Hidden Seed But Still Accessible

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()))
    else:
        random.seed(round(time.time()))  # ⚠️ Same as Level 2!

Difference: Server does not include id_level3 in Socket.IO events (or it's redacted):

// gameData event payload
{
    id_level1: 13371337,
    id_level2: 1760086470,
    id_level3: "***REDACTED***",  // Hidden!
    time: 8
}

However: The seed value might still be accessible via:

  1. Browser console.log() if debug code is present
  2. Checking Socket.IO messages
  3. Timing correlation with Level 2
  4. JavaScript source code if client-side seed usage exists

Exploitation Techniques

Method 1: Detecting console.log()

Hypothesis: Developer might have left debug code:

// Somewhere in client-side JS
console.log('Level 3 seed:', level3_seed);  # ⚠️ Debug code!

Exploitation:

  1. Open Browser DevTools (F12)
  2. Go to Console tab
  3. Find logged values
  4. Look for patterns matching timestamp format
// Filter console for numbers
console.log = (function(oldLog) {
    return function(...args) {
        args.forEach(arg => {
            if (typeof arg === 'number' && arg > 1700000000) {
                console.warn('🎯 Potential see found:', arg);
            }
        });
        oldLog.apply(console, args);
    };
})(console.log);

Method 2: Reusing Seed from Level 2

Key Point: Level 2 and Level 3 both use round(time.time())!

Exploitation:

import random

# Assumption: Level 3 uses same seed as Level 2
# because they occur in the same time window

def predict_level3(level2_seed):
    # If rounds happen in same second, seeds are identical
    random.seed(level2_seed)
    dice = [random.randint(1, 6) for _ in range(3)]
    result = 'tai' if sum(dice) >= 11 else 'xiu'
    return result, dice

# Get seed from last round of Level 2
last_seed = 1760086470  # From Level 2

# Try same seed for Level 3
result, dice = predict_level3(last_seed)
print(f"Prediction Level 3: {result}")

If timing is different:

import time

# Get current timestamp
current_time = round(time.time())

# Try current time ±2 seconds
for offset in [-2, -1, 0, 1, 2]:
    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}")

Method 3: Intercepting Socket.IO Messages

Even if not in gameData, seed might be in other events:

// Intercept ALL Socket.IO messages
socket.onAny((eventName, ...args) => {
    console.log('Event:', eventName, args);
    
    // Deep search for values looking like seeds
    JSON.stringify(args).match(/\d{10}/g)?.forEach(num => {
        console.log('🔍 Potential Seed:', num);
    });
});

Method 4: Analyzing JavaScript Source Code

Check client-side seed usage:

  1. DevTools → Sources tab
  2. Search keywords: seed, level3, random, id_level3
  3. Check all .js files
  4. Find accidental exposures
// Vulnerable code example
var seeds = {
    level1: 13371337,
    level2: getCurrentTime(),
    level3: getCurrentTime()  // ⚠️ Visible in source!
};

Method 5: Analyzing Network Request

Check HTTP responses for leaked data:

# Intercept all requests
curl -v http://target.com/level3 \
  -H "Cookie: session=..." \
  | grep -E '\d{10}'

Or in DevTools:

  • Network tab → All requests
  • Find JSON responses
  • Look for numbers resembling timestamp

Complete Automated Solution

import socketio
import random
import requests
import re
import time

class Level3Solver:
    def __init__(self, base_url):
        self.base_url = base_url
        self.http = requests.Session()
        self.sio = socketio.Client()
        self.level3_seed = None
        self.level2_seed = None
        
        # Setup event handlers
        self.sio.on('gameData', self.on_game_data)
        self.sio.onAny(self.on_any_event)
    
    def on_any_event(self, event, *args):
        """Intercept ALL events to find seed"""
        data_str = str(args)
        
        # Find number resembling timestamp
        timestamps = re.findall(r'\b(17\d{8})\b', data_str)
        for ts in timestamps:
            print(f"[?] Potential seed found: {ts}")
            if not self.level3_seed:
                self.level3_seed = int(ts)
    
    def on_game_data(self, data):
        # Capture Level 2 seed as fallback
        if 'id_level2' in data:
            self.level2_seed = data['id_level2']
        
        # Try finding Level 3 seed (even if redacted in display)
        if 'id_level3' in data and data['id_level3'] != "***REDACTED***":
            self.level3_seed = data['id_level3']
            print(f"[+] Found Level 3 seed: {self.level3_seed}")
    
    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 get_seed_estimate(self):
        """Try multiple methods to get seed"""
        # Method 1: Use captured Level 3 seed
        if self.level3_seed:
            return self.level3_seed
        
        # Method 2: Use Level 2 seed (might work if same time window)
        if self.level2_seed:
            print("[*] Using Level 2 seed as estimate")
            return self.level2_seed
        
        # Method 3: Use current timestamp
        print("[*] Using current timestamp as estimate")
        return round(time.time())
    
    def place_bet_with_prediction(self):
        seed = self.get_seed_estimate()
        prediction = self.predict_result(seed)
        
        money = self.get_current_money()
        bet_amount = min(money, 100)  # Heavy bet
        
        self.sio.emit('pull', {
            'id': self.sio.sid,
            'dice': prediction,
            'level': 'level3',
            'name': self.username,
            'money': bet_amount
        })
        
        print(f"[+] Bet {bet_amount} on {prediction} (seed: {seed})")
    
    def solve(self):
        # ... (login and connection code same as Level 2) ...
        
        # Navigate to level3
        self.http.get(f"{self.base_url}/level3")
        
        # Farm until 50,000
        while self.get_current_money() < 50000:
            self.place_bet_with_prediction()
            time.sleep(3)
        
        print("[+] Completed Level 3! 🎉")
        
        # Get flags
        resp = self.http.get(f"{self.base_url}/")
        flags = re.findall(r'FLAG \d+: ([^\n<]+)', resp.text)
        for i, flag in enumerate(flags, 1):
            print(f"[+] FLAG {i}: {flag}")

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

Real World Exploitation Steps

Step-by-Step Manual Method

1. Open Level 3 page

2. Open DevTools → Console

3. Run full scan:

// Log everything
let foundSeeds = new Set();

// Override console methods
['log', 'debug', 'info', 'warn'].forEach(method => {
    let original = console[method];
    console[method] = function(...args) {
        original.apply(console, args);
        args.forEach(arg => {
            let str = String(arg);
            let matches = str.match(/\b17\d{8}\b/g);
            if (matches) {
                matches.forEach(m => {
                    foundSeeds.add(m);
                    console.error('🎯 SEED FOUND:', m);
                });
            }
        });
    };
});

// Intercept all Socket.IO
socket.onAny((event, ...args) => {
    console.log('📡', event, args);
});

console.log('🔍 Monitoring seeds...');

4. Wait for round start

5. Check console output for seed value

6. Use seed in Python:

import random
seed = 1760086500  # From console
random.seed(seed)
dice = [random.randint(1,6) for _ in range(3)]
print('Bet:', 'TAI' if sum(dice) >= 11 else 'XIU')

7. Place bet and profit!

Why Level 3 is "Harder"

Obscurity ≠ Security

  • Seed is hidden from obvious display
  • But same vulnerability as Level 2
  • Just requires more reconnaissance

Still Predictable

# Level 2 vs Level 3
def level2_seed():
    return round(time.time())  # Exposed

def level3_seed():
    return round(time.time())  # Hidden but identical!

The Real Challenge

Level 3 difficulty comes from:

  1. Finding the seed value (reconnaissance)
  2. Large bet requirement (50,000 coins)
  3. Timing - rounds are fast

Mitigation

Same as Level 2, plus:

1. Remove ALL Debug Code

// ❌ Remove before production
console.log('seed:', seed);
console.debug('level3_seed:', level3_seed);

2. Use Secure Random

import secrets

def generate_level3_dice():
    # Cryptographically secure
    return [secrets.randbelow(6) + 1 for _ in range(3)]

3. Server-Side Logic Only

def play_round(level):
    # Create dice server-side
    dice = generate_secure_dice()
    
    # NEVER send seed or dice to client before betting closes
    # Only send result after round ends
    
    return {
        'result': calculate_result(dice),
        'dice': dice  # Only after bets are locked
    }

4. Input Validation

@app.route('/bet', methods=['POST'])
def place_bet():
    # Verify bet placed BEFORE round ends
    if time.time() > round_end_time:
        return {'error': 'Round ended'}, 403
    
    # Process bet
    ...

Key Takeaways

  1. Security through obscurity doesn't work
  2. Debug code leaks sensitive info
  3. Client-side code is always readable
  4. Timing attacks work even without exposed seed
  5. Use cryptographic random for all security-sensitive operations

Tools Used

  • Browser DevTools - Monitor console, network analysis
  • Python socketio - Automated exploitation
  • Burp Suite - Intercept requests
  • Python random - PRNG prediction

Flag

HCMUS{h1dd3n_s33d_st1ll_pr3d1ct4bl3}

Bonus: All Three Flags

After completing Level 3 with 50,000+ coins:

FLAG 1: HCMUS{st4t1c_s33d_1s_pr3d1ct4bl3}
FLAG 2: HCMUS{t1m3st4mp_s33d_3xp0s3d}
FLAG 3: HCMUS{h1dd3n_s33d_st1ll_pr3d1ct4bl3}

References

300
Points
Medium
Difficulty
Web
Category