介绍
OpenSSH用户通过GitHub提交公开枚举漏洞(CVE-2018-15473)。
此漏洞不会生成有效用户名列表,但它允许猜测用户名。
WF曲速区将在这篇文章中讲解这个漏洞并提出了缓解和监控措施。
技术细节
WF曲速未来表示此漏洞在OpenSSH的多个身份验证功能中体现出来。然后仔细研究了Ubuntu的OpenSSH实现公钥身份认证中的这个漏洞。
通过向OpenSSH服务器发送格式错误的公钥身份认证消息,可以确定是否存在特定用户名。如果用户不存在,则将向客户端发送身份验证失败消息。如果用户存在,则无法解析消息将中止通信:连接将关闭而不发回任何消息。此漏洞利用在Python PoC脚本中实现。
该漏洞的存在是因为在完全解析消息之前,会发生有关用户名不存在的通信。修复漏洞本质上很简单:反转逻辑。首先完全解析消息,然后进行通信。
测试PoC的一种方法是在调试模式下启动OpenSSH服务器:
之后,使用现有用户名运行PoC脚本:
在服务器端,将发生错误:
也可以在/var/log/auth.log中找到此错误:
无法解析消息导致客户端和服务器之间的连接关闭而没有来自服务器的消息:
请注意,最后一个数据包是粉红色的(即客户端数据包),没有后续的蓝色数据包(即服务器数据包)。
使用不存在的用户名执行PoC脚本时:
没有出现“不完整消息”错误:
服务器向客户端发回消息:
注意通信结束时的蓝色服务器数据包。
这就是如何利用公钥认证中的漏洞来披露用户名的有效性。
当然,OpenSSH的行为在源代码中定义。函数userauth_pubkey是已实现的身份验证功能之一,特定于通过公钥进行身份验证。验证失败时返回0,验证成功时返回1。当收到消息SSH2_MSG_USERAUTH_REQUEST(类型为publickey)时调用它,之后结果用于将消息SSH2_MSG_USERAUTH_FAILURE或SSH2_MSG_USERAUTH_SUCCESS发送回客户端。
该函数的逻辑如下:
- 如果用户名不明确 - > 0
- 如果已知用户名的**不正确 - > 0
- 如果已知用户名正确** - > 1
这可能是因为函数packet_get_string:
如果存在用户名,则在步骤1之后将从消息中提取字段。
要提取的第一个字段是布尔值(1个字节),带有函数packet_get_char()。当认证类型为publickey时,该字段等于1。接下来是2个字符串:算法和**。在SSH消息中,字符串被编码为长度值对。字符串由4个字节(字符串的长度)组成,后跟包含字符串的可变字节数(等于长度)。长度编码为bigendian,即首先放置4字节整数的最高有效字节,然后是较低有效字节。
函数packet_get_string从消息中提取字符串,同时验证它,即检查指定的长度是否正确。此功能依赖于其他功能:
首先有一个函数ssh_packet_get_string的定义:
函数ssh_packet_get_string调用函数sshpkt_get_string,如果其返回值不为0,则调用函数致命。函数致命记录致命错误事件,然后终止生成的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进程终止。
这正是PoC Python脚本触发的内容。首先,它与OpenSSH服务器建立加密连接,然后发送格式错误的SSH2_MSG_USERAUTH_REQUEST(类型公钥)消息。该脚本将Paramiko的add_boolean函数重新定义为NULL函数。Paramiko是用于SSH通信的Python模块。通过重新定义add_boolean函数,消息中省略了布尔字段(就在算法和键字符串字段之前)。
当函数userauth_pubkey解析此格式错误的消息时,首先读取布尔字段。由于该字段实际上是丢失的,因此读取下一个字段的第一个字节(函数packet_get_char):算法字符串的4字节长度的最高有效字节。调用下一个函数packet_get_string来读取(并验证)算法字符串。由于缺少布尔字段,这将失败。
以下是格式良好的消息的解析:
对于格式错误的消息,缺少布尔值。解析函数当然不知道这一点,因此它将字符串的第一个字节解析为布尔字段:它看起来像消息向左移一个字节:
结果是解析了1907字节的字符串长度(0x00000773十六进制),这比消息本身长。因此,函数ssh_packet_get_string将调用函数致命以导致OpenSSH进程终止。
漏洞摘要
这是一个微妙的错误。它不是缓冲区溢出导致远程代码执行或缺少输入验证。
没有缓冲区溢出,所有输入在使用前都经过验证。这里的问题是输入验证在一些功能处理已经发生之后发生:可能出于性能原因,首先检查用户名以查看它是否存在。如果它不存在,则不必进行进一步的输入验证和处理。
使用现有用户名,将进行输入验证,并且可以在不发送消息的情况下关闭连接。这可用于导出用户名的存在。
这个问题的解决方案很简单:在任何功能处理之前切换顺序并首先进行所有输入验证。
在其他身份验证功能中可能会出现相同的错误。检查这个的粗略,不完整的方法是检查表达式“!authctc-> valid”,如下所示:
确实在基于主机的身份验证中犯了同样的错误(可以在GitHub提交中看到):
和Kerberos身份验证:
并且可能是SSH1 RSA身份验证(我们还没有进一步检查,因为它在OpenBSD等实现中不再存在):
请注意,评论甚至警告这种风险!
结论
根据你对OpenSSH的使用,可以减轻此漏洞。区块链安全公司WF曲速未来提醒在修补程序可用并部署之前,可以禁用易受攻击的身份验证机制。例如,通过禁用公钥身份验证,PoC脚本不再有效,因为拒绝了格式错误的身份验证请求。
当然,如果你不使用公钥认证,我们只建议你禁用公钥认证。如果你使用它,请不要切换到密码验证,但继续使用公钥验证!这不是远程执行代码漏洞,而是一个信息泄露漏洞。
你还可以检查日志中是否有利用此漏洞的迹象。致命错误可能是一个迹象。在Ubuntu上使用此PoC,致命错误是“不完整的消息”。但是,此消息可能略有不同,具体取决于你的OpenSSH版本,还有其他方法可生成格式错误的消息,这可能会导致另一个致命错误。例如,可以创建一个字符串长度超过最大允许值的身份验证请求。
在默认配置中,你只会收到此致命错误。例如,不会记录客户端的IP地址。可以通过将日志级别(LogLevel)从INFO增加到VERBOSE来包含此信息:这将创建额外的日志条目,其中包含客户端的IP地址。请注意,这将生成更大的日志,并且你应该监视日志不会超过你的容量。