Skip to main content

RPS picoCTF 2022 Solution

Beat a remote rock-paper-scissors service by finding an unexpected flaw in its input handling.

Published: July 20, 2023Updated: August 25, 2026

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 1Understand the flawed input validation
    Observation
    The description mentions server-side randomization, which means the bug is not about guessing right but about how moves are compared. Worth checking whether the server tests substring containment rather than 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 2Win all 5 rounds
    Observation
    strstr() succeeds if the needle appears anywhere in the input. Send 'rockpaperscissors' as one string and all three winning conditions match at once, which beats whatever the server picked.
    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 at one-in-three odds, so five consecutive wins by guessing come up about once in 243 attempts. The challenge is built to make that impractical. The real exploit is one input satisfying all three substring checks at once, with no guessing at all.

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

    recvall blocks until the connection closes, and that only happens once all five rounds are done. Call it inside the loop, or before the last response goes out, and the script stalls while the server waits for more input. Put it after the loop.

    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 3Collect the flag after winning 5 rounds
    Observation
    The server only prints the flag after five consecutive wins. Use sendlineafter to stay in step with the prompts, then recvall once the loop finishes to catch the flag as 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.

Interactive tools
  • Strings ExtractorPull printable text from any binary, library, or image. ASCII and UTF-16 detection, configurable minimum length, flag-like highlight, no command line needed.

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 appear when code uses a weaker test, substring containment, a prefix match, a loose regex, where exact equality was required. The strstr flaw here is one instance of a broad class where the developer's idea of a valid input diverges from what the code actually accepts. The same shape drives WAF bypasses that bury blocked keywords inside allowed strings, authentication bypasses matching on username substrings, and command injection through arguments that pass an allowlist check while carrying attacker content along.

Related reading

Useful tools for Binary Exploitation

Where to go next