Rickroll
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 read-only • copy allowedstart_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 read-only • copy alloweddef 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
- Record time before and after calling
/api/game/start - Get the first
short_codevia/api/game/link/{session_id} - Bruteforce seeds in the range ±5000ms from the approximate start time
- For each seed, reproduce the link generation
- When the first
short_codematches — the correct seed is found - Now we know which links are rickroll (skip) and which are safe (click)
- Click all 25 safe links and get the flag
Exploit
python read-only • copy allowed#!/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
- Cryptographically secure RNG:
python read-only • copy allowed
import secrets short_code = ''.join(secrets.choice(string.ascii_lowercase) for _ in range(9))
-
Unpredictable seed:
python read-only • copy allowedimport os random.seed(os.urandom(32)) -
Server-side validation: Don't rely on client-side logic, store game state on the server with protection against manipulation.
References
- Python random module security
- Predictable PRNG attacks