python

WAF 指纹识别原理与实现:从 sqlmap 源码学起

2026-07-06 #渗透测试#python#WAF

WAF(Web Application Firewall)是渗透测试的第一道坎。扫之前先搞清楚对面是什么 WAF——Cloudflare?阿里云 WAF?安全狗?不同 WAF 的绕过策略天差地别。这篇从 sqlmap 的 WAF 检测模块拆解指纹识别的底层原理。


WAF 指纹识别的两种思路

  1. 主动探测:发送特定 Payload,根据响应特征判断。sqlmap 用的就是这招。
  2. 被动识别:看响应头里的 ServerX-Powered-By,或者 Cookie 里 WAF 注入的标记。

vibeWaf 结合了两种思路——先被动扫一眼,不命中再主动发 Payload。


被动识别:响应头里的证据

很多 WAF 会在响应中留下痕迹:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
PASSIVE_FINGERPRINTS = {
'Cloudflare': {
'headers': {
'Server': r'cloudflare',
'CF-Ray': r'.+',
},
},
'Aliyun WAF': {
'headers': {
'Server': r'ALB|AliyunOSS',
'X-Security-Token': r'.+',
},
'cookies': ['aliyungf_tc'],
},
'Safedog': {
'headers': {
'X-Powered-By': r'WAF/\d+\.\d+',
},
'cookies': ['Safedog-Flow-Plugin'],
},
'Tencent Cloud WAF': {
'body': [
r'腾讯云 WAF',
r'stgw_403_forbidden',
],
},
'Baidu Yunjiasu': {
'headers': {
'Server': r'yunjiasu',
'X-Server': r'yunjiasu',
},
},
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def passive_detect(target_url, session):
"""被动识别 WAF"""
try:
resp = session.get(target_url, timeout=10)

for waf_name, fingerprint in PASSIVE_FINGERPRINTS.items():
score = 0

# 检查响应头
for header, pattern in fingerprint.get('headers', {}).items():
value = resp.headers.get(header, '')
if re.search(pattern, value, re.IGNORECASE):
score += 1

# 检查 Cookie
for cookie_name in fingerprint.get('cookies', []):
if cookie_name in resp.cookies:
score += 1

# 检查页面内容
for pattern in fingerprint.get('body', []):
if re.search(pattern, resp.text[:5000], re.IGNORECASE):
score += 1

if score >= 2:
return {'waf': waf_name, 'method': 'passive', 'confidence': 'high'}

except Exception as e:
pass

return None

主动探测:从 sqlmap 学攻击性检测

sqlmap 的 WAF 检测逻辑在 lib/request/connect.py 中。核心思路很简单:

发送一个显然恶意的 Payload → 看响应是否被拦截 → 根据拦截页面的特征判断 WAF 类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import hashlib

# 构造一个明显恶意的请求
# ?id=1 AND 1=1 UNION SELECT ... → 正常 WAF 都会拦截
MALICIOUS_PAYLOAD = (
"%d' AND %d=%d UNION ALL SELECT NULL,NULL,NULL,NULL,"
"NULL-- " + hashlib.md5(b"waf_test").hexdigest()
)

def active_detect(target_url, session):
"""主动探测 WAF 类型"""
# 1. 发送正常请求获取基线
baseline = session.get(target_url, timeout=10)
baseline_status = baseline.status_code
baseline_length = len(baseline.text)

# 2. 发送恶意 Payload
test_url = f"{target_url}?id={MALICIOUS_PAYLOAD}"
try:
response = session.get(test_url, timeout=10)
except requests.exceptions.ConnectionError:
return {'waf': 'Unknown', 'method': 'active', 'note': '连接被重置(可能被 WAF 阻断)'}

# 3. 对比响应差异
status_changed = response.status_code != baseline_status
length_changed = abs(len(response.text) - baseline_length) > 200
content_contains_block = any(
w in response.text.lower()
for w in ['blocked', 'forbidden', 'waf', '防火墙', '拦截']
)

if not (status_changed or length_changed or content_contains_block):
return None # 可能没有 WAF

# 4. 根据响应特征识别 WAF 类型
return identify_waf_by_response(response)

WAF 指纹规则库

这是 vibeWaf 的核心——一个包含 60+ 条 WAF 指纹规则的 JSON 数据库:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
{
"wafs": [
{
"name": "Cloudflare",
"detection": [
{
"type": "status",
"value": 403
},
{
"type": "header",
"name": "Server",
"pattern": "cloudflare"
},
{
"type": "body",
"pattern": "Cloudflare Ray ID: [0-9a-f]{16}"
},
{
"type": "body",
"pattern": "Attention Required! \\| Cloudflare"
}
]
},
{
"name": "ModSecurity",
"detection": [
{
"type": "status",
"value": 403
},
{
"type": "body",
"pattern": "ModSecurity|mod_security|This error was generated by Mod_Security"
},
{
"type": "body",
"pattern": "rules of the requested resource"
}
]
},
{
"name": "阿里云 WAF",
"detection": [
{
"type": "body",
"pattern": "阿里云 Web应用防火墙|Aliyun Web Application Firewall"
},
{
"type": "status",
"value": 405
}
]
},
{
"name": "安全狗",
"detection": [
{
"type": "header",
"name": "X-Powered-By",
"pattern": "WAF"
},
{
"type": "body",
"pattern": "Safedog|安全狗|safedog"
}
]
},
{
"name": "D盾",
"detection": [
{
"type": "body",
"pattern": "D盾|D盾_防火墙|D-Security"
},
{
"type": "status",
"value": 403
}
]
}
]
}

每条规则包含多个检测维度(status + header + body),全部命中才判定为该 WAF,降低误报。


检测引擎实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import json
import re

class WafDetector:
def __init__(self, rules_file='data.json'):
with open(rules_file, 'r', encoding='utf-8') as f:
self.rules = json.load(f)

def detect(self, response):
"""根据 HTTP 响应识别 WAF 类型"""
results = []

for waf in self.rules['wafs']:
name = waf['name']
detections = waf['detection']
matched = 0

for rule in detections:
if rule['type'] == 'status':
if response.status_code == rule['value']:
matched += 1

elif rule['type'] == 'header':
header_val = response.headers.get(rule['name'], '')
if re.search(rule['pattern'], header_val, re.IGNORECASE):
matched += 1

elif rule['type'] == 'body':
if re.search(rule['pattern'], response.text, re.IGNORECASE):
matched += 1

# 所有检测规则都命中才算匹配
if matched == len(detections):
results.append({'waf': name, 'confidence': 'high'})

return results

绕过策略速查表

识别了 WAF 类型,绕过策略就有方向了:

WAF 弱项 绕过 Payload 示例
Cloudflare 分块传输编码 Transfer-Encoding: chunked
ModSecurity 参数污染 ?id=1&id=1' OR '1'='1
阿里云 WAF 大小写混写 SeLeCt * FrOm users
安全狗 内联注释 /*!50000UNION*/ SELECT
360 主机卫士 HTTP/0.9 使用 HTTP/0.9 协议
D盾 特殊字符编码 %2527 (双重 URL 编码)

实现中的细节

1. sqlmap 的 Payload 为什么用 MD5

1
payload = f"%d' AND %d=%d UNION ALL SELECT NULL... -- {md5('waf_test')}"

md5 字符串有两个作用:

  • 唯一标识:如果响应中出现这个 md5,说明 Payload 未被执行(被 WAF 拦截了),但如果 WAF 拦截后返回了原始请求内容,这个 md5 也会出现。通过对比正常请求和恶意请求的响应差异来判断。
  • 避免缓存:每次测试生成新的 Payload,绕过 CDN 缓存。

2. 多 Payload 确认

不要用一个 Payload 就下结论。至少发 2-3 个不同类型的恶意请求交叉验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
PAYLOADS = [
"1' AND '1'='1", # SQL 注入
"<script>alert(1)</script>", # XSS
"../../../etc/passwd", # 路径穿越
]

def multi_payload_verify(target_url, session):
"""多 Payload 交叉验证 WAF 存在性"""
baseline = session.get(target_url)

blocks = 0
for payload in PAYLOADS:
resp = session.get(f"{target_url}?q={payload}")
if resp.status_code == 403 or len(resp.text) < len(baseline.text) * 0.3:
blocks += 1

return blocks / len(PAYLOADS) # 拦截率

总结

WAF 指纹识别本质上是一个模式匹配问题。被动识别看响应头和 Cookie,主动探测发送恶意 Payload 看反应。sqlmap 的思路很经典——用一个明显恶意的请求试探,根据 WAF 的拦截页面特征反推类型。

知道对面是什么 WAF,后面的绕过才有方向。


评论
分享