报告 #3442060 - cURL项目状态机中的无限循环问题
时间线
kak1 向 cURL 提交了一份报告。9小时前
摘要
漏洞影响:当 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] 调用 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] 调用 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
|
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; /* real error */
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;
/* 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
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
|
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
/* If CURLE_RECV_ERROR happens early enough, we assume it was a race
* condition and the server closed the reused connection exactly when we
* wanted to use it, so figure out if that is indeed the case.
*/
CURLcode ret = Curl_retry_request(data, &newurl);
if(!ret)
retry = !!newurl;
else if(!result)
result = ret;
if(retry) {
/* if we are to retry, set the result to OK and consider the
request as done */
result = CURLE_OK;
data->req.done = TRUE;
}
}
...
if(result) { //[6] result==CURLE_OK
/*
* The transfer phase returned error, we mark the connection to get closed
* to prevent being reused. This is because we cannot possibly know if the
* connection is in a good shape or not now. Unless it is a protocol which
* uses two "channels" like FTP, as then the error happened in the data
* connection.
*/
if(!(data->conn->handler->flags & PROTOPT_DUAL) &&
result != CURLE_HTTP2_STREAM)
streamclose(data->conn, "Transfer returned error");
multi_posttransfer(data);
multi_done(data, result, TRUE);
}
else if(data->req.done && !Curl_cwriter_is_paused(data)) { //[7] data->req.done == 0
const struct Curl_handler *handler = data->conn->handler;
/* call this even if the readwrite function returned error */
multi_posttransfer(data);
/* When we follow redirects or is set to retry the connection, we must to
go back to the CONNECT state */
if(data->req.newurl || retry) {
...
else { /* not errored, not done */
mspeed_check(data, nowp); // [8]
}
|
由于没有接收到数据,data->req.done 标志保持为 0。此外,因为 result 是 CURLE_OK,[5]、[6] 和 [7] 处的条件检查都评估为 false。代码随后继续执行到 [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
|
复现步骤
- 下载附带的
./ftp_poc.py 文件并运行:sudo python3 ./ftp_poc.py。这将在当前机器的端口 21 上启动一个恶意的 FTP 服务。
- 运行以下 cURL 命令,将命令中的 192.168.23.1 替换为您自己的恶意 FTP 服务器地址:
1
|
./curl -u anonymous:123 'ftp://192.168.23.1/test' -o ./test
|
cURL 程序将进入无限代码循环,并且不会自行退出。
影响
总结:当 cURL 尝试从恶意 FTP 服务器下载文件时,会触发代码执行中的无限循环。
附件
1 个附件:ftp_poc.py
讨论与官方回复
bagder (cURL 工作人员) 发布了评论。 9小时前
感谢您的报告!
我们将花一些时间调查您的报告,并尽快向您反馈详细信息和可能的后续问题!很可能在 24 小时内。
我们始终努力尽快修复报告的问题。严重性为低或中等的问题,我们会将其合并到普通发布周期中的下一个版本。只有更严重的问题我们才会提前发布修复。
bagder (cURL 工作人员) 发布了评论。 9小时前
@kak1 这与服务器只是停止发送任何数据,并且 cURL 只是坐在那里等待更多数据直到永远(除非提供了额外的超时选项)有什么区别?这似乎是 cURL 的设计工作方式?
dgustafsson (cURL 工作人员) 发布了评论。 9小时前
如果文件传输是无限的,那么 cURL 的正确行为是将其下载到无限。
kak1 发布了评论。 9小时前
区别在于,在这个问题中,即使手动设置了超时,cURL 进程也不会终止。但是,我没有在循环中找到任何内存分配代码,因此不会有内存泄漏。目前,它只能导致 CPU 使用率。如果此类问题不被视为安全漏洞,请关闭此报告。
bagder (cURL 工作人员) 发布了评论。 9小时前
即使手动设置了超时,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 发布了评论。 9小时前
请使用我提供的 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 发布了评论。 9小时前
在添加了 -m 2 参数后,cURL 超时了。看来这个问题可以缓解。请关闭此报告。
bagder (cURL 工作人员) 发布了评论。 更新于 9小时前
您需要使用 -m (--max-time)。--connect-timeout 对您的情况没有任何作用,因为它能及时连接。
bagder (cURL 工作人员) 关闭了报告并将状态更改为“不适用”。 9小时前
被认为不是安全问题。
bagder (cURL 工作人员) 请求披露此报告。 9小时前
根据项目的透明政策,我们希望所有报告都被披露并公开。
bagder (cURL 工作人员) 披露了此报告。 8小时前
报告详情
- 报告时间: 2025年11月26日,UTC时间上午8:34
- 报告人: kak1
- 报告对象: cURL
- 参与者: 无
- 报告ID: #3442060
- 严重性: 无评级 (—)
- 披露时间: 2025年11月26日,UTC时间上午9:32
- 弱点: 无
- CVE ID: 无
- 赏金: 无
- 账户详情: 无