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.
Setup
Connect to the challenge server with netcat.
nc mercury.picoctf.net 61858Solution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1
Understand why Python 2 input() is dangerousObservationI 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 foreval(raw_input()). Theevalcall executes your text as Python code. If you typecity, Python evaluates the namecityand 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 usinginput()in Python 2 to read untrusted data has this vulnerability.Step 2
Connect and read the server's variable by nameObservationI 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.bashnc mercury.picoctf.net 61858bash# Prompt: What's your favorite number?bash# Type: citybash# The server prints the city it was looking for, e.g. Round Lake Beachbash# 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
cityat the first prompt, Python 2 evaluates the identifiercityand returns its value. The script's comparison becomes something likeif "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
cityis already defined in the script's global scope beforeinput()is called. Python's eval runs in that same scope, so all previously defined names are accessible.Step 3
Alternative: spawn a shell directlyObservationI 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.bashnc mercury.picoctf.net 61858bash# 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 theosmodule and callssystem(), runningcat flagon 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 withlsand reading the flag withcat 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.