漏洞概述
在curl的OpenSSL后端中,ossl_get_channel_binding函数通过SSL_get1_peer_certificate获取服务器X509证书的新引用,但从未释放它。当使用协商(SPNEGO) over TLS时,每次触发此路径都会泄漏一个X509对象。在长时间运行的libcurl客户端中,这会导致无限制的内存增长和潜在的拒绝服务。
触发条件
- 仅在使用OpenSSL后端(>= 1.1.0)时发生
- 当curl/libcurl使用GSSAPI构建(HAVE_GSSAPI)
- 在HTTPS上处理协商质询时
技术细节
代码问题
在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。
影响范围
- 需要libcurl使用OpenSSL(>=1.1.0)和GSSAPI支持构建
- 仅影响在HTTPS上使用协商认证的客户端
- 线性泄漏率(未放大),但随时间推移无限制
复现步骤
构建环境
1
2
3
4
|
./buildconf
./configure --with-ssl --with-gssapi
make -j
./src/curl -V # 应显示OpenSSL和GSS-API/SPNEGO
|
测试服务器
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()
|
漏洞验证
使用valgrind检测可观察到约100个与X509相关的泄漏分配(每个请求一个),源自ossl_get_channel_binding中的SSL_get1_peer_certificate使用。
修复方案
建议修复方法是将SSL_get1_peer_certificate替换为SSL_get0_peer_certificate:
1
2
|
- cert = SSL_get1_peer_certificate(octx->ssl);
+ cert = SSL_get0_peer_certificate(octx->ssl);
|
严重性评估
低危 - 范围有限,需要特定的构建配置和认证方法,但对受影响的部署存在真正的DoS风险。
每个新的TLS连接泄漏通常少于500字节,仅在使用协商认证的新TLS连接上发生,且使用此认证的客户端较为罕见,不太可能是极长寿命的进程。