Picker II picoGym Exclusive Solution

Published: March 5, 2024

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.

Python sandbox bypassDownload picker-II.py

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.

bash
wget https://artifacts.picoctf.net/c/523/picker-II.py
bash
cat picker-II.py

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1
    Copy the critical line
    Observation
    I 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 the win function 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 getRandomNumber and nothing else, making the win function completely unreachable regardless of how the input is crafted.

  2. Step 2
    Submit the payload remotely
    Observation
    I 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.
    bash
    printf "print(open('flag.txt','r').read())\n" | nc saturn.picoctf.net 56771

    Expected 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 file flag.txt in 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 full eval().

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.

Key takeaway

A blacklist that forbids one keyword does nothing when the underlying primitive, here eval() of arbitrary Python, lets you express the same goal a different way, such as print(open('flag.txt').read()) to read the file directly. This is textbook server-side code injection, the same class behind template injection (SSTI) and unsafe config parsers, and it usually means full server compromise because eval has the entire standard library. The fix is to never pass user input to eval or exec; use a whitelist of allowed names or ast.literal_eval for data-only parsing.

Related reading

Want more picoGym Exclusive writeups?

Useful tools for Reverse Engineering

What to try next