Skip to main content

Picker III picoGym Exclusive Solution

Reverse engineer a Python script and manipulate its runtime behavior to call the right function.

Published: March 5, 2024Updated: August 25, 2026

Description

Picker III adds a primitive menu with helper functions for reading and writing names. Overwrite getRandomNumber with win so option 4 triggers the flag routine.

Menu-driven exploitDownload picker-III.py

Run the script locally and issue the help command to list the numbered actions.

Identify option 3 (write_variable) and option 4 (getRandomNumber). Overwriting getRandomNumber with win unlocks the final option.

bash
wget https://artifacts.picoctf.net/c/526/picker-III.py
python
python3 picker-III.py

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1Use write_variable
    Observation
    Option 3 overwrites any named variable, and Python functions are first-class objects living in the same namespace as ordinary variables. Point the name getRandomNumber at win instead.
    Choose menu item 3. When prompted for the variable name, enter getRandomNumber and when prompted for the new value, enter win. This hijacks the pointer used by option 4.
    Learn more

    Picker III wraps everything in a numbered menu, and the two filters it adds are what block the earlier attacks: filter_var_name accepts only a plain identifier, and filter_value rejects any value containing ;, (, or ), so you cannot type a call expression any more. Underneath, the menu still funnels names into eval and exec. The vulnerability is in the write_variable option: it allows users to overwrite named variables in the program's namespace. Since Python functions are first-class objects stored in the same namespace as variables, overwriting getRandomNumber with the function object win effectively replaces one function with another.

    This is a form of function pointer hijacking- a concept that appears in both scripting language exploits and compiled binary exploitation. In C, function pointers stored in writable memory can be overwritten by buffer overflows or use-after-free vulnerabilities to redirect execution to attacker-controlled code. Picker III demonstrates the same concept in a Python context, where the "function pointer" is just a variable name bound to a function object.

    The root cause is that the write_variable function uses Python's exec() to dynamically declare and assign a global variable (exec("global "+var_name+"; "+var_name+" = "+value)) without a whitelist of permitted names or values. A secure implementation would restrict which variable names can be modified and would validate that the new value is of the expected type (e.g., an integer, not a function reference).

  2. Step 2Trigger the modified function
    Observation
    Option 4 calls whatever that name currently references, so once it points at win, option 4 prints the flag.
    Back in the main menu pick option 4. Because getRandomNumber now resolves to win, the service prints the flag (again as a stream of hex bytes).
    bash
    nc saturn.picoctf.net <PORT_FROM_INSTANCE>

    Expected output

    0x70 0x69 0x63 0x6f 0x43 0x54 0x46 0x7b ...
    What didn't work first

    Tried: Pick option 4 immediately without using write_variable first

    Untouched, option 4 calls the original getRandomNumber, which just prints 4 (the xkcd joke; it is not random at all) and goes back to the prompt. Rebind the name first.

    Tried: Enter 'win' as the value for a different variable name, such as answer or result

    Only the name getRandomNumber is invoked by option 4. Rebinding an unrelated variable name to win has no effect because the menu hardcodes which name it calls. The target variable name must match exactly what option 4 dereferences.

    Learn more

    Option 4 looks up the name getRandomNumber in the function table and evaluates it, so it calls whatever that name currently refers to. Before the exploit, that is the stock function that prints 4. After using write_variable to rebind it to win, option 4 silently calls win() instead. The interface gives no indication of the swap: print_table still lists entry 4 as getRandomNumber, because the table is a string of names and only the binding behind the name changed.

    This technique is called hooking in the context of software security and reverse engineering. Attackers hook functions to intercept calls, modify arguments, or redirect execution. In legitimate contexts, hooking is used for profiling, debugging, and monkey-patching in tests. The difference between legitimate and malicious hooking is authorization and intent - which is why environments that need integrity (kernel modules, security software) use code signing, kernel patches, and other mechanisms to prevent unauthorized hooking.

    The challenge also demonstrates why exposing internal variable names through an API is dangerous. The write_variable menu option is essentially an unrestricted reflection API - it exposes the program's internal structure to external callers. Real APIs sometimes have similar issues: overly permissive object deserialization, unrestricted reflection in Java, or Python's setattr called on user-supplied strings without a whitelist.

  3. Step 3Decode the hex output
    Observation
    win returns space-separated hex values, the same encoding the rest of the Picker series uses. Convert them to ASCII.
    Paste the 0x-prefixed values into CyberChef (From Hex) or use xxd -r to turn them into ASCII. The decoded string is the final flag.
    Learn more

    As in Picker I and II, the flag comes back as a sequence of hex-encoded bytes rather than plaintext, because win() itself does that conversion before printing. Picker II never called win() at all, so there the file contents came back as plain text; here you go through the real function again and have to undo the hex encoding.

    For the hex output format used here (space-separated 0x?? values), a convenient Python one-liner is: bytes([int(x, 16) for x in output.split()]).decode(). This splits the output on whitespace, converts each token from hex to an integer, collects the integers into a bytes object, and decodes the bytes as UTF-8 (or ASCII). This approach is more robust than xxd when the input includes the 0x prefix, which xxd's plain mode does not expect.

    Completing the Picker series has taken you through three progressively hardened versions of the same underlying application. The progression - from unrestricted eval, to a string blacklist bypass, to function pointer hijacking through a write API - models how real vulnerability research works: each fix introduces a new, slightly more subtle vulnerability that requires a different approach to exploit. This iterative cat-and-mouse between defenders and attackers is a central dynamic in security.

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{7h15_15_wh47_w3_g37_w17h_u53r5_1n_ch4...dd285}

Once getRandomNumber points at win, option 4 keeps printing the hex-encoded flag for the rest of that session, even after 'reset', because reset only restores the table text and not the rebound name.

Key takeaway

Unrestricted write access to a program's namespace is arbitrary code execution when functions are values sitting alongside ordinary variables. Any API that sets an arbitrary name to an arbitrary value, whether setattr or exec or object deserialization, can be turned into rebinding a callable. A safe design allowlists the names and checks the types, so only the expected scalars can move.

Related reading

Useful tools for Reverse Engineering

Where to go next