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.
Setup
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.
wget https://artifacts.picoctf.net/c/526/picker-III.pypython3 picker-III.pySolution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1Use write_variable
ObservationOption 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_nameaccepts only a plain identifier, andfilter_valuerejects any value containing;,(, or), so you cannot type a call expression any more. Underneath, the menu still funnels names intoevalandexec. 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, overwritinggetRandomNumberwith the function objectwineffectively 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_variablefunction uses Python'sexec()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).Step 2Trigger the modified function
ObservationOption 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).bashnc 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
getRandomNumberin 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 usingwrite_variableto rebind it towin, option 4 silently callswin()instead. The interface gives no indication of the swap:print_tablestill lists entry 4 asgetRandomNumber, 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_variablemenu 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'ssetattrcalled on user-supplied strings without a whitelist.Step 3Decode the hex output
Observationwin 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 calledwin()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 the0xprefix, 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.