OpenSSL HTTP/3 中 CURLINFO_TLS_SSL_PTR 返回无效指针导致潜在安全风险

本文详细分析了 libcurl 8.15.0 版本中 OpenSSL HTTP/3 连接时 CURLINFO_TLS_SSL_PTR 返回无效 SSL 指针的问题,包括漏洞复现步骤、潜在影响(从拒绝服务到远程代码执行)以及相关修复讨论。

OpenSSL HTTP/3 bogus CURLINFO_TLS_SSL_PTR

摘要

curl_easy_getinfo CURLINFO_TLS_SSL_PTR 在 OpenSSL HTTP/3 连接中返回无效的 SSL 连接指针。使用此 SSL 连接会导致崩溃,并可能产生其他影响。

此问题在 libcurl 8.14.1 中未出现,表明该错误存在于 libcurl 本身(或 libcurl 8.14.1 未触发其他支持库中的错误)。

一些调试输出表明这可能是一个释放后使用/悬垂指针问题。如果是这样,此问题可能导致远程代码执行。在撰写本报告时,这一点尚未得到确认。

受影响版本

  • 8.15.0(发布版)
  • 8.15.1-DEV(7c23e88d17e0939b4e01c8d05f430e167e148f4b)

复现步骤

  1. 针对 OpenSSL 3.5.1 及足够新的 nghttp2、nghttp3 和 ngtcp2 编译 libcurl(--with-openssl --with-nghttp2 --with-ngtcp2 --with-nghttp3
  2. 使用 -fsanitize=address 编译以下概念验证应用:
 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
#include <curl/curl.h>
#include <openssl/ssl.h>
#include <stdio.h>

static size_t header_callback(char *buffer, size_t size,
                              size_t nitems, void *userdata)
{
  size_t actual = nitems * size;
  CURL *curl = (CURL *) userdata;
  size_t printactual = actual;

  if (printactual && buffer[printactual - 1] == '\n')
  {
    printactual--;
    if (printactual && buffer[printactual - 1] == '\r')
      printactual--;
  }
  printf("H: %.*s\n", (int) printactual, buffer);

  if ((actual == 1 && buffer[0] == '\n') ||
      (actual == 2 && buffer[0] == '\r' && buffer[1] == '\n'))
  {
    const struct curl_tlssessioninfo *info = NULL;
    CURLcode res = curl_easy_getinfo(curl, CURLINFO_TLS_SSL_PTR, &info);
    printf("D: headers completed, res %d info %p\n", res, info);
    if (info && !res)
    {
      printf("D: backend %d internals %p\n", info->backend,  info->internals);
      if(CURLSSLBACKEND_OPENSSL == info->backend)
      {
        SSL *ssl = info->internals;
        printf("D: OpenSSL ver. %s\n", SSL_get_version(ssl));
        STACK_OF(X509)* chain = SSL_get_peer_cert_chain(ssl);
        printf("D: chain %p\n", chain);
      }
    }
  }
  return actual;
}

int main(void)
{
  printf("libcurl version: %s\n", curl_version());

  CURL *curl = curl_easy_init();
  if (curl)
  {
    curl_easy_setopt(curl, CURLOPT_URL, "https://curl.se");
    curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, (long)CURL_HTTP_VERSION_3ONLY);
    curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, header_callback);
    curl_easy_setopt(curl, CURLOPT_HEADERDATA, curl);
    curl_easy_perform(curl);
    curl_easy_cleanup(curl);
  }

  return 0;
}
  1. 执行 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
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
$ ./curlpoc
libcurl version: libcurl/8.15.1-DEV OpenSSL/3.5.1 zlib/1.3.1 brotli/1.1.0 zstd/1.5.7 libidn2/2.3.8 libpsl/0.21.5 nghttp2/1.66.0 ngtcp2/1.14.0-DEV nghttp3/1.1 OpenLDAP/2.6.10
H: HTTP/3 200
H: content-length: 11441
H: server: nginx/1.27.5
H: content-type: text/html
H: x-frame-options: SAMEORIGIN
H: last-modified: Sun, 20 Jul 2025 02:11:09 GMT
H: etag: "2cb1-63a52df596e87"
H: cache-control: max-age=60
H: expires: Sun, 20 Jul 2025 02:14:40 GMT
H: x-content-type-options: nosniff
H: content-security-policy: default-src 'self' curl.haxx.se www.curl.se curl.se; style-src 'unsafe-inline' 'self' curl.haxx.se www.curl.se curl.se; require-trusted-types-for 'script';
H: strict-transport-security: max-age=31536000
H: via: 1.1 varnish, 1.1 varnish
H: accept-ranges: bytes
H: date: Sun, 20 Jul 2025 04:44:47 GMT
H: age: 69
H: x-served-by: cache-bma-essb1270054-BMA, cache-hel1410021-HEL
H: x-cache: HIT, HIT
H: x-cache-hits: 1, 15
H: x-timer: S1752986688.638651,VS0,VE0
H: vary: Accept-Encoding
H: alt-svc: h3=":443";ma=86400,h3-29=":443";ma=86400,h3-27=":443";ma=86400
H:
D: headers completed, res 0 info 0x622000010600
D: backend 1 internals 0x61c000001080
D: OpenSSL ver. unknown
AddressSanitizer:DEADLYSIGNAL
=================================================================
==81895==ERROR: AddressSanitizer: SEGV on unknown address 0x0000000005c9 (pc 0x00010472785c bp 0x00016b6ed690 sp 0x00016b6ed690 T0)
==81895==The signal is caused by a READ memory access.
==81895==Hint: address points to the zero page.
    #0 0x00010472785c in SSL_get_peer_cert_chain+0x28 (libssl.3.dylib:arm64+0x1385c)
    #1 0x000104700c5c in header_callback+0x2d0 (curlpoc:arm64+0x100000c5c)
    #2 0x00010488a494 in cw_out_ptr_flush+0x10c (libcurl.4.dylib:arm64+0x1a494)
    #3 0x00010488a000 in cw_out_do_write+0xa8 (libcurl.4.dylib:arm64+0x1a000)
    #4 0x000104889c74 in cw_out_write+0x70 (libcurl.4.dylib:arm64+0x19c74)
    #5 0x00010488a870 in cw_pause_write+0x200 (libcurl.4.dylib:arm64+0x1a870)
    #6 0x0001048c6d1c in cw_download_write+0x94 (libcurl.4.dylib:arm64+0x56d1c)
    #7 0x0001048c59cc in Curl_client_write+0x48 (libcurl.4.dylib:arm64+0x559cc)
    #8 0x0001048a3180 in http_write_header+0x54 (libcurl.4.dylib:arm64+0x33180)
    #9 0x0001048a2068 in http_rw_hd+0x1598 (libcurl.4.dylib:arm64+0x32068)
    #10 0x00010489dfac in Curl_http_write_resp_hd+0x28 (libcurl.4.dylib:arm64+0x2dfac)
    #11 0x0001048f55ac in cb_h3_end_headers+0xe0 (libcurl.4.dylib:arm64+0x855ac)
    #12 0x00010497ecd8 in nghttp3_conn_read_bidi nghttp3_conn.c
    #13 0x00010497e4c4 in nghttp3_conn_read_stream nghttp3_conn.c:526
    #14 0x0001048f4790 in cb_recv_stream_data+0x4c (libcurl.4.dylib:arm64+0x84790)
    #15 0x000104a2c4dc in conn_recv_stream ngtcp2_conn.c:7103
    #16 0x000104a2b178 in conn_recv_pkt ngtcp2_conn.c:9077
    #17 0x000104a1fcc4 in ngtcp2_conn_read_pkt_versioned ngtcp2_conn.c:9855
    #18 0x0001048f5cb8 in recv_pkt+0x80 (libcurl.4.dylib:arm64+0x85cb8)
    #19 0x0001048f67c4 in vquic_recv_packets+0x12c (libcurl.4.dylib:arm64+0x867c4)
    #20 0x0001048f34cc in cf_ngtcp2_recv+0xe8 (libcurl.4.dylib:arm64+0x834cc)
    #21 0x0001048d5730 in Curl_sendrecv+0x254 (libcurl.4.dylib:arm64+0x65730)
    #22 0x0001048b96d8 in multi_runsingle+0x980 (libcurl.4.dylib:arm64+0x496d8)
    #23 0x0001048b8b68 in curl_multi_perform+0x200 (libcurl.4.dylib:arm64+0x48b68)
    #24 0x00010488db84 in curl_easy_perform+0x188 (libcurl.4.dylib:arm64+0x1db84)
    #25 0x00010470096c in main+0x84 (curlpoc:arm64+0x10000096c)
    #26 0x0001991feb94 in start+0x17b8 (dyld:arm64e+0xfffffffffff3ab94)

==81895==Register values:
 x[0] = 0x000061c000001080   x[1] = 0x0000000000000000   x[2] = 0x00000000000120a8   x[3] = 0x0000000000000002
 x[4] = 0x0000000104700fa0   x[5] = 0x000000016b6ed690   x[6] = 0x000000016af04000   x[7] = 0x0000000000000001
 x[8] = 0x0000000000000301   x[9] = 0x00000002076b8588  x[10] = 0x0000000000000002  x[11] = 0x0000010000000000
x[12] = 0x00000000fffffffd  x[13] = 0x0000000000000000  x[14] = 0x0000000000000000  x[15] = 0x0000000000000000
x[16] = 0x0000000104727834  x[17] = 0x00000002086fde70  x[18] = 0x0000000000000000  x[19] = 0x000000016b6ed6e0
x[20] = 0x0000000000000000  x[21] = 0x0000000000000002  x[22] = 0x000061c000001080  x[23] = 0x000000016b6ed6c0
x[24] = 0x0000000000000001  x[25] = 0x000000016b6ed6a0  x[26] = 0x000000702d6fdad4  x[27] = 0x0000007000020000
x[28] = 0x000000010470098c     fp = 0x000000016b6ed690     lr = 0x0000000104700c60     sp = 0x000000016b6ed690
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV (libssl.3.dylib:arm64+0x1385c) in SSL_get_peer_cert_chain+0x28
==81895==ABORTING
Abort trap: 6

影响

摘要:

  • 至少:访问 HTTP/3 站点时导致拒绝服务
  • 潜在:如果返回的指针指向已释放的内存,可能导致内存损坏。根据目标平台的不同,可能实现远程代码执行。需要进一步分析以确定完整影响

支持材料/参考

本报告不含 AI 内容,供您愉快阅读。停止垃圾内容!

时间线活动

nyymi 在 13 天前发表评论:

我们能弄清楚需要做什么才能让其中一个回调被调用吗?

正在处理中。需要一些时间来检测最具代表性的示例(一些可能被真实世界应用程序调用的东西)。我可能会在 GitHub 上搜索一些真实代码,看看它们实际使用 SSL 句柄调用了哪些函数,并尝试找到一些可以按照我描述的方式被利用的东西)。

到目前为止,这似乎仅限于单个平台上的单个应用程序

我知道索尼确实使用相同的浏览器引擎和 curl 后端(事实上,索尼贡献了 curl 后端)。自 PS4 以来,它一直在 PlayStation 设备中使用。如果这个漏洞未被发现,这个错误可能会出现在新的索尼控制台或其他设备上,这并非不可想象。这是我所知道的最大的潜在可利用的东西。

nyymi 在 13 天前发表评论:

对我之前的内容进行小修正:在 OpenSSL 3.5 中,SSL 实际上是 struct ssl_connection_st 类型(https://github.com/openssl/openssl/blob/f4fb4a65d5de45fa84665c30ecaf509ae27302a6/ssl/ssl_local.h#L1244)

他们稍微调整了一些内容,这让我一时感到困惑。较旧的 OpenSSL 版本使用 struct ssl_st 来表示这个东西。

nyymi 在 13 天前发表评论:

一如既往,@bagder 是对的,再次如此。正如我应该已经想到的,我发现很难在不使代码在错误结构中较早地遇到无效指针的情况下到达回调。这些对较早字段的访问实际上屏蔽了回调,至少是我到目前为止梳理过的那些。

这意味着这个漏洞的影响很可能比我最初的估计要低得多。

最大的影响可能是从 SSL 读取简单状态,例如: SSL_get_verify_result,而验证状态可能会错误地返回 X509_V_OK,具体取决于堆内存内容。

OpenSSL 中获取验证结果的代码只是从结构中的变量读取它:

1
2
3
4
5
6
7
8
9
long SSL_get_verify_result(const SSL *ssl)
{
    const SSL_CONNECTION *sc = SSL_CONNECTION_FROM_CONST_SSL(ssl);

    if (sc == NULL)
        return 0;

    return sc->verify_result;
}

如果堆中的长字恰好为 0,验证将错误地成功。 如果某些代码显式禁用 CURLOPT_SSL_VERIFYPEER,然后通过类似以下代码检查验证状态,则可能发生这种情况:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
    const struct curl_tlssessioninfo *info = NULL;
    CURLcode res = curl_easy_getinfo(curl, CURLINFO_TLS_SSL_PTR, &info);
    int valid = 0;
    if (info && !res)
    {
      if(CURLSSLBACKEND_OPENSSL == info->backend)
      {
        SSL *ssl = info->internals;
        valid = SSL_get_verify_result(ssl) == X509_V_OK;
     }
   }

这当然远非 RCE,但它仍然是除了崩溃之外的安全影响。

可能需要检查更多潜在影响:

  • 源自解释错误结构的其他类似上述的简单缺陷。这些可能需要是不太依赖结构内容的简单功能。
  • 在将 SSL_CTX 解释为 SSL 或将 SSL 解释为 SSL_CTX 时泄露敏感信息。我需要覆盖结构并查看是否可以通过 OpenSSL 功能访问任何敏感数据,从而意外泄露。

bagder curl staff 在 13

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