Curl FTP状态机无限循环漏洞分析

本文详细分析了curl项目中FTP功能的状态机无限循环漏洞。当curl从恶意FTP服务器下载文件时,由于特定协议交互会导致代码进入无限循环,即使设置超时参数也无法正常终止进程。

报告 #3442060 - curl项目状态机中的无限循环问题

摘要

漏洞影响:当curl尝试从恶意FTP服务器下载文件时,会触发代码执行中的无限循环。

我在curl项目的FTP功能中发现了这个问题。根据https://github.com/curl/curl/blob/master/docs/cmdline-opts/disable-epsv.md中的描述,curl使用EPSV模式作为默认的FTP文件传输方法。简单来说,EPSV模式的工作方式如下:FTP服务器打开一个TCP端口并等待客户端连接,然后通过该端口向客户端发送数据。

技术分析

状态机问题

在curl的状态机中,/lib/multi.c中的state_performing函数在[1]处调用Curl_sendrecv函数从对等端接收数据:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
static CURLMcode state_performing(struct Curl_easy *data,
                                  struct curltime *nowp,
                                  bool *stream_errorp,
                                  CURLcode *resultp)
{
  char *newurl = NULL;
  bool retry = FALSE;
  CURLMcode rc = CURLM_OK;
  CURLcode result = *resultp = CURLE_OK;
  *stream_errorp = FALSE;

  if(mspeed_check(data, nowp) == CURLE_AGAIN)
    return CURLM_OK;

  /* read/write data if it is ready to do so */
  result = Curl_sendrecv(data, nowp); // [1] call Curl_sendrecv

Curl_sendrecv函数在[2]处调用sendrecv_dl

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
CURLcode Curl_sendrecv(struct Curl_easy *data, struct curltime *nowp)
{
  struct SingleRequest *k = &data->req;
  CURLcode result = CURLE_OK;

  DEBUGASSERT(nowp);
  if(Curl_xfer_is_blocked(data)) {
    result = CURLE_OK;
    goto out;
  }

  /* We go ahead and do a read if we have a readable socket or if the stream
     was rewound (in which case we have data in a buffer) */
  if(k->keepon & KEEP_RECV) {
    result = sendrecv_dl(data, k); //[2] call sendrecv_dl
    if(result || data->req.done)
      goto out;
  }

sendrecv_dl函数进一步调用xfer_recv_resp来接收数据。如果我们的恶意FTP服务器打开了一个EPSV端口但不向客户端发送任何数据,xfer_recv_resp将返回-1,并且result的值将被设置为CURLE_AGAIN。随后,在[4]处,result被设置为CURLE_OK。这意味着即使xfer_recv_resp未能接收到任何数据,sendrecv_dl函数仍然返回CURLE_OK

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
static CURLcode sendrecv_dl(struct Curl_easy *data,
                            struct SingleRequest *k)
{
...
    rcvd_eagain = FALSE;
    nread = xfer_recv_resp(data, buf, bytestoread, is_multiplex, &result); // [3] call xfer_recv_resp
    if(nread < 0) {
      if(CURLE_AGAIN != result)
        goto out; /* real error */
      rcvd_eagain = TRUE;
      result = CURLE_OK; //[4]set result to CURLE_OK

无限循环形成

继续检查state_performing函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
static CURLMcode state_performing(struct Curl_easy *data,
                                  struct curltime *nowp,
                                  bool *stream_errorp,
                                  CURLcode *resultp)
{
  // ... 省略部分代码
  
  if(data->req.done || (result == CURLE_RECV_ERROR)) { // [5] data->req.done == 0, result==CURLE_OK
    // ... 条件判断为false
  }

  if(result) { //[6] result==CURLE_OK
    // ... 条件判断为false
  }
  else if(data->req.done && !Curl_cwriter_is_paused(data)) { //[7] data->req.done == 0
    // ... 条件判断为false
  }
  else { /* not errored, not done */
    mspeed_check(data, nowp); // [8]
  }

由于没有接收到任何数据,data->req.done标志保持为0。此外,由于resultCURLE_OK,[5]、[6]和[7]处的条件检查都评估为false。代码随后继续执行到[8](没有错误,没有完成)。

这导致curl的当前状态机标志(data->mstate)保持不变,仍然设置为MSTATE_PERFORMING。随后,curl代码重复进入state_performing函数,导致无限循环。

受影响版本

1
2
3
4
5
➜  src ./curl -V 
curl 8.17.0-DEV (x86_64-pc-linux-gnu) libcurl/8.17.0-DEV zlib/1.2.11 libpsl/0.19.1
Release-Date: [unreleased]
Protocols: dict file ftp gopher http imap ipfs ipns mqtt pop3 rtsp smtp telnet tftp ws
Features: alt-svc AsynchDNS IPv6 Largefile libz PSL threadsafe UnixSockets

复现步骤

  1. 下载附带的./ftp_poc.py文件并运行:sudo python3 ./ftp_poc.py。这将在当前机器的21端口上启动一个恶意FTP服务。
  2. 运行以下curl命令,将命令中的192.168.23.1替换为您自己的恶意FTP服务器地址:
1
./curl -u anonymous:123 'ftp://192.168.23.1/test' -o ./test

curl程序将进入无限代码循环,不会自行退出。

影响

摘要:当curl尝试从恶意FTP服务器下载文件时,会触发代码执行中的无限循环。

讨论记录

bagder (curl staff) 评论:3小时前 “感谢您的报告!我们将花时间调查您的报告,并尽快向您提供详细信息和可能的后续问题!很可能在接下来的24小时内。

我们始终努力尽快修复报告的问题。严重性为低或中等的问题我们会合并到普通发布周期中的下一个版本中。只有更严重的问题我们可能会提前发布修复。”

bagder (curl staff) 评论:3小时前 “@kak1 这与服务器只是停止发送任何数据并且curl只是坐在那里等待更多数据直到时间结束(除非提供了额外的超时选项)有什么不同?这似乎是curl设计的工作方式?”

dgustafsson (curl staff) 评论:3小时前 “如果文件传输是无限的,那么curl的正确行为是无限下载它。”

kak1 评论:3小时前 “不同之处在于,在这个问题中,即使手动设置了超时,curl进程也不会终止。但是,我在循环中没有找到任何内存分配代码,所以不会有内存泄漏。目前,它只能导致CPU使用率。如果这样的问题不被视为安全漏洞,请关闭此报告。”

bagder (curl staff) 评论:3小时前 “即使手动设置了超时,curl进程也不会终止”

这与curl对我的POC服务器的工作方式不匹配。它可以正常超时:

1
2
3
4
5
6
$ curl ftp://localhost:9021/ -v -m2
...
curl: (28) Operation timed out after 2002 milliseconds with 0 bytes received
$ curl ftp://localhost:9021/RE -v -m2
...
curl: (28) Operation timed out after 2002 milliseconds with 0 out of 4 bytes received

kak1 评论:3小时前 “请使用我提供的PoC文件和命令,因为触发此问题需要特定的协议交互。我只为此目的调整了提供的命令。”

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
➜  curl sudo python3 ./ftp_poc.py
FTP server 0.0.0.0:21
waiting for connect...
client ('192.168.23.1', 28513) connect
-> 220 Welcome to Simple FTP Server
<- USER anonymous
-> 331 Please specify the password.
<- PASS 123
-> 230 Login successful.
<- PWD
-> 257 "/" is the current directory
<- EPSV
-> 229 Entering Extended Passive Mode (|||32971|)
EPSV port: 32971
<- TYPE I
-> 200 Switching to Binary mode.
<- SIZE test
-> 213 213 4
<- RETR test
-> 150 Opening BINARY mode data connection for test (4 bytes).
-> 226 Transfer complete.
1
2
3
4
➜  src ./curl -u anonymous:123 'ftp://192.168.23.1/test' -o ./test --connect-timeout 30
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     4   0     0   0     0     0     0  --:--:--  0:09:32 --:--:--     0

kak1 评论:3小时前 “添加-m 2参数后,curl超时了。看来这个问题可以缓解。请关闭此报告。”

bagder (curl staff) 评论:3小时前(更新) “您需要使用-m(–max-time)。–connect-timeout对您的情况没有任何作用,因为它及时连接良好。”

bagder (curl staff) 关闭报告并将状态更改为"不适用":3小时前 “被认为不是安全问题。”

bagder (curl staff) 请求披露此报告:3小时前 “根据项目的透明政策,我们希望所有报告都被披露并公开。”

bagder (curl staff) 披露此报告:2小时前

报告信息

  • 报告时间: 2025年11月26日 8:34 UTC
  • 报告者: kak1
  • 报告对象: curl
  • 报告ID: #3442060
  • 严重性: 无评级 (—)
  • 披露时间: 2025年11月26日 9:32 UTC
  • 弱点: 无
  • CVE ID: 无
  • 赏金: 无
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计