cURL库POST数据缓冲区释放后使用漏洞分析

本文详细分析了cURL库中一个POST数据缓冲区释放后使用漏洞,包括漏洞复现步骤、技术原理和影响评估,涉及堆内存管理和网络安全风险。

cURL库POST数据缓冲区释放后使用漏洞分析

报告摘要

我在libcurl中本地复现了一个堆释放后使用漏洞,通过设置CURLOPT_POSTFIELDSIZE和CURLOPT_POSTFIELDS指向堆缓冲区,然后在curl_easy_perform之前释放该缓冲区。AddressSanitizer(ASan)在请求发送路径中报告了堆释放后使用读取错误。

受影响版本

从GitHub仓库获取的版本(CMake配置报告:curl版本=[8.17.0-DEV]),在macOS上使用AppleClang和ASan构建。

复现步骤

1. 配置和构建带AddressSanitizer的libcurl

1
2
3
4
5
6
7
8
cmake -S . -B build-asan \
  -DCMAKE_BUILD_TYPE=RelWithDebInfo \
  -DBUILD_SHARED_LIBS=ON \
  -DCURL_USE_LIBPSL=OFF \
  -DCMAKE_C_FLAGS='-fsanitize=address -fno-omit-frame-pointer' \
  -DCMAKE_SHARED_LINKER_FLAGS='-fsanitize=address' \
  -DCMAKE_EXE_LINKER_FLAGS='-fsanitize=address'
cmake --build build-asan -j 8

2. 启动本地HTTP服务器

1
python3 -m http.server 18080 --bind 127.0.0.1 &

3. 创建PoC程序

 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
#include <curl/curl.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>

int main(void) {
  curl_global_init(CURL_GLOBAL_DEFAULT);
  CURL *h = curl_easy_init();
  if(!h) return 1;

  const char *url = "http://127.0.0.1:18080/";
  curl_easy_setopt(h, CURLOPT_URL, url);
  curl_easy_setopt(h, CURLOPT_VERBOSE, 1L);
  curl_easy_setopt(h, CURLOPT_CONNECTTIMEOUT_MS, 2000L);
  curl_easy_setopt(h, CURLOPT_TIMEOUT_MS, 3000L);

  const char payload[] = "test data";
  size_t n = sizeof(payload) - 1;

  char *cp = (char*)malloc(n);
  memcpy(cp, payload, n);

  curl_easy_setopt(h, CURLOPT_POSTFIELDSIZE, (long)n);
  curl_easy_setopt(h, CURLOPT_POSTFIELDS, cp);

  free(cp); // libcurl发送请求体时发生释放后使用

  CURLcode res = curl_easy_perform(h);
  fprintf(stderr, "perform result: %d\n", (int)res);

  curl_easy_cleanup(h);
  curl_global_cleanup();
  return 0;
}

4. 编译和运行PoC

1
2
3
4
5
6
7
8
9
# 针对新构建的ASan libcurl编译PoC
cc -fsanitize=address -fno-omit-frame-pointer -I include \
   build-asan/poc_uaf.c build-asan/lib/libcurl.dylib \
   -o build-asan/poc_uaf_local

# 使用ASan运行并确保可执行文件找到本地libcurl
DYLD_LIBRARY_PATH=build-asan/lib \
ASAN_OPTIONS=halt_on_error=1:detect_invalid_pointer_pairs=1:strict_string_checks=1:strict_memcmp=1 \
./build-asan/poc_uaf_local

ASan报告

运行PoC时观察到的实际ASan报告:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
*   Trying 127.0.0.1:18080...
* Established connection to 127.0.0.1 (127.0.0.1 port 18080) from 127.0.0.1 port XXXXX 
* using HTTP/1.x
=================================================================
==XXXXX==ERROR: AddressSanitizer: heap-use-after-free on address 0xXXXXXXXXXXXX at pc 0xXXXXXXXXXXXX bp 0xXXXXXXXXXXXX sp 0xXXXXXXXXXXXX
READ of size 9 at 0xXXXXXXXXXXXX thread T0
    #0 0xXXXXXXXX in __asan_memcpy+0xXXX (libclang_rt.asan_osx_dynamic.dylib+0xXXXXX)
    #1 0xXXXXXXXX in cr_buf_read sendf.c:1316
    #2 0xXXXXXXXX in Curl_client_read sendf.c:1223
    #3 0xXXXXXXXX in add_from_client request.c:349
    #4 0xXXXXXXXX in Curl_bufq_sipn bufq.c:574
    #5 0xXXXXXXXX in Curl_req_send request.c:407
    #6 0xXXXXXXXX in Curl_http http.c:2926
    #7 0xXXXXXXXX in multi_runsingle multi.c:2508
    #8 0xXXXXXXXX in curl_multi_perform multi.c:2771
    #9 0xXXXXXXXX in curl_easy_perform easy.c:846
    #10 0xXXXXXXXX in main (poc_uaf_local+0xXXXX)

代码位置分析

ASan标记UAF的读取路径(基于回溯):lib/sendf.c中的cr_buf_read函数,在发送期间memcpy请求体缓冲区:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
static CURLcode cr_buf_read(struct Curl_easy *data,
                            struct Curl_creader *reader,
                            char *buf, size_t blen,
                            size_t *pnread, bool *peos)
{
  struct cr_buf_ctx *ctx = reader->ctx;
  size_t nread = ctx->blen - ctx->index;
  ...
  memcpy(buf, ctx->buf + ctx->index, nread);
  ...
  return CURLE_OK;
}

当前树中的确切memcpy行:

1
2
3
4
if(nread > blen)
  nread = blen;
memcpy(buf, ctx->buf + ctx->index, nread);
*pnread = nread;

OS/400 CCSID包装器问题

在OS/400包装器中处理CURLOPT_COPYPOSTFIELDS时,包装器将转换后的缓冲区传递给CURLOPT_POSTFIELDS,并将其分配给STRING_COPYPOSTFIELDS,但在返回之前释放了相同的缓冲区。

传递转换后的缓冲区并分配给STRING_COPYPOSTFIELDS:

1
2
3
result = curl_easy_setopt(easy, CURLOPT_POSTFIELDS, s);
data->set.str[STRING_COPYPOSTFIELDS] = s;   /* Give to library. */
break;

在函数结束时释放缓冲区:

1
2
3
va_end(arg);
free(cp);
return result;

影响分析

基于直接观察,在使用CURLOPT_POSTFIELDS(带有显式CURLOPT_POSTFIELDSIZE)设置后释放应用程序提供的POST体缓冲区,会导致libcurl在传输期间解引用已释放的内存,导致进程崩溃(堆释放后使用读取)。

虽然此演示是本地误用PoC,但它显示了在客户端在curl_easy_perform之前无意释放POST缓冲区时的具体内存损坏/DoS风险。

修复方案

简单的补丁提案:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
diff --git a/packages/OS400/ccsidcurl.c b/packages/OS400/ccsidcurl.c
index a9c04d5a4..258923189 100644
--- a/packages/OS400/ccsidcurl.c
+++ b/packages/OS400/ccsidcurl.c
@@ -1246,6 +1246,7 @@ curl_easy_setopt_ccsid(CURL *easy, CURLoption tag, ...)
 
       data->set.postfieldsize = pfsize;         /* Replace data size. */
       s = cp;
+      cp = NULL;
     }
 
     result = curl_easy_setopt(easy, CURLOPT_POSTFIELDS, s);

最终状态

该报告被标记为"信息性",因为它正确识别了一个已修复的错误,但由于以下原因不被视为安全问题:

  1. 使用的函数既不是公共API也不是文档化的API函数
  2. 如果触发带有错误的代码路径,问题会在作业日志中记录
  3. 攻击者不太可能触发此问题,且利用该问题以受控方式泄露敏感数据的可能性几乎不可能
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计