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.c的ossl_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 提交。