opencode-api-security-testing 2.0.0 → 2.1.1
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.
- package/README.md +30 -24
- package/SKILL.md +1797 -0
- package/core/advanced_recon.py +788 -0
- package/core/agentic_analyzer.py +445 -0
- package/core/analyzers/api_parser.py +210 -0
- package/core/analyzers/response_analyzer.py +212 -0
- package/core/analyzers/sensitive_finder.py +184 -0
- package/core/api_fuzzer.py +422 -0
- package/core/api_interceptor.py +525 -0
- package/core/api_parser.py +955 -0
- package/core/browser_tester.py +479 -0
- package/core/cloud_storage_tester.py +1330 -0
- package/core/collectors/__init__.py +23 -0
- package/core/collectors/api_path_finder.py +300 -0
- package/core/collectors/browser_collect.py +645 -0
- package/core/collectors/browser_collector.py +411 -0
- package/core/collectors/http_client.py +111 -0
- package/core/collectors/js_collector.py +490 -0
- package/core/collectors/js_parser.py +780 -0
- package/core/collectors/url_collector.py +319 -0
- package/core/context_manager.py +682 -0
- package/core/deep_api_tester_v35.py +844 -0
- package/core/deep_api_tester_v55.py +366 -0
- package/core/dynamic_api_analyzer.py +532 -0
- package/core/http_client.py +179 -0
- package/core/models.py +296 -0
- package/core/orchestrator.py +890 -0
- package/core/prerequisite.py +227 -0
- package/core/reasoning_engine.py +1042 -0
- package/core/response_classifier.py +606 -0
- package/core/runner.py +938 -0
- package/core/scan_engine.py +599 -0
- package/core/skill_executor.py +435 -0
- package/core/skill_executor_v2.py +670 -0
- package/core/skill_executor_v3.py +704 -0
- package/core/smart_analyzer.py +687 -0
- package/core/strategy_pool.py +707 -0
- package/core/testers/auth_tester.py +264 -0
- package/core/testers/idor_tester.py +200 -0
- package/core/testers/sqli_tester.py +211 -0
- package/core/testing_loop.py +655 -0
- package/core/utils/base_path_dict.py +255 -0
- package/core/utils/payload_lib.py +167 -0
- package/core/utils/ssrf_detector.py +220 -0
- package/core/verifiers/vuln_verifier.py +536 -0
- package/package.json +17 -13
- package/references/asset-discovery.md +119 -612
- package/references/graphql-guidance.md +65 -641
- package/references/intake.md +84 -0
- package/references/report-template.md +131 -38
- package/references/rest-guidance.md +55 -526
- package/references/severity-model.md +52 -264
- package/references/test-matrix.md +65 -263
- package/references/validation.md +53 -400
- package/scripts/postinstall.js +46 -0
- package/src/index.ts +259 -275
- package/agents/cyber-supervisor.md +0 -55
- package/agents/probing-miner.md +0 -42
- package/agents/resource-specialist.md +0 -31
- package/commands/api-security-testing-scan.md +0 -59
- package/commands/api-security-testing-test.md +0 -49
- package/commands/api-security-testing.md +0 -72
- package/tsconfig.json +0 -17
|
@@ -0,0 +1,1330 @@
|
|
|
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')}")
|