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#

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#

python
def 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

  1. Mở DevTools (F12) → Console
  2. 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);
    }
});
  1. 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

  1. DevTools → Network → WS (WebSocket)
  2. Click vào kết nối Socket.IO
  3. Xem tab Messages
  4. Tìm sự kiện gameData với trường id_level2

Bước 2: Dự Đoán Kết Quả#

python
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

# 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#

python
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
        
        # 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#

  1. Mở trang level 2
  2. Mở DevTools Console
  3. 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!');
});
  1. 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()}')
"
  1. Đặ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:

python
import 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#

python
import 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#

python
def 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#

  1. Không bao giờ lộ PRNG seeds cho clients
  2. Seed dựa trên thời gian có thể dự đoán được ngay cả không bị lộ
  3. Socket.IO events có thể leak dữ liệu nhạy cảm
  4. Dùng secure random (secrets module) cho games
  5. 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