Picker III

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 (get_flag). Overwriting getRandomNumber with win unlocks the final option.

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

Solution

  1. Step 1Use write_variable
    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 globals() dictionary (or setattr()) without any validation of what names or values are allowed. 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
    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).
    nc saturn.picoctf.net 49706
    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 3Decode the hex output
    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 0xprefix, 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.

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.

Want more picoGym Exclusive writeups?

Useful tools for Reverse Engineering

Related reading

What to try next