$ cat writeup.md…
$ cat writeup.md…
TJCTF 2026
Task: Python pickle deserialization server with RestrictedUnpickler that whitelists builtins module and blocklists dangerous names (eval, exec, __import__, open). Solution: getattr is not blocked — chain through license.__class__.__init__.__globals__ to recover __import__, import os, and execute commands via os.popen.
$ cat /etc/rate-limit
Rate limit reached (20 reads/hour per IP). Showing preview only — full content returns at the next hour roll-over.
Rick has open sourced his mind blowers program! Now you can upload your own mind blowers and view them! Don't upload any malicious mind blowers!
Connection: nc tjc.tf 31422 Source file: server.py
A Python server accepts base64-encoded pickle data and deserializes it using a RestrictedUnpickler. The goal is to bypass the restrictions and achieve remote code execution to read the flag.
The server implements a custom RestrictedUnpickler with two layers of defense:
builtins module is allowed — any other module in find_class raises an errorBLOCKED_NAMES = { "eval", "exec", "compile", "__import__", "open", "breakpoint", "input", "exit", "quit", }
class RestrictedUnpickler(pickle.Unpickler): def find_class(self, module, name): if module != "builtins": raise pickle.UnpicklingError("banned") if name in BLOCKED_NAMES: raise pickle.UnpicklingError("blocked") return super().find_class(module, name)
getattr is Not BlockedThe blocklist only covers 9 names. Crucially, getattr is not among them. This is the key vulnerability because:
find_class method, which is invoked by GLOBAL/STACK_GLOBAL opcodes during deserializationbuiltins.getattr as a callable, we can use pickle's REDUCE opcode to call it at runtimegetattr calls do not go through find_class — they are normal Python attribute lookups__import__ via getattr even though it's blocked in find_classStarting from builtins.license (a _sitebuiltins._Printer instance available in the builtins namespace), we can traverse the Python object graph to reach __import__:
...
$ grep --similar