Skip to main content

What's your input? picoCTF 2021 Solution

A service that processes your input unsafely, letting you run arbitrary code and read the flag remotely.

Published: April 2, 2026Updated: August 25, 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 <PORT_FROM_INSTANCE>

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1Understand why Python 2 input() is dangerous
    Observation
    The description mentions a Python 2 script reading answers with input(). Python 2's input() behaves quite differently from Python 3's, and that difference is a well-known source of code execution.
    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 2Connect and read the server's variable by name
    Observation
    The server script keeps the expected city in a variable named city before calling input(). Type that bare identifier at the prompt and Python 2's eval-based input() resolves the variable and prints 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 echoes its value back as 'You said: Round Lake Beach', which leaks the city it picked. At the second prompt type city again: the evaluated answer is the same string the script is comparing against, so the check passes and the flag prints.
    bash
    nc mercury.picoctf.net <PORT_FROM_INSTANCE>
    bash
    # Prompt: What's your favorite number?
    bash
    # Type:   city
    bash
    # The server echoes the city it picked, e.g. You said: Round Lake Beach
    bash
    # Prompt: What's the best city to visit?
    bash
    # Type:   city   (or the leaked name as a quoted string, e.g. 'Round Lake Beach')
    What didn't work first

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

    Python 2's input() evaluates a quoted string as a string literal, so it compares that literal text against the expected number and fails. Type the identifier without quotes, and Python resolves it as a variable 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's input() evaluates the quoted value and returns the string, which does match the expected city, so this works. The leak from step one already gave you the raw string, so quote it exactly as printed; an unquoted multi-word phrase is a syntax error, not a value.

    Learn more

    By typing city at the first prompt, Python 2 evaluates the identifier city and returns its value, which the script echoes back, leaking the name it picked. Typing city again at the second prompt makes the check if "Round Lake Beach" == "Round Lake Beach", which passes.

    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 3Alternative: spawn a shell directly
    Observation
    Python 2's input() evaluates any expression, not just variable names. So __import__('os').system('cat flag') runs a shell command and reads the flag without passing 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 <PORT_FROM_INSTANCE>
    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__.

    eval() runs a single expression, never a statement, and import is a statement, so an eval containing it raises a SyntaxError and nothing runs. The __import__() builtin is a function call, which is an expression, so eval accepts it. Use __import__('os').system('cat flag') wherever a top-level import is unavailable.

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

    The server script may never import os at module level, so that name is not in scope when eval runs your input and referencing it raises a NameError. The self-contained __import__ form avoids that by importing inline within the same expression, assuming nothing about what the script already loaded.

    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
  • 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.
  • Regex TesterTest regular expressions against a string with live match highlighting, flag toggles, and common CTF pattern shortcuts.
  • Reverse Shell GeneratorGenerate reverse shell payloads (bash, nc, python, perl, ruby, php, node, powershell) and matching listeners. Set host and port once, copy any variant.

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, so any path that calls it on untrusted data is remote code execution. The same shape appears wherever a language conflates reading user input with evaluating it: PHP's eval(), Ruby's open() with backtick expansion, Perl string eval. The fix is the string-only alternative, raw_input() in Python 2 and input() in Python 3, and never passing untrusted data to eval in any form.

Related reading

Useful tools for Binary Exploitation

Where to go next