前言

因为要改写下CVE-2018-15473的脚本从python到go,这里复现并分析的漏洞和网上的脚本,我太菜了。

正文

基本原理

漏洞具体的代码不想赘述了,这里都有https://www.anquanke.com/post/id/157607#h2-2。简单的原因就是通过向OpenSSH服务器发送一个错误格式的公钥认证请求,可以判断是否存在特定的用户名。如果用户名不存在,那么服务器会发给客户端一个验证失败的消息。如果用户名存在,那么将因为解析失败(这里就是通过我们构造的恶意公钥格式来使他强制中断的),不返回任何信息,直接中断通讯。

当openssh中的userauth_pubkey函数解析该恶意构造的信息时,会先读取布尔字段,如果缺少了该字段,那么会读取下一个字段的第一位字节(packet_get_char函数):也就是算法字符串中的4位最高有效字节。之后pakcet_get_string函数会调用来读取(并验证)算法字符串。这一过程将会因为缺少布尔字节而发生错误,直接中断。

正确的解析过程是这个:

如果是缺少布尔字节的恶意格式信息,那么解析函数在事先不知道的情况下,先将第一位字节解析为布尔字节,使得信息整体左移了一位。

这会导致尝试解析一个1907字节长度的信息(0x00000773的十六进制),也就是超过实际信息长度的信息。那么ssh_packet_get_string函数就会调用fatal函数来结束OpenSSH进程。

exploit分析

分析一波网上的exploit,paramiko这个ssh的库注意设定为2.4.1,因为2.4.2后_handler_table就是个属性对象,不是字典了,所以这里脚本中的requriements.txt记得指定下版本paramiko==2.4.1。

这里的old_parse_service_accept跟进下指的是_parse_service_accept这个函数

重点是这个函数,替换了add_boolean为空,然后调用old_parse_service_accept就是_parse_service_accept这个函数

看下的add_boolean是什么

写入0或者1,看下哪个函数调用了add_boolen

看到是_parse_service_accept这里调用了,具体的代码是auth_handler中的257到262行

看到是根据是利用password还是用公钥登录来填充bool字段,所以当我们将add_boolen这个函数置空时,那么就没有那个布尔字段了,导致openssh错误。

现在我们来看下哪里调用了_parse_service_accept这个函数,主要的判断逻辑的代码是checkUsername这个函数,当返回BadUsername时为用户名错误,当返回的是paramiko.ssh_exception.AuthenticationException这个错误是说明存在用户名。

transport.auth_publickey(username, paramiko.RSAKey.generate(1024))这段代码是主要的逻辑,跟进下auth_publickey这个函数到self.auth_handler.auth_publickey(username, key, my_event),继续跟进self._request_auth(),

到这里我找半天没找到,后来想了下,生成信息肯定在发送消息之前吧,跟进cMSG_SERVICE_REQUEST,发现是cMSG_SERVICE_REQUEST = byte_chr(MSG_SERVICE_REQUEST),MSG_SERVICE_REQUEST又是什么呢,在auth_handler中有定义

发现是_parse_service_request这个函数,看下

发现调用了cMSG_SERVICE_ACCEPT,而这个cMSG_SERVICE_ACCEPT就是MSG_SERVICE_ACCEPT写成byte流,同上MSG_SERVICE_ACCEPT对应的函数就是_parse_service_accept,终于我们找到了最后调用的函数,add_boolean就在_parse_service_accept中了,over。(整个调用也真的长)

go ssh包分析

分析半天终于找到调用链了。正向没找到,最终是反向找到调用链的,首先是上面的在paramiko库里面,在_parse_service_accept我们调用了一个cMSG_USERAUTH_REQUEST这个,然后根据这个我去搜了下发现USERAUTH_REQUEST这个

看下这个结构体在哪里被调用了,

这个函数调用了这个结构体,验证了拥有私钥,看下rfc 4252的第七部分,这一段描述

接着往上追溯,看下哪里调用了buildDataSignedForAuth这个函数,发现是在client_auth的auth函数中的212行,调用了

然后我们能看到这里申明了一个pubilckeyAuthMsg的struct msg,仔细看下里面的格式,有没有发现和我们在paramiko中的_parse_service_accept中的m参数很像?比对了下发现,应该就是指这个结构体,我们来看一下这个struct的定义

然后还是看下rfc 4252的第七部分,看下这段描述

这里发送的实际的验证包(用私钥进行签名),对比一下每个参数,在_parse_service_accept函数中

走publickey这个逻辑,发现都是一一对应的,同样对应go ssh包中auth里的msg这个struct,然后根据上面的分析我们要改的就是HasSig这个布尔值,要把它删掉。

再次之前我们先继续回溯一下调用,哪里调用了这个auth呢,发现是在clientAuthenticate函数中调用了

继续是在clientHandshake,当握手的时候

然后是NewClientConn,建立一个ssh链接时

最后到了dial函数

至此,我们整个的调用链完毕,这里顺着捋一下

Dial()->NewClientConn()->clientHandshake()->clientAuthenticate()->publicKeyCallback.auth()->msg struct->HasSig

然后又经过一天的折腾,因为go是静态语言没法像python那样简单的进行运行时函数替换,想了很多方法,比如打桩,但是发现类似的库都不能做到版本或者平台的兼容问题,又查找了有没有第三方库也无果,如果socket的话也太麻烦了,需要从握手,版本协商,算法协商一直写到验证,等于重构。最终改写失败(能力有限,目前我是搞不定了,,,未来再看看有没有什么好的办法吧)

总结

虽然这次改写失败了,但是从调试分析的过程也学习到了很多东西,还是记录一下吧。