miscfreemedium

mind blowers

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.

$ ls tags/ techniques/
restricted_pickle_bypass_via_getattrobject_graph_traversal_license_class_init_globalsdict_get_method_for_key_access_in_picklebuiltins_blocklist_evasion

$ cat /etc/rate-limit

Rate limit reached (20 reads/hour per IP). Showing preview only — full content returns at the next hour roll-over.

mind blowers — TJCTF 2026

Description

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.

Analysis

RestrictedUnpickler

The server implements a custom RestrictedUnpickler with two layers of defense:

  1. Module whitelist: Only builtins module is allowed — any other module in find_class raises an error
  2. Name blocklist: Specific dangerous builtins are blocked:
    BLOCKED_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)

Critical Oversight: getattr is Not Blocked

The blocklist only covers 9 names. Crucially, getattr is not among them. This is the key vulnerability because:

  • The blocklist only applies to pickle's find_class method, which is invoked by GLOBAL/STACK_GLOBAL opcodes during deserialization
  • Once we have builtins.getattr as a callable, we can use pickle's REDUCE opcode to call it at runtime
  • Runtime getattr calls do not go through find_class — they are normal Python attribute lookups
  • This means we can access __import__ via getattr even though it's blocked in find_class

Object Graph Traversal Chain

Starting from builtins.license (a _sitebuiltins._Printer instance available in the builtins namespace), we can traverse the Python object graph to reach __import__:

...

$ grep --similar

Similar writeups