BP88 Level 3

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:
- Browser console.log() if debug code is present
- Checking Socket.IO messages
- Timing correlation with Level 2
- 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:
- Open Browser DevTools (F12)
- Go to Console tab
- Find logged values
- 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:
- DevTools → Sources tab
- Search keywords:
seed,level3,random,id_level3 - Check all
.jsfiles - 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:
- Finding the seed value (reconnaissance)
- Large bet requirement (50,000 coins)
- 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
- Security through obscurity doesn't work
- Debug code leaks sensitive info
- Client-side code is always readable
- Timing attacks work even without exposed seed
- 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}