opencode-api-security-testing 3.0.10 → 3.0.11
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 +74 -0
- 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 +1 -1
- package/references/README.md +72 -0
- package/references/asset-discovery.md +119 -0
- package/references/fuzzing-patterns.md +129 -0
- package/references/graphql-guidance.md +108 -0
- package/references/intake.md +84 -0
- package/references/pua-agent.md +192 -0
- package/references/report-template.md +156 -0
- package/references/rest-guidance.md +76 -0
- package/references/severity-model.md +76 -0
- package/references/test-matrix.md +86 -0
- package/references/validation.md +78 -0
- package/references/vulnerabilities/01-sqli-tests.md +1128 -0
- package/references/vulnerabilities/02-user-enum-tests.md +423 -0
- package/references/vulnerabilities/03-jwt-tests.md +499 -0
- package/references/vulnerabilities/04-idor-tests.md +362 -0
- package/references/vulnerabilities/05-sensitive-data-tests.md +466 -0
- package/references/vulnerabilities/06-biz-logic-tests.md +501 -0
- package/references/vulnerabilities/07-security-config-tests.md +511 -0
- package/references/vulnerabilities/08-brute-force-tests.md +457 -0
- package/references/vulnerabilities/09-vulnerability-chains.md +465 -0
- package/references/vulnerabilities/10-auth-tests.md +537 -0
- package/references/vulnerabilities/11-graphql-tests.md +355 -0
- package/references/vulnerabilities/12-ssrf-tests.md +396 -0
- package/references/vulnerabilities/README.md +148 -0
- package/references/workflows.md +192 -0
package/core/runner.py
ADDED
|
@@ -0,0 +1,938 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
SKILL.md 的完整可执行实现
|
|
5
|
+
|
|
6
|
+
模块联动流程:
|
|
7
|
+
1. TestContext - 共享测试上下文(session、endpoints、vulnerabilities)
|
|
8
|
+
2. PrerequisiteChecker - 前置检查
|
|
9
|
+
3. AssetDiscovery - 端点发现(静态+动态+Hook)
|
|
10
|
+
4. VulnerabilityTester - 漏洞测试
|
|
11
|
+
5. APIFuzzer - 模糊测试
|
|
12
|
+
6. CloudStorageTester - 云存储测试
|
|
13
|
+
7. ReportGenerator - 报告生成
|
|
14
|
+
|
|
15
|
+
使用方式:
|
|
16
|
+
python3 -m core.runner http://target.com
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import sys
|
|
20
|
+
import re
|
|
21
|
+
import time
|
|
22
|
+
import json
|
|
23
|
+
import logging
|
|
24
|
+
from datetime import datetime
|
|
25
|
+
from typing import Dict, List, Optional, Any, Set
|
|
26
|
+
from dataclasses import dataclass, field
|
|
27
|
+
|
|
28
|
+
sys.path.insert(0, '/workspace/skill-play/API-Security-Testing-Optimized')
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class TestContext:
|
|
35
|
+
"""
|
|
36
|
+
测试上下文 - 各模块共享数据
|
|
37
|
+
"""
|
|
38
|
+
target: str
|
|
39
|
+
session: Any = None
|
|
40
|
+
|
|
41
|
+
# 共享数据
|
|
42
|
+
endpoints: List[Dict] = field(default_factory=list)
|
|
43
|
+
vulnerabilities: List[Dict] = field(default_factory=list)
|
|
44
|
+
cloud_findings: List[Dict] = field(default_factory=list)
|
|
45
|
+
parent_paths: Dict = field(default_factory=dict)
|
|
46
|
+
tech_stack: Dict = field(default_factory=dict)
|
|
47
|
+
|
|
48
|
+
# Hook 到的 API
|
|
49
|
+
hooked_apis: List[Dict] = field(default_factory=list)
|
|
50
|
+
sensitive_apis: List[Dict] = field(default_factory=list)
|
|
51
|
+
test_vectors: List[Dict] = field(default_factory=list)
|
|
52
|
+
|
|
53
|
+
# 状态
|
|
54
|
+
playwright_available: bool = False
|
|
55
|
+
backend_reachable: bool = True
|
|
56
|
+
nginx_fallback: bool = False
|
|
57
|
+
|
|
58
|
+
def add_endpoints(self, endpoints: List[Dict]):
|
|
59
|
+
"""添加端点(去重)"""
|
|
60
|
+
existing = set((e.get('method', 'GET'), e.get('path', '')) for e in self.endpoints)
|
|
61
|
+
for ep in endpoints:
|
|
62
|
+
key = (ep.get('method', 'GET'), ep.get('path', ''))
|
|
63
|
+
if key not in existing:
|
|
64
|
+
self.endpoints.append(ep)
|
|
65
|
+
existing.add(key)
|
|
66
|
+
|
|
67
|
+
def add_vulnerability(self, vuln: Dict):
|
|
68
|
+
"""添加漏洞(去重)"""
|
|
69
|
+
key = (vuln.get('type', ''), vuln.get('endpoint', ''))
|
|
70
|
+
existing = set((v.get('type', ''), v.get('endpoint', '')) for v in self.vulnerabilities)
|
|
71
|
+
if key not in existing:
|
|
72
|
+
self.vulnerabilities.append(vuln)
|
|
73
|
+
|
|
74
|
+
def get_all_endpoints(self) -> List[Dict]:
|
|
75
|
+
"""获取所有端点(包括 Hook 到的)"""
|
|
76
|
+
all_eps = list(self.endpoints)
|
|
77
|
+
for hook in self.hooked_apis:
|
|
78
|
+
path = hook.get('path', hook.get('url', ''))
|
|
79
|
+
method = hook.get('method', 'GET')
|
|
80
|
+
if not any(e.get('path') == path and e.get('method') == method for e in all_eps):
|
|
81
|
+
all_eps.append(hook)
|
|
82
|
+
return all_eps
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class PrerequisiteChecker:
|
|
86
|
+
"""阶段 0: 前置检查"""
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
def check(ctx: TestContext) -> bool:
|
|
90
|
+
"""检查所有依赖,设置上下文"""
|
|
91
|
+
from core.prerequisite import prerequisite_check
|
|
92
|
+
|
|
93
|
+
print("[0] 前置检查")
|
|
94
|
+
print("-" * 70)
|
|
95
|
+
|
|
96
|
+
# 使用新的前置检查模块 (支持平替检测和自动安装)
|
|
97
|
+
playwright_available, browser_type, can_proceed = prerequisite_check()
|
|
98
|
+
|
|
99
|
+
ctx.playwright_available = playwright_available
|
|
100
|
+
|
|
101
|
+
# requests 检查
|
|
102
|
+
try:
|
|
103
|
+
import requests
|
|
104
|
+
ctx.session = requests.Session()
|
|
105
|
+
ctx.session.headers.update({
|
|
106
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
|
107
|
+
})
|
|
108
|
+
print(" [OK] requests")
|
|
109
|
+
except ImportError:
|
|
110
|
+
print(" [FAIL] requests")
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
if not can_proceed:
|
|
114
|
+
print("\n[FATAL] 前置检查失败 - 缺少无头浏览器支持")
|
|
115
|
+
return False
|
|
116
|
+
|
|
117
|
+
print()
|
|
118
|
+
return ctx.playwright_available and ctx.session is not None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class AssetDiscovery:
|
|
122
|
+
"""阶段 1: 资产发现
|
|
123
|
+
|
|
124
|
+
使用 TestContext 共享数据
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
def __init__(self, ctx: TestContext):
|
|
128
|
+
self.ctx = ctx
|
|
129
|
+
self.target = ctx.target
|
|
130
|
+
self.session = ctx.session
|
|
131
|
+
self.js_files = []
|
|
132
|
+
|
|
133
|
+
def run(self):
|
|
134
|
+
"""执行资产发现(联动各模块)"""
|
|
135
|
+
print("[1] 资产发现")
|
|
136
|
+
print("-" * 70)
|
|
137
|
+
|
|
138
|
+
# 1.1 目标探测
|
|
139
|
+
self._probe_target()
|
|
140
|
+
|
|
141
|
+
# 1.2 静态分析 (api_parser)
|
|
142
|
+
self._parse_with_api_parser()
|
|
143
|
+
|
|
144
|
+
# 1.3 动态分析 (dynamic_api_analyzer)
|
|
145
|
+
self._analyze_dynamic()
|
|
146
|
+
|
|
147
|
+
# 1.4 API Hook (api_interceptor) - 获取真实参数
|
|
148
|
+
self._hook_apis()
|
|
149
|
+
|
|
150
|
+
# 1.5 父路径探测
|
|
151
|
+
self._probe_parent_paths()
|
|
152
|
+
|
|
153
|
+
# 更新上下文
|
|
154
|
+
self.ctx.backend_reachable = len([p for p in self.ctx.parent_paths.values() if p.get('is_api')]) > 0
|
|
155
|
+
self.ctx.nginx_fallback = not self.ctx.backend_reachable
|
|
156
|
+
|
|
157
|
+
print(f"\n 发现端点: {len(self.ctx.endpoints)}")
|
|
158
|
+
print(f" Hook API: {len(self.ctx.hooked_apis)}")
|
|
159
|
+
print(f" 敏感 API: {len(self.ctx.sensitive_apis)}")
|
|
160
|
+
print(f" 父路径: {len(self.ctx.parent_paths)}")
|
|
161
|
+
print(f" 技术栈: {self.ctx.tech_stack}")
|
|
162
|
+
print()
|
|
163
|
+
|
|
164
|
+
return self.ctx
|
|
165
|
+
|
|
166
|
+
def _probe_target(self):
|
|
167
|
+
"""基础探测"""
|
|
168
|
+
try:
|
|
169
|
+
r = self.session.get(self.target, timeout=10)
|
|
170
|
+
|
|
171
|
+
server = r.headers.get('Server', 'Unknown')
|
|
172
|
+
print(f" Server: {server}")
|
|
173
|
+
|
|
174
|
+
html = r.text.lower()
|
|
175
|
+
if 'vue' in html:
|
|
176
|
+
self.ctx.tech_stack['frontend'] = 'Vue.js'
|
|
177
|
+
if 'react' in html:
|
|
178
|
+
self.ctx.tech_stack['frontend'] = 'React'
|
|
179
|
+
if 'jquery' in html:
|
|
180
|
+
self.ctx.tech_stack['jquery'] = True
|
|
181
|
+
if 'element' in html:
|
|
182
|
+
self.ctx.tech_stack['ui'] = 'ElementUI'
|
|
183
|
+
if 'angular' in html:
|
|
184
|
+
self.ctx.tech_stack['frontend'] = 'Angular'
|
|
185
|
+
|
|
186
|
+
cors = r.headers.get('Access-Control-Allow-Origin', '未设置')
|
|
187
|
+
print(f" CORS: {cors}")
|
|
188
|
+
|
|
189
|
+
except Exception as e:
|
|
190
|
+
print(f" [WARN] 目标探测失败: {e}")
|
|
191
|
+
|
|
192
|
+
def _infer_semantic_type(self, path: str) -> str:
|
|
193
|
+
"""推断路径的语义类型"""
|
|
194
|
+
path_lower = path.lower()
|
|
195
|
+
|
|
196
|
+
mappings = {
|
|
197
|
+
'auth': ['/auth', '/login', '/logout', '/token', '/signin'],
|
|
198
|
+
'user': ['/user', '/profile', '/account', '/avatar'],
|
|
199
|
+
'admin': ['/admin', '/manage', '/system', '/config'],
|
|
200
|
+
'file': ['/file', '/upload', '/download', '/attachment', '/image'],
|
|
201
|
+
'order': ['/order', '/cart', '/checkout'],
|
|
202
|
+
'product': ['/product', '/goods', '/sku'],
|
|
203
|
+
'data': ['/data', '/statistics', '/report', '/analytics'],
|
|
204
|
+
'api': ['/api', '/v1', '/v2', '/rest'],
|
|
205
|
+
'search': ['/search', '/query', '/find'],
|
|
206
|
+
'list': ['/list', '/items', '/records'],
|
|
207
|
+
'detail': ['/detail', '/info', '/view'],
|
|
208
|
+
'create': ['/create', '/add', '/new'],
|
|
209
|
+
'update': ['/update', '/edit', '/modify'],
|
|
210
|
+
'delete': ['/delete', '/remove'],
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
for semantic, keywords in mappings.items():
|
|
214
|
+
for keyword in keywords:
|
|
215
|
+
if keyword in path_lower:
|
|
216
|
+
return semantic
|
|
217
|
+
|
|
218
|
+
return 'unknown'
|
|
219
|
+
|
|
220
|
+
def _parse_with_api_parser(self):
|
|
221
|
+
"""静态分析 (api_parser)"""
|
|
222
|
+
try:
|
|
223
|
+
from core.api_parser import APIEndpointParser
|
|
224
|
+
|
|
225
|
+
parser = APIEndpointParser(self.target, self.session)
|
|
226
|
+
self.js_files = parser.discover_js_files()
|
|
227
|
+
print(f" 发现 JS 文件: {len(self.js_files)}")
|
|
228
|
+
|
|
229
|
+
parsed_endpoints = parser.parse_js_files(self.js_files)
|
|
230
|
+
|
|
231
|
+
for ep in parsed_endpoints:
|
|
232
|
+
self.ctx.add_endpoints([{
|
|
233
|
+
'path': ep.path,
|
|
234
|
+
'method': ep.method,
|
|
235
|
+
'params': [{'name': p.name, 'type': p.param_type.value, 'required': p.required} for p in ep.params],
|
|
236
|
+
'source': ep.source,
|
|
237
|
+
'semantic_type': ep.semantic_type,
|
|
238
|
+
'has_params': ep.has_params(),
|
|
239
|
+
}])
|
|
240
|
+
|
|
241
|
+
print(f" 静态解析: {len(parsed_endpoints)} 端点")
|
|
242
|
+
|
|
243
|
+
except Exception as e:
|
|
244
|
+
print(f" [WARN] API Parser 失败: {e}")
|
|
245
|
+
|
|
246
|
+
except Exception as e:
|
|
247
|
+
print(f" [WARN] API Parser 失败: {e}")
|
|
248
|
+
|
|
249
|
+
def _analyze_dynamic(self):
|
|
250
|
+
"""动态分析 (dynamic_api_analyzer)"""
|
|
251
|
+
try:
|
|
252
|
+
from core.dynamic_api_analyzer import DynamicAPIAnalyzer
|
|
253
|
+
|
|
254
|
+
analyzer = DynamicAPIAnalyzer(self.target)
|
|
255
|
+
results = analyzer.analyze_full()
|
|
256
|
+
|
|
257
|
+
for ep in results.get('endpoints', []):
|
|
258
|
+
path = ep.get('path', '')
|
|
259
|
+
method = ep.get('method', 'GET')
|
|
260
|
+
params_data = ep.get('params', [])
|
|
261
|
+
if isinstance(params_data, list):
|
|
262
|
+
params_dict = {p: True for p in params_data}
|
|
263
|
+
else:
|
|
264
|
+
params_dict = params_data
|
|
265
|
+
|
|
266
|
+
self.ctx.add_endpoints([{
|
|
267
|
+
'path': path,
|
|
268
|
+
'method': method,
|
|
269
|
+
'params': params_dict,
|
|
270
|
+
'source': f"dynamic_{ep.get('source', 'unknown')}",
|
|
271
|
+
'semantic_type': self._infer_semantic_type(path),
|
|
272
|
+
}])
|
|
273
|
+
|
|
274
|
+
print(f" 动态分析: {results.get('unique_endpoints', 0)} 端点")
|
|
275
|
+
|
|
276
|
+
except Exception as e:
|
|
277
|
+
print(f" [WARN] Dynamic API Analyzer 失败: {e}")
|
|
278
|
+
|
|
279
|
+
def _hook_apis(self):
|
|
280
|
+
"""API Hook (api_interceptor)"""
|
|
281
|
+
if not self.ctx.playwright_available:
|
|
282
|
+
print(" [SKIP] Playwright 不可用")
|
|
283
|
+
return
|
|
284
|
+
|
|
285
|
+
try:
|
|
286
|
+
from core.api_interceptor import APIInterceptor
|
|
287
|
+
|
|
288
|
+
print(" [API Hook] 启动...")
|
|
289
|
+
interceptor = APIInterceptor(self.target)
|
|
290
|
+
hook_results = interceptor.hook_all_apis()
|
|
291
|
+
|
|
292
|
+
# 保存 Hook 结果到上下文
|
|
293
|
+
self.ctx.hooked_apis = hook_results.get('endpoints', [])
|
|
294
|
+
self.ctx.sensitive_apis = hook_results.get('sensitive', [])
|
|
295
|
+
self.ctx.test_vectors = hook_results.get('test_vectors', [])
|
|
296
|
+
|
|
297
|
+
# 将 Hook 到的端点添加到上下文的端点列表
|
|
298
|
+
for hooked_ep in hook_results.get('endpoints', []):
|
|
299
|
+
path = hooked_ep.get('path', hooked_ep.get('url', ''))
|
|
300
|
+
if path and '/' in path:
|
|
301
|
+
self.ctx.add_endpoints([{
|
|
302
|
+
'path': path,
|
|
303
|
+
'method': hooked_ep.get('method', 'GET'),
|
|
304
|
+
'params': hooked_ep.get('params', {}),
|
|
305
|
+
'source': f"hooked_{hooked_ep.get('source', 'unknown')}",
|
|
306
|
+
'semantic_type': hooked_ep.get('semantic', 'unknown'),
|
|
307
|
+
}])
|
|
308
|
+
|
|
309
|
+
print(f" API Hook: {len(self.ctx.hooked_apis)} 个 API 调用")
|
|
310
|
+
print(f" 敏感操作: {len(self.ctx.sensitive_apis)} 个")
|
|
311
|
+
print(f" 测试向量: {len(self.ctx.test_vectors)} 个")
|
|
312
|
+
|
|
313
|
+
except Exception as e:
|
|
314
|
+
print(f" [WARN] API Hook 失败: {e}")
|
|
315
|
+
|
|
316
|
+
def _probe_parent_paths(self):
|
|
317
|
+
"""父路径探测"""
|
|
318
|
+
try:
|
|
319
|
+
from core.api_parser import APIEndpointParser
|
|
320
|
+
|
|
321
|
+
parser = APIEndpointParser(self.target, self.session)
|
|
322
|
+
parser.discover_js_files()
|
|
323
|
+
|
|
324
|
+
# 获取解析后的父路径(set 格式)
|
|
325
|
+
parsed_endpoints = parser.parse_js_files(self.js_files)
|
|
326
|
+
|
|
327
|
+
# 转换 set 为 dict 格式
|
|
328
|
+
for parent in parser.parent_paths:
|
|
329
|
+
url = self.target.rstrip('/') + parent
|
|
330
|
+
try:
|
|
331
|
+
r = self.session.get(url, timeout=5, allow_redirects=False)
|
|
332
|
+
self.ctx.parent_paths[parent] = {
|
|
333
|
+
'path': parent,
|
|
334
|
+
'status': r.status_code,
|
|
335
|
+
'is_api': 'json' in r.headers.get('Content-Type', '').lower() or '{' in r.text[:100],
|
|
336
|
+
}
|
|
337
|
+
except:
|
|
338
|
+
pass
|
|
339
|
+
|
|
340
|
+
print(f" 父路径: {len(self.ctx.parent_paths)} 个")
|
|
341
|
+
|
|
342
|
+
except Exception as e:
|
|
343
|
+
print(f" [WARN] 父路径探测失败: {e}")
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
class VulnerabilityTester:
|
|
347
|
+
"""阶段 2: 多维度漏洞分析"""
|
|
348
|
+
|
|
349
|
+
def __init__(self, ctx: TestContext):
|
|
350
|
+
self.ctx = ctx
|
|
351
|
+
self.target = ctx.target
|
|
352
|
+
self.session = ctx.session
|
|
353
|
+
|
|
354
|
+
def run(self) -> List:
|
|
355
|
+
"""执行漏洞测试"""
|
|
356
|
+
print("[2] 多维度漏洞分析")
|
|
357
|
+
print("-" * 70)
|
|
358
|
+
|
|
359
|
+
# 使用上下文中所有端点(包括 Hook 到的)
|
|
360
|
+
self.endpoints = self.ctx.get_all_endpoints()
|
|
361
|
+
|
|
362
|
+
# 2.1 SQL 注入测试
|
|
363
|
+
self._test_sqli()
|
|
364
|
+
|
|
365
|
+
# 2.2 XSS 测试
|
|
366
|
+
self._test_xss()
|
|
367
|
+
|
|
368
|
+
# 2.3 路径遍历测试
|
|
369
|
+
self._test_path_traversal()
|
|
370
|
+
|
|
371
|
+
# 2.4 敏感信息泄露
|
|
372
|
+
self._test_sensitive_exposure()
|
|
373
|
+
|
|
374
|
+
# 2.5 认证绕过测试
|
|
375
|
+
self._test_auth_bypass()
|
|
376
|
+
|
|
377
|
+
# 2.6 GraphQL 测试 (如果发现 GraphQL 端点)
|
|
378
|
+
self._test_graphql()
|
|
379
|
+
|
|
380
|
+
# 2.7 暴力破解测试 (如果发现登录端点)
|
|
381
|
+
self._test_brute_force()
|
|
382
|
+
|
|
383
|
+
# 2.8 IDOR 测试
|
|
384
|
+
self._test_idor()
|
|
385
|
+
|
|
386
|
+
print(f"\n 发现漏洞: {len(self.ctx.vulnerabilities)}")
|
|
387
|
+
print()
|
|
388
|
+
|
|
389
|
+
return self.ctx.vulnerabilities
|
|
390
|
+
|
|
391
|
+
def _test_sqli(self):
|
|
392
|
+
"""SQL 注入测试"""
|
|
393
|
+
print(" [SQL注入] 测试...")
|
|
394
|
+
|
|
395
|
+
sqli_payloads = [
|
|
396
|
+
"' OR '1'='1",
|
|
397
|
+
"' OR 1=1--",
|
|
398
|
+
"' UNION SELECT NULL--",
|
|
399
|
+
"admin'--",
|
|
400
|
+
]
|
|
401
|
+
|
|
402
|
+
for ep in self.endpoints[:20]:
|
|
403
|
+
if ep.get('method') != 'GET':
|
|
404
|
+
continue
|
|
405
|
+
|
|
406
|
+
url = ep.get('url', self.target + ep.get('path', ''))
|
|
407
|
+
if '?' not in url:
|
|
408
|
+
url = url + '?id=1'
|
|
409
|
+
|
|
410
|
+
try:
|
|
411
|
+
for payload in sqli_payloads[:2]:
|
|
412
|
+
test_url = url.replace('id=1', f'id={payload}')
|
|
413
|
+
r = self.session.get(test_url, timeout=5)
|
|
414
|
+
|
|
415
|
+
# 跳过 HTML 响应(通常是 nginx fallback)
|
|
416
|
+
content_type = r.headers.get('Content-Type', '')
|
|
417
|
+
if 'text/html' in content_type or r.text.strip().startswith('<!DOCTYPE'):
|
|
418
|
+
continue
|
|
419
|
+
|
|
420
|
+
# 检查是否是 JSON 响应
|
|
421
|
+
if 'application/json' not in content_type and '{' not in r.text[:100]:
|
|
422
|
+
continue
|
|
423
|
+
|
|
424
|
+
# SQL 注入特征检测(排除假阳性)
|
|
425
|
+
text_lower = r.text.lower()
|
|
426
|
+
# 真正的 SQL 错误特征
|
|
427
|
+
sql_patterns = ['sql syntax', 'sql error', 'mysql', 'oracle', 'sqlite',
|
|
428
|
+
'sqlstate', 'postgresql', 'sqlserver', 'column', 'table',
|
|
429
|
+
'mysqli_', 'pdo_', 'odbc_']
|
|
430
|
+
if any(p in text_lower for p in sql_patterns):
|
|
431
|
+
self.ctx.add_vulnerability({
|
|
432
|
+
'type': 'SQL Injection',
|
|
433
|
+
'severity': 'CRITICAL',
|
|
434
|
+
'endpoint': url,
|
|
435
|
+
'payload': payload,
|
|
436
|
+
'evidence': 'SQL error detected'
|
|
437
|
+
})
|
|
438
|
+
print(f" [!] {ep['path']}: SQL注入")
|
|
439
|
+
break
|
|
440
|
+
except:
|
|
441
|
+
pass
|
|
442
|
+
|
|
443
|
+
def _test_xss(self):
|
|
444
|
+
"""XSS 测试"""
|
|
445
|
+
print(" [XSS] 测试...")
|
|
446
|
+
|
|
447
|
+
xss_payloads = [
|
|
448
|
+
'<script>alert(1)</script>',
|
|
449
|
+
'<img src=x onerror=alert(1)>',
|
|
450
|
+
'"><script>alert(1)</script>',
|
|
451
|
+
]
|
|
452
|
+
|
|
453
|
+
for ep in self.endpoints[:20]:
|
|
454
|
+
if ep.get('method') != 'GET':
|
|
455
|
+
continue
|
|
456
|
+
|
|
457
|
+
url = ep.get('url', self.target + ep.get('path', ''))
|
|
458
|
+
|
|
459
|
+
try:
|
|
460
|
+
for payload in xss_payloads[:1]:
|
|
461
|
+
if '?' in url:
|
|
462
|
+
test_url = url + '&q=' + payload
|
|
463
|
+
else:
|
|
464
|
+
test_url = url + '?q=' + payload
|
|
465
|
+
|
|
466
|
+
r = self.session.get(test_url, timeout=5)
|
|
467
|
+
|
|
468
|
+
if payload in r.text:
|
|
469
|
+
self.ctx.add_vulnerability({
|
|
470
|
+
'type': 'XSS (Reflected)',
|
|
471
|
+
'severity': 'HIGH',
|
|
472
|
+
'endpoint': url,
|
|
473
|
+
'payload': payload,
|
|
474
|
+
'evidence': 'Payload reflected'
|
|
475
|
+
})
|
|
476
|
+
print(f" [!] {ep['path']}: XSS")
|
|
477
|
+
break
|
|
478
|
+
except:
|
|
479
|
+
pass
|
|
480
|
+
|
|
481
|
+
def _test_path_traversal(self):
|
|
482
|
+
"""路径遍历测试"""
|
|
483
|
+
print(" [路径遍历] 测试...")
|
|
484
|
+
|
|
485
|
+
pt_payloads = ['../../etc/passwd', '..\\..\\windows\\win.ini', '%2e%2e%2f%2e%2e%2fetc%2fpasswd']
|
|
486
|
+
|
|
487
|
+
for ep in self.endpoints[:10]:
|
|
488
|
+
url = ep.get('url', self.target + ep.get('path', ''))
|
|
489
|
+
|
|
490
|
+
try:
|
|
491
|
+
for payload in pt_payloads[:1]:
|
|
492
|
+
test_url = url + '/' + payload if url.endswith('/') else url + '/' + payload
|
|
493
|
+
r = self.session.get(test_url, timeout=5)
|
|
494
|
+
|
|
495
|
+
if 'root:' in r.text or '[extensions]' in r.text:
|
|
496
|
+
self.ctx.add_vulnerability({
|
|
497
|
+
'type': 'Path Traversal',
|
|
498
|
+
'severity': 'HIGH',
|
|
499
|
+
'endpoint': url,
|
|
500
|
+
'payload': payload,
|
|
501
|
+
'evidence': 'Sensitive file content exposed'
|
|
502
|
+
})
|
|
503
|
+
print(f" [!] {ep['path']}: 路径遍历")
|
|
504
|
+
break
|
|
505
|
+
except:
|
|
506
|
+
pass
|
|
507
|
+
|
|
508
|
+
def _test_sensitive_exposure(self):
|
|
509
|
+
"""敏感信息泄露测试"""
|
|
510
|
+
print(" [敏感信息] 检测...")
|
|
511
|
+
|
|
512
|
+
sensitive_patterns = [
|
|
513
|
+
('password', 'password', 'MEDIUM'),
|
|
514
|
+
('secret', 'api_key', 'HIGH'),
|
|
515
|
+
('token', 'token', 'MEDIUM'),
|
|
516
|
+
('private_key', 'private_key', 'CRITICAL'),
|
|
517
|
+
]
|
|
518
|
+
|
|
519
|
+
for ep in self.endpoints[:30]:
|
|
520
|
+
url = ep.get('url', self.target + ep.get('path', ''))
|
|
521
|
+
|
|
522
|
+
try:
|
|
523
|
+
r = self.session.get(url, timeout=5)
|
|
524
|
+
|
|
525
|
+
for pattern, name, severity in sensitive_patterns:
|
|
526
|
+
if pattern in r.text.lower() and 'password' not in r.text.lower()[:500]:
|
|
527
|
+
# 避免误报,只在实际内容中检测
|
|
528
|
+
content_sample = r.text[:1000].lower()
|
|
529
|
+
if content_sample.count(pattern) > 2:
|
|
530
|
+
self.ctx.add_vulnerability({
|
|
531
|
+
'type': 'Sensitive Data Exposure',
|
|
532
|
+
'severity': severity,
|
|
533
|
+
'endpoint': url,
|
|
534
|
+
'evidence': f'{name} found in response',
|
|
535
|
+
})
|
|
536
|
+
print(f" [!] {ep['path']}: 敏感信息 ({name})")
|
|
537
|
+
break
|
|
538
|
+
except:
|
|
539
|
+
pass
|
|
540
|
+
|
|
541
|
+
def _test_auth_bypass(self):
|
|
542
|
+
"""认证绕过测试"""
|
|
543
|
+
print(" [认证绕过] 测试...")
|
|
544
|
+
|
|
545
|
+
# 测试不需要认证就能访问的敏感端点
|
|
546
|
+
sensitive_paths = ['/admin', '/api/admin', '/api/users', '/api/config']
|
|
547
|
+
|
|
548
|
+
for path in sensitive_paths:
|
|
549
|
+
url = self.target.rstrip('/') + path
|
|
550
|
+
try:
|
|
551
|
+
r = self.session.get(url, timeout=5)
|
|
552
|
+
|
|
553
|
+
if r.status_code == 200 and len(r.text) > 100:
|
|
554
|
+
ct = r.headers.get('Content-Type', '')
|
|
555
|
+
if 'json' in ct.lower() or '{' in r.text[:100]:
|
|
556
|
+
self.ctx.add_vulnerability({
|
|
557
|
+
'type': 'Authentication Bypass',
|
|
558
|
+
'severity': 'HIGH',
|
|
559
|
+
'endpoint': path,
|
|
560
|
+
'evidence': f'No auth required, status: {r.status_code}'
|
|
561
|
+
})
|
|
562
|
+
print(f" [!] {path}: 无需认证")
|
|
563
|
+
except:
|
|
564
|
+
pass
|
|
565
|
+
|
|
566
|
+
def _test_graphql(self):
|
|
567
|
+
"""GraphQL 测试"""
|
|
568
|
+
graphql_paths = ['/graphql', '/api/graphql', '/query']
|
|
569
|
+
|
|
570
|
+
for path in graphql_paths:
|
|
571
|
+
url = self.target.rstrip('/') + path
|
|
572
|
+
try:
|
|
573
|
+
r = self.session.post(url, json={'query': '{__schema{types{name}}}'}, timeout=5)
|
|
574
|
+
|
|
575
|
+
if r.status_code == 200 and 'data' in r.text:
|
|
576
|
+
self.ctx.add_vulnerability({
|
|
577
|
+
'type': 'GraphQL Introspection Enabled',
|
|
578
|
+
'severity': 'MEDIUM',
|
|
579
|
+
'endpoint': path,
|
|
580
|
+
'evidence': 'GraphQL schema exposed'
|
|
581
|
+
})
|
|
582
|
+
print(f" [!] {path}: GraphQL 开启 introspection")
|
|
583
|
+
|
|
584
|
+
# 检查 mutation
|
|
585
|
+
r2 = self.session.post(url, json={'query': 'mutation{__typename}'}, timeout=5)
|
|
586
|
+
if r2.status_code == 200:
|
|
587
|
+
print(f" [!] {path}: mutation 可用")
|
|
588
|
+
except:
|
|
589
|
+
pass
|
|
590
|
+
|
|
591
|
+
def _test_brute_force(self):
|
|
592
|
+
"""暴力破解测试"""
|
|
593
|
+
login_paths = ['/login', '/api/login', '/auth/login', '/signin']
|
|
594
|
+
|
|
595
|
+
for path in login_paths:
|
|
596
|
+
url = self.target.rstrip('/') + path
|
|
597
|
+
try:
|
|
598
|
+
# 测试多次登录
|
|
599
|
+
for i in range(3):
|
|
600
|
+
r = self.session.post(url, json={'username': f'test{i}', 'password': 'wrong'}, timeout=5)
|
|
601
|
+
|
|
602
|
+
# 检查是否有 rate limit
|
|
603
|
+
if r.status_code in [200, 400, 401, 403]:
|
|
604
|
+
# 发送大量请求测试
|
|
605
|
+
for i in range(10):
|
|
606
|
+
r = self.session.post(url, json={'username': 'admin', 'password': 'test'}, timeout=5)
|
|
607
|
+
|
|
608
|
+
# 检查响应是否变化
|
|
609
|
+
if r.status_code != 429: # 没有 rate limit
|
|
610
|
+
self.ctx.add_vulnerability({
|
|
611
|
+
'type': 'Brute Force Risk',
|
|
612
|
+
'severity': 'MEDIUM',
|
|
613
|
+
'endpoint': path,
|
|
614
|
+
'evidence': 'No rate limiting detected'
|
|
615
|
+
})
|
|
616
|
+
print(f" [!] {path}: 无暴力破解防护")
|
|
617
|
+
except:
|
|
618
|
+
pass
|
|
619
|
+
|
|
620
|
+
def _test_idor(self):
|
|
621
|
+
"""IDOR 测试"""
|
|
622
|
+
idor_paths = ['/user/1', '/users/1', '/profile/1', '/api/user/1']
|
|
623
|
+
|
|
624
|
+
for path in idor_paths:
|
|
625
|
+
url = self.target.rstrip('/') + path
|
|
626
|
+
try:
|
|
627
|
+
r = self.session.get(url, timeout=5)
|
|
628
|
+
|
|
629
|
+
if r.status_code == 200:
|
|
630
|
+
ct = r.headers.get('Content-Type', '')
|
|
631
|
+
if 'json' in ct.lower():
|
|
632
|
+
self.ctx.add_vulnerability({
|
|
633
|
+
'type': 'Potential IDOR',
|
|
634
|
+
'severity': 'MEDIUM',
|
|
635
|
+
'endpoint': path,
|
|
636
|
+
'evidence': 'Direct object reference without auth check'
|
|
637
|
+
})
|
|
638
|
+
print(f" [!] {path}: 可能的 IDOR")
|
|
639
|
+
break
|
|
640
|
+
except:
|
|
641
|
+
pass
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
class CloudStorageTester:
|
|
645
|
+
"""阶段 3: 云存储安全测试"""
|
|
646
|
+
|
|
647
|
+
def __init__(self, target: str, session):
|
|
648
|
+
self.target = target
|
|
649
|
+
self.session = session
|
|
650
|
+
self.findings = []
|
|
651
|
+
|
|
652
|
+
def run(self) -> List:
|
|
653
|
+
"""执行云存储测试"""
|
|
654
|
+
print("[3] 云存储安全测试")
|
|
655
|
+
print("-" * 70)
|
|
656
|
+
|
|
657
|
+
# 检查云存储特征
|
|
658
|
+
cloud_patterns = [
|
|
659
|
+
('oss', 'aliyun'),
|
|
660
|
+
('cos', 'qcloud'),
|
|
661
|
+
('s3', 'aws'),
|
|
662
|
+
('minio', 'minio'),
|
|
663
|
+
('obs', 'huawei'),
|
|
664
|
+
]
|
|
665
|
+
|
|
666
|
+
for keyword, provider in cloud_patterns:
|
|
667
|
+
try:
|
|
668
|
+
r = self.session.get(self.target, timeout=10)
|
|
669
|
+
|
|
670
|
+
if keyword in r.text.lower():
|
|
671
|
+
self.findings.append({
|
|
672
|
+
'type': f'{provider.upper()} Storage',
|
|
673
|
+
'severity': 'INFO',
|
|
674
|
+
'endpoint': self.target,
|
|
675
|
+
'evidence': f'Cloud storage keyword found: {keyword}'
|
|
676
|
+
})
|
|
677
|
+
print(f" [发现] {provider} 云存储特征")
|
|
678
|
+
|
|
679
|
+
# 检查响应头
|
|
680
|
+
for header in r.headers:
|
|
681
|
+
if keyword in header.lower():
|
|
682
|
+
print(f" [发现] {provider} header: {header}")
|
|
683
|
+
except:
|
|
684
|
+
pass
|
|
685
|
+
|
|
686
|
+
if not self.findings:
|
|
687
|
+
print(" 未发现云存储特征")
|
|
688
|
+
|
|
689
|
+
print()
|
|
690
|
+
return self.findings
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
class ReportGenerator:
|
|
694
|
+
"""阶段 4: 报告生成"""
|
|
695
|
+
|
|
696
|
+
@staticmethod
|
|
697
|
+
def generate(results: Dict) -> str:
|
|
698
|
+
"""生成 Markdown 报告"""
|
|
699
|
+
|
|
700
|
+
report = []
|
|
701
|
+
report.append("# API 安全测试报告")
|
|
702
|
+
report.append("")
|
|
703
|
+
report.append(f"**目标**: {results.get('target', 'N/A')}")
|
|
704
|
+
report.append(f"**时间**: {results.get('timestamp', 'N/A')}")
|
|
705
|
+
report.append(f"**耗时**: {results.get('duration', 0):.1f}s")
|
|
706
|
+
report.append("")
|
|
707
|
+
|
|
708
|
+
# 统计
|
|
709
|
+
report.append("## 发现统计")
|
|
710
|
+
report.append("")
|
|
711
|
+
report.append(f"- API 端点: {len(results.get('endpoints', []))}")
|
|
712
|
+
report.append(f"- 漏洞: {len(results.get('vulnerabilities', []))}")
|
|
713
|
+
report.append(f"- 云存储: {len(results.get('cloud_findings', []))}")
|
|
714
|
+
report.append("")
|
|
715
|
+
|
|
716
|
+
# 技术栈
|
|
717
|
+
if results.get('tech_stack'):
|
|
718
|
+
report.append("## 技术栈")
|
|
719
|
+
report.append("")
|
|
720
|
+
for key, value in results['tech_stack'].items():
|
|
721
|
+
report.append(f"- {key}: {value}")
|
|
722
|
+
report.append("")
|
|
723
|
+
|
|
724
|
+
# 父路径探测结果
|
|
725
|
+
if results.get('parent_paths'):
|
|
726
|
+
parent_paths = results['parent_paths']
|
|
727
|
+
html_fallback = sum(1 for p in parent_paths.values() if not p.get('is_api'))
|
|
728
|
+
real_api = sum(1 for p in parent_paths.values() if p.get('is_api'))
|
|
729
|
+
|
|
730
|
+
report.append("## 父路径分析")
|
|
731
|
+
report.append("")
|
|
732
|
+
report.append(f"- 总父路径: {len(parent_paths)}")
|
|
733
|
+
report.append(f"- HTML fallback: {html_fallback} (nginx fallback 配置)")
|
|
734
|
+
report.append(f"- JSON API: {real_api}")
|
|
735
|
+
report.append("")
|
|
736
|
+
|
|
737
|
+
# 检测 nginx fallback 问题
|
|
738
|
+
if html_fallback > 0 and real_api == 0:
|
|
739
|
+
report.append("### 安全问题: nginx fallback 配置")
|
|
740
|
+
report.append("")
|
|
741
|
+
report.append("**问题**: 所有 API 路径都返回 HTML 而不是 JSON API")
|
|
742
|
+
report.append("")
|
|
743
|
+
report.append("**可能原因**:")
|
|
744
|
+
report.append("1. 后端 API 服务未运行 (端口 667 不可达)")
|
|
745
|
+
report.append("2. nginx 未正确配置 API 路径代理")
|
|
746
|
+
report.append("3. API 服务部署在不同的服务器/端口")
|
|
747
|
+
report.append("")
|
|
748
|
+
report.append("**影响**: 前端无法连接后端 API,系统功能不可用")
|
|
749
|
+
report.append("")
|
|
750
|
+
report.append("**建议**:")
|
|
751
|
+
report.append("1. 检查后端服务是否运行")
|
|
752
|
+
report.append("2. 检查 nginx proxy_pass 配置")
|
|
753
|
+
report.append("3. 检查防火墙/安全组规则")
|
|
754
|
+
report.append("")
|
|
755
|
+
|
|
756
|
+
# 添加为安全问题
|
|
757
|
+
results['vulnerabilities'].insert(0, {
|
|
758
|
+
'type': 'Backend API Unreachable / nginx fallback',
|
|
759
|
+
'severity': 'HIGH',
|
|
760
|
+
'endpoint': 'Multiple paths',
|
|
761
|
+
'evidence': f'{html_fallback} paths return HTML fallback instead of JSON API'
|
|
762
|
+
})
|
|
763
|
+
elif real_api > 0:
|
|
764
|
+
report.append("**状态**: 发现可访问的 JSON API 端点")
|
|
765
|
+
report.append("")
|
|
766
|
+
|
|
767
|
+
# 漏洞详情
|
|
768
|
+
if results.get('vulnerabilities'):
|
|
769
|
+
report.append("## 漏洞详情")
|
|
770
|
+
report.append("")
|
|
771
|
+
|
|
772
|
+
# 按严重程度分组
|
|
773
|
+
severity_order = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW', 'INFO']
|
|
774
|
+
vulns_by_severity = {s: [] for s in severity_order}
|
|
775
|
+
|
|
776
|
+
for v in results['vulnerabilities']:
|
|
777
|
+
sev = v.get('severity', 'INFO').upper()
|
|
778
|
+
if sev in vulns_by_severity:
|
|
779
|
+
vulns_by_severity[sev].append(v)
|
|
780
|
+
else:
|
|
781
|
+
vulns_by_severity['INFO'].append(v)
|
|
782
|
+
|
|
783
|
+
for severity in severity_order:
|
|
784
|
+
vulns = vulns_by_severity[severity]
|
|
785
|
+
if vulns:
|
|
786
|
+
report.append(f"### {severity} ({len(vulns)})")
|
|
787
|
+
report.append("")
|
|
788
|
+
for v in vulns:
|
|
789
|
+
report.append(f"#### {v.get('type', 'Unknown')}")
|
|
790
|
+
report.append(f"- **端点**: {v.get('endpoint', 'N/A')}")
|
|
791
|
+
report.append(f"- **证据**: {v.get('evidence', 'N/A')}")
|
|
792
|
+
if v.get('payload'):
|
|
793
|
+
report.append(f"- **Payload**: `{v.get('payload')}`")
|
|
794
|
+
report.append("")
|
|
795
|
+
|
|
796
|
+
# 云存储发现
|
|
797
|
+
if results.get('cloud_findings'):
|
|
798
|
+
report.append("## 云存储发现")
|
|
799
|
+
report.append("")
|
|
800
|
+
for f in results['cloud_findings']:
|
|
801
|
+
report.append(f"- {f.get('type')}: {f.get('evidence')}")
|
|
802
|
+
report.append("")
|
|
803
|
+
|
|
804
|
+
# 端点列表
|
|
805
|
+
if results.get('endpoints'):
|
|
806
|
+
report.append("## API 端点列表")
|
|
807
|
+
report.append("")
|
|
808
|
+
report.append(f"共发现 {len(results['endpoints'])} 个端点")
|
|
809
|
+
report.append("")
|
|
810
|
+
|
|
811
|
+
for ep in results['endpoints'][:50]:
|
|
812
|
+
report.append(f"- `{ep.get('method', 'GET')}` {ep.get('path', ep.get('url', ''))} ({ep.get('source', '')})")
|
|
813
|
+
|
|
814
|
+
if len(results['endpoints']) > 50:
|
|
815
|
+
report.append(f"- ... 还有 {len(results['endpoints']) - 50} 个端点")
|
|
816
|
+
report.append("")
|
|
817
|
+
|
|
818
|
+
return "\n".join(report)
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
def run_skill(target: str) -> Dict:
|
|
822
|
+
"""
|
|
823
|
+
执行完整的 SKILL.md 测试流程
|
|
824
|
+
|
|
825
|
+
使用 TestContext 在模块间共享数据,实现模块联动
|
|
826
|
+
"""
|
|
827
|
+
print("=" * 70)
|
|
828
|
+
print(" API Security Testing Skill")
|
|
829
|
+
print("=" * 70)
|
|
830
|
+
print()
|
|
831
|
+
|
|
832
|
+
# 创建测试上下文
|
|
833
|
+
ctx = TestContext(target=target)
|
|
834
|
+
|
|
835
|
+
# 阶段 0: 前置检查
|
|
836
|
+
if not PrerequisiteChecker.check(ctx):
|
|
837
|
+
print("[FATAL] 前置检查失败")
|
|
838
|
+
return {'error': '前置检查失败', 'target': target}
|
|
839
|
+
|
|
840
|
+
start_time = time.time()
|
|
841
|
+
|
|
842
|
+
# 阶段 1: 资产发现 (联动)
|
|
843
|
+
discovery = AssetDiscovery(ctx)
|
|
844
|
+
discovery.run()
|
|
845
|
+
|
|
846
|
+
# 阶段 2: 漏洞分析
|
|
847
|
+
print("[2] 漏洞分析")
|
|
848
|
+
print("-" * 70)
|
|
849
|
+
tester = VulnerabilityTester(ctx)
|
|
850
|
+
tester.run()
|
|
851
|
+
|
|
852
|
+
# 阶段 2.5: Fuzzing
|
|
853
|
+
print("[2.5] API Fuzzing")
|
|
854
|
+
print("-" * 70)
|
|
855
|
+
_run_fuzzing(ctx)
|
|
856
|
+
|
|
857
|
+
# 阶段 3: 云存储测试
|
|
858
|
+
print("[3] 云存储测试")
|
|
859
|
+
print("-" * 70)
|
|
860
|
+
cloud_tester = CloudStorageTester(ctx.target, ctx.session)
|
|
861
|
+
ctx.cloud_findings = cloud_tester.run()
|
|
862
|
+
|
|
863
|
+
# 更新上下文时间
|
|
864
|
+
ctx.duration = time.time() - start_time
|
|
865
|
+
|
|
866
|
+
# 阶段 4: 报告生成
|
|
867
|
+
print("[4] 生成报告")
|
|
868
|
+
print("-" * 70)
|
|
869
|
+
|
|
870
|
+
report = ReportGenerator.generate(ctx.__dict__)
|
|
871
|
+
print(report)
|
|
872
|
+
|
|
873
|
+
report_file = f"security_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.md"
|
|
874
|
+
with open(report_file, 'w', encoding='utf-8') as f:
|
|
875
|
+
f.write(report)
|
|
876
|
+
|
|
877
|
+
print(f"\n报告已保存: {report_file}")
|
|
878
|
+
|
|
879
|
+
return vars(ctx)
|
|
880
|
+
|
|
881
|
+
|
|
882
|
+
def _run_fuzzing(ctx: TestContext):
|
|
883
|
+
"""执行 Fuzzing(使用上下文)"""
|
|
884
|
+
try:
|
|
885
|
+
from core.api_parser import APIFuzzer, ParsedEndpoint, APIParam, ParamType, ParamLocation
|
|
886
|
+
|
|
887
|
+
parsed_eps = []
|
|
888
|
+
for ep_data in ctx.get_all_endpoints():
|
|
889
|
+
ep = ParsedEndpoint(
|
|
890
|
+
path=ep_data.get('path', ''),
|
|
891
|
+
method=ep_data.get('method', 'GET'),
|
|
892
|
+
source=ep_data.get('source', ''),
|
|
893
|
+
semantic_type=ep_data.get('semantic_type', ''),
|
|
894
|
+
)
|
|
895
|
+
params = ep_data.get('params', {})
|
|
896
|
+
if isinstance(params, dict):
|
|
897
|
+
for p_name, p_val in params.items():
|
|
898
|
+
ep.params.append(APIParam(
|
|
899
|
+
name=p_name,
|
|
900
|
+
param_type=ParamType.QUERY,
|
|
901
|
+
location=ParamLocation.URL,
|
|
902
|
+
required=False,
|
|
903
|
+
))
|
|
904
|
+
elif isinstance(params, list):
|
|
905
|
+
for p in params:
|
|
906
|
+
if isinstance(p, dict) and 'name' in p:
|
|
907
|
+
ep.params.append(APIParam(
|
|
908
|
+
name=p['name'],
|
|
909
|
+
param_type=ParamType.QUERY,
|
|
910
|
+
location=ParamLocation.URL,
|
|
911
|
+
required=p.get('required', False),
|
|
912
|
+
))
|
|
913
|
+
parsed_eps.append(ep)
|
|
914
|
+
|
|
915
|
+
fuzzer = APIFuzzer(ctx.target, ctx.session)
|
|
916
|
+
fuzz_results = fuzzer.fuzz_endpoints(parsed_eps, ctx.parent_paths)
|
|
917
|
+
|
|
918
|
+
for vuln in fuzz_results:
|
|
919
|
+
ctx.add_vulnerability(vuln)
|
|
920
|
+
|
|
921
|
+
print(f" [Fuzzing] 发现 {len(fuzz_results)} 个问题")
|
|
922
|
+
|
|
923
|
+
except Exception as e:
|
|
924
|
+
print(f" [WARN] Fuzzing 失败: {e}")
|
|
925
|
+
|
|
926
|
+
|
|
927
|
+
if __name__ == "__main__":
|
|
928
|
+
import argparse
|
|
929
|
+
|
|
930
|
+
parser = argparse.ArgumentParser(description='API Security Testing Skill')
|
|
931
|
+
parser.add_argument('target', help='目标 URL')
|
|
932
|
+
args = parser.parse_args()
|
|
933
|
+
|
|
934
|
+
target = args.target
|
|
935
|
+
if not target.startswith('http'):
|
|
936
|
+
target = 'http://' + target
|
|
937
|
+
|
|
938
|
+
run_skill(target)
|