cURL 状态机无限循环漏洞:恶意 FTP 服务器可触发代码执行死循环

本文详细披露了在cURL项目中发现的FTP功能漏洞。当cURL尝试从恶意FTP服务器下载文件时,其状态机会陷入无限循环。报告通过深入分析相关源码,揭示了触发条件与影响,并提供了复现步骤。

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

报告人: kak1 提交时间: 5天前 目标项目: 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
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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] 调用 Curl_sendrecv

  if(data->req.done || (result == CURLE_RECV_ERROR)) {
    /* 如果 CURLE_RECV_ERROR 发生得足够早,我们假设这是一个竞态条件,
       服务器恰好在我们要使用时关闭了重用的连接,因此请确认情况是否确实如此。 */
    CURLcode ret = Curl_retry_request(data, &newurl);
    if(!ret)
      retry = !!newurl;
    else if(!result)
      result = ret;

    if(retry) {
      /* 如果要重试,将结果设置为 OK 并将请求视为已完成 */
      result = CURLE_OK;
      data->req.done = TRUE;
    }
  }
...

Curl_sendrecv 函数在 [2] 处调用 sendrecv_dl

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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;
  }

  /* 如果我们有一个可读的套接字,或者流被回退(在这种情况下,我们在缓冲区中有数据),
     我们就继续执行读取操作 */
  if(k->keepon & KEEP_RECV) {
    result = sendrecv_dl(data, k); //[2] 调用 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
12
13
14
15
16
17
18
19
20
21
22
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] 调用 xfer_recv_resp
    if(nread < 0) {
      if(CURLE_AGAIN != result)
        goto out; /* 真实错误 */
      rcvd_eagain = TRUE;
      result = CURLE_OK; //[4] 将 result 设置为 CURLE_OK
      if(data->req.download_done && data->req.no_body &&
         !data->req.resp_trailer) {
        DEBUGF(infof(data, "EAGAIN, download done, no trailer announced, "
               "not waiting for EOS"));
        nread = 0;
        /* 继续执行,就像我们收到了 EOS 一样 */
      }
      else
        break; /* 跳出循环 */
    }
...

让我们继续检查 state_performing 函数:

 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
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);

  if(data->req.done || (result == CURLE_RECV_ERROR)) { // [5] data->req.done == 0, result==CURLE_OK
    /* 如果 CURLE_RECV_ERROR 发生得足够早,我们假设这是一个竞态条件,
       服务器恰好在我们要使用时关闭了重用的连接,因此请确认情况是否确实如此。 */
    CURLcode ret = Curl_retry_request(data, &newurl);
    if(!ret)
      retry = !!newurl;
    else if(!result)
      result = ret;

    if(retry) {
      /* 如果要重试,将结果设置为 OK 并将请求视为已完成 */
      result = CURLE_OK;
      data->req.done = TRUE;
    }
  }
...

由于没有接收到数据,data->req.done 标志保持为 0。此外,因为 resultCURLE_OK,所以 [5][6][7] 处的条件检查都为假。代码随后执行到 [8](没有错误,没有完成)。

1
2
3
4
5
6
7
8
  ...
  else { /* not errored, not done */
    mspeed_check(data, nowp); // [8]
  }
  free(newurl);
  *resultp = result;
  return rc;
}

这导致 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) 在 5 天前发表评论:

谢谢您的报告! 我们将花一些时间调查您的报告,并尽快向您提供详细信息以及可能的后续问题!很可能在接下来的 24 小时内。 我们始终致力于尽快修复报告的问题。低严重性或中严重性的问题,我们会将其合并到普通发布周期的下一个版本中。只有更严重的问题我们才会提前发布修复。

bagder (curl staff) 在 5 天前发表评论:

@kak1 这与服务器只是停止发送任何数据,并且 cURL 只是坐在那里等待更多数据直到永远(除非提供了额外的超时选项)有什么不同?这似乎是 cURL 的设计工作方式?

dgustafsson (curl staff) 在 5 天前发表评论:

如果文件传输是无限的,那么 cURL 的正确行为就是无限下载它。

kak1 在 5 天前发表评论:

区别在于,在这个问题中,即使手动设置了超时,cURL 进程也不会终止。然而,我在循环中没有找到任何内存分配代码,所以不会发生内存泄漏。目前,它只能导致 CPU 使用率。如果此类问题不被视为安全漏洞,请关闭此报告。

bagder (curl staff) 在 5 天前发表评论:

即使手动设置了超时,cURL 进程也不会终止 这与我使用您的 PoC 服务器时 cURL 的工作方式不符。它正常超时了:

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 在 5 天前发表评论:

请使用我提供的 PoC 文件和命令,因为触发此问题需要特定的协议交互。我仅为此目的调整了提供的命令。

kak1 在 5 天前发表评论:

在添加了 -m 2 参数后,cURL 确实超时了。看来这个问题可以缓解。请关闭此报告。

bagder (curl staff) 在 5 天前更新了评论:

您需要使用 -m (--max-time)。--connect-timeout 对您的情况没有作用,因为它能及时连接成功。

bagder (curl staff) 在 5 天前关闭了报告并将状态更改为“Not Applicable”(不适用)。

bagder (curl staff) 在 5 天前请求披露此报告。

根据项目透明度政策,我们希望所有报告都被披露并公开。

bagder (curl staff) 在 5 天前披露了此报告。

报告时间:November 26, 2025, 8:34am UTC 报告人:kak1 报告对象:curl 报告 ID:#3442060 严重性:无评级 (—) 披露时间:November 26, 2025, 9:32am UTC 弱点:无 CVE ID:无 奖金:无

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计