OpenSSH用户枚举漏洞(CVE-2018-15473)分析

简介

OpenSSH用户枚举漏洞(CVE-2018-15473)已经通过Github公开(https://github.com/openbsd/src/commit/779974d35b4859c07bc3cb8a12c74b43b0a7d1e0)。尽管该漏洞不能用来生成有效的用户名列表,但依旧可以拿来枚举猜测用户名。

在本文中,我们将详细分析该漏洞的利用和防范技巧。

 

技术细节

该漏洞涉及到多个OpenSSH中的用户身份验证函数。首先我们来研究下UbuntuOpenSSH中的公钥认证中的这个漏洞。

通过向OpenSSH服务器发送一个错误格式的公钥认证请求,可以判断是否存在特定的用户名。如果用户名不存在,那么服务器会发给客户端一个验证失败的消息。如果用户名存在,那么将因为解析失败,不返回任何信息,直接中断通讯。相关的漏洞利用PoC脚本链接如下:http://www.openwall.com/lists/oss-security/2018/08/16/1

这里涉及的漏洞发生在解析请求前就收到的用户名不存在的请求。修复漏洞的方式也很简单:修改下判断逻辑,先解析信息,再建立通讯。

用来测试该PoC的方式如下:先在调试模式下启动OpenSSH服务器,

然后运行带有已知用户名的脚本:

在服务器端可以看到报错信息:

这个报错信息也可以在/var/log/auth.log目录下找到:

解析请求失败会导致服务器在不返回任何信息的情况下中断客户端和服务器间的通讯:

注意最后一个粉红色的数据包(客户端的数据包),这里没有后续返回的蓝色数据包(服务器数据包)。

当执行一个不存在的用户名的PoC脚本时:

没有发生“incomplete message”的报错信息:

服务器也返回给客户端一条信息:

注意最后的蓝色(服务器)数据包。

这就是利用公钥身份认证中的漏洞来判断用户名是否存在的过程。

通过分析OpenSSH源码中的userauth_pubkey函数,可以发现它对公钥身份认证的过程如下:收到SSH2_MSG-USERAUTH_REQUEST(公钥类型)发来的数据后,调用函数进行验证,验证失败时返回0,验证成功时返回1。然后将结果(SSH2_MSG_USERAUTH_FAILURESSH2_MSG_USERAUTH_SUCCESS)发回给客户端。

函数的相关逻辑如下:

  1. 如果是未知用户名 -> 0
  2. 如果是错误密码的已知用户名 -> 0
  3. 如果是正确密码的用户名 -> 1

漏洞提交者找到的是在第一步和第二步之间,能够设法停止userauth_pubkey函数的执行,从而在第一步userauth_pubkey函数获取客户端发来的信息字符串时,构造特定格式的字符串,在不返回任何信息的情况下停止并关闭这一通讯连接。

这里的关键是packet_get_string函数:

如果存在该用户名,那么第一步后会从消息中提取有效字符串。

第一部分packet_get_char()函数提取的字符串是一个布尔变量(1字节),之后是两个字符串:算法和对应的密钥。在SSH信息中,字符串以长度值的配对方式编码:一个字符串包含4字节(字符串长度)的开头和包含字符串内容的可变大小的字节数(根据之前声明的长度)。这里的长度采用大端的方式进行编码:先声明重要的4字节最高有效字节,再添加次要的有效字节。

packet_get_string函数在验证的同时,也负责提取信息中的字符串,从而用于验证信息的长度是否正确。这个函数也依赖其他函数:

首先是函数中的定义到的ssh_packet_get_string

ssh_packet_get_string函数调用sshpkt_get_string函数,如果该函数返回的值不是0,那么调用fatal函数。Fatal函数记录发生的报错信息,然后不返回任何信息,直接终结生成的OpenSSH进程。

现在再看下涉及的另一组函数:sshpkt_get_string调用了sshbuf_get_string

sshbuf_get_string调用了sshbuf_get_string_direct:

sshbuf_get_string_direct调用sshbuf_peek_string_direct:

最后sshbuf_peek_string_direct对字符串进行验证:

如果剩下的数据长度小于4字节(也就是不能包含字符串长度的声明)或者剩下的数据长度小于字符串的长度,那么会返回SSH_ERR_MESSAGE_INCOMPLETE错误(可以在日志中找到该信息)

总的来讲,函数这一过程如下:先用packet_get_string函数来提取信息中的字符串,如果遇到特定的恶意格式字符串,那么会发生一个严重的报错,从而关闭OpenSSH进程。

这也就是Python编写的PoC脚本触发原理:先创建与OpenSSH服务器连接的加密请求,然后发送恶意构造的SSH2_MSG_USERAUTH_REQUEST(公钥格式)请求信息。脚本中将Paramikoadd_boolean函数重新定义为一个空函数,ParamikoPython中用于SSH通讯的模块。通过重新定义add_boolean函数,从而省略了信息中的布尔字段(也就是算法和字符串值之前的内容)。

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

下面是一个正确格式的信息解析过程:

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

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

 

漏洞总结

这是一个非常巧妙的漏洞,因为它没有涉及到可以远程代码执行的缓冲区溢出漏洞和输入的验证过程。

在整个过程中既没有缓冲区溢出,使用到的输入内容也都符合验证要求。唯一的问题是可能出于性能需求的考虑,在一些函数处理后才进行对输入的验证,先判断用户名是否存在,如果不存在,那么就没必要进一步验证它的后续请求过程。

如果用户名存在,再对后续请求进行验证,那么会导致不返回信息就中断连接。这样就可以用来枚举一个用户名是否存在。

这个问题的解决方法也很简单:改变下顺序,在调用其他函数前先对所有输入进行验证。

在其他的身份验证函数中也有可能存在类似的问题,一个粗暴的,简单的判断方法是:检查所有类似”authctc -> valid”的表达式。

同样的问题也出现在基于主机的身份认证中(相关链接如下:https://github.com/openbsd/src/commit/779974d35b4859c07bc3cb8a12c74b43b0a7d1e0

Kerberos身份认证中也存在该问题:

SSH1 RSA身份认证中也可能存在该问题(考虑到现在的OpenBSD中都已经停止使用,我们就没有进一步进行检查)。

注意备注信息中也提到该风险问题。

 

总结

如果你还在使用OpenSSH,那么在新的官方修复补丁推送前,禁用易受攻击的身份认证机制。例如关闭公钥身份认证后,能够有效地防止PoC脚本和类似的恶意格式认证请求攻击。

也可以检查日志中的报错信息来判断该漏洞是否遭到利用。例如Ubuntu下,会有”incomplete message”的报错信息。然而根据OpenSSH版本的不同,报错信息也会有一些区别。同样对恶意格式的请求进行修改,比如说构造一个字符串超过最大允许值长度的恶意信息,也有可能会产生其他的报错信息。

在默认的配置中,你只会收到一个简单的报错信息,不会记录客户端的IP地址。因此你可以通过调整日志等级(LogLevel),从INFO改为VERBOSE,从而创建包含客户端IP地址等具体信息的日制。但要注意这会生成更大的日志文件,因此还需要监控日志是否超过容量限制。

(完)