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.
Setup
Connect to the challenge server via netcat.
Enter 'rockpaperscissors' (or any string containing all three) as your move for each round.
nc saturn.picoctf.net <PORT_FROM_INSTANCE>Solution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1Understand the flawed input validation
ObservationThe 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") == 0for exact equality, or in Pythonif player_input == "rock". This type of bug - using a substring test where equality is intended - is a common logic error.Step 2Win all 5 rounds
Observationstrstr() 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 roundpythonpython3 -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.
Step 3Collect the flag after winning 5 rounds
ObservationThe 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
rockpaperscissorseach 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.