curl WebSocket逻辑错误:大数据传输期间控制帧饥饿导致连接中断(DoS)

本文详细报告了curl库WebSocket实现中的一个逻辑缺陷,该缺陷在发送大量数据时未能优先处理PING/PONG控制帧,可能导致服务器因保活超时而断开连接。报告包含复现步骤、示例代码和根本原因分析。

漏洞报告 #3480039 - WebSocket逻辑错误:控制帧(PING/PONG)饥饿导致大数据传输期间连接中断(DoS)

提交者: efrsxcv 提交日期: 3天前

摘要

我在 lib/ws.c 中发现了一个关于WebSocket控制帧(PING/PONG)处理的逻辑缺陷。根据RFC 6455,控制帧应尽可能快地处理,即使是在处理分片数据帧的过程中,以维持连接状态(保活)。然而,当libcurl通过 curl_ws_send 主动发送大量数据流时,它未能优先处理PONG响应。如果 payload_remain > 0,PONG响应会被排在用户数据之后。如果传输时间超过服务器的保活超时时间,服务器将断开连接,导致合法的操作遭到拒绝服务攻击。

受影响版本: 在最新的curl master分支(以及支持WebSocket的近期版本)上复现。(请在终端中运行 curl -V 并粘贴输出)

复现步骤

为了复现此问题,我们需要一个强制执行较短保活超时的“严格”WebSocket服务器,以及一个使发送队列饱和的客户端。

1. 设置恶意服务器(strict_ws_server.py)

此Python脚本模拟一个服务器,每1秒发送一个PING,如果在3秒内未收到PONG则断开连接。

 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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import socket
import struct
import time
import threading
import select

OP_PING = 0x9
OP_PONG = 0xA
def create_frame(opcode, payload=b""):
    b1 = 0x80 | opcode
    b2 = len(payload) & 0x7F
    return struct.pack("!BB", b1, b2) + payload
def handle_client(conn):
    print("[+] Client connected. Handshaking...")
    try:
        req = conn.recv(4096)
        resp = (
            b"HTTP/1.1 101 Switching Protocols\r\n"
            b"Upgrade: websocket\r\n"
            b"Connection: Upgrade\r\n"
            b"Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n"
            b"\r\n"
        )
        conn.sendall(resp)
        print("[+] Handshake OK. Monitoring Heartbeat...")
        last_pong_time = time.time()
        running = True
        def pinger():
            nonlocal last_pong_time, running
            while running:
                try:
                    time.sleep(1)
                    conn.sendall(create_frame(OP_PING, b"alive?"))
                    if time.time() - last_pong_time > 3:
                        print("\n[!!!] TIMEOUT: Client did not reply PONG in 3s!")
                        running = False
                        conn.close()
                        return
                except:
                    running = False
                    return
        t = threading.Thread(target=pinger)
        t.start()
        while running:
            ready = select.select([conn], [], [], 1)
            if ready[0]:
                data = conn.recv(1024)
                if not data: break
                opcode = data[0] & 0x0F
                if opcode == OP_PONG:
                    last_pong_time = time.time()
    except:
        pass
def start_server():
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    s.bind(('0.0.0.0', 9090))
    s.listen(1)
    print("[*] Strict Server on 9090...")
    while True:
        conn, addr = s.accept()
        handle_client(conn)
if __name__ == "__main__":
    start_server()

2. 设置客户端PoC(poc_ws_starve.c)

使用以下命令编译:gcc poc_ws_starve.c -o poc_ws_starve -lcurl 此客户端在发送大量数据的同时,人为地稍微减慢速度以模拟网络延迟/繁忙循环,迫使PONG被排队。

 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
30
31
32
33
34
35
36
37
38
39
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <curl/curl.h>

int main(void) {
    CURL *curl;
    CURLcode res;
    curl_global_init(CURL_GLOBAL_DEFAULT);
    curl = curl_easy_init();
    if(curl) {
        // Connect to local strict server
        curl_easy_setopt(curl, CURLOPT_URL, "ws://127.0.0.1:9090/");
        curl_easy_setopt(curl, CURLOPT_CONNECT_ONLY, 2L);
        res = curl_easy_perform(curl);
        if(res != CURLE_OK) return 1;
        size_t sent;
        char buffer[1024];
        memset(buffer, 'A', sizeof(buffer));
        const struct curl_ws_frame *meta;
        size_t rlen;
        char rbuf[256];
        // Send massive data stream
        for(int i=0; i<100000; i++) {
            // 1. Send Data (CURLWS_BINARY)
            res = curl_ws_send(curl, buffer, sizeof(buffer), &sent, 0, CURLWS_BINARY);
            if(res != CURLE_OK) break;
            // 2. Sleep briefly to allow Server to send PING
            usleep(10000); 
            // 3. Call recv to process incoming PING
            // EXPECTED: Curl should reply PONG immediately.
            // ACTUAL: Curl queues PONG behind the data stream because payload_remain > 0.
            curl_ws_recv(curl, rbuf, sizeof(rbuf), &rlen, &meta);
        }
    }
    curl_easy_cleanup(curl);
    curl_global_cleanup();
    return 0;
}

3. 执行

  • 在终端1运行 python3 strict_ws_server.py
  • 在终端2运行 ./poc_ws_starve

支持材料/参考资料

  • RFC 6455 第5.5节:“控制帧(参见第5.5节)可以注入到分片消息的中间。”
  • 根本原因:lib/ws.cws_enc_add_cntrl 函数中,代码检查 if(!ws->enc.payload_remain)。如果正在发送数据,则返回 CURLE_OK,实际上导致PONG被排队而未发送,引起饥饿。

观察到的输出(服务器终端)

1
[!!!] TIMEOUT: Client did not reply PONG in 3s!

尽管客户端是活跃的,服务器还是断开了与客户端的连接,这证实了逻辑错误。

影响

摘要: 在有效操作中,由于控制帧饥饿导致连接意外断开,严格来说这影响了“可用性”。

时间线

  • 3天前: efrsxcv 提交报告。

  • 3天前: efrsxcv 附加了两张截图(Jepretan_layar_20251228_010812.png, Jepretan_layar_20251228_010808.png)。

  • 3天前: bagder (curl 员工) 评论感谢报告,并表示将进行调查。

  • 3天前: bagder 评论表示未看到安全角度。服务器同时发送内容和PING。这可能是错误,但可以像处理其他错误一样管理。

  • 3天前: bagder 同意。

  • 2天前: bagder 关闭报告并将状态改为“信息性”,认为这不是安全问题。

  • 2天前: bagder 根据项目透明度政策请求披露此报告。

  • 2天前: bagder 披露此报告。

报告详情

  • 报告日期: 2025年12月27日,下午6:12 UTC
  • 报告者: efrsxcv
  • 报告对象: curl
  • 报告ID: #3480039
  • 状态: 信息性
  • 严重性: 中等(4 ~ 6.9)
  • 披露日期: 2025年12月28日,下午9:29 UTC
  • 弱项类型: 业务逻辑错误
  • CVE ID:
  • 赏金:
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计