概述
ZMQ(Zero Message Queue)是一种基于消息队列得多线程网络库,C++编写,可以使得Socket编程更加简单高效。
该编号为CVE-2019-6250的远程执行漏洞,主要出现在ZMQ的核心引擎libzmq(4.2.x以及4.3.1之后的4.3.x)定义的ZMTP v2.0协议中。
环境搭建
环境
先搭建个测试poc和后面调试漏洞的环境,这里用到了:
- Ubuntu 20.4.2
- libzmq 4.2.x以及4.3.1之后的4.3.x这几个版本都可以触发漏洞,下载后切换版本安装即可测试 项目地址
- cppzmq 用来调用libzmq执行代码,测试poc 项目地址
- poc地址在这
poc测试
将libzmp和cppzmq都安装好,其中libzmp是configure来安装的,cppzmq是cmake来安装的,安装过程简单。
都安装完后,cppzmq下有个demo目录,下面也 cmake ./
编译,完成后即可在demo这里堆main.cpp修改来进行漏洞测试,main.cpp代码如下:
1 |
|
主要是引入了zmq.hpp头文件,这个决定了poc是否能测试成功,输出个hello world测试一下,成功说明环境配置都ok了:
去拿到poc,为了省事,我这里也贴一份现成的exp出来:
1 |
|
修改main.cpp内容,然后编译执行,成功复现:
漏洞成因
通过对cve作者提交的说明来看,漏洞主要是在 src/v2_decoder.cpp zmq::v2_decoder_t::size_ready()
中 read_pos_ + msg_size_
表达式 未做过滤导致一个整型溢出漏洞,使得可以通过设置一个较大的msg_zize来让判断条件恒不成立,造成第二次读取向小缓冲区写入大数据进而溢出,具体溢出位置如下:
1 | if (unlikely (!_zero_copy |
整个通信流程
该版本ZMQ中使用到了ZMTP v2.0版本作为消息传递的传输层协议,客户端和服务端的通信主要以如下步骤展开:
- clinet和server首次进行协商,clinet和server建立TCP链接后,首先是client发送greeting报文进行协商,其协商内容主要是ZMTP的版本信息, 抓包分析,三次握手之后client直接发送greeting报文:
1 | 对应代码如下: |
1 | const uint8_t greeting[] = { |
- server收到greeting报文后,会进行初始化操作,通过调试发现首先进入src/stream_engine.cpp中的zmq::stream_engine_t::in_event()方法中,调用栈大致如下:
1 | 在调用tcp_read()读取client消息前,会创建一个缓冲区来存储消息,调用src/raw_decoder.cpp中的get_buffer()方法来申请缓冲区,其内部实现如下,主要使用src/decoder_allocators.cpp中的allocate()方法: |
1 | void zmq::raw_decoder_t::get_buffer (unsigned char **data_, size_t *size_) |
1 | zmq::shared_message_memory_allocator::allocate()中调用std::malloc()创建一块用于存储稍后所有通信消息的缓冲区,缓冲区大小为0x2008( |
_max_size + sizeof (zmq::atomic_counter_t)
=0x2000+0x8
),申请成功后在_buf前8个字节存放0x00000001,所以 _buf
起始位置要从 _buf+0x8
开始算,我调试时 _buf
为0x7fffe800bb30,:
之后存放一个 content_t
结构体在 _buf
之后,该结构体用于client端close()关闭连接后来进行server端析构处理,其对于后续缓冲区溢出利用有着关键性的作用:
1 | struct content_t |
- client接着发送
msg_size
报文给server。报文格式0x02+msg_size
,用来标识接下来server等待接收消息的大小,server在zmq::v2_decoder_t::size_ready方法中处理msg_size
,正常流程是如果我们设置的msg_size
值大于初始化空间的0x2000则重新分配缓冲区,但由于存在整数溢出漏洞,使得可以绕过缓冲区大小校验进入else流程,else中init()函数直接使用初始化的缓冲区读取client申请的非常大的如0xffffffffffffffff大小的数据,进而导致后续通信过程中产生缓冲区溢出:
1 | 依据如上理论,client端设置 |
msg_size=0xffffffffffffffff
然后发送:
1 | const uint8_t v2msg[] = { |
1 | 进行调试发现确实进入了else流程,且read_pos位置为初始化的缓冲区 |
_buf
+0x8处,msg_size_
=0xffffffffffffffff:
- 依据第三步理论,client就可以继续发送大小为0x2008-0x8-0x9=0x1FF7=8183字节的数据来填充整个缓冲区,也就是poc中的client端第二次send()发送的内容,这里稍微修改一下,把pc中的pl指向空间用’a’填充,填充后进行通信,server端整个缓冲区的内容如下:
- 最后就是对缓冲区后面的content_t结构体进行溢出覆盖,该结构体中几个字段解释如下:
data:函数ffn的第一个参数
size:未使用
ffn:ffh函数指针地址
hint:函数ffn第二个参数
该处关键点在于client端在执行close()关闭连接时,会进入server端的zmq::msg_t::close ()方法,该方法内部会调用
content_t->ffn
函数指针来进行析构:
1 | 所以我们只需要溢出覆盖此结构体中的data、ffn、hint三个字段,即可达到任意函数执行的目的。比如poc中的通过两次覆盖来达到使用system()执行任意命令。 |
漏洞利用
本地利用没什么好说的,远程利用可以对前面那0x1FF7大小的缓冲区进行利用执行到任意shellcode,这里不再详述,说太多容易刑,有兴趣可以自己研究。
参考: