What's your input? picoCTF 2021 Solution

Published: April 2, 2026

Description

This challenge runs a Python 2 script on a remote server that asks you two questions. The vulnerability is in the Python 2 input() function, which evaluates whatever you type as a Python expression rather than treating it as a plain string. By typing a variable name instead of a literal answer, you can read the server's internal state - or spawn a shell entirely.

Remote

Connect to the challenge server with netcat.

bash
nc mercury.picoctf.net 61858

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1
    Understand why Python 2 input() is dangerous
    Observation
    I noticed the challenge description mentioned a Python 2 script using input() to read answers, which suggested looking up how Python 2's input() differs from Python 3's, since that difference is a well-known source of code-execution vulnerabilities.
    Python 2's input() is equivalent to eval(raw_input()). When the server calls input(), it takes your text and runs it as Python code before comparing the result. This means you can type any valid Python expression - including a variable name - and the script will evaluate it and use the result as your answer.
    Learn more

    In Python 3, input() always returns a plain string. But in Python 2, input() is shorthand for eval(raw_input()). The eval call executes your text as Python code. If you type city, Python evaluates the name city and returns whatever value that variable holds in the script's scope - rather than the string "city".

    This is a classic Python 2 footgun. The safe alternative is raw_input(), which always returns a string. Any script using input() in Python 2 to read untrusted data has this vulnerability.

  2. Step 2
    Connect and read the server's variable by name
    Observation
    I noticed that the server script stores the expected city in a variable named city before calling input(), which suggested that typing the bare identifier city at the prompt would cause Python 2's eval-based input() to look up that variable and reveal its value.
    When prompted for your favorite number, type the variable name city. Because Python 2 input() evaluates this, the server looks up the variable city in its scope and returns its string value to the comparison. The server will print what city it expected. You can then type that city name when prompted for the best city to visit.
    bash
    nc mercury.picoctf.net 61858
    bash
    # Prompt: What's your favorite number?
    bash
    # Type:   city
    bash
    # The server prints the city it was looking for, e.g. Round Lake Beach
    bash
    # Prompt: What's the best city to visit?
    bash
    # Type:   Round Lake Beach   (or whatever was printed)
    What didn't work first

    Tried: Typing a quoted string like "city" at the first prompt to answer literally.

    Python 2 input() evaluates the quoted string as a string literal, so it compares your literal text "city" against the expected number. The comparison fails because "city" is not a number. The exploit requires typing the bare identifier city without quotes so Python resolves it as a variable name in scope rather than as a string.

    Tried: Typing the city name as a quoted string at the city prompt, e.g. "Round Lake Beach".

    Python 2 input() evaluates the quoted value and returns the string Round Lake Beach, which does match the expected city, so this actually works. However, many solvers forget the quotes are optional here - the leak from step one already told you the raw string, so typing it unquoted also works because Python resolves an unquoted multi-word phrase as a syntax error. Use the exact unquoted value the server printed, treating the second prompt as a raw_input-style entry.

    Learn more

    By typing city at the first prompt, Python 2 evaluates the identifier city and returns its value. The script's comparison becomes something like if "Round Lake Beach" == "Round Lake Beach", which passes. This leaks the expected city name so you can supply it at the second prompt.

    This technique works because the variable city is already defined in the script's global scope before input() is called. Python's eval runs in that same scope, so all previously defined names are accessible.

  3. Step 3
    Alternative: spawn a shell directly
    Observation
    I noticed that Python 2's eval-based input() accepts any expression, not just variable names, which suggested injecting __import__('os').system('cat flag') to execute arbitrary shell commands and read the flag without needing to pass either comparison.
    Because input() runs eval(), you can type any Python expression - including one that imports a module and executes a shell command. Typing __import__('os').system('cat flag') at either prompt causes the server to run that command directly and print the flag without needing to pass any comparison.
    bash
    nc mercury.picoctf.net 61858
    bash
    # Prompt: What's your favorite number?
    bash
    # Type:   __import__('os').system('cat flag')
    bash
    # The flag is printed immediately.
    What didn't work first

    Tried: Typing import os; os.system('cat flag') at the prompt instead of using __import__.

    Python's eval() executes a single expression, not a statement. The import keyword is a statement, so eval('import os; os.system("cat flag")') raises a SyntaxError and nothing executes. The __import__() builtin is a function call, which is an expression, so it can be passed directly to eval. Always use __import__('os').system('cat flag') when you cannot use a top-level import statement.

    Tried: Trying to inject os.system('cat flag') directly, assuming os is already imported.

    The server script may not import os at the module level, so the name os is not in scope when eval runs your input. Referencing an undefined name raises a NameError. The self-contained __import__('os').system('cat flag') form bypasses this because it imports the module inline within the same expression, making no assumption about what the script has imported.

    Learn more

    __import__('os').system('cat flag') is a one-liner that works without a prior import statement. Python evaluates it, which imports the os module and calls system(), running cat flag on the server. The flag contents are sent back over the netcat connection.

    For broader shell access you can inject __import__('os').system('sh') instead. This drops you into an interactive shell on the remote server, from which you can run any command including listing directory contents with ls and reading the flag with cat flag.

Interactive tools
  • Cyclic Pattern GeneratorGenerate de Bruijn cyclic patterns and find buffer overflow offsets. The browser equivalent of pwntools cyclic and cyclic_find.
  • pwntools Payload BuilderPack integers into little-endian bytes (p32 / p64), unpack bytes back to integers, and build flat ROP payloads with offset-based insertion.

Flag

Reveal flag

picoCTF{v4lua4bl3_1npu7_...}

This challenge exploits Python 2 input() evaluating your input as code. Type a variable name to leak its value, or inject __import__('os').system('cat flag') to read the flag directly.

Key takeaway

Python 2's input() is a silent eval() wrapper, meaning any code path that calls it on untrusted data is a remote code execution vulnerability. The same pattern appears anywhere a language or framework conflates 'read user input' with 'evaluate user input', including PHP eval(), Ruby's open() with backtick expansion, and Perl string eval. The fix is always to use the safe string-only alternative (raw_input() in Python 2, input() in Python 3) and never pass untrusted data to any form of eval.

Related reading

Want more picoCTF 2021 writeups?

Useful tools for Binary Exploitation

What to try next