opencode-api-security-testing 3.0.9 → 3.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/agents/api-cyber-supervisor.md +22 -19
  2. package/agents/api-probing-miner.md +34 -10
  3. package/agents/api-resource-specialist.md +49 -20
  4. package/agents/api-vuln-verifier.md +69 -18
  5. package/package.json +1 -1
  6. package/postinstall.mjs +1 -0
  7. package/preuninstall.mjs +43 -32
  8. package/src/index.ts +6 -3
  9. package/README.md +0 -74
  10. package/SKILL.md +0 -1797
  11. package/core/advanced_recon.py +0 -788
  12. package/core/agentic_analyzer.py +0 -445
  13. package/core/analyzers/api_parser.py +0 -210
  14. package/core/analyzers/response_analyzer.py +0 -212
  15. package/core/analyzers/sensitive_finder.py +0 -184
  16. package/core/api_fuzzer.py +0 -422
  17. package/core/api_interceptor.py +0 -525
  18. package/core/api_parser.py +0 -955
  19. package/core/browser_tester.py +0 -479
  20. package/core/cloud_storage_tester.py +0 -1330
  21. package/core/collectors/__init__.py +0 -23
  22. package/core/collectors/api_path_finder.py +0 -300
  23. package/core/collectors/browser_collect.py +0 -645
  24. package/core/collectors/browser_collector.py +0 -411
  25. package/core/collectors/http_client.py +0 -111
  26. package/core/collectors/js_collector.py +0 -490
  27. package/core/collectors/js_parser.py +0 -780
  28. package/core/collectors/url_collector.py +0 -319
  29. package/core/context_manager.py +0 -682
  30. package/core/deep_api_tester_v35.py +0 -844
  31. package/core/deep_api_tester_v55.py +0 -366
  32. package/core/dynamic_api_analyzer.py +0 -532
  33. package/core/http_client.py +0 -179
  34. package/core/models.py +0 -296
  35. package/core/orchestrator.py +0 -890
  36. package/core/prerequisite.py +0 -227
  37. package/core/reasoning_engine.py +0 -1042
  38. package/core/response_classifier.py +0 -606
  39. package/core/runner.py +0 -938
  40. package/core/scan_engine.py +0 -599
  41. package/core/skill_executor.py +0 -435
  42. package/core/skill_executor_v2.py +0 -670
  43. package/core/skill_executor_v3.py +0 -704
  44. package/core/smart_analyzer.py +0 -687
  45. package/core/strategy_pool.py +0 -707
  46. package/core/testers/auth_tester.py +0 -264
  47. package/core/testers/idor_tester.py +0 -200
  48. package/core/testers/sqli_tester.py +0 -211
  49. package/core/testing_loop.py +0 -655
  50. package/core/utils/base_path_dict.py +0 -255
  51. package/core/utils/payload_lib.py +0 -167
  52. package/core/utils/ssrf_detector.py +0 -220
  53. package/core/verifiers/vuln_verifier.py +0 -536
  54. package/references/README.md +0 -72
  55. package/references/asset-discovery.md +0 -119
  56. package/references/fuzzing-patterns.md +0 -129
  57. package/references/graphql-guidance.md +0 -108
  58. package/references/intake.md +0 -84
  59. package/references/pua-agent.md +0 -192
  60. package/references/report-template.md +0 -156
  61. package/references/rest-guidance.md +0 -76
  62. package/references/severity-model.md +0 -76
  63. package/references/test-matrix.md +0 -86
  64. package/references/validation.md +0 -78
  65. package/references/vulnerabilities/01-sqli-tests.md +0 -1128
  66. package/references/vulnerabilities/02-user-enum-tests.md +0 -423
  67. package/references/vulnerabilities/03-jwt-tests.md +0 -499
  68. package/references/vulnerabilities/04-idor-tests.md +0 -362
  69. package/references/vulnerabilities/05-sensitive-data-tests.md +0 -466
  70. package/references/vulnerabilities/06-biz-logic-tests.md +0 -501
  71. package/references/vulnerabilities/07-security-config-tests.md +0 -511
  72. package/references/vulnerabilities/08-brute-force-tests.md +0 -457
  73. package/references/vulnerabilities/09-vulnerability-chains.md +0 -465
  74. package/references/vulnerabilities/10-auth-tests.md +0 -537
  75. package/references/vulnerabilities/11-graphql-tests.md +0 -355
  76. package/references/vulnerabilities/12-ssrf-tests.md +0 -396
  77. package/references/vulnerabilities/README.md +0 -148
  78. package/references/workflows.md +0 -192
@@ -1,1330 +0,0 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- """
4
- Cloud Storage Security Tester - 云存储安全测试模块
5
-
6
- 支持: 阿里云 OSS, 腾讯云 COS, 华为云 OBS, AWS S3, MinIO, Azure Blob
7
-
8
- 智能识别逻辑:
9
- 1. URL 模式识别 - 域名/路径特征
10
- 2. 响应头识别 - X-OSS-, X-Amz-, 特定 header
11
- 3. 响应内容识别 - XML 格式、API 字段
12
- 4. 路径模式识别 - /minio/, /bucket/, /file/ 等
13
-
14
- 参考:
15
- - OSS_scanner (bitboy-sys): https://github.com/bitboy-sys/OSS_scanner
16
- - BucketTool (libaibaia): https://github.com/libaibaia/BucketTool
17
- """
18
-
19
- import requests
20
- import xml.etree.ElementTree as ET
21
- from typing import Dict, List, Optional, Tuple
22
- import re
23
- import logging
24
-
25
- logger = logging.getLogger(__name__)
26
-
27
-
28
- class CloudStorageTester:
29
- """云存储安全测试器"""
30
-
31
- # ========== 识别模式 ==========
32
-
33
- # URL 域名模式
34
- DOMAIN_PATTERNS = {
35
- 'aliyun': [
36
- r'\.oss-[a-zA-Z0-9-]+\.aliyuncs\.com',
37
- r'\.oss\.aliyuncs\.com',
38
- r'oss-[a-zA-Z0-9-]+-internal',
39
- ],
40
- 'tencent': [
41
- r'\.cos\.[a-zA-Z0-9-]+\.myqcloud\.com',
42
- r'\.cos\.myqcloud\.com',
43
- r'\.cos\.tencent\.com',
44
- ],
45
- 'huawei': [
46
- r'\.obs\.[a-zA-Z0-9-]+\.myhwclouds\.com',
47
- r'\.obs\.myhwclouds\.com',
48
- r'\.obs\.cn-north-1\.myhwclouds\.com',
49
- ],
50
- 'aws': [
51
- r'\.s3\.[a-zA-Z0-9-]+\.amazonaws\.com',
52
- r'\.s3\.amazonaws\.com',
53
- r's3-[a-zA-Z0-9-]+\.amazonaws\.com',
54
- ],
55
- 'minio': [
56
- r'minio',
57
- r':9000',
58
- r':9001',
59
- r'play\.minio\.io',
60
- ],
61
- 'azure': [
62
- r'\.blob\.core\.[a-zA-Z0-9.]+\.microsoft\.com',
63
- r'\.blob\.core\.windows\.net',
64
- ]
65
- }
66
-
67
- # URL 路径模式 (当域名不是标准云存储域名时使用)
68
- PATH_PATTERNS = {
69
- 'minio': [
70
- r'/minio/',
71
- r'/minio-api/',
72
- r'/api/s3/',
73
- r'/bucket/',
74
- r'/newbucket/',
75
- ],
76
- 'oss': [
77
- r'/oss/',
78
- r'/aliyun/',
79
- r'/aliyunoss/',
80
- ],
81
- 'cos': [
82
- r'/cos/',
83
- r'/tencentcos/',
84
- ],
85
- 's3': [
86
- r'/s3/',
87
- r'/aws-s3/',
88
- r'/s3-api/',
89
- ],
90
- 'file': [
91
- r'/file/',
92
- r'/files/',
93
- r'/upload/',
94
- r'/uploads/',
95
- r'/storage/',
96
- r'/storages/',
97
- ]
98
- }
99
-
100
- # 响应 Header 模式
101
- HEADER_PATTERNS = {
102
- 'aliyun': [
103
- 'x-oss-',
104
- 'x-oss-meta-',
105
- 'x-oss-request-id',
106
- 'x-oss-server-time',
107
- ],
108
- 'tencent': [
109
- 'x-cos-',
110
- 'x-cos-meta-',
111
- 'x-cos-request-id',
112
- ],
113
- 'huawei': [
114
- 'x-obs-',
115
- 'x-obs-meta-',
116
- 'x-obs-request-id',
117
- ],
118
- 'aws': [
119
- 'x-amz-',
120
- 'x-amz-meta-',
121
- 'x-amz-request-id',
122
- 'x-amz-id-2',
123
- ],
124
- 'minio': [
125
- 'x-minio-',
126
- 'x-minio-deployment-id',
127
- 'x-minio-zone',
128
- 'minio-ext',
129
- ],
130
- 'azure': [
131
- 'x-ms-',
132
- 'x-ms-request-id',
133
- 'x-ms-blob',
134
- ]
135
- }
136
-
137
- # 响应内容 XML 模式
138
- XML_PATTERNS = [
139
- '<ListBucketResult',
140
- '<ListAllMyBucketsResult',
141
- '<ListBucketResponse',
142
- '<CreateBucketConfiguration',
143
- '<AccessControlPolicy',
144
- '<?xml version',
145
- '<LocationConstraint',
146
- '<Bucket>',
147
- ]
148
-
149
- # 敏感文件路径
150
- SENSITIVE_PATHS = [
151
- '.env', '.git/config', '.git/HEAD', 'id_rsa', 'id_rsa.pub',
152
- 'access_key', 'secret_key', 'credentials', 'aws_key',
153
- '.sql', '.bak', '.backup', '.db', '.dump',
154
- '.pem', '.key', '.crt', '.p12', '.pfx',
155
- 'wp-config.php', 'config.php', 'settings.py',
156
- 'database.yml', 'credentials.json',
157
- 'backup.sql', 'db_backup', 'data.sql',
158
- 'passwd', 'shadow', 'hosts', 'nginx.conf',
159
- 'httpd.conf', 'apache2.conf'
160
- ]
161
-
162
- # 日志路径
163
- LOG_PATHS = [
164
- '/logs/', '/log/', '/accesslog/', '/access_log/',
165
- '/error_log/', '/debug.log', '/app.log',
166
- '/analytics/', '/stats/', '/monitoring/'
167
- ]
168
-
169
- # 常见存储目录
170
- COMMON_STORAGE_PATHS = [
171
- # 通用
172
- '', '/', '/tmp/', '/temp/', '/temp',
173
- '/public/', '/public',
174
- '/private/', '/private',
175
- '/data/', '/data',
176
- '/backup/', '/backup', '/backups/',
177
- '/logs/', '/logs', '/log/',
178
- '/config/', '/configs/', '/configuration/',
179
- '/uploads/', '/uploads',
180
- '/files/', '/files',
181
- '/documents/', '/docs/',
182
- '/images/', '/img/', '/pictures/',
183
- '/videos/', '/video/',
184
- '/audio/', '/music/',
185
- '/archives/', '/archive/',
186
- '/cache/', '/.cache/',
187
-
188
- # 年份目录
189
- '/2020/', '/2021/', '/2022/', '/2023/', '/2024/', '/2025/',
190
- '/2020/', '/2021/', '/2022/', '/2023/', '/2024/', '/2025/',
191
-
192
- # OSS 特有
193
- '/oss/', '/ossfs/',
194
- '/dump/', '/dumps/',
195
- '/sql/', '/mysql/',
196
- '/mongo/', '/mongodump/',
197
- '/es/', '/elastic/',
198
-
199
- # 备份相关
200
- '/db/', '/database/', '/databases/',
201
- '/bak/', '/BAK/', '/backup/db/',
202
- '/backup/sql/',
203
- '/backup/mysql/',
204
-
205
- # 日志相关
206
- '/accesslog/', '/access_log/',
207
- '/errorlog/', '/error_log/',
208
- '/applog/', '/app_log/',
209
- '/nginx/', '/apache/', '/httpd/',
210
-
211
- # 配置密钥
212
- '/keys/', '/certs/', '/certificates/',
213
- '/secrets/', '/.secrets/',
214
- '/credentials/', '/.credentials/',
215
- '/env/', '/.env/', '/environments/',
216
-
217
- # 用户数据
218
- '/users/', '/user/', '/profiles/',
219
- '/accounts/', '/customers/',
220
- '/photos/', '/avatars/',
221
- '/attachments/', '/uploads/user/',
222
-
223
- # 敏感文件位置
224
- '/www/', '/web/', '/html/',
225
- '/static/', '/assets/',
226
- '/uploads/static/',
227
- ]
228
-
229
- # 目录递减探测深度
230
- MAX_DEPTH = 5
231
-
232
- # 常见文件扩展名
233
- COMMON_EXTENSIONS = [
234
- '.txt', '.log', '.json', '.xml', '.yaml', '.yml',
235
- '.sql', '.bak', '.backup', '.db', '.dump',
236
- '.env', '.conf', '.config', '.cfg', '.ini',
237
- '.key', '.pem', '.crt', '.p12', '.pfx', '.jks',
238
- '.pdf', '.doc', '.docx', '.xls', '.xlsx',
239
- '.jpg', '.jpeg', '.png', '.gif', '.bmp', '.svg',
240
- '.mp4', '.avi', '.mov', '.wmv',
241
- '.mp3', '.wav', '.flac',
242
- '.zip', '.tar', '.gz', '.rar', '.7z',
243
- '.csv', '.tsv', '.dat',
244
- ]
245
-
246
- # 目录递减探测深度
247
- MAX_DEPTH = 5
248
-
249
- # 常见文件扩展名
250
- COMMON_EXTENSIONS = [
251
- '.txt', '.log', '.json', '.xml', '.yaml', '.yml',
252
- '.sql', '.bak', '.backup', '.db', '.dump',
253
- '.env', '.conf', '.config', '.cfg', '.ini',
254
- '.key', '.pem', '.crt', '.p12', '.pfx', '.jks',
255
- '.pdf', '.doc', '.docx', '.xls', '.xlsx',
256
- '.jpg', '.jpeg', '.png', '.gif', '.bmp', '.svg',
257
- '.mp4', '.avi', '.mov', '.wmv',
258
- '.mp3', '.wav', '.flac',
259
- '.zip', '.tar', '.gz', '.rar', '.7z',
260
- '.csv', '.tsv', '.dat',
261
- ]
262
-
263
- def __init__(self, session: requests.Session = None):
264
- """初始化云存储测试器"""
265
- self.session = session or requests.Session()
266
- self.findings: List[Dict] = []
267
-
268
- def detect_storage_from_url(self, url: str) -> Optional[str]:
269
- """从 URL 模式识别存储类型"""
270
- url_lower = url.lower()
271
-
272
- # 1. 检查域名模式
273
- for storage_type, patterns in self.DOMAIN_PATTERNS.items():
274
- for pattern in patterns:
275
- if re.search(pattern, url_lower):
276
- return storage_type
277
-
278
- # 2. 检查路径模式
279
- for storage_type, patterns in self.PATH_PATTERNS.items():
280
- for pattern in patterns:
281
- if re.search(pattern, url_lower):
282
- return storage_type
283
-
284
- return None
285
-
286
- def detect_storage_from_response(self, resp: requests.Response) -> Tuple[Optional[str], str]:
287
- """从响应内容识别存储类型"""
288
- # 1. 检查响应头
289
- headers_lower = {k.lower(): v.lower() for k, v in resp.headers.items()}
290
- headers_text = str(headers_lower)
291
-
292
- for storage_type, patterns in self.HEADER_PATTERNS.items():
293
- for pattern in patterns:
294
- if pattern.lower() in headers_text:
295
- return storage_type, f"Header: {pattern}"
296
-
297
- # 2. 检查响应内容 (XML)
298
- try:
299
- content = resp.text
300
- for pattern in self.XML_PATTERNS:
301
- if pattern in content:
302
- # 进一步判断
303
- if '<ListBucket' in content:
304
- return 'unknown_bucket', f"XML: ListBucket"
305
- elif 'AccessControlPolicy' in content:
306
- return 'unknown_bucket', f"XML: ACL"
307
- else:
308
- return 'unknown', f"XML: {pattern[:30]}"
309
- except:
310
- pass
311
-
312
- # 3. 检查状态码和内容的特定组合
313
- if resp.status_code == 200:
314
- # 必须是真正的 XML,不是 HTML
315
- ct = resp.headers.get('Content-Type', '')
316
- if 'xml' in ct.lower() or resp.text.strip().startswith('<?xml'):
317
- if '<!' in resp.text and '<' not in resp.text[:50].replace('<!', '').replace('-->', ''):
318
- return 'unknown_bucket', "XML content"
319
-
320
- if resp.status_code == 403:
321
- if 'AccessDenied' in resp.text:
322
- return 'private_bucket', "Private bucket (AccessDenied)"
323
-
324
- return None, "No storage signature found"
325
-
326
- def is_storage_endpoint(self, url: str, resp: requests.Response = None) -> Tuple[bool, Optional[str], str]:
327
- """
328
- 综合判断是否为存储端点
329
-
330
- Returns:
331
- (is_storage, storage_type, reason)
332
- """
333
- # 1. URL 模式识别
334
- url_type = self.detect_storage_from_url(url)
335
- if url_type:
336
- return True, url_type, f"URL pattern: {url_type}"
337
-
338
- # 2. 如果没有 URL 模式,检查响应
339
- if resp:
340
- resp_type, resp_reason = self.detect_storage_from_response(resp)
341
- if resp_type:
342
- return True, resp_type, f"Response: {resp_reason}"
343
-
344
- # 3. 检查 URL 是否包含存储相关路径
345
- storage_paths = ['/minio/', '/oss/', '/cos/', '/s3/', '/bucket/', '/file/', '/upload/', '/storage/']
346
- for path in storage_paths:
347
- if path in url.lower():
348
- return True, 'unknown', f"Path pattern: {path}"
349
-
350
- return False, None, "Not a storage endpoint"
351
-
352
- def test_public_listing(self, bucket_url: str, storage_type: str = None) -> Tuple[bool, str]:
353
- """测试公开可列目录"""
354
- try:
355
- resp = self.session.get(bucket_url, timeout=10)
356
-
357
- # 检查是否返回 XML 文件列表
358
- if resp.status_code == 200:
359
- content = resp.text
360
- for pattern in self.XML_PATTERNS:
361
- if pattern in content:
362
- try:
363
- root = ET.fromstring(content)
364
- files = [elem.text for elem in root.iter()
365
- if elem.tag.endswith('Key') and elem.text]
366
- if files:
367
- return True, f"公开可列目录 - 找到 {len(files)} 个文件"
368
- return True, f"公开可列目录 - 返回 XML 格式列表 ({len(content)} bytes)"
369
- except:
370
- return True, f"公开可列目录 - 返回 XML 内容"
371
-
372
- if 'AccessDenied' in resp.text or resp.status_code == 403:
373
- return False, "正确拒绝 (403)"
374
-
375
- if resp.status_code == 404:
376
- return False, "资源不存在 (404)"
377
-
378
- return False, f"状态码: {resp.status_code}"
379
-
380
- except requests.exceptions.Timeout:
381
- return False, "请求超时"
382
- except Exception as e:
383
- return False, f"请求失败: {e}"
384
-
385
- def test_anonymous_put(self, bucket_url: str, storage_type: str = None) -> Tuple[bool, str]:
386
- """测试匿名 PUT 上传"""
387
- import time
388
- test_content = f"STORAGE_TEST_{time.time()}"
389
- test_key = f"test_{int(time.time())}.txt"
390
-
391
- try:
392
- resp = self.session.put(
393
- f"{bucket_url}/{test_key}",
394
- data=test_content,
395
- timeout=10
396
- )
397
-
398
- if resp.status_code in [200, 201]:
399
- # 尝试删除测试文件
400
- try:
401
- self.session.delete(f"{bucket_url}/{test_key}", timeout=10)
402
- except:
403
- pass
404
- return True, f"可匿名上传 (状态码: {resp.status_code})"
405
-
406
- return False, f"PUT 上传失败 (状态码: {resp.status_code})"
407
-
408
- except requests.exceptions.Timeout:
409
- return False, "PUT 请求超时"
410
- except Exception as e:
411
- return False, f"请求失败: {e}"
412
-
413
- def test_anonymous_post(self, bucket_url: str, storage_type: str = None) -> Tuple[bool, str]:
414
- """测试匿名 POST 表单上传"""
415
- import time
416
- test_content = f"STORAGE_TEST_{time.time()}"
417
-
418
- try:
419
- files = {'file': ('test.txt', test_content, 'text/plain')}
420
- resp = self.session.post(bucket_url, files=files, timeout=10)
421
-
422
- if resp.status_code in [200, 201]:
423
- return True, f"可匿名 POST 上传 (状态码: {resp.status_code})"
424
-
425
- return False, f"POST 上传失败 (状态码: {resp.status_code})"
426
-
427
- except Exception as e:
428
- return False, f"请求失败: {e}"
429
-
430
- def test_anonymous_delete(self, bucket_url: str, storage_type: str = None) -> Tuple[bool, str]:
431
- """测试匿名 DELETE 权限"""
432
- import time
433
- test_key = f"test_del_{int(time.time())}.txt"
434
-
435
- try:
436
- # 先上传测试文件
437
- self.session.put(
438
- f"{bucket_url}/{test_key}",
439
- data="test",
440
- timeout=10
441
- )
442
-
443
- # 尝试删除
444
- resp = self.session.delete(f"{bucket_url}/{test_key}", timeout=10)
445
-
446
- if resp.status_code in [200, 204]:
447
- return True, f"可匿名删除 (状态码: {resp.status_code})"
448
-
449
- return False, f"DELETE 失败 (状态码: {resp.status_code})"
450
-
451
- except Exception as e:
452
- return False, f"请求失败: {e}"
453
-
454
- def test_sensitive_files(self, bucket_url: str, storage_type: str = None) -> Tuple[bool, List[str]]:
455
- """测试敏感文件泄露"""
456
- found = []
457
-
458
- for path in self.SENSITIVE_PATHS:
459
- try:
460
- resp = self.session.get(f"{bucket_url}/{path}", timeout=10)
461
-
462
- if resp.status_code == 200 and len(resp.content) > 0:
463
- content = resp.text[:500].lower()
464
- sensitive_keywords = [
465
- 'password', 'secret', 'aws_access', 'aws_secret',
466
- 'api_key', 'api-key', 'token', 'private_key',
467
- '-----begin', 'begin rsa', 'begin ecdsa',
468
- 'database', 'db_password', 'mysql'
469
- ]
470
-
471
- if any(kw in content for kw in sensitive_keywords):
472
- found.append(f"{path} (包含敏感信息)")
473
- elif len(resp.content) > 100:
474
- found.append(f"{path} ({len(resp.content)} bytes)")
475
-
476
- except:
477
- pass
478
-
479
- return len(found) > 0, found[:10]
480
-
481
- def test_directory_traversal(self, bucket_url: str, storage_type: str = None) -> Tuple[bool, str]:
482
- """测试目录遍历"""
483
- traversal_paths = [
484
- '../../etc/passwd',
485
- '../../../etc/passwd',
486
- '..%2F..%2F..%2Fetc%2Fpasswd',
487
- '....//....//etc/passwd',
488
- '..././..././etc/passwd'
489
- ]
490
-
491
- for path in traversal_paths:
492
- try:
493
- resp = self.session.get(f"{bucket_url}/{path}", timeout=10)
494
-
495
- if resp.status_code == 200:
496
- content = resp.text[:200]
497
- if 'root:' in content or 'Administrator' in content:
498
- return True, f"目录遍历成功 - 读取了系统文件"
499
- elif len(resp.text) > 50 and 'AccessDenied' not in resp.text:
500
- return True, f"可能存在目录遍历 (路径: {path})"
501
-
502
- except:
503
- pass
504
-
505
- return False, "未发现目录遍历"
506
-
507
- def test_cors_misconfiguration(self, bucket_url: str, storage_type: str = None) -> Tuple[bool, str]:
508
- """测试 CORS 配置过宽"""
509
- try:
510
- resp = self.session.options(
511
- bucket_url,
512
- headers={
513
- 'Origin': 'http://evil.com',
514
- 'Access-Control-Request-Method': 'PUT'
515
- },
516
- timeout=10
517
- )
518
-
519
- allow_origin = resp.headers.get('Access-Control-Allow-Origin', '')
520
- allow_methods = resp.headers.get('Access-Control-Allow-Methods', '')
521
- allow_credentials = resp.headers.get('Access-Control-Allow-Credentials', '')
522
-
523
- if allow_origin == '*':
524
- if 'PUT' in allow_methods or 'POST' in allow_methods:
525
- return True, f"CORS 严重过宽 - Origin:*, Methods: {allow_methods}"
526
- return True, f"CORS 允许任意 Origin"
527
-
528
- if 'http://evil.com' in allow_origin:
529
- if allow_credentials.lower() == 'true':
530
- return True, f"CORS 可利用 - Origin: evil.com, Credentials: true"
531
- return True, f"CORS 允许特定恶意 Origin"
532
-
533
- return False, "CORS 配置正常"
534
-
535
- except Exception as e:
536
- return False, f"CORS 检测失败: {e}"
537
-
538
- def test_log_exposure(self, bucket_url: str, storage_type: str = None) -> Tuple[bool, List[str]]:
539
- """测试访问日志泄露"""
540
- found = []
541
-
542
- for log_path in self.LOG_PATHS:
543
- try:
544
- resp = self.session.get(f"{bucket_url}/{log_path}", timeout=10)
545
-
546
- if resp.status_code == 200 and len(resp.content) > 100:
547
- found.append(f"{log_path} ({len(resp.content)} bytes)")
548
-
549
- except:
550
- pass
551
-
552
- return len(found) > 0, found[:5]
553
-
554
- def test_version_exposure(self, bucket_url: str, storage_type: str = None) -> Tuple[bool, List[str]]:
555
- """测试版本控制泄露"""
556
- found = []
557
- version_paths = [
558
- '?versions', '?versioning', '/.versions/',
559
- '/_version/', '/versions/', '?uploads'
560
- ]
561
-
562
- for path in version_paths:
563
- try:
564
- resp = self.session.get(f"{bucket_url}/{path}", timeout=10)
565
-
566
- if resp.status_code == 200 and 'version' in resp.text.lower():
567
- found.append(path)
568
-
569
- except:
570
- pass
571
-
572
- return len(found) > 0, found
573
-
574
- def test_acl_public(self, bucket_url: str, storage_type: str = None) -> Tuple[bool, str]:
575
- """测试 ACL 公共权限"""
576
- try:
577
- resp = self.session.get(f"{bucket_url}?acl", timeout=10)
578
-
579
- if resp.status_code == 200:
580
- content = resp.text
581
- if 'AllUsers' in content or 'AuthenticatedUsers' in content:
582
- return True, "ACL 配置为公共访问"
583
- if '<Grant>' in content and 'FULL_CONTROL' in content:
584
- return True, "ACL 存在完全控制权限"
585
-
586
- return False, "ACL 检查未发现明显问题"
587
-
588
- except Exception as e:
589
- return False, f"ACL 检测失败: {e}"
590
-
591
- def test_common_paths(self, bucket_url: str, storage_type: str = None) -> Tuple[bool, List[Dict]]:
592
- """
593
- 测试常见存储目录
594
- 探测存储桶中的常见目录结构,发现更多敏感路径
595
- """
596
- print(f"[CloudStorage] 测试常见存储目录...")
597
- found = []
598
-
599
- for path in self.COMMON_STORAGE_PATHS:
600
- try:
601
- test_url = bucket_url.rstrip('/') + path
602
- resp = self.session.get(test_url, timeout=10)
603
-
604
- if resp.status_code == 200:
605
- content_len = len(resp.content)
606
-
607
- # 检查是否是文件列表或目录
608
- is_list = False
609
- file_count = 0
610
-
611
- # 尝试解析 XML
612
- try:
613
- root = ET.fromstring(resp.text)
614
- files = [e.text for e in root.iter()
615
- if e.tag.endswith('Key') and e.text]
616
- if files:
617
- is_list = True
618
- file_count = len(files)
619
- except:
620
- pass
621
-
622
- # 检查内容类型
623
- if resp.headers.get('Content-Type', '').startswith('application/xml'):
624
- is_list = True
625
-
626
- if is_list:
627
- found.append({
628
- 'path': path,
629
- 'type': 'listable_directory',
630
- 'file_count': file_count,
631
- 'size': content_len,
632
- 'url': test_url
633
- })
634
- print(f" [FOUND] 目录: {path} (文件数: {file_count})")
635
- elif content_len > 1000:
636
- # 可能是文件
637
- found.append({
638
- 'path': path,
639
- 'type': 'file',
640
- 'size': content_len,
641
- 'url': test_url
642
- })
643
- print(f" [FOUND] 文件: {path} ({content_len} bytes)")
644
-
645
- except requests.exceptions.Timeout:
646
- pass
647
- except Exception as e:
648
- pass
649
-
650
- return len(found) > 0, found
651
-
652
- def test_directory_depth(self, bucket_url: str, storage_type: str = None, max_depth: int = None) -> Tuple[bool, List[Dict]]:
653
- """
654
- 目录递减探测
655
- 逐层深入探测目录结构,发现深层敏感文件
656
-
657
- Args:
658
- bucket_url: 存储桶 URL
659
- storage_type: 存储类型
660
- max_depth: 最大探测深度,默认 5
661
- """
662
- if max_depth is None:
663
- max_depth = self.MAX_DEPTH
664
-
665
- print(f"[CloudStorage] 目录递减探测 (最大深度: {max_depth})...")
666
- found = []
667
-
668
- # 第一步:获取根目录文件列表
669
- try:
670
- resp = self.session.get(bucket_url, timeout=10)
671
- if resp.status_code == 200:
672
- try:
673
- root = ET.fromstring(resp.text)
674
- # 提取所有 Key
675
- all_keys = [e.text for e in root.iter()
676
- if e.tag.endswith('Key') and e.text]
677
-
678
- if all_keys:
679
- print(f" [Depth 0] 根目录: {len(all_keys)} 个文件/目录")
680
-
681
- # 分类文件和目录
682
- dirs = set()
683
- files = []
684
-
685
- for key in all_keys:
686
- if key.endswith('/'):
687
- dirs.add(key)
688
- else:
689
- files.append(key)
690
-
691
- # 记录发现
692
- if dirs:
693
- found.append({
694
- 'depth': 0,
695
- 'path': '/',
696
- 'type': 'directory',
697
- 'subdirs': list(dirs)[:20], # 最多记录20个
698
- 'file_count': len(files)
699
- })
700
-
701
- # 递归探测子目录 (限制深度)
702
- def explore_directory(parent_url: str, keys: List[str], depth: int):
703
- if depth >= max_depth:
704
- return
705
-
706
- subdirs = set()
707
- for key in keys:
708
- if '/' in key and not key.endswith('/'):
709
- # 文件可能在子目录中
710
- subdir = key.rsplit('/', 1)[0] + '/'
711
- subdirs.add(subdir)
712
-
713
- for subdir in subdirs:
714
- if depth + 1 < max_depth:
715
- try:
716
- subdir_url = parent_url.rstrip('/') + '/' + subdir.lstrip('/')
717
- subdir_url = parent_url + subdir if parent_url.endswith('/') else parent_url + '/' + subdir
718
-
719
- resp = self.session.get(subdir_url, timeout=10)
720
- if resp.status_code == 200:
721
- try:
722
- sub_root = ET.fromstring(resp.text)
723
- sub_keys = [e.text for e in sub_root.iter()
724
- if e.tag.endswith('Key') and e.text]
725
-
726
- if sub_keys:
727
- print(f" [Depth {depth+1}] {subdir}: {len(sub_keys)} 个文件")
728
-
729
- sub_files = [k for k in sub_keys if not k.endswith('/')]
730
- if sub_files:
731
- found.append({
732
- 'depth': depth + 1,
733
- 'path': subdir,
734
- 'type': 'directory',
735
- 'sample_files': sub_files[:10],
736
- 'file_count': len(sub_files)
737
- })
738
-
739
- # 继续递归
740
- if depth + 1 < max_depth:
741
- explore_directory(subdir_url, sub_keys, depth + 1)
742
-
743
- except:
744
- pass
745
- except:
746
- pass
747
-
748
- # 开始递归探测
749
- explore_directory(bucket_url, all_keys, 0)
750
-
751
- except:
752
- pass
753
-
754
- except Exception as e:
755
- print(f" [ERROR] 目录递减探测失败: {e}")
756
-
757
- return len(found) > 0, found
758
-
759
- def test_file_extensions(self, bucket_url: str, storage_type: str = None) -> Tuple[bool, List[Dict]]:
760
- """
761
- 测试常见文件扩展名
762
- 尝试访问常见文件类型,检查是否可下载
763
- """
764
- print(f"[CloudStorage] 测试常见文件扩展名...")
765
- found = []
766
-
767
- # 测试常见的敏感文件扩展名
768
- test_files = [
769
- '.env', '.git/config', '.git/HEAD',
770
- '.htaccess', '.htpasswd',
771
- 'wp-config.php', 'config.php', 'settings.py',
772
- 'database.yml', 'credentials.json',
773
- 'id_rsa', 'id_rsa.pub', 'authorized_keys',
774
- 'web.config', 'global.asax',
775
- '.sql', '.bak', '.backup', '.db', '.dump',
776
- '.log', '.txt', '.pdf', '.xlsx', '.docx',
777
- ]
778
-
779
- for test_file in test_files:
780
- try:
781
- test_url = bucket_url.rstrip('/') + '/' + test_file.lstrip('/')
782
- resp = self.session.get(test_url, timeout=10)
783
-
784
- if resp.status_code == 200 and len(resp.content) > 0:
785
- content = resp.text[:200].lower()
786
-
787
- # 检查是否包含敏感内容
788
- sensitive = any(kw in content for kw in [
789
- 'password', 'secret', 'key', 'token', 'api',
790
- 'database', 'db_', 'mysql', 'postgres',
791
- 'aws_access', 'aws_secret'
792
- ])
793
-
794
- found.append({
795
- 'file': test_file,
796
- 'size': len(resp.content),
797
- 'has_sensitive': sensitive,
798
- 'url': test_url,
799
- 'content_preview': resp.text[:100]
800
- })
801
-
802
- if sensitive:
803
- print(f" [FOUND] 敏感文件: {test_file} ({len(resp.content)} bytes)")
804
- else:
805
- print(f" [FOUND] 文件: {test_file} ({len(resp.content)} bytes)")
806
-
807
- except:
808
- pass
809
-
810
- return len(found) > 0, found
811
-
812
- def full_test(self, url: str, storage_type: str = None) -> Tuple[List[Dict], str]:
813
- """
814
- 执行完整云存储安全测试
815
-
816
- Args:
817
- url: 存储桶 URL 或 API 端点
818
- storage_type: 已知的存储类型 (可选)
819
-
820
- Returns:
821
- (findings, detected_type)
822
- """
823
- print(f"[CloudStorage] 开始测试: {url}")
824
-
825
- # 1. 智能识别存储类型
826
- if not storage_type:
827
- # 先尝试获取响应来辅助判断
828
- try:
829
- resp = self.session.get(url, timeout=10)
830
- is_storage, storage_type, reason = self.is_storage_endpoint(url, resp)
831
- print(f"[CloudStorage] 识别结果: {storage_type} ({reason})")
832
- except:
833
- is_storage, storage_type, reason = self.is_storage_endpoint(url, None)
834
- print(f"[CloudStorage] 识别结果: {storage_type} ({reason})")
835
- else:
836
- is_storage = True
837
- reason = f"User specified: {storage_type}"
838
-
839
- if not is_storage:
840
- print(f"[CloudStorage] 不是存储端点,跳过: {reason}")
841
- return [], storage_type or 'unknown'
842
-
843
- results = []
844
-
845
- # 2. 公开可列目录
846
- print("[CloudStorage] [1/8] 测试公开可列目录...")
847
- is_public, msg = self.test_public_listing(url, storage_type)
848
- if is_public:
849
- results.append({
850
- 'type': 'Public Listing',
851
- 'severity': 'Critical',
852
- 'evidence': msg,
853
- 'url': url,
854
- 'provider': storage_type
855
- })
856
-
857
- # 3. 匿名 PUT
858
- print("[CloudStorage] [2/8] 测试匿名 PUT 上传...")
859
- can_put, msg = self.test_anonymous_put(url, storage_type)
860
- if can_put:
861
- results.append({
862
- 'type': 'Anonymous PUT Upload',
863
- 'severity': 'Critical',
864
- 'evidence': msg,
865
- 'url': url,
866
- 'provider': storage_type
867
- })
868
-
869
- # 4. 敏感文件
870
- print("[CloudStorage] [3/8] 测试敏感文件泄露...")
871
- has_sensitive, files = self.test_sensitive_files(url, storage_type)
872
- if has_sensitive:
873
- results.append({
874
- 'type': 'Sensitive File Exposure',
875
- 'severity': 'Critical',
876
- 'evidence': ', '.join(files),
877
- 'url': url,
878
- 'provider': storage_type
879
- })
880
-
881
- # 5. 目录遍历
882
- print("[CloudStorage] [4/8] 测试目录遍历...")
883
- can_traverse, msg = self.test_directory_traversal(url, storage_type)
884
- if can_traverse:
885
- results.append({
886
- 'type': 'Directory Traversal',
887
- 'severity': 'High',
888
- 'evidence': msg,
889
- 'url': url,
890
- 'provider': storage_type
891
- })
892
-
893
- # 6. CORS
894
- print("[CloudStorage] [5/8] 测试 CORS...")
895
- cors_vuln, msg = self.test_cors_misconfiguration(url, storage_type)
896
- if cors_vuln:
897
- results.append({
898
- 'type': 'CORS Misconfiguration',
899
- 'severity': 'High',
900
- 'evidence': msg,
901
- 'url': url,
902
- 'provider': storage_type
903
- })
904
-
905
- # 7. 日志泄露
906
- print("[CloudStorage] [6/8] 测试日志泄露...")
907
- has_logs, log_files = self.test_log_exposure(url, storage_type)
908
- if has_logs:
909
- results.append({
910
- 'type': 'Log Exposure',
911
- 'severity': 'Medium',
912
- 'evidence': ', '.join(log_files),
913
- 'url': url,
914
- 'provider': storage_type
915
- })
916
-
917
- # 8. 版本控制
918
- print("[CloudStorage] [7/8] 测试版本控制泄露...")
919
- has_versions, version_paths = self.test_version_exposure(url, storage_type)
920
- if has_versions:
921
- results.append({
922
- 'type': 'Version Control Exposure',
923
- 'severity': 'Medium',
924
- 'evidence': ', '.join(version_paths),
925
- 'url': url,
926
- 'provider': storage_type
927
- })
928
-
929
- # 9. 常见存储目录
930
- print("[CloudStorage] [9/11] 测试常见存储目录...")
931
- has_common, common_paths = self.test_common_paths(url, storage_type)
932
- if has_common:
933
- for cp in common_paths[:10]: # 最多记录10个
934
- results.append({
935
- 'type': 'Common Storage Path',
936
- 'severity': 'Medium',
937
- 'evidence': f"{cp.get('path')} ({cp.get('type')}, {cp.get('file_count', cp.get('size', 'N/A'))}",
938
- 'url': cp.get('url', url),
939
- 'provider': storage_type
940
- })
941
-
942
- # 10. 目录递减探测
943
- print("[CloudStorage] [10/11] 目录递减探测...")
944
- has_depth, depth_results = self.test_directory_depth(url, storage_type)
945
- if has_depth:
946
- for dr in depth_results[:5]: # 最多记录5个深度
947
- results.append({
948
- 'type': 'Directory Depth Discovery',
949
- 'severity': 'Medium',
950
- 'evidence': f"Depth {dr.get('depth')}: {dr.get('path')} ({dr.get('file_count', 'N/A')} files)",
951
- 'url': url,
952
- 'provider': storage_type
953
- })
954
-
955
- # 11. 文件扩展名测试
956
- print("[CloudStorage] [11/11] 测试文件扩展名...")
957
- has_ext, ext_results = self.test_file_extensions(url, storage_type)
958
- if has_ext:
959
- for er in ext_results[:10]: # 最多记录10个
960
- if er.get('has_sensitive'):
961
- results.append({
962
- 'type': 'Sensitive File Extension',
963
- 'severity': 'High',
964
- 'evidence': f"{er.get('file')} ({er.get('size')} bytes) - 包含敏感内容",
965
- 'url': er.get('url', url),
966
- 'provider': storage_type
967
- })
968
-
969
- # ACL
970
- print("[CloudStorage] [12/12] 测试 ACL 配置...")
971
- acl_issue, msg = self.test_acl_public(url, storage_type)
972
- if acl_issue:
973
- results.append({
974
- 'type': 'ACL Public Access',
975
- 'severity': 'High',
976
- 'evidence': msg,
977
- 'url': url,
978
- 'provider': storage_type
979
- })
980
-
981
- print(f"[CloudStorage] 测试完成,发现 {len(results)} 个问题")
982
- return results, storage_type
983
-
984
- def test_log_transfer_exposure(self, bucket_url: str, storage_type: str = None) -> Tuple[bool, List[str]]:
985
- """
986
- 测试日志转存泄露 (OSS_scanner v1.1 新增)
987
- 检测日志是否被转存到存储桶
988
- """
989
- found = []
990
- log_transfer_paths = [
991
- '/logs transfer/', '/logs_archive/',
992
- '/archived_logs/', '/logs_old/',
993
- '/old_logs/', '/bak_logs/',
994
- '/s3_logs/', '/oss_logs/',
995
- '/bucket-logs/', '/access_logs/',
996
- '/year=', '/month=', '/date=',
997
- '/logs/date=', '/logs/dt=',
998
- ]
999
-
1000
- for path in log_transfer_paths:
1001
- try:
1002
- resp = self.session.get(bucket_url.rstrip('/') + path, timeout=10)
1003
- if resp.status_code == 200 and len(resp.content) > 100:
1004
- found.append(f"{path} ({len(resp.content)} bytes)")
1005
- except:
1006
- pass
1007
-
1008
- return len(found) > 0, found
1009
-
1010
- def test_encryption_config(self, bucket_url: str, storage_type: str = None) -> Tuple[bool, List[str]]:
1011
- """
1012
- 测试加密配置检测 (OSS_scanner v1.1 新增)
1013
- 检测存储桶加密配置
1014
- """
1015
- found = []
1016
- encryption_paths = [
1017
- '?encryption', '?policy', '?acl',
1018
- '?tags', '?tagging', '?cors',
1019
- '/.encryption/', '/.policy/', '/.settings/',
1020
- ]
1021
-
1022
- for path in encryption_paths:
1023
- try:
1024
- resp = self.session.get(bucket_url.rstrip('/') + path, timeout=10)
1025
- if resp.status_code == 200:
1026
- content = resp.text[:300].lower()
1027
- if any(kw in content for kw in ['encryption', 'kms', 'aes256', 'base64', 'aws:kms']):
1028
- found.append(f"{path} (包含加密配置)")
1029
- elif len(resp.content) > 50:
1030
- found.append(f"{path} ({len(resp.content)} bytes)")
1031
- except:
1032
- pass
1033
-
1034
- return len(found) > 0, found
1035
-
1036
- def batch_test(self, bucket_list: List[str], storage_type: str = None,
1037
- max_workers: int = 5, show_progress: bool = True) -> List[Dict]:
1038
- """
1039
- 批量扫描多个存储桶 (多线程)
1040
-
1041
- Args:
1042
- bucket_list: 存储桶 URL 列表
1043
- storage_type: 存储类型 (可选)
1044
- max_workers: 最大并发数
1045
- show_progress: 是否显示进度
1046
- """
1047
- from concurrent.futures import ThreadPoolExecutor, as_completed
1048
-
1049
- results = []
1050
- total = len(bucket_list)
1051
-
1052
- if show_progress:
1053
- print(f"[CloudStorage] 开始批量扫描 {total} 个存储桶 (并发数: {max_workers})")
1054
-
1055
- with ThreadPoolExecutor(max_workers=max_workers) as executor:
1056
- future_to_url = {
1057
- executor.submit(self.full_test, url, storage_type): url
1058
- for url in bucket_list
1059
- }
1060
-
1061
- for future in as_completed(future_to_url):
1062
- url = future_to_url[future]
1063
- try:
1064
- bucket_results, detected = future.result()
1065
- if bucket_results:
1066
- results.extend(bucket_results)
1067
- if show_progress:
1068
- print(f"[CloudStorage] [+] {url}: 发现 {len(bucket_results)} 个问题")
1069
- else:
1070
- if show_progress:
1071
- print(f"[CloudStorage] [-] {url}: 无问题")
1072
- except Exception as e:
1073
- if show_progress:
1074
- print(f"[CloudStorage] [ERROR] {url}: {e}")
1075
-
1076
- if show_progress:
1077
- print(f"[CloudStorage] 批量扫描完成: {total} 个桶, {len(results)} 个问题")
1078
-
1079
- return results
1080
-
1081
- def parse_hostid_url(self, hostid_url: str) -> Tuple[Optional[str], Optional[str], Optional[str]]:
1082
- """
1083
- 解析 hostid-url,自动识别 bucket 和 region
1084
-
1085
- Returns:
1086
- (bucket_name, region, provider) 或 None
1087
- """
1088
- # 阿里云: http://bucket.oss-region.aliyuncs.com
1089
- aliyun_match = re.search(r'([a-zA-Z0-9-]+)\.oss-([a-zA-Z0-9-]+)\.aliyuncs\.com', hostid_url)
1090
- if aliyun_match:
1091
- return aliyun_match.group(1), aliyun_match.group(2), 'aliyun'
1092
-
1093
- # 腾讯云: http://bucket.cos.region.myqcloud.com
1094
- tencent_match = re.search(r'([a-zA-Z0-9-]+)\.cos\.([a-zA-Z0-9-]+)\.myqcloud\.com', hostid_url)
1095
- if tencent_match:
1096
- return tencent_match.group(1), tencent_match.group(2), 'tencent'
1097
-
1098
- # AWS: http://bucket.s3.region.amazonaws.com
1099
- aws_match = re.search(r'([a-zA-Z0-9-]+)\.s3\.([a-zA-Z0-9-]+)\.amazonaws\.com', hostid_url)
1100
- if aws_match:
1101
- return aws_match.group(1), aws_match.group(2), 'aws'
1102
-
1103
- # 华为云: http://bucket.obs.region.myhwclouds.com
1104
- huawei_match = re.search(r'([a-zA-Z0-9-]+)\.obs\.([a-zA-Z0-9-]+)\.myhwclouds\.com', hostid_url)
1105
- if huawei_match:
1106
- return huawei_match.group(1), huawei_match.group(2), 'huawei'
1107
-
1108
- return None, None, None
1109
-
1110
- def generate_report(self, results: List[Dict], output_format: str = 'text') -> str:
1111
- """
1112
- 生成扫描报告
1113
-
1114
- Args:
1115
- results: 扫描结果
1116
- output_format: 报告格式 (text/json/html)
1117
- """
1118
- if output_format == 'json':
1119
- import json
1120
- return json.dumps(results, indent=2, ensure_ascii=False)
1121
-
1122
- elif output_format == 'html':
1123
- html = ['<html><head><meta charset="utf-8"><title>Cloud Storage Security Report</title>']
1124
- html.append('<style>body{font-family:Arial;margin:20px}h1{color:#333}</style></head><body>')
1125
- html.append('<h1>Cloud Storage Security Report</h1>')
1126
- html.append(f'<p>Total Findings: {len(results)}</p>')
1127
-
1128
- severity_groups = {}
1129
- for r in results:
1130
- sev = r.get('severity', 'Unknown')
1131
- if sev not in severity_groups:
1132
- severity_groups[sev] = []
1133
- severity_groups[sev].append(r)
1134
-
1135
- for sev in ['Critical', 'High', 'Medium', 'Low']:
1136
- if sev in severity_groups:
1137
- css_class = sev.lower()
1138
- html.append(f'<h2 class="{css_class}">{sev} ({len(severity_groups[sev])})</h2>')
1139
- html.append('<ul>')
1140
- for r in severity_groups[sev]:
1141
- html.append(f'<li><strong>{r.get("type")}</strong>: {r.get("evidence")}<br/>URL: {r.get("url")}<br/>Provider: {r.get("provider")}</li>')
1142
- html.append('</ul>')
1143
-
1144
- html.append('</body></html>')
1145
- return '\n'.join(html)
1146
-
1147
- else:
1148
- lines = ['='*60, 'Cloud Storage Security Report', '='*60]
1149
- lines.append(f'Total Findings: {len(results)}')
1150
- lines.append('')
1151
-
1152
- for i, r in enumerate(results, 1):
1153
- lines.append(f"[{i}] {r.get('type')} ({r.get('severity')})")
1154
- lines.append(f" Evidence: {r.get('evidence')}")
1155
- lines.append(f" URL: {r.get('url')}")
1156
- lines.append(f" Provider: {r.get('provider')}")
1157
- lines.append('')
1158
-
1159
- return '\n'.join(lines)
1160
-
1161
- def discover_from_text(self, text: str) -> List[Dict]:
1162
- """
1163
- 从文本 (JS/HTML/API 响应) 中发现存储桶 URL
1164
-
1165
- Returns:
1166
- List of {url, type, reason}
1167
- """
1168
- found = []
1169
-
1170
- # 阿里云 OSS
1171
- aliyun_patterns = [
1172
- r'https?://[a-zA-Z0-9.-]+\.oss-[a-zA-Z0-9-]+\.aliyuncs\.com[^\s"\'<>]*',
1173
- r'https?://[a-zA-Z0-9.-]+\.oss\.aliyuncs\.com[^\s"\'<>]*',
1174
- ]
1175
-
1176
- # 腾讯云 COS
1177
- tencent_patterns = [
1178
- r'https?://[a-zA-Z0-9.-]+\.cos\.[a-zA-Z0-9.-]+\.myqcloud\.com[^\s"\'<>]*',
1179
- ]
1180
-
1181
- # AWS S3
1182
- aws_patterns = [
1183
- r'https?://[a-zA-Z0-9.-]+\.s3\.[a-zA-Z0-9.-]+\.amazonaws\.com[^\s"\'<>]*',
1184
- ]
1185
-
1186
- # MinIO (通常是路径模式)
1187
- minio_patterns = [
1188
- r'["\']/(?:minio|minio-api|bucket|file|upload)[^\s"\'<>]*',
1189
- r'["\']/(?:api/s3)[^\s"\'<>]*',
1190
- ]
1191
-
1192
- all_patterns = aliyun_patterns + tencent_patterns + aws_patterns + minio_patterns
1193
-
1194
- for pattern in all_patterns:
1195
- matches = re.findall(pattern, text, re.IGNORECASE)
1196
- for match in matches:
1197
- # 去重
1198
- if not any(m['url'] == match for m in found):
1199
- if 'oss' in match.lower():
1200
- found.append({'url': match, 'type': 'aliyun', 'reason': 'URL pattern'})
1201
- elif 'cos' in match.lower():
1202
- found.append({'url': match, 'type': 'tencent', 'reason': 'URL pattern'})
1203
- elif 's3' in match.lower() or 'aws' in match.lower():
1204
- found.append({'url': match, 'type': 'aws', 'reason': 'URL pattern'})
1205
- else:
1206
- found.append({'url': match, 'type': 'minio', 'reason': 'Path pattern'})
1207
-
1208
- # 检查内网域名
1209
- internal_patterns = [
1210
- r'["\'](http[s]?://)[^"\']*?:900[01][^\s"\'<>]*',
1211
- r'["\'](http[s]?://)[^"\']*?/minio[^\s"\'<>]*',
1212
- ]
1213
-
1214
- for pattern in internal_patterns:
1215
- matches = re.findall(pattern, text, re.IGNORECASE)
1216
- for match in matches:
1217
- if not any(m['url'] == match for m in found):
1218
- found.append({'url': match, 'type': 'minio', 'reason': 'Internal/Path pattern'})
1219
-
1220
- return found
1221
-
1222
-
1223
- def test_current_domain_storage(target_url: str) -> List[Dict]:
1224
- """
1225
- 测试当前域名的存储桶漏洞
1226
-
1227
- 适用场景:
1228
- - 域名是主站,但路由 /minio/, /file/ 等指向存储服务
1229
- - 内网存储服务暴露在主站域名下
1230
- """
1231
- print(f"[CloudStorage] 测试当前域名存储服务: {target_url}")
1232
-
1233
- tester = CloudStorageTester()
1234
- all_results = []
1235
-
1236
- # 常见的存储路径
1237
- storage_paths = [
1238
- '/minio/',
1239
- '/minio-api/',
1240
- '/file/',
1241
- '/files/',
1242
- '/upload/',
1243
- '/uploads/',
1244
- '/storage/',
1245
- '/bucket/',
1246
- '/oss/',
1247
- '/cos/',
1248
- '/s3/',
1249
- '/api/file/',
1250
- '/api/upload/',
1251
- '/api/storage/',
1252
- '/api/minio/',
1253
- ]
1254
-
1255
- for path in storage_paths:
1256
- url = target_url.rstrip('/') + path
1257
- print(f"\n[CloudStorage] 测试路径: {path}")
1258
-
1259
- # 尝试检测存储类型
1260
- try:
1261
- resp = tester.session.head(url, timeout=10, allow_redirects=True)
1262
- except:
1263
- try:
1264
- resp = tester.session.get(url, timeout=10)
1265
- except Exception as e:
1266
- print(f"[CloudStorage] 请求失败: {e}")
1267
- continue
1268
-
1269
- # 智能判断是否为存储端点
1270
- is_storage, storage_type, reason = tester.is_storage_endpoint(url, resp)
1271
-
1272
- if is_storage:
1273
- print(f"[CloudStorage] 识别为存储: {storage_type} ({reason})")
1274
-
1275
- # 执行完整测试
1276
- results, detected_type = tester.full_test(url, storage_type)
1277
- all_results.extend(results)
1278
-
1279
- # 如果确认是存储,尝试更多路径
1280
- if detected_type in ['minio', 'oss', 'cos', 's3']:
1281
- # 尝试子路径
1282
- for subpath in ['', '/public/', '/data/', '/backup/']:
1283
- suburl = url.rstrip('/') + subpath
1284
- try:
1285
- subresp = tester.session.head(suburl, timeout=5, allow_redirects=True)
1286
- subis, subtype, subreason = tester.is_storage_endpoint(suburl, subresp)
1287
- if subis and subtype == detected_type:
1288
- subresults, _ = tester.full_test(suburl, subtype)
1289
- all_results.extend(subresults)
1290
- except:
1291
- pass
1292
- else:
1293
- print(f"[CloudStorage] 不是存储端点 ({reason})")
1294
-
1295
- return all_results
1296
-
1297
-
1298
- if __name__ == '__main__':
1299
- import sys
1300
-
1301
- if len(sys.argv) < 2:
1302
- print("用法:")
1303
- print(" python cloud_storage_tester.py <bucket_url>")
1304
- print(" python cloud_storage_tester.py --domain <main_domain>")
1305
- print("\n示例:")
1306
- print(" python cloud_storage_tester.py http://test.oss-cn-region.aliyuncs.com")
1307
- print(" python cloud_storage_tester.py --domain http://58.215.18.57:91")
1308
- sys.exit(1)
1309
-
1310
- if sys.argv[1] == '--domain':
1311
- target = sys.argv[2] if len(sys.argv) > 2 else input("输入域名: ")
1312
- results = test_current_domain_storage(target)
1313
- else:
1314
- bucket_url = sys.argv[1]
1315
- tester = CloudStorageTester()
1316
- results, detected = tester.full_test(bucket_url)
1317
-
1318
- print("\n" + "="*60)
1319
- print("云存储安全测试结果")
1320
- print("="*60)
1321
-
1322
- if not results:
1323
- print("\n未发现云存储安全漏洞")
1324
- else:
1325
- for i, r in enumerate(results, 1):
1326
- print(f"\n[{i}] {r['type']}")
1327
- print(f" Severity: {r['severity']}")
1328
- print(f" Evidence: {r['evidence']}")
1329
- print(f" URL: {r['url']}")
1330
- print(f" Provider: {r.get('provider', 'unknown')}")