opencode-api-security-testing 2.1.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/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/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,599 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
ScanEngine - 统一扫描引擎
|
|
4
|
+
提供 Collector → Analyzer → Tester 的三阶段 Pipeline 架构
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import time
|
|
9
|
+
import logging
|
|
10
|
+
from typing import Dict, List, Optional, Set, Any, Callable
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from enum import Enum
|
|
14
|
+
from urllib.parse import urlparse
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
import requests
|
|
18
|
+
HAS_REQUESTS = True
|
|
19
|
+
except ImportError:
|
|
20
|
+
HAS_REQUESTS = False
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
from .collectors import JSCollector, ApiPathFinder, URLCollector, BrowserCollectorFacade, JSFingerprintCache
|
|
24
|
+
except ImportError:
|
|
25
|
+
from collectors import JSCollector, ApiPathFinder, URLCollector, BrowserCollectorFacade, JSFingerprintCache
|
|
26
|
+
from .models import APIEndpoint, Vulnerability, ScanResult, Severity
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ScanStage(Enum):
|
|
32
|
+
"""扫描阶段"""
|
|
33
|
+
COLLECT = "collect"
|
|
34
|
+
ANALYZE = "analyze"
|
|
35
|
+
TEST = "test"
|
|
36
|
+
REPORT = "report"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class ScanEngineConfig:
|
|
41
|
+
"""扫描引擎配置"""
|
|
42
|
+
target: str
|
|
43
|
+
concurrency: int = 50
|
|
44
|
+
timeout: int = 30
|
|
45
|
+
js_depth: int = 3
|
|
46
|
+
cookies: str = ""
|
|
47
|
+
proxy: Optional[str] = None
|
|
48
|
+
verify_ssl: bool = True
|
|
49
|
+
output_dir: str = "./results"
|
|
50
|
+
|
|
51
|
+
# 各阶段开关
|
|
52
|
+
enable_js_collect: bool = True
|
|
53
|
+
enable_api_collect: bool = True
|
|
54
|
+
enable_browser_collect: bool = True
|
|
55
|
+
enable_sqli_test: bool = True
|
|
56
|
+
enable_xss_test: bool = True
|
|
57
|
+
enable_idor_test: bool = True
|
|
58
|
+
enable_info_test: bool = True
|
|
59
|
+
|
|
60
|
+
# 评分阈值
|
|
61
|
+
high_value_threshold: int = 5
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class ScanProgress:
|
|
66
|
+
"""扫描进度"""
|
|
67
|
+
stage: ScanStage
|
|
68
|
+
phase: str
|
|
69
|
+
total_apis: int = 0
|
|
70
|
+
alive_apis: int = 0
|
|
71
|
+
high_value_apis: int = 0
|
|
72
|
+
vulnerabilities: int = 0
|
|
73
|
+
start_time: float = field(default_factory=time.time)
|
|
74
|
+
|
|
75
|
+
def elapsed(self) -> float:
|
|
76
|
+
return time.time() - self.start_time
|
|
77
|
+
|
|
78
|
+
def to_dict(self) -> Dict:
|
|
79
|
+
return {
|
|
80
|
+
'stage': self.stage.value,
|
|
81
|
+
'phase': self.phase,
|
|
82
|
+
'total_apis': self.total_apis,
|
|
83
|
+
'alive_apis': self.alive_apis,
|
|
84
|
+
'high_value_apis': self.high_value_apis,
|
|
85
|
+
'vulnerabilities': self.vulnerabilities,
|
|
86
|
+
'elapsed_seconds': round(self.elapsed(), 2)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class ScanEngine:
|
|
91
|
+
"""
|
|
92
|
+
统一扫描引擎
|
|
93
|
+
|
|
94
|
+
三阶段 Pipeline:
|
|
95
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
96
|
+
│ Stage 1: COLLECT (采集) │
|
|
97
|
+
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐│
|
|
98
|
+
│ │ JS采集 │→ │ API发现 │→ │ URL采集 │→ │ 浏览器采集││
|
|
99
|
+
│ │ (递归深度3)│ │ (25+正则) │ │ (域名/Base│ │ (动态渲染││
|
|
100
|
+
│ └───────────┘ └───────────┘ └───────────┘ └───────────┘│
|
|
101
|
+
└─────────────────────────────────────────────────────────────┘
|
|
102
|
+
↓
|
|
103
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
104
|
+
│ Stage 2: ANALYZE (分析) │
|
|
105
|
+
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
|
|
106
|
+
│ │ HTTP探测 │→ │ API评分 │→ │ 敏感信息 │ │
|
|
107
|
+
│ │ (验证存活)│ │ (高价值) │ │ 检测 │ │
|
|
108
|
+
│ └───────────┘ └───────────┘ └───────────┘ │
|
|
109
|
+
└─────────────────────────────────────────────────────────────┘
|
|
110
|
+
↓
|
|
111
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
112
|
+
│ Stage 3: TEST (测试) │
|
|
113
|
+
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐│
|
|
114
|
+
│ │ SQL注入 │→ │ XSS测试 │→ │ IDOR测试 │→ │ 信息泄露 ││
|
|
115
|
+
│ └───────────┘ └───────────┘ └───────────┘ └───────────┘│
|
|
116
|
+
└─────────────────────────────────────────────────────────────┘
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
def __init__(self, config: ScanEngineConfig):
|
|
120
|
+
self.config = config
|
|
121
|
+
self.session = requests.Session() if HAS_REQUESTS else None
|
|
122
|
+
|
|
123
|
+
self._js_cache: Optional[JSFingerprintCache] = None
|
|
124
|
+
self._js_collector: Optional[JSCollector] = None
|
|
125
|
+
self._api_finder: Optional[ApiPathFinder] = None
|
|
126
|
+
self._url_collector: Optional[URLCollector] = None
|
|
127
|
+
self._browser_collector: Optional[BrowserCollectorFacade] = None
|
|
128
|
+
|
|
129
|
+
self.progress = ScanProgress(stage=ScanStage.COLLECT, phase="init")
|
|
130
|
+
self.result: Optional[ScanResult] = None
|
|
131
|
+
|
|
132
|
+
self._callbacks: Dict[str, List[Callable]] = {
|
|
133
|
+
'stage_start': [],
|
|
134
|
+
'stage_progress': [],
|
|
135
|
+
'stage_complete': [],
|
|
136
|
+
'finding': [],
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
self._running = False
|
|
140
|
+
|
|
141
|
+
def on(self, event: str, callback: Callable):
|
|
142
|
+
"""注册事件回调"""
|
|
143
|
+
if event in self._callbacks:
|
|
144
|
+
self._callbacks[event].append(callback)
|
|
145
|
+
|
|
146
|
+
def _emit(self, event: str, data: Any):
|
|
147
|
+
"""触发事件"""
|
|
148
|
+
for callback in self._callbacks.get(event, []):
|
|
149
|
+
try:
|
|
150
|
+
callback(data)
|
|
151
|
+
except Exception as e:
|
|
152
|
+
logger.warning(f"Callback error for {event}: {e}")
|
|
153
|
+
|
|
154
|
+
def _update_progress(self, stage: ScanStage, phase: str, **kwargs):
|
|
155
|
+
"""更新进度"""
|
|
156
|
+
self.progress.stage = stage
|
|
157
|
+
self.progress.phase = phase
|
|
158
|
+
for key, value in kwargs.items():
|
|
159
|
+
if hasattr(self.progress, key):
|
|
160
|
+
setattr(self.progress, key, value)
|
|
161
|
+
self._emit('stage_progress', self.progress.to_dict())
|
|
162
|
+
|
|
163
|
+
async def initialize(self):
|
|
164
|
+
"""初始化扫描引擎"""
|
|
165
|
+
self._running = True
|
|
166
|
+
|
|
167
|
+
if self.session:
|
|
168
|
+
self.session.headers.update({
|
|
169
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
|
170
|
+
})
|
|
171
|
+
if self.config.cookies:
|
|
172
|
+
self.session.headers['Cookie'] = self.config.cookies
|
|
173
|
+
|
|
174
|
+
self._js_collector = JSCollector(session=self.session, max_depth=self.config.js_depth)
|
|
175
|
+
self._api_finder = ApiPathFinder()
|
|
176
|
+
self._url_collector = URLCollector(session=self.session)
|
|
177
|
+
self._browser_collector = BrowserCollectorFacade(headless=True)
|
|
178
|
+
|
|
179
|
+
self.result = ScanResult(
|
|
180
|
+
target_url=self.config.target,
|
|
181
|
+
start_time=datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
async def run(self) -> ScanResult:
|
|
185
|
+
"""运行完整扫描流程"""
|
|
186
|
+
await self.initialize()
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
# Stage 1: 采集
|
|
190
|
+
self._emit('stage_start', {'stage': 'collect'})
|
|
191
|
+
self._update_progress(ScanStage.COLLECT, 'js_collection')
|
|
192
|
+
await self._run_collectors()
|
|
193
|
+
self._emit('stage_complete', {'stage': 'collect'})
|
|
194
|
+
|
|
195
|
+
# Stage 2: 分析
|
|
196
|
+
self._emit('stage_start', {'stage': 'analyze'})
|
|
197
|
+
self._update_progress(ScanStage.ANALYZE, 'api_scoring')
|
|
198
|
+
await self._run_analyzers()
|
|
199
|
+
self._emit('stage_complete', {'stage': 'analyze'})
|
|
200
|
+
|
|
201
|
+
# Stage 3: 测试
|
|
202
|
+
if self.result and self.result.api_endpoints:
|
|
203
|
+
self._emit('stage_start', {'stage': 'test'})
|
|
204
|
+
self._update_progress(ScanStage.TEST, 'vulnerability_testing')
|
|
205
|
+
await self._run_testers()
|
|
206
|
+
self._emit('stage_complete', {'stage': 'test'})
|
|
207
|
+
|
|
208
|
+
# Stage 4: 报告
|
|
209
|
+
self._emit('stage_start', {'stage': 'report'})
|
|
210
|
+
self._update_progress(ScanStage.REPORT, 'report_generation')
|
|
211
|
+
self._emit('stage_complete', {'stage': 'report'})
|
|
212
|
+
|
|
213
|
+
if self.result:
|
|
214
|
+
self.result.status = "completed"
|
|
215
|
+
|
|
216
|
+
except Exception as e:
|
|
217
|
+
logger.error(f"Scan error: {e}")
|
|
218
|
+
if self.result:
|
|
219
|
+
self.result.errors.append(str(e))
|
|
220
|
+
self.result.status = "failed"
|
|
221
|
+
|
|
222
|
+
return self.result or ScanResult(target_url=self.config.target)
|
|
223
|
+
|
|
224
|
+
async def _run_collectors(self):
|
|
225
|
+
"""运行采集阶段"""
|
|
226
|
+
collected_js = []
|
|
227
|
+
collected_apis = []
|
|
228
|
+
|
|
229
|
+
try:
|
|
230
|
+
resp = self.session.get(self.config.target, timeout=self.config.timeout)
|
|
231
|
+
html = resp.text
|
|
232
|
+
except Exception as e:
|
|
233
|
+
logger.error(f"Failed to fetch target: {e}")
|
|
234
|
+
html = ""
|
|
235
|
+
|
|
236
|
+
# JS 采集
|
|
237
|
+
if self.config.enable_js_collect and html:
|
|
238
|
+
self._update_progress(ScanStage.COLLECT, phase='js_collect')
|
|
239
|
+
|
|
240
|
+
js_urls = self._js_collector.extract_js_from_html(html, self.config.target)
|
|
241
|
+
logger.info(f"Found {len(js_urls)} JS files in HTML")
|
|
242
|
+
|
|
243
|
+
for js_url in js_urls[:20]:
|
|
244
|
+
try:
|
|
245
|
+
js_resp = self.session.get(js_url, timeout=self.config.timeout)
|
|
246
|
+
if js_resp.status_code == 200:
|
|
247
|
+
content = js_resp.text
|
|
248
|
+
collected_js.append({'url': js_url, 'content': content})
|
|
249
|
+
|
|
250
|
+
apis = self._api_finder.find_api_paths_in_text(content, js_url)
|
|
251
|
+
collected_apis.extend(apis)
|
|
252
|
+
|
|
253
|
+
self._js_collector.parse_js_content(js_url, content)
|
|
254
|
+
except Exception as e:
|
|
255
|
+
logger.debug(f"JS fetch error for {js_url}: {e}")
|
|
256
|
+
|
|
257
|
+
logger.info(f"JS collection: {len(collected_js)} files, {len(collected_apis)} APIs")
|
|
258
|
+
|
|
259
|
+
# URL 采集
|
|
260
|
+
if self.config.enable_url_collect and html:
|
|
261
|
+
self._update_progress(ScanStage.COLLECT, phase='url_collect')
|
|
262
|
+
|
|
263
|
+
url_result = self._url_collector.collect_from_html(html, self.config.target)
|
|
264
|
+
logger.info(f"URL collection: domains={len(url_result.domains)}, static={len(url_result.static_urls)}")
|
|
265
|
+
|
|
266
|
+
# 浏览器动态采集
|
|
267
|
+
if self.config.enable_browser_collect:
|
|
268
|
+
self._update_progress(ScanStage.COLLECT, phase='browser_collect')
|
|
269
|
+
|
|
270
|
+
try:
|
|
271
|
+
browser_result = self._browser_collector.collect_all(self.config.target, {
|
|
272
|
+
'capture_console': True,
|
|
273
|
+
'capture_storage': True
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
for js_url in browser_result.get('js_urls', []):
|
|
277
|
+
if js_url not in [j['url'] for j in collected_js]:
|
|
278
|
+
try:
|
|
279
|
+
js_resp = self.session.get(js_url, timeout=self.config.timeout)
|
|
280
|
+
if js_resp.status_code == 200:
|
|
281
|
+
collected_js.append({'url': js_url, 'content': js_resp.text})
|
|
282
|
+
except:
|
|
283
|
+
pass
|
|
284
|
+
|
|
285
|
+
for req in browser_result.get('api_requests', []):
|
|
286
|
+
if req.get('method') in ['POST', 'GET', 'PUT', 'DELETE']:
|
|
287
|
+
collected_apis.append({
|
|
288
|
+
'path': req['url'],
|
|
289
|
+
'method': req['method'],
|
|
290
|
+
'source': 'browser'
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
logger.info(f"Browser collection: {len(browser_result.get('js_urls', []))} JS, {len(browser_result.get('api_requests', []))} API requests")
|
|
294
|
+
except Exception as e:
|
|
295
|
+
logger.warning(f"Browser collection failed: {e}")
|
|
296
|
+
|
|
297
|
+
# 存储采集结果
|
|
298
|
+
if not hasattr(self.result, 'collector_data'):
|
|
299
|
+
self.result.collector_data = {}
|
|
300
|
+
|
|
301
|
+
self.result.collector_data['js_files'] = collected_js
|
|
302
|
+
self.result.collector_data['api_paths'] = collected_apis
|
|
303
|
+
self.result.collector_data['js_cache'] = self._js_collector.cache
|
|
304
|
+
|
|
305
|
+
self._update_progress(
|
|
306
|
+
ScanStage.COLLECT,
|
|
307
|
+
phase='complete',
|
|
308
|
+
total_apis=len(collected_apis)
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
async def _run_analyzers(self):
|
|
312
|
+
"""运行分析阶段"""
|
|
313
|
+
if not self.result or 'api_paths' not in self.result.collector_data:
|
|
314
|
+
return
|
|
315
|
+
|
|
316
|
+
apis = self.result.collector_data['api_paths']
|
|
317
|
+
api_endpoints: List[APIEndpoint] = []
|
|
318
|
+
alive_apis = []
|
|
319
|
+
high_value_apis = []
|
|
320
|
+
|
|
321
|
+
self._update_progress(ScanStage.ANALYZE, phase='http_probe')
|
|
322
|
+
|
|
323
|
+
seen_paths = set()
|
|
324
|
+
for api in apis:
|
|
325
|
+
path = api.get('path', '') or api.get('url', '')
|
|
326
|
+
method = api.get('method', 'GET')
|
|
327
|
+
|
|
328
|
+
if not path or path in seen_paths:
|
|
329
|
+
continue
|
|
330
|
+
seen_paths.add(path)
|
|
331
|
+
|
|
332
|
+
full_url = path if path.startswith('http') else f"{self.config.target.rstrip('/')}{path}"
|
|
333
|
+
|
|
334
|
+
endpoint = APIEndpoint(
|
|
335
|
+
path=path,
|
|
336
|
+
method=method,
|
|
337
|
+
source=api.get('source', 'unknown'),
|
|
338
|
+
full_url=full_url
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
score = self._score_endpoint(endpoint)
|
|
342
|
+
endpoint.score = score
|
|
343
|
+
endpoint.is_high_value = score >= self.config.high_value_threshold
|
|
344
|
+
|
|
345
|
+
api_endpoints.append(endpoint)
|
|
346
|
+
|
|
347
|
+
if self._probe_endpoint(endpoint):
|
|
348
|
+
alive_apis.append(endpoint)
|
|
349
|
+
if endpoint.is_high_value:
|
|
350
|
+
high_value_apis.append(endpoint)
|
|
351
|
+
|
|
352
|
+
self.result.api_endpoints = api_endpoints
|
|
353
|
+
self.result.alive_apis = len(alive_apis)
|
|
354
|
+
self.result.high_value_apis = len(high_value_apis)
|
|
355
|
+
self.result.total_apis = len(api_endpoints)
|
|
356
|
+
|
|
357
|
+
logger.info(f"Analysis: {len(api_endpoints)} total, {len(alive_apis)} alive, {len(high_value_apis)} high-value")
|
|
358
|
+
|
|
359
|
+
self._update_progress(
|
|
360
|
+
ScanStage.ANALYZE,
|
|
361
|
+
phase='complete',
|
|
362
|
+
total_apis=len(api_endpoints),
|
|
363
|
+
alive_apis=len(alive_apis),
|
|
364
|
+
high_value_apis=len(high_value_apis)
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
def _score_endpoint(self, endpoint: APIEndpoint) -> int:
|
|
368
|
+
"""
|
|
369
|
+
API 评分算法
|
|
370
|
+
|
|
371
|
+
评分维度:
|
|
372
|
+
- 路径特征: 包含 admin/user/auth 等关键字 (+3分)
|
|
373
|
+
- HTTP方法: POST/PUT/DELETE (+2分), GET (+1分)
|
|
374
|
+
- 参数数量: 有参数 (+1分)
|
|
375
|
+
- 敏感关键字: 包含 token/key/secret/password (+5分)
|
|
376
|
+
"""
|
|
377
|
+
score = 0
|
|
378
|
+
path_lower = endpoint.path.lower()
|
|
379
|
+
|
|
380
|
+
if any(k in path_lower for k in ['admin', 'user', 'auth', 'login', 'pass', 'token']):
|
|
381
|
+
score += 3
|
|
382
|
+
|
|
383
|
+
if endpoint.method in ['POST', 'PUT', 'DELETE']:
|
|
384
|
+
score += 2
|
|
385
|
+
elif endpoint.method == 'GET':
|
|
386
|
+
score += 1
|
|
387
|
+
|
|
388
|
+
if endpoint.parameters:
|
|
389
|
+
score += 1
|
|
390
|
+
|
|
391
|
+
sensitive_keywords = ['token', 'key', 'secret', 'password', 'jwt', 'bearer', 'auth']
|
|
392
|
+
if any(k in path_lower for k in sensitive_keywords):
|
|
393
|
+
score += 5
|
|
394
|
+
|
|
395
|
+
return score
|
|
396
|
+
|
|
397
|
+
def _probe_endpoint(self, endpoint: APIEndpoint) -> bool:
|
|
398
|
+
"""探测端点是否存活"""
|
|
399
|
+
try:
|
|
400
|
+
resp = self.session.request(
|
|
401
|
+
endpoint.method,
|
|
402
|
+
endpoint.full_url,
|
|
403
|
+
timeout=self.config.timeout,
|
|
404
|
+
allow_redirects=False
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
endpoint.status_code = resp.status_code
|
|
408
|
+
endpoint.content_length = len(resp.content)
|
|
409
|
+
|
|
410
|
+
return resp.status_code < 500
|
|
411
|
+
except Exception as e:
|
|
412
|
+
logger.debug(f"Probe failed for {endpoint.path}: {e}")
|
|
413
|
+
return False
|
|
414
|
+
|
|
415
|
+
async def _run_testers(self):
|
|
416
|
+
"""运行测试阶段"""
|
|
417
|
+
if not self.result or not self.result.api_endpoints:
|
|
418
|
+
return
|
|
419
|
+
|
|
420
|
+
vulnerabilities: List[Vulnerability] = []
|
|
421
|
+
|
|
422
|
+
high_value_endpoints = [ep for ep in self.result.api_endpoints if ep.is_high_value]
|
|
423
|
+
test_targets = high_value_endpoints[:50]
|
|
424
|
+
|
|
425
|
+
self._update_progress(ScanStage.TEST, phase='sqli_test', vulnerabilities=len(vulnerabilities))
|
|
426
|
+
|
|
427
|
+
if self.config.enable_sqli_test:
|
|
428
|
+
for endpoint in test_targets:
|
|
429
|
+
vulns = await self._test_sqli(endpoint)
|
|
430
|
+
vulnerabilities.extend(vulns)
|
|
431
|
+
|
|
432
|
+
self._update_progress(ScanStage.TEST, phase='xss_test', vulnerabilities=len(vulnerabilities))
|
|
433
|
+
|
|
434
|
+
if self.config.enable_xss_test:
|
|
435
|
+
for endpoint in test_targets:
|
|
436
|
+
vulns = await self._test_xss(endpoint)
|
|
437
|
+
vulnerabilities.extend(vulns)
|
|
438
|
+
|
|
439
|
+
if self.config.enable_idor_test:
|
|
440
|
+
for endpoint in test_targets:
|
|
441
|
+
vulns = await self._test_idor(endpoint)
|
|
442
|
+
vulnerabilities.extend(vulns)
|
|
443
|
+
|
|
444
|
+
if self.config.enable_info_test:
|
|
445
|
+
for endpoint in test_targets:
|
|
446
|
+
vulns = await self._test_info_disclosure(endpoint)
|
|
447
|
+
vulnerabilities.extend(vulns)
|
|
448
|
+
|
|
449
|
+
self.result.vulnerabilities = vulnerabilities
|
|
450
|
+
|
|
451
|
+
self._update_progress(
|
|
452
|
+
ScanStage.TEST,
|
|
453
|
+
phase='complete',
|
|
454
|
+
vulnerabilities=len(vulnerabilities)
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
logger.info(f"Testing: {len(vulnerabilities)} vulnerabilities found")
|
|
458
|
+
|
|
459
|
+
async def _test_sqli(self, endpoint: APIEndpoint) -> List[Vulnerability]:
|
|
460
|
+
"""SQL 注入测试"""
|
|
461
|
+
vulns = []
|
|
462
|
+
|
|
463
|
+
sqli_payloads = [
|
|
464
|
+
"' OR '1'='1",
|
|
465
|
+
"' OR 1=1--",
|
|
466
|
+
"admin'--",
|
|
467
|
+
"' UNION SELECT NULL--",
|
|
468
|
+
"' AND SLEEP(3)--",
|
|
469
|
+
]
|
|
470
|
+
|
|
471
|
+
for payload in sqli_payloads:
|
|
472
|
+
try:
|
|
473
|
+
test_url = f"{endpoint.full_url}?id={payload}" if endpoint.method == 'GET' else endpoint.full_url
|
|
474
|
+
|
|
475
|
+
resp = self.session.post(
|
|
476
|
+
test_url,
|
|
477
|
+
data={'id': payload} if endpoint.method == 'POST' else None,
|
|
478
|
+
timeout=self.config.timeout
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
text_lower = resp.text.lower()
|
|
482
|
+
if any(s in text_lower for s in ['sql', 'syntax', 'error', 'mysql', 'oracle', 'warning']):
|
|
483
|
+
vulns.append(Vulnerability(
|
|
484
|
+
vuln_type='SQL Injection',
|
|
485
|
+
severity=Severity.HIGH,
|
|
486
|
+
endpoint=endpoint.path,
|
|
487
|
+
method=endpoint.method,
|
|
488
|
+
payload=payload,
|
|
489
|
+
evidence=f"SQL error detected in response"
|
|
490
|
+
))
|
|
491
|
+
break
|
|
492
|
+
except:
|
|
493
|
+
pass
|
|
494
|
+
|
|
495
|
+
return vulns
|
|
496
|
+
|
|
497
|
+
async def _test_xss(self, endpoint: APIEndpoint) -> List[Vulnerability]:
|
|
498
|
+
"""XSS 测试"""
|
|
499
|
+
vulns = []
|
|
500
|
+
|
|
501
|
+
xss_payloads = [
|
|
502
|
+
"<script>alert(1)</script>",
|
|
503
|
+
"<img src=x onerror=alert(1)>",
|
|
504
|
+
"<svg onload=alert(1)>",
|
|
505
|
+
"javascript:alert(1)",
|
|
506
|
+
]
|
|
507
|
+
|
|
508
|
+
for payload in xss_payloads:
|
|
509
|
+
try:
|
|
510
|
+
test_url = f"{endpoint.full_url}?q={payload}"
|
|
511
|
+
|
|
512
|
+
resp = self.session.get(test_url, timeout=self.config.timeout)
|
|
513
|
+
|
|
514
|
+
if payload in resp.text:
|
|
515
|
+
vulns.append(Vulnerability(
|
|
516
|
+
vuln_type='XSS',
|
|
517
|
+
severity=Severity.MEDIUM,
|
|
518
|
+
endpoint=endpoint.path,
|
|
519
|
+
method=endpoint.method,
|
|
520
|
+
payload=payload,
|
|
521
|
+
evidence=f"Payload reflected in response"
|
|
522
|
+
))
|
|
523
|
+
break
|
|
524
|
+
except:
|
|
525
|
+
pass
|
|
526
|
+
|
|
527
|
+
return vulns
|
|
528
|
+
|
|
529
|
+
async def _test_idor(self, endpoint: APIEndpoint) -> List[Vulnerability]:
|
|
530
|
+
"""IDOR 测试"""
|
|
531
|
+
vulns = []
|
|
532
|
+
|
|
533
|
+
if endpoint.method != 'GET' or 'id' not in endpoint.path.lower():
|
|
534
|
+
return vulns
|
|
535
|
+
|
|
536
|
+
try:
|
|
537
|
+
resp1 = self.session.get(endpoint.full_url, timeout=self.config.timeout)
|
|
538
|
+
resp2 = self.session.get(f"{endpoint.full_url}?id=99999", timeout=self.config.timeout)
|
|
539
|
+
|
|
540
|
+
if resp1.status_code == resp2.status_code and len(resp1.content) != len(resp2.content):
|
|
541
|
+
vulns.append(Vulnerability(
|
|
542
|
+
vuln_type='IDOR',
|
|
543
|
+
severity=Severity.HIGH,
|
|
544
|
+
endpoint=endpoint.path,
|
|
545
|
+
method=endpoint.method,
|
|
546
|
+
payload='id=99999',
|
|
547
|
+
evidence=f"Different response for different ID values"
|
|
548
|
+
))
|
|
549
|
+
except:
|
|
550
|
+
pass
|
|
551
|
+
|
|
552
|
+
return vulns
|
|
553
|
+
|
|
554
|
+
async def _test_info_disclosure(self, endpoint: APIEndpoint) -> List[Vulnerability]:
|
|
555
|
+
"""信息泄露测试"""
|
|
556
|
+
vulns = []
|
|
557
|
+
|
|
558
|
+
sensitive_patterns = [
|
|
559
|
+
(r'AKIA[0-9A-Z]{16}', 'AWS Access Key'),
|
|
560
|
+
(r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}', 'Email'),
|
|
561
|
+
(r'"password"\s*:\s*"[^"]+"', 'Password in response'),
|
|
562
|
+
(r'"token"\s*:\s*"[^"]+"', 'Token in response'),
|
|
563
|
+
(r'"secret"\s*:\s*"[^"]+"', 'Secret in response'),
|
|
564
|
+
]
|
|
565
|
+
|
|
566
|
+
try:
|
|
567
|
+
resp = self.session.get(endpoint.full_url, timeout=self.config.timeout)
|
|
568
|
+
|
|
569
|
+
for pattern, info_type in sensitive_patterns:
|
|
570
|
+
import re
|
|
571
|
+
matches = re.findall(pattern, resp.text, re.IGNORECASE)
|
|
572
|
+
if matches:
|
|
573
|
+
vulns.append(Vulnerability(
|
|
574
|
+
vuln_type='Information Disclosure',
|
|
575
|
+
severity=Severity.LOW,
|
|
576
|
+
endpoint=endpoint.path,
|
|
577
|
+
method=endpoint.method,
|
|
578
|
+
payload='',
|
|
579
|
+
evidence=f"{info_type} found in response"
|
|
580
|
+
))
|
|
581
|
+
except:
|
|
582
|
+
pass
|
|
583
|
+
|
|
584
|
+
return vulns
|
|
585
|
+
|
|
586
|
+
async def cleanup(self):
|
|
587
|
+
"""清理资源"""
|
|
588
|
+
self._running = False
|
|
589
|
+
if self._browser_collector:
|
|
590
|
+
try:
|
|
591
|
+
self._browser_collector.collector.stop()
|
|
592
|
+
except:
|
|
593
|
+
pass
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def create_scan_engine(target: str, **kwargs) -> ScanEngine:
|
|
597
|
+
"""创建扫描引擎的工厂函数"""
|
|
598
|
+
config = ScanEngineConfig(target=target, **kwargs)
|
|
599
|
+
return ScanEngine(config)
|