Description
Picker II adds a simple blacklist so you cannot type win directly, but eval still executes arbitrary Python expressions. Abuse the file read inside win() yourself.
Setup
Review picker-II.py to see the string blacklist that prevents submitting win.
Look at the win() helper; the first line reads flag.txt, and nothing stops you from doing the same.
wget https://artifacts.picoctf.net/c/523/picker-II.pycat picker-II.pySolution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1
Copy the critical lineObservationI noticed the source code for win() contained a plain Python file read expression that was separate from the function name, which suggested I could reproduce that expression directly via eval() to bypass the blacklist without ever invoking win by name.Inside win(), the statement flag = open('flag.txt', 'r').read() loads the flag. Because user input still flows into eval, you can execute the exact same print call.Learn more
A blacklist (also called a denylist) is a security control that blocks specific known-bad values while allowing everything else. Picker II blocks the string
"win"directly, preventing you from calling thewinfunction by name. However, blacklists are notoriously fragile because they must anticipate every possible attack vector - missing even one route defeats the protection entirely.The vulnerability here is that the service still passes user input directly into
eval()or an equivalent dynamic execution mechanism.eval()in Python executes an arbitrary expression, meaning any valid Python code you submit runs with the service's privileges. The blacklist only checks the literal string"win", so any other Python code that accomplishes the same goal bypasses the filter.This pattern - input validation via blacklist rather than whitelist - is one of the most common root causes of injection vulnerabilities. Whitelists (allowlists) are almost always safer: instead of blocking known-bad values, you only permit known-good ones. For this service, a whitelist approach might only accept the function names
getRandomNumberand nothing else, making thewinfunction completely unreachable regardless of how the input is crafted.Step 2
Submit the payload remotelyObservationI noticed the service accepted arbitrary Python expressions through eval() and only blocked the literal substring 'win', which suggested sending the payload print(open('flag.txt','r').read()) directly to the remote instance via netcat would execute with the server's file permissions and return the flag.Connect to the remote host and paste print(open('flag.txt', 'r').read()). The service executes your code server-side and prints the flag.bashprintf "print(open('flag.txt','r').read())\n" | nc saturn.picoctf.net 56771Expected output
picoCTF{f1l73r5_f41l_c0d3_r3f4c70r_m1gh7_5ucc3...44590}What didn't work first
Tried: Submitting 'win' directly to call the win() function by name
The service checks input for the literal substring 'win' and rejects it before evaluation, printing a blocked message. The blacklist does not block arbitrary Python expressions - only that specific string - so replicating what win() does internally (opening flag.txt with open()) bypasses the check entirely.
Tried: Using __import__('os').system('cat flag.txt') to read the flag via a shell command
This may work conceptually since eval() allows arbitrary Python including os.system(), but os.system() prints to stdout and returns an exit code - the output is not guaranteed to be captured and returned by the service in the same way a print() call is. Using print(open('flag.txt','r').read()) is more direct and reliable because it stays within Python's own file I/O rather than spawning a subprocess.
Learn more
The payload
print(open('flag.txt', 'r').read())is a complete Python expression: it opens the fileflag.txtin read mode, reads its entire contents, and prints the result. Because the service evaluates this expression with the same permissions it uses to read flag.txt itself, the file is accessible and its contents are returned directly.This is a textbook example of server-side code injection - specifically, arbitrary Python execution. In real applications, eval() vulnerabilities have appeared in template engines (SSTI - Server-Side Template Injection), configuration parsers, and scripting APIs. The impact is typically full server compromise because eval() has access to the entire Python standard library, including file I/O, network sockets, and subprocess spawning.
The key lesson: never pass user-controlled data to eval(), exec(), or any dynamic code execution function. If dynamic dispatch is required (calling functions by name), use a whitelist of allowed function names checked against a pre-built dictionary, never raw eval. Libraries like
ast.literal_eval()can safely evaluate Python literals (strings, numbers, lists, dicts) without the risks of fulleval().
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.
- Hex ViewerView text or raw hex bytes as a xxd-style hex dump with byte offset, hex columns, and ASCII sidebar. Highlights printable characters and null bytes.
Flag
Reveal flag
picoCTF{f1l73r5_f41l_c0d3_r3f4c70r_m1gh7_5ucc3...44590}
Because the blacklist only checks for win, any other Python code that reads flag.txt still succeeds.