OpenSSL后端X509证书内存泄漏漏洞分析与修复

本文详细分析了curl的OpenSSL后端在ossl_get_channel_binding函数中存在的X509证书内存泄漏问题。该漏洞在使用Negotiate认证的HTTPS连接时触发,每次请求会泄漏约500字节内存,长期运行可能导致拒绝服务。文章包含完整的漏洞复现步骤和修复方案。

OpenSSL后端:X509对等证书在ossl_get_channel_binding中未释放导致每次请求内存泄漏(长期运行客户端面临DoS风险)

摘要

在curl的OpenSSL后端中,ossl_get_channel_binding通过SSL_get1_peer_certificate获取服务器X509证书的新引用,但从未释放它。当使用Negotiate(SPNEGO)over TLS时,此路径被调用并每次触发泄漏一个X509对象。在长期运行的libcurl客户端中进行多次请求后,这会导致无限制的内存增长和潜在的拒绝服务。

此问题仅在使用OpenSSL后端(>= 1.1.0),当curl/libcurl使用GSSAPI(HAVE_GSSAPI)构建,并在处理Negotiate质询的HTTPS上时发生。

技术细节

漏洞触发条件

  • 使用HTTPS的Negotiate质询足以触发通道绑定检索
  • 功能正常的Kerberos设置不是此泄漏发生的必要条件
  • 每次触发泄漏一个X509引用

代码问题位置

lib/vtls/openssl.cossl_get_channel_binding函数中:

1
2
3
4
5
cert = SSL_get1_peer_certificate(octx->ssl);
if(!cert) {
  /* No server certificate, don't do channel binding */
  return CURLE_OK;
}

该函数调用SSL_get1_peer_certificate(增加X509引用计数),但在任何返回路径(成功或错误)上从未调用X509_free

影响

影响范围

一个远程HTTPS服务器如果通告WWW-Authenticate: Negotiate,可以导致libcurl客户端(使用OpenSSL和GSSAPI构建)在检索TLS通道绑定时,每次身份验证尝试泄漏一个X509证书引用。

在长期运行进程中进行多次请求后,这会导致无限制的内存增长和由于资源耗尽导致的拒绝服务状况。

约束条件

  • 要求libcurl使用OpenSSL(>=1.1.0)和GSSAPI支持构建
  • 仅影响在HTTPS上使用Negotiate身份验证的客户端
  • 线性泄漏速率(未放大),但随时间推移无限制

严重性:低(范围有限,需要特定的构建配置和身份验证方法,但对受影响的部署存在真正的DoS风险)

复现步骤

构建环境

1
2
3
4
./buildconf
./configure --with-ssl --with-gssapi
make -j
./src/curl -V   # 应显示OpenSSL和GSS-API/SPNEGO

设置HTTPS服务器

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import ssl, http.server, socketserver
class H(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(401)
        self.send_header("WWW-Authenticate", "Negotiate")
        self.send_header("Content-Length", "0")
        self.end_headers()
    def log_message(self, *args): pass
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.load_cert_chain("/tmp/cert.pem", "/tmp/key.pem")
with socketserver.TCPServer(("127.0.0.1", 4443), H) as httpd:
    httpd.socket = ctx.wrap_socket(httpd.socket, server_side=True)
    httpd.serve_forever()

概念验证代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include <curl/curl.h>
#include <stdio.h>
int main(void) {
  curl_global_init(CURL_GLOBAL_DEFAULT);
  for(int i = 0; i < 100; i++) {
    CURL *h = curl_easy_init();
    curl_easy_setopt(h, CURLOPT_URL, "https://127.0.0.1:4443/");
    curl_easy_setopt(h, CURLOPT_HTTPAUTH, (long)CURLAUTH_NEGOTIATE);
    curl_easy_setopt(h, CURLOPT_USERPWD, ":");
    curl_easy_setopt(h, CURLOPT_SSL_VERIFYPEER, 0L);
    curl_easy_setopt(h, CURLOPT_SSL_VERIFYHOST, 0L);
    curl_easy_perform(h);
    curl_easy_cleanup(h);
  }
  curl_global_cleanup();
  return 0;
}

泄漏检测

  • Linux: valgrind --leak-check=full --show-leak-kinds=definite /tmp/poc
  • macOS: MallocStackLogging=1 /tmp/poc & pid=$!; sleep 1; leaks $pid

预期结果:约100个泄漏的X509相关分配(每个请求一个),源自ossl_get_channel_binding()中对SSL_get1_peer_certificate()的使用。

修复方案

临时修复

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
diff --git a/lib/vtls/openssl.c b/lib/vtls/openssl.c
index 039eb51c9a..63263c8f01 100644
--- a/lib/vtls/openssl.c
+++ b/lib/vtls/openssl.c
@@ -5645,11 +5645,11 @@ static CURLcode ossl_get_channel_binding(struct Curl_easy *data, int sockindex,
   if(!octx) {
     failf(data, "Failed to find the SSL filter");
     return CURLE_BAD_FUNCTION_ARGUMENT;
   }
 
-  cert = SSL_get1_peer_certificate(octx->ssl);
+  cert = SSL_get0_peer_certificate(octx->ssl);
   if(!cert) {
     /* No server certificate, don't do channel binding */
     return CURLE_OK;
   }

最终处理

curl团队经过长时间审议和讨论后,决定将此问题作为非安全问题处理。

理由:

  • 泄漏仅在使用Negotiate的新TLS连接上发生
  • 每次新连接的泄漏通常少于500字节
  • 泄漏需要Negotiate身份验证,这使得这不是"通用"泄漏
  • 此类客户端很少见,且不太可能是极长期运行的

该修复已通过PR #18917 提交。

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