cURL 磁盘空间耗尽漏洞导致拒绝服务攻击分析

本文详细分析了cURL工具在使用--trace或--trace-ascii选项时可能导致的磁盘空间耗尽漏洞。攻击者可通过发送大量数据使调试日志无限增长,最终导致系统磁盘空间耗尽和服务中断。文章包含漏洞代码分析、POC示例和修复方案。

cURL | 报告 #3250490 - 磁盘空间耗尽导致拒绝服务(DoS) | HackerOne

时间线

tryhackplanet 向 curl 提交了一份报告 2025年7月14日,UTC时间上午2:20

描述

当使用 –trace 或 –trace-ascii 选项处理大量数据时,tool_debug_cb 函数可以向日志文件写入大量调试数据。如果攻击者能够使 cURL 下载或上传大量数据(例如通过非常大的 HTTP 响应或无限制上传),此调试函数生成的日志文件可能会无限增长。这可能导致运行 cURL 的系统磁盘空间耗尽,进而破坏同一服务器上运行的其他服务。

如果使用 –dump-header,此部分将原始 HTTP 头数据写入 heads->stream。如果 heads->stream 恰好是与跟踪输出相同的文件,或者是另一个具有无限增长潜力的文件,则会加剧此问题。

漏洞代码分析

代码段 1 (431 字节)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/* In tool_debug_cb */
if(per->config->headerfile && heads->stream) {
    size_t rc = fwrite(ptr, size, nmemb, heads->stream); // <-- 易受攻击的写入
    if(rc != cb)
      return rc;
    /* flush the stream to send off what we got earlier */
    if(fflush(heads->stream)) {
      errorf(per->config->global, "Failed writing headers to %s",
                     per->config->headerfile);
      return CURL_WRITEFUNC_ERROR;
    }
}

代码段 2 (976 字节)

 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
/* In tool_debug_cb, in the TRACE_PLAIN section */
case CURLINFO_HEADER_OUT:
    if(size > 0) {
        size_t st = 0;
        size_t i;
        for(i = 0; i < size - 1; i++) {
          if(data[i] == '\n') { /* LF */
            if(!newl) {
              log_line_start(output, timebuf, idsbuf, type);
            }
            (void)fwrite(data + st, i - st + 1, 1, output); // <-- 易受攻击的写入
            st = i + 1;
            newl = FALSE;
          }
        }
        if(!newl)
          log_line_start(output, timebuf, idsbuf, type);
        (void)fwrite(data + st, i - st + 1, 1, output); // <-- 易受攻击的写入
    }
    newl = (size && (data[size - 1] != '\n'));
    traced_data = FALSE;
    break;
case CURLINFO_TEXT:
case CURLINFO_HEADER_IN:
    if(!newl)
      log_line_start(output, timebuf, idsbuf, type);
    (void)fwrite(data, size, 1, output); // <-- 易受攻击的写入
    newl = (size && (data[size - 1] != '\n'));
    traced_data = FALSE;
    break;

代码段 3 (283 字节)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
case CURLINFO_DATA_OUT:
case CURLINFO_DATA_IN:
case CURLINFO_SSL_DATA_IN:
case CURLINFO_SSL_DATA_OUT:
    if(!traced_data) {
        // ...
        fprintf(output, "[%zu bytes data]\n", size); // <-- 易受攻击的写入
        newl = FALSE;
        traced_data = TRUE;
    }
    break;

代码段 4 (907 字节)

 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
/* In function dump */
// ...
fprintf(stream, "%s%s%s, %zu bytes (0x%zx)\n", timebuf, idsbuf,
             text, size, size); // <-- 易受攻击的写入(初始行)

for(i = 0; i < size; i += width) {

    fprintf(stream, "%04zx: ", i); // <-- 易受攻击的写入(行前缀)

    if(tracetype == TRACE_BIN) {
        for(c = 0; c < width; c++)
            if(i + c < size)
              fprintf(stream, "%02x ", ptr[i + c]); // <-- 易受攻击的写入(十六进制数据)
            else
              fputs("   ", stream); // <-- 易受攻击的写入(填充)
    }

    for(c = 0; (c < width) && (i + c < size); c++) {
        // ...
        fprintf(stream, "%c", ((ptr[i + c] >= 0x20) && (ptr[i + c] < 0x7F)) ?
                     ptr[i + c] : UNPRINTABLE_CHAR); // <-- 易受攻击的写入(ASCII表示)
        // ...
    }
    fputc('\n', stream); // <-- 易受攻击的写入(换行符)
}
fflush(stream);

本质上,漏洞不是 fwrite 或 fprintf 调用本身的错误,而是缺少围绕这些调用的保护包装器或检查来限制写入跟踪日志文件的累积数据。修复涉及添加缺失的检查。

POC

无限制数据服务器设置 [unli.py]

 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
import http.server
import socketserver
import time

PORT = 8002

class MyHandler(http.server.SimpleHTTPRequestHandler):
    def do_GET(self):
        print(f"[{time.ctime()}] Received GET request from {self.client_address[0]}")
        self.send_response(200)
        self.send_header("Content-Type", "application/octet-stream")
        self.send_header("Transfer-Encoding", "chunked")
        # FIX: Changed self0 to self
        self.end_headers()

        # Stream data continuously
        try:
            i = 0
            while True:
                chunk = f"This is chunk {i}: {'A' * 1024}\n".encode('utf-8') # 1KB of data per chunk
                self.wfile.write(f"{len(chunk):X}\r\n".encode('ascii')) # Chunk size in hexadecimal
                self.wfile.write(chunk)
                self.wfile.write(b"\r\n")
                self.wfile.flush()
                i += 1
                # Optional: add a small delay to observe file growth more easily
                # time.sleep(0.01)
        except Exception as e:
            print(f"[{time.ctime()}] Client disconnected or error: {e}")
        finally:
            # End chunked encoding
            self.wfile.write(b"0\r\n\r\n")
            self.wfile.flush()

with socketserver.TCPServer(("", PORT), MyHandler) as httpd:
    print(f"serving at port {PORT}")
    try:
        httpd.serve_forever()
    except KeyboardInterrupt:
        print("\nServer shutting down.")

使用跟踪选项运行 curl

打开第二个终端并执行 curl 命令:

1
curl http://localhost:8002/test.txt -o /dev/null --trace output.log
1
2
3
4
└─# curl http://localhost:8002/test.txt -o /dev/null --trace output.log
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 37.0M    0 37.0M    0     0  7857k      0 --:--:--  0:00:04 --:--:-- 7856k

您现在将观察到 output.log 文件大小迅速增加,因为服务器持续流式传输数据,而 curl 记录每一个比特。这演示了磁盘空间耗尽导致拒绝服务(DoS)。

如何修复

为了解决您演示的磁盘空间耗尽漏洞,主要修复需要关注在无任何大小限制的情况下向跟踪日志文件写入数据的代码部分。在您提供的代码片段中,最相关的实现位置是:

在每次指向 output(或 heads->stream)的 fwrite() 或 fprintf() 操作之前:这是实际数据写入文件的地方。您需要在此处添加文件大小检查。

在 dump() 函数内部:dump() 函数负责写入格式化的二进制或 ASCII 数据。这是一个关键位置,因为大量数据在此处处理并写入输出。

以下是需要修改的代码中的特定区域:

a. 在 tool_debug_cb() 中:

 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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
int tool_debug_cb(CURL *handle, curl_infotype type,
                  char *data, size_t size,
                  void *userdata)
{
  struct OperationConfig *operation = userdata;
  struct GlobalConfig *global = operation->global;
  FILE *output = tool_stderr;
  // ... 其他变量声明 ...

  // --- 开始修复 ---
  // 确保跟踪流已打开并且我们设置了大小限制
  if (global->trace_stream && global->trace_fopened && global->max_trace_log_size > 0) {
      // 估计此数据块要写入的数据大小
      // 需要创建此 `estimated_write_size` 函数。
      // 它应考虑格式化开销(例如,十六进制转储占用更多空间)。
      size_t estimated_write_size = estimate_formatted_data_size(type, size, global->tracetype);

      // 检查当前写入是否会超过限制
      if (global->current_trace_log_size + estimated_write_size > global->max_trace_log_size) {
          // 关闭文件并禁用跟踪以防止进一步写入
          warnf(global, "Trace log file '%s' reached maximum size (%s). Stopping further trace logging.",
                global->trace_dump, /* format size with a helper like tool_strbytel(global->max_trace_log_size) */);
          fclose(global->trace_stream);
          global->trace_stream = NULL; // 确保不再发生写入
          global->trace_fopened = FALSE;
          // 您可能还想完全禁用跟踪选项
          global->tracetype = TRACE_NONE;
          global->trace_dump = NULL;
          return 0; // 停止回调进一步处理
      }
  }
  // --- 结束修复 ---

  // ... tool_debug_cb 代码的其余部分 ...

  // 写入 headerfile 的部分:
  if(per->config->headerfile && heads->stream) {
    size_t rc = fwrite(ptr, size, nmemb, heads->stream);
    // --- 开始修复(条件性)---
    // 如果 heads->stream 是指定的 trace_stream,则更新 current_trace_log_size。
    // 这可能需要检查 heads->stream 是否与 global->trace_stream 相同。
    // 或者,如果 `headerfile` 是与 `trace_dump` 不同的概念,
    // 它可能需要自己的大小限制机制。为简单起见,我们假设
    // 'output' 是要监视的主要流。
    if (heads->stream == global->trace_stream) { // 示例条件
        global->current_trace_log_size += rc;
    }
    // --- 结束修复 ---
    if(rc != cb)
      return rc;
    // ...
  }

  // TRACE_PLAIN 部分 (CURLINFO_HEADER_OUT, CURLINFO_TEXT, CURLINFO_HEADER_IN, 等):
  // 在此部分的每个 `fwrite` 或 `fprintf` 位置:
  // 在每个成功的 `fwrite` 或 `fprintf` 调用之后:
  // global->current_trace_log_size += <写入的字节数>;
  // CURLINFO_TEXT/HEADER_IN 的示例:
  // (void)fwrite(data, size, 1, output);
  // if (output == global->trace_stream) { // 检查 output 是否是跟踪流
  //     global->current_trace_log_size += size;
  // }

  // ...
  // TRACE_PLAIN 中的 CURLINFO_DATA_OUT/IN/SSL_DATA_OUT/IN:
  // fprintf(output, "[%zu bytes data]\n", size);
  // if (output == global->trace_stream) { // 检查
  //     global->current_trace_log_size += strlen("[X bytes data]\n") + (size 中的数字位数); // 近似值
  // }

  // ... 调用 dump() ...
  // dump() 函数将在内部处理其自己的大小增量,
  // 或者它可以返回写入的字节数供 tool_debug_cb 添加。
  dump(timebuf, idsbuf, text, output, (unsigned char *) data, size,
       global->tracetype, type, global); // 将全局配置传递给 dump
  return 0;
}

b. 在 dump() 函数中:

 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
55
56
57
58
59
60
61
62
63
64
65
66
static void dump(const char *timebuf, const char *idsbuf, const char *text,
                 FILE *stream, const unsigned char *ptr, size_t size,
                 trace tracetype, curl_infotype infotype, struct GlobalConfig *global) // 添加了全局参数
{
  size_t i;
  size_t c;
  unsigned int width = 0x10;

  if(tracetype == TRACE_ASCII)
    width = 0x40;

  // --- 开始修复 ---
  // 将初始行的大小添加到总数中
  int written_bytes = fprintf(stream, "%s%s%s, %zu bytes (0x%zx)\n", timebuf, idsbuf, text, size, size);
  if (global && stream == global->trace_stream && written_bytes > 0) {
      global->current_trace_log_size += written_bytes;
  }
  // --- 结束修复 ---


  for(i = 0; i < size; i += width) {
    // --- 开始修复 ---
    // 在写入每行之前检查限制
    if (global && stream == global->trace_stream && global->current_trace_log_size >= global->max_trace_log_size) {
        // 如果已经超过限制,只需中断并刷新剩余内容
        break;
    }
    // --- 结束修复 ---

    // 写入行头(例如,"0000: ")
    written_bytes = fprintf(stream, "%04zx: ", i);
    if (global && stream == global->trace_stream && written_bytes > 0) {
        global->current_trace_log_size += written_bytes;
    }

    if(tracetype == TRACE_BIN) {
      for(c = 0; c < width; c++)
        if(i + c < size) {
          written_bytes = fprintf(stream, "%02x ", ptr[i + c]);
          if (global && stream == global->trace_stream && written_bytes > 0) {
              global->current_trace_log_size += written_bytes;
          }
        } else {
          written_bytes = fputs("   ", stream);
          if (global && stream == global->trace_stream && written_bytes > 0) {
              global->current_trace_log_size += written_bytes;
          }
        }
    }

    for(c = 0; (c < width) && (i + c < size); c++) {
      // ... (CRLF 处理逻辑保持不变) ...
      written_bytes = fprintf(stream, "%c", ((ptr[i + c] >= 0x20) && (ptr[i + c] < 0x7F)) ?
                               ptr[i + c] : UNPRINTABLE_CHAR);
      if (global && stream == global->trace_stream && written_bytes > 0) {
          global->current_trace_log_size += written_bytes;
      }
      // ... (CRLF 处理逻辑) ...
    }
    written_bytes = fputc('\n', stream); // 换行
    if (global && stream == global->trace_stream && written_bytes > 0) {
        global->current_trace_log_size += written_bytes;
    }
  }
  fflush(stream);
}

附加说明

禁用跟踪:一旦达到限制,必须有效禁用跟踪机制(例如,通过设置 global->trace_stream = NULL 并将 global->tracetype 更改为 TRACE_NONE)以防止任何进一步的写入尝试。

影响

系统不稳定和崩溃(高影响)

附件

1 个附件 F4562562: image.png

评论

dfandrich curl staff 发表评论。2025年7月14日,UTC时间上午2:59 是的,确实,如果您将大量数据下载到磁盘,您将需要大量磁盘空间。但安全角度在哪里?调试日志不适用于生产系统,但如果应用程序希望使用它们,则必须有足够的资源来保存它们。通过资源使用进行 DoS 的问题已在 libcurl-security.md 下的文档中涵盖,健壮的应用程序必须考虑到这一点。

jimfuller2024 curl staff 发表评论。2025年7月14日,UTC时间上午3:55 除非我遗漏了什么,这看起来不像是特定于 curl 的安全问题。

bagder curl staff 发表评论。2025年7月14日,UTC时间上午6:34 认为不是安全问题。 我猜想是以某种方式使用 AI 编写了这段文本?

bagder curl staff 发表评论。2025年7月14日,UTC时间上午7:39 @tryhackplanet 请向我们解释您提交此报告的理由。您是如何发现它的?您是如何得出这是安全漏洞的结论的?

bagder curl staff 发表评论。2025年7月14日,UTC时间上午11:28 鉴于没有回答,我不得不得出结论,这又是另一个愚蠢的 AI 生成的谎言,人类只是转发给我们,没有过滤或应用思考过程。滥用是这个术语。

bagder curl staff 关闭了报告并将状态更改为垃圾邮件。2025年7月14日,UTC时间上午11:30 AI 垃圾

bagder curl staff 请求披露此报告。2025年7月14日,UTC时间上午11:30 根据项目的透明政策,我们希望所有报告都公开披露并公开。

bagder curl staff 披露了此报告。2025年7月14日,UTC时间上午11:31

报告信息

报告时间:2025年7月14日,UTC时间上午2:20 报告者:tryhackplanet 报告对象:curl 参与者: 报告ID:#3250490 状态:垃圾邮件 严重性:中等(4

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