更新历史
- 2024-09-14:补充一些说明内容。
影响范围
Openssh Server 9.1
漏洞触发
漏洞利用很简单,在连接建立与版本协商的时候将 softwareid 设置成特定版本(如 PuTTY_Release_0.64)即可触发。
漏洞分析
这个漏洞的成因位于算法协商与密钥交换阶段:
do_ssh2_kex(ssh);
-> kex_proposal_populate_entries
-> compat_kex_proposal(ssh, cp); # First Free
do_authentication2(ssh)
-> ssh_dispatch_run_fatal
-> ssh_dispatch_run
-> input_userauth_request
-> mm_getpwnamallow
-> copy_set_server_options
-> assemble_algorithms
-> kex_assemble_names # Double Free首先查看上游修复位置,主要修复了这一段代码,根据 BugZilla 中的报告,这里应该是第一次被 free 的位置:
char *
compat_kex_proposal(struct ssh *ssh, char *p)
{
char *cp = NULL;
if ((ssh->compat & (SSH_BUG_CURVE25519PAD|SSH_OLD_DHGEX)) == 0)
return xstrdup(p);
debug2_f("original KEX proposal: %s", p);
if ((ssh->compat & SSH_BUG_CURVE25519PAD) != 0)
if ((p = match_filter_denylist(p,
"[email protected]")) == NULL)
fatal("match_filter_denylist failed");
if ((ssh->compat & SSH_OLD_DHGEX) != 0) { [1]
cp = p; [2]
if ((p = match_filter_denylist(p,
"diffie-hellman-group-exchange-sha256,"
"diffie-hellman-group-exchange-sha1")) == NULL)
fatal("match_filter_denylist failed");
free(cp); [3]
}
debug2_f("compat KEX proposal: %s", p);
if (*p == '\0')
fatal("No supported key exchange algorithms found");
return p;
}在 SSH_OLD_DHGEX 被设置时(也就是代码中的 [1] 处),会将字符串 p 赋值给 cp([2] 处)然后释放掉([3] 处)。此时传进来的字符串 p 是 option->kex_algorithms。此时这个指针已经变成了一个悬空指针。
崩溃现场位于 kex_assemble_names 函数。kex_assemble_names 函数在 assemble_algorithms 中被调用,传进来的参数也是 option->kex_algorithms 导致了 double free。
接下来看一下漏洞路径,重点在于 SSH_OLD_DHGEX 这个参数,这个参数是在 compat_banner 函数中定义的:
static struct {
char *pat;
int bugs;
} check[] = {
... ...
{ "PuTTY_Local:*," /* dev versions < Sep 2014 */
... ...
"PuTTY_Release_0.64*",
SSH_OLD_DHGEX },
... ...
};除了 PuTTY 的旧版本外,理论上其它带有 SSH_OLD_DHGEX 标志的客户端也能触发漏洞。
补丁分析
补丁的做法很简单,在上面通过 ssh->compat & (SSH_BUG_CURVE25519PAD|SSH_OLD_DHGEX 保证传入的都是满足条件的值,然后分别对 SSH_BUG_CURVE25519PAD 和 SSH_OLD_DHGEX 进行处理:
diff --git a/compat.c b/compat.c
index 46dfe3a9c..478a9403e 100644
--- a/compat.c
+++ b/compat.c
@@ -1,4 +1,4 @@
-/* $OpenBSD: compat.c,v 1.120 2022/07/01 03:35:45 dtucker Exp $ */
+/* $OpenBSD: compat.c,v 1.121 2023/02/02 12:10:05 djm Exp $ */
/*
* Copyright (c) 1999, 2000, 2001, 2002 Markus Friedl. All rights reserved.
*
@@ -190,26 +190,26 @@ compat_pkalg_proposal(struct ssh *ssh, char *pkalg_prop)
char *
compat_kex_proposal(struct ssh *ssh, char *p)
{
- char *cp = NULL;
+ char *cp = NULL, *cp2 = NULL;
if ((ssh->compat & (SSH_BUG_CURVE25519PAD|SSH_OLD_DHGEX)) == 0)
return xstrdup(p);
debug2_f("original KEX proposal: %s", p);首先处理 SSH_BUG_CURVE25519PAD 的情况,这里会选出满足条件的字符串放到 cp 中(而不是 p):
if ((ssh->compat & SSH_BUG_CURVE25519PAD) != 0)
- if ((p = match_filter_denylist(p,
+ if ((cp = match_filter_denylist(p,
"[email protected]")) == NULL)
fatal("match_filter_denylist failed");接下来处理 SSH_OLD_DHGEX 的情况,因为上文的 cp 已经被赋值了,接下来会将 cp 传入 match_filter_denylist 并返回 cp2,在结束的时候释放 cp。最后返回 cp2,这个过程中只有 cp 在中途创建并被释放,p 并未被释放,避免了上文所说的第一次释放的问题。
if ((ssh->compat & SSH_OLD_DHGEX) != 0) {
- cp = p;
- if ((p = match_filter_denylist(p,
+ if ((cp2 = match_filter_denylist(cp ? cp : p,
"diffie-hellman-group-exchange-sha256,"
"diffie-hellman-group-exchange-sha1")) == NULL)
fatal("match_filter_denylist failed");
free(cp);
+ cp = cp2;
}
- debug2_f("compat KEX proposal: %s", p);
- if (*p == '\0')
+ if (cp == NULL || *cp == '\0')
fatal("No supported key exchange algorithms found");
- return p;
+ debug2_f("compat KEX proposal: %s", cp);
+ return cp;
}