Picker III picoGym Exclusive Solution

Published: March 5, 2024

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 1
    Use write_variable
    Observation
    I noticed the menu exposed option 3 (write_variable) which lets callers overwrite any named variable, and since Python functions are first-class objects stored in the same global namespace as ordinary variables, this suggested redirecting the name getRandomNumber to the win function instead of its original value.
    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 presents a structured menu interface rather than raw eval, which means direct code injection is no longer possible. Instead, 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 2
    Trigger the modified function
    Observation
    I noticed that option 4 is hardcoded to call whatever getRandomNumber currently references, so after rebinding it to win via write_variable, selecting option 4 would invoke win and print 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 49706

    Expected output

    picoCTF{7h15_15_wh47_w3_g37_w17h_u53r5_1n_ch4...dd285}
    What didn't work first

    Tried: Pick option 4 immediately without using write_variable first

    Option 4 calls the original getRandomNumber function, which returns a random integer, not the flag. The menu prints a number and returns to the prompt with no flag output. The function pointer must be overwritten via option 3 before option 4 can reach win.

    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 calls whatever getRandomNumber currently refers to. Before the exploit, that is the legitimate random number function. After using write_variable to rebind it to win, option 4 silently calls win() instead. The user interface gives no indication that the function has been replaced - it still says "get flag" or similar, but now it actually delivers the flag.

    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 3
    Decode the hex output
    Observation
    I noticed the win function returned a sequence of space-separated 0x-prefixed values consistent with the hex-encoding pattern used across the Picker series, which indicated the raw bytes needed to be converted to ASCII to read the flag.
    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 is returned as a sequence of hex-encoded bytes rather than plaintext. This is a consistent design decision across the Picker series - it reinforces the hex-to-ASCII conversion skill alongside the primary exploitation concept.

    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, every subsequent run leaks the same hex-encoded flag.

Key takeaway

Unrestricted write access to a program's internal namespace is equivalent to arbitrary code execution when functions are first-class values stored alongside ordinary variables. Any API that lets callers set an arbitrary name to an arbitrary value, including setattr, exec, and object deserialization, can be abused to rebind callable names to attacker-chosen functions. Safe designs enforce both a name allowlist and a type check so that only expected scalar values can be overwritten.

Related reading

Want more picoGym Exclusive writeups?

Useful tools for Reverse Engineering

What to try next