本文翻译自: https://blog.nviso.be/2018/08/21/openssh-user-enumeration-vulnerability-a-close-look/


介绍

OpenSSH用户枚举漏洞(CVE-2018-15473)是由GitHub commit公开的。

这个漏洞虽然不会生成有效用户名的列表名单,但它允许猜测用户名。

在这篇博客文章中,我们对这个漏洞进行了更深入的研究,并提出了缓解措施和监控措施。

技术细节

该漏洞表现在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_FAILURESSH2_MSG_USERAUTH_SUCCESS发送回客户端。

该函数的逻辑如下:

如果是未知用户名->0
如果是已知用户名,但密钥不正确->0
如果是已知用户名,并且密钥正确 ->1

一个很聪明的人想到的是,在步骤1和步骤2之间可以停止执行函数userauth_pubkey。步骤1后,函数userauth_pubkey从客户端发送的消息中检索字符串。如果这个失败,由于字符串格式错误,进程将停止并关闭连接,而不发送任何消息。

这种情况可能是由函数packet_get_string导致的:

如果用户名存在,则在步骤1之后将从消息中提取字段。

第一个字段为boolean(一个字节),使用函数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,则调用函数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进程终止。

这正是PoC Python脚本的触发条件。首先,它与OpenSSH服务器建立加密连接,然后发送格式错误的SSH2_MSG_USERAUTH_REQUEST(类型为publickey)消息。这个脚本将Paramikoadd_boolean函数重新定义为NULL函数。Paramiko是用于SSH通信的Python模块。通过重新定义add_boolean函数,消息中省略了布尔字段(就在算法和关键字符串字段之前)。

当函数userauth_pubkey解析这个格式错误的消息时,首先读取boolean字段。由于该字段实际上是丢失的,因此读取下一个字段的第一个字节(函数packet_get_char):算法字符串的4字节长度的最高有效字节。调用下一个函数packet_get_string来读取(并验证)算法字符串。由于缺少boolean字段,毋庸置疑,这注定失败。

以下是格式稍好的消息的解析过程:

格式错误的消息是缺少boolean值的,但解析函数并不知道这一点,于是它将字符串的第一个字节解析为boolean字段:看起来就像消息向左移一个字节:

结果就是解析了1907字节长的字符串(0x00000773十六进制),这比消息本身长。因此,函数ssh_packet_get_string将调用fatal函数,导致OpenSSH进程终止。

漏洞总结

这是一个微妙的错误, 它不是关于缓冲区溢出导致远程代码执行或缺少输入验证的问题。

没有缓冲区溢出,所有输入在使用前都要经过验证。这里的问题是输入验证是在一些功能处理已经发生了之后再发生的:可能出于性能原因,首先检查用户名以查看它是否存在,如果它不存在,那么进一步的输入验证和处理就毫无必要了。

使用已经存在的用户名,将会进行输入验证,并且可以在不发送消息的情况下关闭连接。这可用于导出用户名的存在与否。

这个问题的解决方案很简单:在任何功能处理之前,切换顺序并首先进行所有的输入验证。

在其他身份验证功能中可能会出现相同的错误。一个粗略的,不完整的方法是检查表达式!authctc-> valid,如下所示:

在基于主机的身份验证中的确犯了同样的错误(可以在GitHub commit中看到):

以及Kerberos的身份验证:

并且潜在的SSH1的RSA身份验证(我们没有进一步检查,因为它不再存在于OpenBSD等实现中):

请注意,这种风险万不可轻视!

结论

您在使用OpenSSH的过程中,可以自己动手来减缓这个漏洞。在修补程序可用并部署之前,可以禁用易受攻击的身份验证机制。例如,通过禁用公钥身份验证,PoC脚本将不再有效,因为已经拒绝了格式错误的身份验证请求。

当然,如果您不使用公钥认证,我们只建议您禁用公钥认证。如果您使用它,请不要切换到密码验证,但继续使用公钥验证!这不是远程执行代码漏洞,而是一个信息泄露漏洞。

您还可以检查日志中是否有利用这个漏洞的迹象。致命错误可能是一个迹象。在Ubuntu上使用这个PoC,致命错误是“不完整的消息”。但是,此消息可能略有不同,具体得看您的OpenSSH版本,其他方法也可以生成格式错误的消息,这可能会导致另一个致命错误。例如,可以构造一个字符串长度超过最大允许值的身份验证请求。

在默认配置中,您只会收到此致命错误。例如,客户端的IP地址将不会被记录。您可以通过将日志级别(LogLevel)从INFO级别提高到VERBOSE级别,从而创建额外的日志条目,其中包含客户端的IP地址。请注意,这将生成更大的日志,并且您应该随时留意日志不会超过您的可用容量。

点击收藏 | 0 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖