libcurl POST缓冲区释放后使用漏洞分析

本文详细分析了libcurl中存在的POST缓冲区释放后使用漏洞,包括漏洞复现步骤、影响版本、技术原理和修复方案。通过AddressSanitizer工具检测到堆内存释放后读取问题,并提供了完整的概念验证代码。

报告 #3355213 - POST主体缓冲区在传输前释放导致的释放后使用漏洞

时间线

giant_anteater 向curl提交报告 - 9天前

摘要

我通过设置CURLOPT_POSTFIELDSIZE和CURLOPT_POSTFIELDS到堆缓冲区,然后在curl_easy_perform之前释放该缓冲区,在本地复现了libcurl中的堆释放后使用漏洞。AddressSanitizer(ASan)在请求发送路径中报告了堆释放后读取。这展示了当传递给libcurl的POST数据缓冲区在传输之前被释放时发生的错误类型。

AI使用:PoC和验证在我的机器上手动执行。AI辅助用于帮助构建此报告。

受影响版本

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

从CMake配置(在构建期间观察):

  • 协议:dict file ftp ftps gopher gophers http https imap imaps ipfs ipns ldap ldaps mqtt pop3 pop3s rtsp scp sftp smb smbs smtp smtps telnet tftp ws wss
  • 功能:alt-svc AsynchDNS brotli HSTS HTTP2 HTTPS-proxy IDN IPv6 Largefile libz NTLM SSL threadsafe TLS-SRP UnixSockets zstd
  • 启用的SSL后端:OpenSSL v3+

复现步骤

这些是我在本地运行以在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程序(反映误用:设置POSTFIELDSIZE,将POSTFIELDS设置为malloc的缓冲区,然后在perform之前释放它):

 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. 针对本地构建的ASan libcurl编译和运行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

支持材料/参考

以下是我在运行PoC时观察到的实际ASan报告:

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

0xXXXXXXXXXXXX is located 0 bytes inside of 9-byte region [...] 
freed by thread T0 here:
    #0 0xXXXXXXXX in wrap_free (libclang_rt.asan_osx_dynamic.dylib+0xXXXXX)
    #1 0xXXXXXXXX in main (poc_uaf_local+0xXXXX)

previously allocated by thread T0 here:
    #0 0xXXXXXXXX in wrap_malloc (libclang_rt.asan_osx_dynamic.dylib+0xXXXXX)
    #1 0xXXXXXXXX in main (poc_uaf_local+0xXXXX)

SUMMARY: AddressSanitizer: heap-use-after-free in __asan_memcpy
==XXXXX==ABORTING

此错误使用上述步骤一致复现。

代码位置(观察到的)

  • ASan标记UAF的读取路径(基于回溯):lib/sendf.c中的cr_buf_read,在发送期间请求主体缓冲区被memcpy复制的位置,例如围绕此函数:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
1300:1323:curl/lib/sendf.c
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;

关于用户提供的POST数据如何存储的上下文:lib/setopt.c更新s->postfields并管理STRING_COPYPOSTFIELDS在CURLOPT_COPYPOSTFIELDS / CURLOPT_POSTFIELDS上的所有权。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
case CURLOPT_COPYPOSTFIELDS:
  ...
  s->postfields = s->str[STRING_COPYPOSTFIELDS];
  s->method = HTTPREQ_POST;
  break;

case CURLOPT_POSTFIELDS:
  s->postfields = ptr;
  /* Release old copied data. */
  Curl_safefree(s->str[STRING_COPYPOSTFIELDS]);
  s->method = HTTPREQ_POST;
  break;

OS/400 CCSID包装器(根本原因模式)

当处理具有显式大小和转换的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的具体风险。

讨论时间线

bagder (curl staff) 发表评论 - 9天前:

谢谢您的报告!我们将花时间调查您的报告,并尽快回复您详细信息和可能的后续问题!很可能在接下来的24小时内。

bagder (curl staff) 发表评论 - 9天前:

引用文档: 指向的数据不会被库复制:因此,调用应用程序必须保留它,直到相关的传输完成。 按文档工作

bagder (curl staff) 关闭报告并将状态更改为不适用 - 9天前

giant_anteater 发表评论 - 9天前:

完全同意通过CURLOPT_POSTFIELDS释放缓冲区是文档化的误用。 但此报告是关于curl自己的OS/400 CCSID包装器在将其交给libcurl后释放缓冲区,导致即使应用程序正确使用包装器也会发生UAF:

在packages/OS400/ccsidcurl.c中,case CURLOPT_COPYPOSTFIELDS(显式大小+转换路径):

  • 它将转换后的缓冲区传递给非复制setter并存储它
  • 然后在返回之前释放相同的缓冲区

因为CURLOPT_POSTFIELDS不复制,包装器本身违反了生命周期要求,并在传输期间使libcurl具有悬空指针。

bagder (curl staff) 发表评论 - 8天前:

curl_easy_setopt_ccsid()函数是否实际使用过?它似乎是为EBCDIC制作的,我们在几年前放弃了对它的支持。

giant_anteater 发表评论 - 8天前:

这不是关于应用程序释放CURLOPT_POSTFIELDS。UAF发生在正确使用curl自己的OS/400 CCSID包装器时。 存储库仍然提供包装器和调用它的示例。

bagder (curl staff) 重新打开此报告 - 7天前

monnerat 加入此报告作为具有默认权限的参与者 - 7天前

monnerat 发表评论 - 7天前:

我确认OS400代码中的问题:在分配给s后,cp应该被清除。 然而,我没有看到攻击者如何使用它,因为这不依赖于输入数据。 此外,它在释放句柄时触发双重释放错误。

giant_anteater 发表评论 - 7天前:

我的总结:这是在OS/400包装器(packages/OS400/ccsidcurl.c)中的确定性UAF/双重释放,当CURLOPT_COPYPOSTFIELDS与显式大小+非ASCII转换一起使用时。包装器将堆缓冲区交给libcurl然后释放它,导致:

  • 在POST主体发送期间的UAF(潜在的堆数据暴露)
  • 在句柄清理时的双重释放(崩溃/DoS)

修复:如@monnerat所述,在所有权转移后清除cp

bagder (curl staff) 关闭报告并将状态更改为信息性 - 7天前:

非常感谢您的报告和对此的工作@giant_anteater! 不被视为安全问题。由于以下原因按重要性降序排列:

  1. 此处使用的函数既不是公共也不是文档化的API函数
  2. 如果仍然触发带有错误的代码路径,问题会在作业日志中记录从而揭示它
  3. 问题不太可能被攻击者触发,并且以受控方式利用问题泄漏敏感数据几乎不可能发生

giant_anteater 发表评论 - 7天前:

听起来不错,您最有资格判断其实际重要性和影响@bagder,因此我不会质疑您的推理。非常感谢您花时间处理这个问题!-Stan

bagder (curl staff) 请求披露此报告 - 7天前

monnerat 发表评论 - 6天前:

bagder (curl staff) 披露此报告 - 6天前

报告详情

  • 报告时间:2025年9月23日,下午3:08 UTC
  • 报告者:giant_anteater
  • 报告给:curl
  • 报告ID:#3355213
  • 状态:信息性
  • 严重性:中等(4 ~ 6.9)
  • 披露时间:2025年9月26日,上午7:04 UTC
  • 弱点:释放后使用
  • CVE ID:无
  • 赏金:无
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计