Curl项目状态机无限循环漏洞深度解析

本文详细分析了curl工具在处理恶意FTP服务器的EPSV模式响应时,其状态机可能陷入无限循环的安全漏洞。文章包含漏洞触发条件、代码执行流程剖析以及复现步骤,为网络安全研究人员提供了深入的技术参考。

报告ID #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
17
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
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;
  }

  /* 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
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] 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
      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;
        /* continue as if we received the EOS */
      }
      else
        break; /* get out of loop */
    }
...

让我们继续检查 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
static CURLMcode state_performing(struct Curl_easy *data,
                                  struct curltime *nowp,
                                  bool *stream_errorp,
                                  CURLcode *resultp)
{
...
  /* 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
...
  }

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

由于没有接收到任何数据,data->req.done 标志保持为 0。此外,由于 resultCURLE_OK,[5]、[6] 和 [7] 处的条件检查结果都为假。代码随后进入 [8] 处(未出错,未完成)。

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

受影响版本 curl : https://github.com/curl/curl/archive/refs/tags/curl-8_17_0.tar.gz 平台 : ubuntu22.04

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服务器下载文件时,会触发代码执行中的无限循环。


后续讨论摘要

报告者 (kak1) 提交报告后,bagder (curl 团队成员) 首先表示感谢并告知会进行调查。

bagder 提问:这与服务器直接停止发送数据而curl无限等待的情况有何不同?这似乎是curl的设计工作方式?

dgustafsson (另一位curl成员) 评论:如果文件传输是无限的,那么curl的正确行为就是无限下载。

kak1 回应:区别在于,在此问题中,即使手动设置了超时,curl进程也不会终止。但他在循环中未找到内存分配代码,因此不会发生内存泄漏。目前它只会导致CPU使用率升高。如果此类问题不被视为安全漏洞,请关闭此报告。

bagder 测试并回应

“即使手动设置了超时,curl进程也不会终止” —— 这与我使用你的PoC服务器测试curl的行为不符。它可以正常超时:

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

kak1 澄清:请使用我提供的PoC文件和命令,因为触发此问题需要特定的协议交互。我只是为了此目的调整了提供的命令。并提供了PoC交互和测试命令的详细日志,显示curl在超时设置下仍在运行。

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

bagder 指出:你需要使用 -m (--max-time)--connect-timeout 在你的情况下不起作用,因为它能及时连接。

bagder 关闭报告,并将状态更改为“不适用”。认为这不是一个安全问题。

bagder 请求公开此报告:根据项目的透明政策,我们希望所有报告都被公开。

bagder 公开了此报告

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