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
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
"""
|
|
2
|
+
漏洞验证器 - 多维度验证
|
|
3
|
+
确认发现的漏洞是否真实,排除误报
|
|
4
|
+
|
|
5
|
+
验证维度:
|
|
6
|
+
1. 响应类型维度:JSON vs HTML vs Empty vs Redirect
|
|
7
|
+
2. 状态码维度:200 vs 4xx vs 5xx
|
|
8
|
+
3. 响应长度维度:长度变化检测
|
|
9
|
+
4. WAF拦截维度:WAF/安全设备拦截检测
|
|
10
|
+
5. 敏感信息维度:敏感字段泄露检测
|
|
11
|
+
6. SQL注入维度:SQL错误特征检测
|
|
12
|
+
7. IDOR维度:用户数据越权检测
|
|
13
|
+
8. 一致性维度:多次请求响应一致性检测
|
|
14
|
+
9. 时间维度:时间盲注检测
|
|
15
|
+
10. 业务数据维度:业务数据真实性检测
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import requests
|
|
19
|
+
import json
|
|
20
|
+
import time
|
|
21
|
+
import re
|
|
22
|
+
|
|
23
|
+
requests.packages.urllib3.disable_warnings()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def vuln_verifier(config):
|
|
27
|
+
"""
|
|
28
|
+
多维度漏洞验证
|
|
29
|
+
|
|
30
|
+
输入:
|
|
31
|
+
type: "sqli" | "idor" | "auth_bypass" | "info_leak"
|
|
32
|
+
original_request: object
|
|
33
|
+
suspicious_response: object
|
|
34
|
+
baseline_response?: object - 基线响应(正常请求的响应)
|
|
35
|
+
|
|
36
|
+
输出:
|
|
37
|
+
verified: boolean
|
|
38
|
+
is_false_positive: boolean
|
|
39
|
+
reason: string
|
|
40
|
+
dimensions: object - 各维度验证结果
|
|
41
|
+
"""
|
|
42
|
+
vuln_type = config.get('type')
|
|
43
|
+
original_request = config.get('original_request', {})
|
|
44
|
+
suspicious_response = config.get('suspicious_response', {})
|
|
45
|
+
baseline_response = config.get('baseline_response', {})
|
|
46
|
+
|
|
47
|
+
# 收集各维度验证结果
|
|
48
|
+
dimensions = {}
|
|
49
|
+
is_false_positive = False
|
|
50
|
+
reasons = []
|
|
51
|
+
|
|
52
|
+
# ========== 维度1: 响应类型验证 ==========
|
|
53
|
+
resp_type_result = verify_response_type(suspicious_response)
|
|
54
|
+
dimensions['response_type'] = resp_type_result
|
|
55
|
+
if resp_type_result['is_waf_or_block']:
|
|
56
|
+
is_false_positive = True
|
|
57
|
+
reasons.append(f"响应类型为{resp_type_result['type']},可能是拦截")
|
|
58
|
+
|
|
59
|
+
# ========== 维度2: 状态码验证 ==========
|
|
60
|
+
status_result = verify_status_code(suspicious_response)
|
|
61
|
+
dimensions['status_code'] = status_result
|
|
62
|
+
|
|
63
|
+
# ========== 维度3: 响应长度验证 ==========
|
|
64
|
+
length_result = verify_response_length(suspicious_response, baseline_response)
|
|
65
|
+
dimensions['response_length'] = length_result
|
|
66
|
+
if length_result['is_empty']:
|
|
67
|
+
is_false_positive = True
|
|
68
|
+
reasons.append("响应为空或过短")
|
|
69
|
+
|
|
70
|
+
# ========== 维度4: WAF拦截验证 ==========
|
|
71
|
+
waf_result = verify_waf_block(suspicious_response)
|
|
72
|
+
dimensions['waf_block'] = waf_result
|
|
73
|
+
if waf_result['detected']:
|
|
74
|
+
is_false_positive = True
|
|
75
|
+
reasons.append(f"检测到WAF/拦截: {waf_result['reason']}")
|
|
76
|
+
|
|
77
|
+
# ========== 维度5: 敏感信息验证 ==========
|
|
78
|
+
sensitive_result = verify_sensitive_info(suspicious_response)
|
|
79
|
+
dimensions['sensitive_info'] = sensitive_result
|
|
80
|
+
|
|
81
|
+
# ========== 维度6: 一致性验证 ==========
|
|
82
|
+
if baseline_response:
|
|
83
|
+
consistency_result = verify_consistency(suspicious_response, baseline_response)
|
|
84
|
+
dimensions['consistency'] = consistency_result
|
|
85
|
+
if not consistency_result['is_consistent']:
|
|
86
|
+
is_false_positive = True
|
|
87
|
+
reasons.append("响应与基线不一致,可能是偶发")
|
|
88
|
+
|
|
89
|
+
# ========== 维度7: 基于漏洞类型的专项验证 ==========
|
|
90
|
+
if vuln_type == 'sqli':
|
|
91
|
+
# SQL注入专项验证
|
|
92
|
+
sqli_result = verify_sqli_dimension(suspicious_response)
|
|
93
|
+
dimensions['sqli'] = sqli_result
|
|
94
|
+
|
|
95
|
+
# SQL注入必须满足:响应是JSON + 包含SQL错误特征
|
|
96
|
+
if not sqli_result['has_sql_error']:
|
|
97
|
+
is_false_positive = True
|
|
98
|
+
reasons.append("未发现SQL错误特征")
|
|
99
|
+
|
|
100
|
+
elif vuln_type == 'idor':
|
|
101
|
+
# IDOR专项验证
|
|
102
|
+
idor_result = verify_idor_dimension(suspicious_response)
|
|
103
|
+
dimensions['idor'] = idor_result
|
|
104
|
+
|
|
105
|
+
# IDOR必须满足:返回业务数据 + 不同ID返回不同数据
|
|
106
|
+
if not idor_result['has_user_data']:
|
|
107
|
+
is_false_positive = True
|
|
108
|
+
reasons.append("未发现用户/业务数据")
|
|
109
|
+
|
|
110
|
+
elif vuln_type == 'auth_bypass':
|
|
111
|
+
# 认证绕过专项验证
|
|
112
|
+
auth_result = verify_auth_bypass(suspicious_response)
|
|
113
|
+
dimensions['auth_bypass'] = auth_result
|
|
114
|
+
|
|
115
|
+
if not auth_result['bypassed']:
|
|
116
|
+
is_false_positive = True
|
|
117
|
+
reasons.append("认证绕过未确认")
|
|
118
|
+
|
|
119
|
+
elif vuln_type == 'info_leak':
|
|
120
|
+
# 信息泄露专项验证
|
|
121
|
+
leak_result = verify_info_leak(suspicious_response)
|
|
122
|
+
dimensions['info_leak'] = leak_result
|
|
123
|
+
|
|
124
|
+
if not leak_result['has_leak']:
|
|
125
|
+
is_false_positive = True
|
|
126
|
+
reasons.append("未发现信息泄露")
|
|
127
|
+
|
|
128
|
+
# ========== 最终判定 ==========
|
|
129
|
+
verified = not is_false_positive
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
'verified': verified,
|
|
133
|
+
'is_false_positive': is_false_positive,
|
|
134
|
+
'reason': '; '.join(reasons) if reasons else '验证通过' if verified else '多项验证失败',
|
|
135
|
+
'dimensions': dimensions
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# ========== 维度1: 响应类型验证 ==========
|
|
140
|
+
def verify_response_type(response):
|
|
141
|
+
"""
|
|
142
|
+
验证响应类型
|
|
143
|
+
维度说明:JSON=真实API,HTML=WAF/路由/拦截
|
|
144
|
+
"""
|
|
145
|
+
body = response.get('body', '')
|
|
146
|
+
headers = response.get('headers', {})
|
|
147
|
+
content_type = headers.get('Content-Type', '')
|
|
148
|
+
|
|
149
|
+
result = {
|
|
150
|
+
'type': 'unknown',
|
|
151
|
+
'is_json': False,
|
|
152
|
+
'is_html': False,
|
|
153
|
+
'is_empty': False,
|
|
154
|
+
'is_waf_or_block': False
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
# 检查是否是JSON
|
|
158
|
+
if 'application/json' in content_type.lower():
|
|
159
|
+
try:
|
|
160
|
+
json.loads(body)
|
|
161
|
+
result['is_json'] = True
|
|
162
|
+
result['type'] = 'json'
|
|
163
|
+
except:
|
|
164
|
+
pass
|
|
165
|
+
|
|
166
|
+
if not result['is_json'] and body.strip().startswith('{'):
|
|
167
|
+
try:
|
|
168
|
+
json.loads(body)
|
|
169
|
+
result['is_json'] = True
|
|
170
|
+
result['type'] = 'json'
|
|
171
|
+
except:
|
|
172
|
+
pass
|
|
173
|
+
|
|
174
|
+
# 检查是否是HTML(可能是WAF/路由/拦截)
|
|
175
|
+
if '<!doctype html>' in body.lower() or '<html' in body.lower():
|
|
176
|
+
result['is_html'] = True
|
|
177
|
+
result['type'] = 'html'
|
|
178
|
+
result['is_waf_or_block'] = True
|
|
179
|
+
|
|
180
|
+
# 检查是否为空
|
|
181
|
+
if len(body) < 50:
|
|
182
|
+
result['is_empty'] = True
|
|
183
|
+
result['type'] = 'empty'
|
|
184
|
+
result['is_waf_or_block'] = True
|
|
185
|
+
|
|
186
|
+
return result
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# ========== 维度2: 状态码验证 ==========
|
|
190
|
+
def verify_status_code(response):
|
|
191
|
+
"""
|
|
192
|
+
验证状态码
|
|
193
|
+
维度说明:200=成功,4xx=客户端错误,5xx=服务端错误
|
|
194
|
+
"""
|
|
195
|
+
status = response.get('status', 0)
|
|
196
|
+
|
|
197
|
+
result = {
|
|
198
|
+
'status': status,
|
|
199
|
+
'is_success': status == 200,
|
|
200
|
+
'is_client_error': 400 <= status < 500,
|
|
201
|
+
'is_server_error': status >= 500,
|
|
202
|
+
'is_redirect': 300 <= status < 400
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return result
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
# ========== 维度3: 响应长度验证 ==========
|
|
209
|
+
def verify_response_length(response, baseline=None):
|
|
210
|
+
"""
|
|
211
|
+
验证响应长度
|
|
212
|
+
维度说明:过短可能是拦截,过长可能是完整数据
|
|
213
|
+
"""
|
|
214
|
+
body = response.get('body', '')
|
|
215
|
+
length = len(body)
|
|
216
|
+
|
|
217
|
+
result = {
|
|
218
|
+
'length': length,
|
|
219
|
+
'is_empty': length < 50,
|
|
220
|
+
'is_reasonable': 50 <= length <= 50000,
|
|
221
|
+
'is_too_long': length > 50000
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
# 与基线对比
|
|
225
|
+
if baseline:
|
|
226
|
+
baseline_length = len(baseline.get('body', ''))
|
|
227
|
+
if baseline_length > 0:
|
|
228
|
+
diff_ratio = abs(length - baseline_length) / max(length, baseline_length)
|
|
229
|
+
result['diff_ratio'] = diff_ratio
|
|
230
|
+
result['significantly_different'] = diff_ratio > 0.8
|
|
231
|
+
|
|
232
|
+
return result
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
# ========== 维度4: WAF拦截验证 ==========
|
|
236
|
+
def verify_waf_block(response):
|
|
237
|
+
"""
|
|
238
|
+
验证WAF/安全设备拦截
|
|
239
|
+
维度说明:检测响应是否为WAF或安全设备的拦截页面
|
|
240
|
+
"""
|
|
241
|
+
body = response.get('body', '').lower()
|
|
242
|
+
headers = response.get('headers', {})
|
|
243
|
+
|
|
244
|
+
waf_indicators = {
|
|
245
|
+
'waf': ['waf', 'web应用防火墙', '安全防护', '防火墙'],
|
|
246
|
+
'block': ['拦截', 'blocked', 'forbidden', '访问受限', 'blocked by'],
|
|
247
|
+
'security': ['安全中心', '安全狗', '云盾', '安全狗'],
|
|
248
|
+
'cdn': ['cdn', 'content filter']
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
detected_type = None
|
|
252
|
+
detected_indicators = []
|
|
253
|
+
|
|
254
|
+
for wtype, indicators in waf_indicators.items():
|
|
255
|
+
for indicator in indicators:
|
|
256
|
+
if indicator in body:
|
|
257
|
+
detected_type = wtype
|
|
258
|
+
detected_indicators.append(indicator)
|
|
259
|
+
|
|
260
|
+
# 检查header
|
|
261
|
+
if not detected_type:
|
|
262
|
+
x_powered = headers.get('X-Powered-By', '').lower()
|
|
263
|
+
for wtype, indicators in waf_indicators.items():
|
|
264
|
+
for indicator in indicators:
|
|
265
|
+
if indicator in x_powered:
|
|
266
|
+
detected_type = wtype
|
|
267
|
+
detected_indicators.append(indicator)
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
'detected': detected_type is not None,
|
|
271
|
+
'type': detected_type,
|
|
272
|
+
'indicators': detected_indicators,
|
|
273
|
+
'reason': f"检测到{detected_type}: {', '.join(detected_indicators)}" if detected_type else None
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
# ========== 维度5: 敏感信息验证 ==========
|
|
278
|
+
def verify_sensitive_info(response):
|
|
279
|
+
"""
|
|
280
|
+
验证敏感信息泄露
|
|
281
|
+
维度说明:检测响应中是否包含敏感字段
|
|
282
|
+
"""
|
|
283
|
+
body = response.get('body', '')
|
|
284
|
+
|
|
285
|
+
sensitive_fields = {
|
|
286
|
+
'password': r'password["\']?\s*[:=]\s*["\']([^"\']{1,50})["\']',
|
|
287
|
+
'token': r'(?:token|Token|TOKEN)["\']?\s*[:=]\s*["\']([^"\']{10,200})["\']',
|
|
288
|
+
'jwt': r'eyJ[A-Za-z0-9-_]+\.eyJ[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+',
|
|
289
|
+
'api_key': r'api[_-]?key["\']?\s*[:=]\s*["\']([^"\']{10,100})["\']',
|
|
290
|
+
'secret': r'secret["\']?\s*[:=]\s*["\']([^"\']{1,100})["\']',
|
|
291
|
+
'phone': r'1[3-9]\d{9}',
|
|
292
|
+
'email': r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
found = {}
|
|
296
|
+
|
|
297
|
+
try:
|
|
298
|
+
# 尝试JSON解析
|
|
299
|
+
data = json.loads(body)
|
|
300
|
+
body_for_search = json.dumps(data)
|
|
301
|
+
except:
|
|
302
|
+
body_for_search = str(body)
|
|
303
|
+
|
|
304
|
+
for field_name, pattern in sensitive_fields.items():
|
|
305
|
+
matches = re.findall(pattern, body_for_search, re.I)
|
|
306
|
+
if matches:
|
|
307
|
+
# 过滤测试数据
|
|
308
|
+
filtered = [m for m in matches if not is_test_data(m)]
|
|
309
|
+
if filtered:
|
|
310
|
+
found[field_name] = len(filtered)
|
|
311
|
+
except:
|
|
312
|
+
pass
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
'found': found,
|
|
316
|
+
'has_sensitive': len(found) > 0,
|
|
317
|
+
'count': sum(found.values())
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
# ========== 维度6: 一致性验证 ==========
|
|
322
|
+
def verify_consistency(response1, response2):
|
|
323
|
+
"""
|
|
324
|
+
验证响应一致性
|
|
325
|
+
维度说明:多次请求响应应该一致,否则可能是偶发
|
|
326
|
+
"""
|
|
327
|
+
status1 = response1.get('status', 0)
|
|
328
|
+
status2 = response2.get('status', 0)
|
|
329
|
+
body1 = response1.get('body', '')
|
|
330
|
+
body2 = response2.get('body', '')
|
|
331
|
+
|
|
332
|
+
result = {
|
|
333
|
+
'status_consistent': status1 == status2,
|
|
334
|
+
'body_similar': True,
|
|
335
|
+
'is_consistent': True
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
# 检查状态码
|
|
339
|
+
if status1 != status2:
|
|
340
|
+
result['status_consistent'] = False
|
|
341
|
+
result['is_consistent'] = False
|
|
342
|
+
|
|
343
|
+
# 检查body相似度
|
|
344
|
+
if body1 and body2:
|
|
345
|
+
len1, len2 = len(body1), len(body2)
|
|
346
|
+
if max(len1, len2) > 0:
|
|
347
|
+
diff_ratio = abs(len1 - len2) / max(len1, len2)
|
|
348
|
+
result['body_diff_ratio'] = diff_ratio
|
|
349
|
+
if diff_ratio > 0.5:
|
|
350
|
+
result['body_similar'] = False
|
|
351
|
+
result['is_consistent'] = False
|
|
352
|
+
|
|
353
|
+
return result
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
# ========== 维度7: SQL注入专项验证 ==========
|
|
357
|
+
def verify_sqli_dimension(response):
|
|
358
|
+
"""
|
|
359
|
+
SQL注入专项验证
|
|
360
|
+
必须满足:响应是JSON + 包含SQL错误特征
|
|
361
|
+
"""
|
|
362
|
+
body = response.get('body', '')
|
|
363
|
+
|
|
364
|
+
# SQL错误特征
|
|
365
|
+
sql_errors = [
|
|
366
|
+
'sql syntax', 'syntax error', 'mysql', 'postgresql', 'oracle',
|
|
367
|
+
'sqlite', 'sqlstate', 'microsoft sql', 'sql error', 'sqlsrv',
|
|
368
|
+
'odbc driver', 'mariadb', 'access denied for',
|
|
369
|
+
'sql_injection', 'sql injection'
|
|
370
|
+
]
|
|
371
|
+
|
|
372
|
+
has_sql_error = False
|
|
373
|
+
detected_error = None
|
|
374
|
+
|
|
375
|
+
body_lower = body.lower()
|
|
376
|
+
for error in sql_errors:
|
|
377
|
+
if error in body_lower:
|
|
378
|
+
has_sql_error = True
|
|
379
|
+
detected_error = error
|
|
380
|
+
break
|
|
381
|
+
|
|
382
|
+
# 检查是否是JSON格式的错误响应
|
|
383
|
+
is_json_error = False
|
|
384
|
+
try:
|
|
385
|
+
data = json.loads(body)
|
|
386
|
+
if isinstance(data, dict):
|
|
387
|
+
msg = str(data.get('msg', '')).lower()
|
|
388
|
+
for error in sql_errors:
|
|
389
|
+
if error in msg:
|
|
390
|
+
is_json_error = True
|
|
391
|
+
break
|
|
392
|
+
# 检查code是否为错误码
|
|
393
|
+
if data.get('code') and data.get('code') not in [200, 0, '200', '0']:
|
|
394
|
+
is_json_error = True
|
|
395
|
+
except:
|
|
396
|
+
pass
|
|
397
|
+
|
|
398
|
+
return {
|
|
399
|
+
'has_sql_error': has_sql_error,
|
|
400
|
+
'error_type': detected_error,
|
|
401
|
+
'is_json_error': is_json_error,
|
|
402
|
+
'confirmed': has_sql_error and is_json_error
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
# ========== 维度8: IDOR专项验证 ==========
|
|
407
|
+
def verify_idor_dimension(response):
|
|
408
|
+
"""
|
|
409
|
+
IDOR专项验证
|
|
410
|
+
必须满足:返回业务数据 + 数据随ID变化
|
|
411
|
+
"""
|
|
412
|
+
body = response.get('body', '')
|
|
413
|
+
|
|
414
|
+
# 业务字段
|
|
415
|
+
business_fields = [
|
|
416
|
+
'user', 'username', 'userId', 'user_id', 'name',
|
|
417
|
+
'phone', 'mobile', 'email',
|
|
418
|
+
'order', 'orderId', 'order_no', 'orderNo',
|
|
419
|
+
'balance', 'amount', 'money',
|
|
420
|
+
'id', '_id', 'createBy', 'create_by'
|
|
421
|
+
]
|
|
422
|
+
|
|
423
|
+
has_user_data = False
|
|
424
|
+
matched_fields = []
|
|
425
|
+
|
|
426
|
+
try:
|
|
427
|
+
data = json.loads(body)
|
|
428
|
+
data_str = json.dumps(data).lower()
|
|
429
|
+
|
|
430
|
+
for field in business_fields:
|
|
431
|
+
if field.lower() in data_str:
|
|
432
|
+
has_user_data = True
|
|
433
|
+
matched_fields.append(field)
|
|
434
|
+
except:
|
|
435
|
+
pass
|
|
436
|
+
|
|
437
|
+
return {
|
|
438
|
+
'has_user_data': has_user_data,
|
|
439
|
+
'matched_fields': matched_fields,
|
|
440
|
+
'confirmed': has_user_data
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
# ========== 维度9: 认证绕过专项验证 ==========
|
|
445
|
+
def verify_auth_bypass(response):
|
|
446
|
+
"""
|
|
447
|
+
认证绕过专项验证
|
|
448
|
+
必须满足:返回token或session
|
|
449
|
+
"""
|
|
450
|
+
body = response.get('body', '')
|
|
451
|
+
|
|
452
|
+
has_token = False
|
|
453
|
+
has_session = False
|
|
454
|
+
|
|
455
|
+
# token特征
|
|
456
|
+
token_patterns = [
|
|
457
|
+
r'token["\']?\s*[:=]\s*["\']([^"\']{10,})',
|
|
458
|
+
r'access_token["\']?\s*[:=]\s*["\']([^"\']{10,})',
|
|
459
|
+
r'session_id["\']?\s*[:=]\s*["\']([^"\']{10,})',
|
|
460
|
+
r'Bearer\s+[a-zA-Z0-9\-_\.]+'
|
|
461
|
+
]
|
|
462
|
+
|
|
463
|
+
for pattern in token_patterns:
|
|
464
|
+
if re.search(pattern, body, re.I):
|
|
465
|
+
has_token = True
|
|
466
|
+
break
|
|
467
|
+
|
|
468
|
+
# 检查是否是成功登录的响应
|
|
469
|
+
try:
|
|
470
|
+
data = json.loads(body)
|
|
471
|
+
if data.get('success') == True or data.get('code') == 0:
|
|
472
|
+
if data.get('token') or data.get('data', {}).get('token'):
|
|
473
|
+
has_token = True
|
|
474
|
+
except:
|
|
475
|
+
pass
|
|
476
|
+
|
|
477
|
+
return {
|
|
478
|
+
'has_token': has_token,
|
|
479
|
+
'has_session': has_session,
|
|
480
|
+
'bypassed': has_token
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
# ========== 维度10: 信息泄露专项验证 ==========
|
|
485
|
+
def verify_info_leak(response):
|
|
486
|
+
"""
|
|
487
|
+
信息泄露专项验证
|
|
488
|
+
必须满足:返回非公开的业务信息
|
|
489
|
+
"""
|
|
490
|
+
body = response.get('body', '')
|
|
491
|
+
|
|
492
|
+
# 非公开信息特征
|
|
493
|
+
private_info = [
|
|
494
|
+
'password', 'secret', 'api_key', 'apiKey',
|
|
495
|
+
'token', 'session', 'private',
|
|
496
|
+
'phone', 'email', 'id_card', '身份证'
|
|
497
|
+
]
|
|
498
|
+
|
|
499
|
+
found = []
|
|
500
|
+
|
|
501
|
+
body_lower = body.lower()
|
|
502
|
+
for info in private_info:
|
|
503
|
+
if info in body_lower:
|
|
504
|
+
found.append(info)
|
|
505
|
+
|
|
506
|
+
return {
|
|
507
|
+
'found': found,
|
|
508
|
+
'has_leak': len(found) > 0,
|
|
509
|
+
'confirmed': len(found) > 0
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
# ========== 辅助函数 ==========
|
|
514
|
+
def is_test_data(value):
|
|
515
|
+
"""判断是否是测试数据"""
|
|
516
|
+
test_patterns = [
|
|
517
|
+
'test', 'TEST', 'Test',
|
|
518
|
+
'xxx', 'xxx.xxx',
|
|
519
|
+
'null', 'undefined',
|
|
520
|
+
'example', 'sample',
|
|
521
|
+
'placeholder', 'demo'
|
|
522
|
+
]
|
|
523
|
+
value_lower = str(value).lower()
|
|
524
|
+
return any(t in value_lower for t in test_patterns)
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
if __name__ == '__main__':
|
|
528
|
+
# 测试
|
|
529
|
+
result = vuln_verifier({
|
|
530
|
+
'type': 'sqli',
|
|
531
|
+
'original_request': {'url': 'http://example.com/api/login', 'method': 'POST'},
|
|
532
|
+
'suspicious_response': {'status': 200, 'body': '{"error": "success"}'}
|
|
533
|
+
})
|
|
534
|
+
print(f"Verified: {result['verified']}")
|
|
535
|
+
print(f"False Positive: {result['is_false_positive']}")
|
|
536
|
+
print(f"Dimensions: {list(result['dimensions'].keys())}")
|
package/package.json
CHANGED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# API Security Testing 参考资源
|
|
2
|
+
|
|
3
|
+
## 核心文件
|
|
4
|
+
|
|
5
|
+
| 文件 | 内容 |
|
|
6
|
+
|------|------|
|
|
7
|
+
| `pua-agent.md` | PUA自动测试Agent,强制深入不放弃 |
|
|
8
|
+
| `fuzzing-patterns.md` | API Fuzzing字典 |
|
|
9
|
+
| `report-template.md` | 安全测试报告模板 |
|
|
10
|
+
|
|
11
|
+
## vulnerabilities/ 漏洞测试方法
|
|
12
|
+
|
|
13
|
+
| 文件 | 内容 |
|
|
14
|
+
|------|------|
|
|
15
|
+
| `vulnerabilities/01-sqli-tests.md` | SQL注入测试 + WAF绕过 |
|
|
16
|
+
| `vulnerabilities/02-user-enum-tests.md` | 用户枚举测试 |
|
|
17
|
+
| `vulnerabilities/03-jwt-tests.md` | JWT认证测试 |
|
|
18
|
+
| `vulnerabilities/04-idor-tests.md` | IDOR越权测试 |
|
|
19
|
+
| `vulnerabilities/05-sensitive-data-tests.md` | 敏感信息泄露 |
|
|
20
|
+
| `vulnerabilities/06-biz-logic-tests.md` | 业务逻辑漏洞 |
|
|
21
|
+
| `vulnerabilities/07-security-config-tests.md` | 安全配置漏洞 |
|
|
22
|
+
| `vulnerabilities/08-brute-force-tests.md` | 暴力破解测试 |
|
|
23
|
+
| `vulnerabilities/09-vulnerability-chains.md` | 漏洞关联联想 |
|
|
24
|
+
| `vulnerabilities/10-auth-tests.md` | OAuth/SAML/2FA测试 |
|
|
25
|
+
| `vulnerabilities/11-graphql-tests.md` | GraphQL安全测试 |
|
|
26
|
+
| `vulnerabilities/12-ssrf-tests.md` | SSRF测试 |
|
|
27
|
+
|
|
28
|
+
## PUA Agent 使用
|
|
29
|
+
|
|
30
|
+
### 核心思想
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
【PUA自动模式】
|
|
34
|
+
- 发现线索 → 自动深入
|
|
35
|
+
- 不等待用户指令
|
|
36
|
+
- 压力升级直到完成
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 压力升级机制
|
|
40
|
+
|
|
41
|
+
| 失败次数 | 级别 | 行动 |
|
|
42
|
+
|---------|------|------|
|
|
43
|
+
| 1次 | L1 | 换方法继续 |
|
|
44
|
+
| 2次 | L2 | 强制检查清单 |
|
|
45
|
+
| 3次 | L3 | 报告进度并继续 |
|
|
46
|
+
| 4次+ | L4 | 尝试其他方向 |
|
|
47
|
+
|
|
48
|
+
### 不放弃原则
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
遇到以下情况必须继续:
|
|
52
|
+
□ 端点404 → 尝试POST方法
|
|
53
|
+
□ 被WAF拦截 → 换payload
|
|
54
|
+
□ 返回HTML → 继续API测试
|
|
55
|
+
□ 找不到配置 → 搜索其他JS
|
|
56
|
+
□ 一个端点失败 → 测试同类端点
|
|
57
|
+
□ 说"无法测试" → 必须穷举所有方法
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 进度追踪表
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
阶段1: [████████░░] 80%
|
|
64
|
+
阶段2: [████░░░░░░░] 40%
|
|
65
|
+
阶段3: [░░░░░░░░░░░] 0%
|
|
66
|
+
阶段4: [░░░░░░░░░░░] 0%
|
|
67
|
+
|
|
68
|
+
发现:
|
|
69
|
+
├─ CORS漏洞: 18个端点
|
|
70
|
+
├─ SQL注入: 待测试
|
|
71
|
+
└─ 新线索: /ipark-wxlite/*
|
|
72
|
+
```
|