libcurl TFTP实现中的堆缓冲区溢出漏洞详解

本文详细披露了在libcurl的TFTP实现中发现的一个堆缓冲区溢出漏洞。该漏洞允许恶意TFTP服务器通过发送包含大尺寸blksize选项的OACK包,触发客户端内存越界写入,可能导致远程代码执行或拒绝服务攻击。

堆缓冲区溢出漏洞报告:libcurl TFTP组件

摘要

一份安全报告(编号#3444904)提交至curl项目组,披露了libcurl的TFTP实现中存在一个堆缓冲区溢出漏洞。该漏洞由研究人员helspy于2天前提交。

漏洞详情

漏洞机理

当恶意TFTP服务器发送一个包含blksize选项(该选项值大于默认块大小512字节)的OACK(选项确认)数据包时,会触发此漏洞。libcurl会更新其内部的块大小变量,但未能重新分配接收和发送缓冲区rpacketspacket)。随后,当应用程序使用更大的块大小接收数据(在tftp_rx函数中)或发送数据(在tftp_tx函数中)时,会写入到已分配缓冲区的末尾之外,从而导致堆缓冲区溢出

受影响版本

  • curl 8.18.0-DEV(基于include/curl/curlver.h中的LIBCURL_VERSION)
  • 平台:Windows(已验证)

复现步骤

  1. 将提供的复现脚本保存为 tftp_repro.py
  2. 运行脚本:python tftp_repro.py。这将在端口6969上启动一个恶意TFTP服务器。
  3. 在另一个终端中,运行curl命令从这个服务器获取文件:curl tftp://localhost:6969/test(注意:请确保使用的curl构建包含此易受攻击的代码。如果使用系统自带的curl测试,它可能不易受攻击或版本不同)。
  4. 服务器将发送一个blksize=2048的OACK,然后是一个2048字节的DATA数据包。
  5. curl客户端将因堆溢出而崩溃或表现出未定义行为。

支持材料/引用

  • 易受攻击的代码位置lib/tftp.c,函数:tftp_parse_option_ack
    • 更新了state->blksize,但未重新分配state->spacket.datastate->rpacket.data
  • 复现脚本tftp_repro.py

影响

总结:此漏洞允许恶意TFTP服务器在客户端上引发堆缓冲区溢出。

  • 远程代码执行:通过精心构造DATA数据包中的有效负载,攻击者可以覆盖关键的堆元数据或函数指针,可能导致在受害者机器上以curl进程的权限执行任意代码。
  • 拒绝服务:溢出可能破坏内存结构,导致curl应用程序崩溃或行为不可预测,从而引发拒绝服务。

互动时间线

  • 2天前helspy 向curl提交了报告。

  • 2天前bagder(curl团队成员)回复:“感谢您的报告!我们将花些时间调查您的报告,并尽快给您回复详细信息和可能的后续问题!很可能在接下来的24小时内。我们一直致力于尽快修复报告的问题。低或中等严重性的问题我们会合并到普通发布周期的下一个版本中。只有更严重的问题我们才可能提前发布修复。”

  • 2天前bagder 指出:“@helspy 没有附带 tftp_repro.py 脚本吗?”

  • 1天前helspy 道歉并附上了复现脚本代码,同时进行了补充说明:

    “似乎在tftp_parse_option_ack中,当服务器发送一个新的块大小时,代码会检查: if(blksize > state->requested_blksize) { failf(data, "server requested blksize larger than allocated"); return CURLE_TFTP_ILLEGAL; } 这个检查强制客户端使用一个比最初分配的更大的块大小,因此可能防止堆溢出。我在撰写报告时(提交得太早)同时尝试复现,这是个错误。抱歉,你们可以关闭这个问题了。”

  • 1天前bagder 关闭了报告并将状态更改为 “Not Applicable”(不适用),并评论:“不被视为安全问题。”

  • 1天前bagder 请求披露此报告:“根据项目的透明度政策,我们希望所有报告都被披露并公开。”

  • 3小时前bagder 披露了此报告。

报告元数据

  • 报告时间:2025年11月29日,下午5:06(UTC)
  • 报告人:helspy
  • 报告对象:curl
  • 报告ID:#3444904
  • 严重性:严重(9 ~ 10)
  • 披露时间:2025年12月1日,上午7:41(UTC)
  • 弱点类型:堆溢出
  • CVE ID:无
  • 赏金:无

附:复现脚本代码

 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
import socket
import struct

PORT = 6969
blksz= 2048
def start_server():
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.bind(('0.0.0.0', PORT))
    print(f" server started on port {PORT}")
    while True:
        data, addr = sock.recvfrom(1024)
        print(addr)
        opcode = struct.unpack('!H', data[:2])[0]

        if opcode == 1 or opcode == 2:
            print("Sending oack")
            oack = struct.pack('!H', 6) + b'blksize\0' + str(blksz).encode() + b'\0'
            sock.sendto(oack, addr)
            if opcode == 1:
                print("sending data packet")
                payload = b'A' * blksz
                data_pkt = struct.pack('!HH', 3, 1) + payload
                sock.sendto(data_pkt, addr)
if __name__ == "__main__":
    start_server()
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计