MoXie25/NanoMQ-Memory-Leak-Research

GitHub: MoXie25/NanoMQ-Memory-Leak-Research

该仓库公开了 EMQ NanoMQ v0.24.9 中由配置不匹配与空指针提前返回导致的内存泄漏型拒绝服务漏洞的完整技术分析、PoC 脚本及 Docker 复现环境。

Stars: 0 | Forks: 0

# CVE-2026-36590:EMQ NanoMQ v0.24.9 中的拒绝服务漏洞 本仓库包含 CVE-2026-36590 的公开安全通告和技术分析,该漏洞是一个影响 EMQ NanoMQ v0.24.9 的拒绝服务漏洞。 ## 通告信息 * CVE ID: CVE-2026-36590 * 厂商/项目: EMQ / NanoMQ * 产品: NanoMQ * 受影响版本: v0.24.9 * 漏洞类型: 拒绝服务 / 资源耗尽 * 受影响组件: `broker_tcp.c` / `nni_qos_db_set` * CWE: CWE-772, CWE-400 * 公开披露日期: 2026-06-17 ## 摘要 当 NanoMQ v0.24.9 在未包含 SQLite 支持的情况下构建,但 `sqlite.enable` 被配置为 `true` 时,QoS 数据库指针可能保持为 `NULL`。在处理 MQTT QoS 消息期间,`nni_qos_db_set` 可能会在未释放克隆的消息引用的情况下提前返回。重复发送 QoS > 0 的 MQTT PUBLISH 消息可能会导致持续的内存消耗,最终引发拒绝服务。 ## 缓解措施 用户应限制对 NanoMQ 实例的访问,避免将 MQTT 服务暴露给不受信任的客户端,并避免在未包含 SQLite 支持的构建版本中启用 SQLite 持久化。厂商应在 `nni_qos_db_set` 的 `db == NULL` 分支中添加 SQLite 配置的启动验证,并释放消息引用。 # NanoMQ-Memory-Leak-研究 ### **附件** 1. **Analysis_Report.md**: 完整的分析报告,包括生命周期图和详细的日志追踪。 2. **249exploit_leak.py**: 用于复现该内存泄漏的 Python PoC 脚本。 3. **nanomq.conf**: 用于触发该不匹配状态的配置文件。 4. **runtime_logs.log**: 展示引用计数异常的插桩日志。 5. **Docker/**: 用于在受控环境下稳定触发并验证该漏洞的容器化复现环境。 详细的构建说明和环境配置步骤请参见: `Docker/Docker-Nanomq Build.md` 6. **249asan_log.txt**: 从 NanoMQ v0.24.9 捕获的 AddressSanitizer (ASAN) 运行时日志,展示了内存泄漏及相关的异常内存行为。 ## **Issue 正文** **摘要** 本仓库包含了针对 NanoMQ 中发现的内存泄漏漏洞的概念验证 (PoC) 和详细分析。该问题允许远程攻击者通过发送特定的 QoS MQTT 消息耗尽系统内存,从而引发拒绝服务。 我对 Nanomq 中的内存泄漏现象进行了深入分析。通过动态插桩和代码审查,我发现了一条关键的资源泄漏路径,该路径在 **运行时配置启用了 SQLite (`sqlite.enable=true`) 但二进制文件在编译时未包含 SQLite 支持(`NNG_SUPP_SQLITE` 未定义)** 的情况下被触发。 **为了保持简明扼要,我在下方总结了关键发现。如需完整的技术剖析(包括详细的 ASAN 追踪、生命周期图和插桩日志),请参阅附件 `Analysis_Report.md`。** ### **分析过程** #### **1. 初步检测 (ASAN 分析)** 使用 AddressSanitizer,我将泄漏内存的来源定位到了 `tcptran_pipe_recv_cb` (`nng/src/sp/transport/mqtt/broker_tcp.c`) 中的 `nni_msg_alloc`。该内存被分配但从未被完全释放。 #### **2. 生命周期建模("3 次克隆 vs. 3 次释放" 基准)** 我分析了 `nni_msg` 的引用计数机制。一个正常的 QoS 1 消息生命周期应该包含: * **分配 (+1)**: 网络接收。 * **克隆 1 (+1)**: 应用层分发。 * **克隆 2 (+1)**: 持久化/重传逻辑。 * **总引用数: 3** -> **需要释放次数: 3**。 #### **3. 动态追踪与验证** 由于 GDB 不适用于这种高并发场景,我使用了动态插桩技术来追踪特定内存对象(例如 `0x60e00002ffa0`)的生命周期。 **发现:** 日志证实了生命周期存在不匹配: * **实际分配**: 3 次 (分配 + 克隆 1 + 克隆 2) * **实际释放**: 2 次 (释放 1 + 释放 2) * **结果**: 引用计数保持为 **1**,导致泄漏。缺失的释放对应于在 `nmq_pipe_send_start_v4` 中生成的 **克隆 2**。 #### **4. 根本原因识别** 追踪执行流程揭示了为什么缺少第 3 次释放: 1. **不匹配状态**: 二进制文件在编译时未包含 `-DNNG_SUPP_SQLITE`,导致 `nano_sock_setdb` 中的初始化逻辑被剥离。然而,`nanomq.conf` 中设置了 `sqlite.enable = true`。这导致 `db` 指针保持为 `NULL`。 2. **缺陷逻辑("提前返回")**: 当消息 (Ref=3) 进入 `nni_qos_db_set` 准备存储时,该函数会检查空指针: ``` // File: nng/src/sp/transport/mqtt/broker_tcp.c void nni_qos_db_set(..., void *db, ..., nng_msg *msg) { if (db == NULL) { // CRITICAL FLAW: // Early return triggers because db is NULL. // The function holds ownership of 'msg' (Ref++) but fails to release it. return; } // ... } ``` 该函数会直接返回而未调用 `nni_msg_free(msg)`。这导致 msg 指针被彻底丢失,实际上使得该内存块成为了孤儿状态。 ### **结论与影响** * **根本原因**: `nni_qos_db_set` 中缺乏防御性编程。当由于无效的 DB 状态而执行提前返回时,它没有处理 `msg` 指针的所有权。 * **影响**: 这会导致隐形的 DoS。持续的 QoS > 0 消息——无论是来自正常的客户端流量还是恶意攻击者——都会逐渐耗尽系统内存 (OOM),最终导致 broker 崩溃。 **建议:** 1. **快速失败(启动检查)**: 在系统启动阶段(`nano_sock_setdb` 或 `main` 函数)添加验证逻辑,以确保 SQLite 正常运行。如果检测到 `conf->sqlite.enable == true`,但 `NNG_SUPP_SQLITE` 宏未定义或 SQLite 异常,系统应当要么报错退出,要么强制将 `enable` 设置为 `false` 并打印警告。 2. **资源安全(故障安全)**: 在 `nni_qos_db_set` 函数的提前返回分支中添加资源释放逻辑。如果 `db == NULL`,必须调用 `nni_msg_free(msg)` 以平衡引用计数并防止内存泄漏。
标签:PoC, 内存泄漏, 拒绝服务, 暴力破解, 漏洞分析, 物联网安全, 请求拦截, 路径探测, 逆向工具, 配置错误