Web
Medium
250 points
BP88 Level 2
Recuite 2025 - HCMUS
6 tháng 10, 2025
PRNG
Dynamic Seed
Timestamp
Socket.IO

Web
BP88 Level 2 - Write-up#
Thông Tin Challenge#
- Danh mục: Web
- Độ khó: Trung bình
- Lỗ hổng: PRNG Dự Đoán Được (Dynamic Seed - Hiển thị)
- Yêu cầu: Hoàn thành Level 1 (50 coins)
Tổng Quan#
Level 2 nâng độ khó bằng cách sử dụng dynamic seed dựa trên time.time(). Tuy nhiên, server phát sóng giá trị seed qua Socket.IO event gameData, khiến attacker có thể dự đoán kết quả xúc xắc.
Cơ Chế Trò Chơi#
Điều Kiện Thắng#
- Level 2: Đạt 1,000 coins để mở khóa Level 3
- Bắt đầu từ 50 coins (từ Level 1)
Lỗ Hổng: Dynamic Seed Bị Lộ#
Phân Tích Source Code#
pythondef prepare_seed(level):
""" Tạo seed cho module `random` """
if level == 1:
random.seed(13371337)
elif level == 2:
random.seed(round(time.time())) # ⚠️ Seed dựa trên timestamp
else:
random.seed()
Server phát sóng seed qua Socket.IO:
javascript// Sự kiện phía client
socket.on('gameData', function(data) {
// data.id_level2 = round(time.time())
console.log('Seed cho Level 2:', data.id_level2);
});
Vấn đề:
- Giá trị seed bị lộ qua WebSocket events
- Attacker có thể dùng seed để dự đoán kết quả xúc xắc
- Server-side và client-side có cùng giá trị seed
Khai Thác#
Bước 1: Bắt Giá Trị Seed#
Phương pháp 1: Browser DevTools
- Mở DevTools (F12) → Console
- Lắng nghe Socket.IO events:
javascript// Chặn sự kiện gameData
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);
}
});
- Seed sẽ hiển thị trên trang (dưới đồng hồ đếm ngược) hoặc trong console
Phương pháp 2: Network Tab
- DevTools → Network → WS (WebSocket)
- Click vào kết nối Socket.IO
- Xem tab Messages
- Tìm sự kiện
gameDatavới trườngid_level2
Bước 2: Dự Đoán Kết Quả#
pythonimport 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
# Ví dụ: Seed bắt được từ gameData
seed_level2 = 1760086470 # Từ sự kiện Socket.IO
result, dice, total = predict_result_from_seed(seed_level2)
print(f"Đặt cược: {result}")
print(f"Xúc xắc: {dice}, Tổng: {total}")
Bước 3: Khai Thác Tự Động#
pythonimport 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
# Thiết lập 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"[+] Bắt được seed: {self.latest_seed}")
def on_game_start(self, data):
print("[*] Round bắt đầu...")
self.place_bet()
def on_game_over(self, data):
print(f"[*] Round kết thúc: {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("[!] Chưa bắt được seed")
return
# Dự đoán kết quả
prediction = self.predict_result(self.latest_seed)
print(f"[+] Dự đoán: {prediction}")
# Lấy số tiền hiện tại
money = self.get_current_money()
bet_amount = min(money, 50) # Đặt 50 hoặc all-in
# Đặt cược qua Socket.IO
self.sio.emit('pull', {
'id': self.sio.sid,
'dice': prediction,
'level': 'level2',
'name': self.username,
'money': bet_amount
})
print(f"[+] Đặt {bet_amount} vào {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):
# Đăng nhập
self.http.post(f"{self.base_url}/signup", data={
'username': 'solver2',
'password': 'pass123'
})
# Kết nối 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})
# Điều hướng đến level2
self.http.get(f"{self.base_url}/level2")
# Đợi và để auto-betting hoạt động
while self.get_current_money() < 1000:
time.sleep(2)
print("[+] Hoàn thành Level 2!")
print(f"[+] Số tiền cuối: {self.get_current_money()}")
# Chạy solver
solver = Level2Solver("http://target.com")
solver.solve()
Khai Thác Thời Gian Thực#
Phương Pháp Thủ Công Nhanh#
- Mở trang level 2
- Mở DevTools Console
- Paste code này:
javascript// Tự động dự đoán và hiển thị
socket.on('gameData', function(data) {
if (!data.id_level2) return;
// Mô phỏng Python's random
let seed = data.id_level2;
console.log('Seed:', seed);
// Bạn cần chạy Python script riêng để lấy dự đoán
// Hoặc implement Mersenne Twister trong JS
console.log('🎲 Dùng seed này trong Python script!');
});
- Chạy dự đoán Python:
bash# Trong terminal
python3 -c "
import random
seed = int(input('Nhập seed từ browser: '))
random.seed(seed)
dice = [random.randint(1,6) for _ in range(3)]
result = 'tai' if sum(dice) >= 11 else 'xiu'
print(f'Đặt cược: {result.upper()}')
"
- Đặt cược vào kết quả dự đoán
Hiểu Về Timing#
Tạo Seed#
python# Server tạo seed khi round bắt đầu
current_time = round(time.time()) # Unix timestamp (giây)
random.seed(current_time)
# Ví dụ 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: Có thể đặt cược trước khi nhận seed không?
A: Không! Server phát sóng seed trước khi chấp nhận cược:
1. Round bắt đầu → Tạo seed
2. Phát sóng sự kiện gameData (bao gồm seed)
3. Chấp nhận cược (cửa sổ 5-10 giây)
4. Round kết thúc → Hiển thị xúc xắc
Timing Attack (Không cần ở đây)#
Nếu seed không được phát sóng, có thể brute-force timestamp:
pythonimport time
import random
def brute_force_seed():
current_time = round(time.time())
# Thử khoảng ±5 giây
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}")
Cách Phòng Chống#
❌ Code Lỗ Hổng#
python# Seed bị lộ cho client
seed = round(time.time())
random.seed(seed)
socket.emit('gameData', {'id_level2': seed}) # ⚠️ Bị leak!
✅ Code An Toàn#
pythonimport secrets
# ĐỪNG lộ seed cho client
def generate_secure_round():
# Dùng random bảo mật mã hóa
dice = [secrets.randbelow(6) + 1 for _ in range(3)]
# Tùy chọn: Commitment scheme cho 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()
# Tiết lộ sau khi đặt cược
return dice, commitment, server_seed
Triển Khai Provably Fair#
pythondef provably_fair_dice(server_seed_hash, client_seed, nonce):
# Client có thể verify sau round
import hashlib
# Kết hợp seeds
combined = f"{server_seed_hash}{client_seed}{nonce}"
hash_output = hashlib.sha256(combined.encode()).hexdigest()
# Tạo xúc xắc từ 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
Bài Học Rút Ra#
- Không bao giờ lộ PRNG seeds cho clients
- Seed dựa trên thời gian có thể dự đoán được ngay cả không bị lộ
- Socket.IO events có thể leak dữ liệu nhạy cảm
- Dùng secure random (
secretsmodule) cho games - Triển khai provably fair nếu cần tính minh bạch
Tools Sử Dụng#
- Browser DevTools - Bắt seed
- Python socketio - Khai thác tự động
- Python random - Dự đoán kết quả
Flag#
HCMUS{t1m3st4mp_s33d_3xp0s3d}
Tài Liệu Tham Khảo#
250
Points
Medium
Difficulty
Web
Category