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 1
Understand the flawed input validationObservationI 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") == 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 2
Win all 5 roundsObservationI 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 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 (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.
Step 3
Collect the flag after winning 5 roundsObservationI 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
rockpaperscissorseach 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.