RPS picoCTF 2022 Solution

Published: July 20, 2023

Description

Win 5 rounds of rock-paper-scissors against a server-side randomized opponent. The vulnerability: the server checks if the winning move name is a substring of your input string, not whether your input exactly equals a valid move.

Sending a string like 'rockpaperscissors' contains 'rock', 'paper', AND 'scissors' as substrings - so it always wins regardless of the server's choice.

Connect to the challenge server via netcat.

Enter 'rockpaperscissors' (or any string containing all three) as your move for each round.

bash
nc saturn.picoctf.net <PORT_FROM_INSTANCE>

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1
    Understand the flawed input validation
    Observation
    I noticed the challenge description mentioned server-side randomization, which suggested the vulnerability was not in guessing correctly but in the move comparison logic itself, pointing me to examine whether the server used substring containment instead of exact equality.
    The server checks for winning moves with strstr() or a substring test instead of an exact equality check, so any input that contains a winning word beats any choice.
    Learn more

    The vulnerable check in C looks something like:

    if (strstr(player_input, "rock")) { /* player chose rock */ }

    strstr(haystack, needle) returns a pointer if needle appears anywhere inside haystack, not just if they match exactly. A string like "rockpaperscissors" passes all three checks simultaneously.

    The correct check would use strcmp(player_input, "rock") == 0 for exact equality, or in Python if player_input == "rock". This type of bug - using a substring test where equality is intended - is a common logic error.

  2. Step 2
    Win all 5 rounds
    Observation
    I noticed that strstr() passes if the needle appears anywhere in the input, which suggested that sending 'rockpaperscissors' as a single string would satisfy all three winning conditions at once and beat any random server choice every round.
    Send 'rockpaperscissors' for each round. The server picks randomly among rock, paper, or scissors; your input always beats all three.
    bash
    # Manual: connect with nc and type 'rockpaperscissors' for each round
    python
    python3 -c "
    from pwn import *
    p = remote('saturn.picoctf.net', <PORT_FROM_INSTANCE>)
    for _ in range(5):
        p.sendlineafter(b'choice:', b'rockpaperscissors')
    print(p.recvall().decode())
    "

    Expected output

    picoCTF{50M3_3X7R3M3_1UCK_C8...}
    What didn't work first

    Tried: Sending just 'rock', 'paper', or 'scissors' as the move, hoping to guess right 5 times in a row.

    Each round is independently random (1-in-3 odds), so winning 5 consecutive rounds by guessing has only a 1/243 chance. The challenge is designed so brute-guessing is impractical. The actual exploit uses a single input that satisfies all three winning substring checks at once - no guessing needed.

    Tried: Using p.recvall() before the loop finishes to collect output, causing the script to hang waiting for data that never arrives.

    p.recvall() blocks until the connection closes, but the connection only closes after all 5 rounds are complete. Calling it inside the loop or before all sendlineafter calls stalls the script because the server is still waiting for more input. The recvall() call must come after the loop sends all 5 responses.

    Learn more

    The winning logic for standard rock-paper-scissors is: rock beats scissors, scissors beats paper, paper beats rock. Normally, guessing the right move against a random opponent gives you a 1-in-3 chance per round, for a 1/243 chance of winning 5 in a row. The substring bug eliminates all randomness.

    This type of vulnerability - where input validation logic can be bypassed by crafting input that satisfies multiple conditions simultaneously - appears in real-world web applications too. For example, WAF (Web Application Firewall) bypass techniques often embed the forbidden string inside a larger string that the WAF's regex doesn't match but the backend does process.

  3. Step 3
    Collect the flag after winning 5 rounds
    Observation
    I noticed the server only prints the flag after 5 consecutive wins, which suggested using pwntools' sendlineafter to synchronize responses and then calling recvall() after the loop to capture the flag output once the connection closes.
    After winning 5 consecutive rounds, the server prints the flag.
    Learn more

    The automated Python script loops 5 times, sending the magic string each round. sendlineafter(b'choice:', ...) waits for the server's prompt before sending the next input, handling any timing between rounds.

    For interactive manual play, you can also type rockpaperscissors each time when prompted. Either approach works in under a minute.

Flag

Reveal flag

picoCTF{50M3_3X7R3M3_1UCK_C8...}

The server uses strstr() instead of strcmp() to check your move. 'rockpaperscissors' contains all three winning substrings and beats any choice.

Key takeaway

Input validation bugs arise when code uses a weaker test (substring containment, prefix match, regex anchoring) where exact equality is required. The strstr vulnerability here is a specific instance of a broad class of logic flaws where the developer's mental model of what constitutes a valid input differs from what the code actually accepts. Real-world equivalents include WAF bypasses that embed blocked keywords inside allowed strings, authentication bypasses that match on username substrings, and command injection via arguments that satisfy an allowlist check while also appending attacker-controlled content.

Related reading

Want more picoCTF 2022 writeups?

Useful tools for Binary Exploitation

What to try next