G.O.S.S.I.P 安全漏洞分析 2025-0220 OpenSSH CVE-2025-26465/26466

G.O.S.S.I.P 安全漏洞分析 2025-0220 OpenSSH CVE-2025-26465/26466

1032 安全研究GoSSIP 2025-02-20 21:16

CVE-2025-26465: MitM attack against OpenSSH’s VerifyHostKeyDNS-enabled

client

CVE-2025-26466: DoS attack against OpenSSH’s client and server

原文:
https://seclists.org/oss-sec/2025/q1/144

Background

OpenSSH的代码库中大量使用了如下范式:

1387 int1388 sshkey_to_base64(const struct sshkey *key, char **b64p)1389 {1390         int r = SSH_ERR_INTERNAL_ERROR;....1398         if ((r = sshkey_putb(key, b)) != 0)1399                 goto out;1400         if ((uu = sshbuf_dtob64_string(b, 0)) == NULL) {1401                 r = SSH_ERR_ALLOC_FAIL;1402                 goto out;1403         }....1409         r = 0;1410  out:....1413         return r;1414 }

用goto处理异常情况,根据返回值来报告出错的情况
(看来祖师爷说过不要用goto是有道理的)。在上述代码中,如果没有1401行,在sshbuf_dtob64_string
出错后,就没有对返回值r
进行赋值,sshkey_to_base64
则会返回0

在[CVE-2023-2283][
https://securitylab.github.com/advisories/GHSL-2023-085_libssh/
]里中的pki_verify_data_signature
函数中:

int rc = SSH_ERROR;rc = pki_key_check_hash_compatible(pubkey, signature->hash_type);if (rc != SSH_OK) {    return SSH_ERROR;}ctx = EVP_MD_CTX_new();if (ctx == NULL) {    SSH_LOG(SSH_LOG_TRACE,            "Failed to create EVP_MD_CTX: %s",            ERR_error_string(ERR_get_error(), NULL));    goto out;}/* Verify the signature */evp_rc = EVP_DigestVerifyInit(ctx, NULL, md, NULL, pkey);if (evp_rc != 1){    SSH_LOG(SSH_LOG_TRACE,            "EVP_DigestVerifyInit() failed: %s",            ERR_error_string(ERR_get_error(), NULL));    goto out;}/...../out:    if (ctx != NULL) {        EVP_MD_CTX_free(ctx);    }    EVP_PKEY_free(pkey);    return rc;

第二行首先将rc
返回值赋值为SSH_OK

可以发现如果EVP_DigestVerifyInit
报错,返回值不为1,就会直接跳转到out返回,并没有对rc进行重新赋值,函数的返回值仍然为SSH_OK

Experiments

研究人员认为在OpenSSH的代码库中,可能仍然存在其他遗漏返回值赋值语句的情况。于是他们进行了两个实验:首先利用CodeQL来审计所有使用goto out
但忘记重新赋值返回值的函数。CodeQL发现了50个结果,其中有37个是误报。

接着他们进一步人工审计了所有使用goto
的函数,发现之前CodeQL的结果里并没有漏报(Ctrl-F大法)。在剩下的13个函数中,虽然实现逻辑上有错误,并不会对安全性有影响。

尽管如此,在CtrlF审计中,他们发现了本次漏洞的主角verify_host_key_callback

------------------------------------------------------------------------  93 static int  94 verify_host_key_callback(struct sshkey *hostkey, struct ssh *ssh)  95 { ... 101         if (verify_host_key(xxx_host, xxx_hostaddr, hostkey, 102             xxx_conn_info) == -1) 103                 fatal("Host key verification failed."); 104         return 0; 105 }------------------------------------------------------------------------1470 int1471 verify_host_key(char *host, struct sockaddr *hostaddr, struct sshkey *host_key,1472     const struct ssh_conn_info *cinfo)1473 {....1538         if (options.verify_host_key_dns) {....1543                 if ((r = sshkey_from_private(host_key, &plain)) != 0)1544                         goto out;....1571 out:....1580         return r;1581 }------------------------------------------------------------------------

在verify_host_key_dns
被启用的情况下,sshkey_from_private
的非0返回值会被直接返回,交给verify_host_key_callback
处理;然而verify_host_key_callback
只考虑了返回值为-1
的情况。实际上,sshkey_from_private
的返回值还有很多,比如-10 (SSH_ERR_INVALID_ARGUMENT), -14 (SSH_ERR_KEY_TYPE_UNKNOWN)还有-2 (SSH_ERR_ALLOC_FAIL)

所以只要sshkey_from_private返回一个非0且非-1的值
,verify_host_key_callback
就会返回0,从而绕过了对主机HostKey的检测

但是就目前为止,研究人员发现唯一的可能是让它返回SSH_ERR_ALLOC_FAIL
,即OOM错误。因此,还需要寻找一个漏洞来耗尽客户端的内存

耗尽内存:DoS attack against OpenSSH’s client and server

经过人工审计,研究人员成功找到了一个无限制的内存泄露漏洞

简单来说,在整个认证过程中,如果客户端收到了SSH2_MSG_PING
类型的数据,它就会去返回一个PONG
数据。在密钥协商过程之外,这个实现是直接把PONG
包添加到output
缓冲区末尾,而这个缓冲区有一个上限SSHBUF_SIZE_MAX
。但是在密钥协商过程中,PONG
的实现变成了调用sshbuf_new()
并添加到一个list
中。OpenSSH并没有对PONG
包的数量进行限制,因此可以发送大量PING
包来让客户端分配内存给PONG
,且分配的内存到密钥协商完全结束后才会被释放,完美符合利用的要求。

除此之外,在密钥协商结束后,客户端会将所有列表中的PONG
包重新添加到output
缓冲区。但是这里的算法复杂度到达了O(n^2):每有一个PONG
包,客户端会malloc()
一个新的缓冲区,然后将旧缓冲区的数据复制到新的缓冲区。

作者进行了一个计算:如果服务器发送128MB的PING
,客户端就需要不断复制总共32TB的数据,进而耗尽CPU资源。

小结

将这两个漏洞结合起来,服务器只要在协商中发生大量PING
包耗尽客户端资源,sshkey_from_private
就会返回SSH_ERR_ALLOC_FAIL
,在客户端启用verify_host_key_dns
的情况下就能绕过对服务器HostKey的校验。