Share
## https://sploitus.com/exploit?id=PACKETSTORM:180974
##  
# This module requires Metasploit: https://metasploit.com/download  
# Current source: https://github.com/rapid7/metasploit-framework  
##  
  
class MetasploitModule < Msf::Auxiliary  
include Msf::Exploit::Remote::DCERPC  
include Msf::Exploit::Remote::SMB::Client  
include Msf::Exploit::Remote::SMB::Client::Authenticated  
include Msf::Exploit::Remote::SMB::Client::PipeAuditor  
  
include Msf::Auxiliary::Scanner  
include Msf::Auxiliary::Report  
  
def initialize(info = {})  
super(update_info(info,  
'Name' => 'MS17-010 SMB RCE Detection',  
'Description' => %q{  
Uses information disclosure to determine if MS17-010 has been patched or not.  
Specifically, it connects to the IPC$ tree and attempts a transaction on FID 0.  
If the status returned is "STATUS_INSUFF_SERVER_RESOURCES", the machine does  
not have the MS17-010 patch.  
  
If the machine is missing the MS17-010 patch, the module will check for an  
existing DoublePulsar (ring 0 shellcode/malware) infection.  
  
This module does not require valid SMB credentials in default server  
configurations. It can log on as the user "\" and connect to IPC$.  
},  
'Author' =>  
[  
'Sean Dillon <sean.dillon@risksense.com>', # @zerosum0x0  
'Luke Jennings' # DoublePulsar detection Python code  
],  
'References' =>  
[  
[ 'CVE', '2017-0143'],  
[ 'CVE', '2017-0144'],  
[ 'CVE', '2017-0145'],  
[ 'CVE', '2017-0146'],  
[ 'CVE', '2017-0147'],  
[ 'CVE', '2017-0148'],  
[ 'MSB', 'MS17-010'],  
[ 'URL', 'https://zerosum0x0.blogspot.com/2017/04/doublepulsar-initial-smb-backdoor-ring.html'],  
[ 'URL', 'https://github.com/countercept/doublepulsar-detection-script'],  
[ 'URL', 'https://web.archive.org/web/20170513050203/https://technet.microsoft.com/en-us/library/security/ms17-010.aspx']  
],  
'License' => MSF_LICENSE,  
'Notes' =>  
{  
'AKA' => [  
'DOUBLEPULSAR',  
'ETERNALBLUE'  
]  
}  
))  
  
register_options(  
[  
OptBool.new('CHECK_DOPU', [false, 'Check for DOUBLEPULSAR on vulnerable hosts', true]),  
OptBool.new('CHECK_ARCH', [false, 'Check for architecture on vulnerable hosts', true]),  
OptBool.new('CHECK_PIPE', [false, 'Check for named pipe on vulnerable hosts', false])  
])  
end  
  
# algorithm to calculate the XOR Key for DoublePulsar knocks  
def calculate_doublepulsar_xor_key(s)  
x = (2 * s ^ (((s & 0xff00 | (s << 16)) << 8) | (((s >> 16) | s & 0xff0000) >> 8)))  
x & 0xffffffff # this line was added just to truncate to 32 bits  
end  
  
# The arch is adjacent to the XOR key in the SMB signature  
def calculate_doublepulsar_arch(s)  
s == 0 ? 'x86 (32-bit)' : 'x64 (64-bit)'  
end  
  
def run_host(ip)  
checkcode = Exploit::CheckCode::Unknown  
details = {}  
  
begin  
ipc_share = "\\\\#{ip}\\IPC$"  
  
tree_id = do_smb_setup_tree(ipc_share)  
vprint_status("Connected to #{ipc_share} with TID = #{tree_id}")  
  
status = do_smb_ms17_010_probe(tree_id)  
vprint_status("Received #{status} with FID = 0")  
  
os = simple.client.peer_native_os.dup  
details[:os] = os.dup  
if status == 'STATUS_INSUFF_SERVER_RESOURCES'  
if datastore['CHECK_ARCH']  
case dcerpc_getarch  
when ARCH_X86  
os << ' x86 (32-bit)'  
details[:arch] = ARCH_X86  
when ARCH_X64  
os << ' x64 (64-bit)'  
details[:arch] = ARCH_X64  
end  
end  
  
print_good("Host is likely VULNERABLE to MS17-010! - #{os}")  
  
checkcode = Exploit::CheckCode::Vulnerable(details: details)  
  
report_vuln(  
host: ip,  
port: rport, # A service is necessary for the analyze command  
name: self.name,  
refs: self.references,  
info: "STATUS_INSUFF_SERVER_RESOURCES for FID 0 against IPC$ - #{os}"  
)  
  
# vulnerable to MS17-010, check for DoublePulsar infection  
if datastore['CHECK_DOPU']  
code, signature1, signature2 = do_smb_doublepulsar_probe(tree_id)  
  
if code == 0x51  
xor_key = calculate_doublepulsar_xor_key(signature1).to_s(16).upcase  
arch = calculate_doublepulsar_arch(signature2)  
print_warning("Host is likely INFECTED with DoublePulsar! - Arch: #{arch}, XOR Key: 0x#{xor_key}")  
report_vuln(  
host: ip,  
name: "MS17-010 DoublePulsar Infection",  
refs: self.references,  
info: "MultiPlexID += 0x10 on Trans2 request - Arch: #{arch}, XOR Key: 0x#{xor_key}"  
)  
end  
end  
  
if datastore['CHECK_PIPE']  
pipe_name, _ = check_named_pipes(return_first: true)  
if pipe_name  
print_good("Named pipe found: #{pipe_name}")  
  
report_note(  
host: ip,  
port: rport,  
proto: 'tcp',  
sname: 'smb',  
type: 'MS17-010 Named Pipe',  
data: pipe_name  
)  
end  
end  
elsif status == "STATUS_ACCESS_DENIED" or status == "STATUS_INVALID_HANDLE"  
# STATUS_ACCESS_DENIED (Windows 10) and STATUS_INVALID_HANDLE (others)  
print_error("Host does NOT appear vulnerable.")  
else  
print_error("Unable to properly detect if host is vulnerable.")  
end  
  
unless (fp_match = Recog::Nizer.match('smb.native_os', simple.client.peer_native_os)).nil?  
report_host(  
host: rhost,  
arch: details[:arch],  
os_family: 'Windows',  
os_flavor: fp_match['os.edition'],  
os_name: fp_match['os.product']  
)  
end  
  
rescue ::Interrupt  
print_status("Exiting on interrupt.")  
raise $!  
rescue ::Rex::Proto::SMB::Exceptions::LoginError  
print_error("An SMB Login Error occurred while connecting to the IPC$ tree.")  
rescue ::Exception => e  
print_error("#{e.class}: #{e.message}")  
ensure  
disconnect  
end  
  
checkcode  
end  
  
def do_smb_setup_tree(ipc_share)  
connect(versions: [1])  
  
# logon as user \  
simple.login(datastore['SMBName'], datastore['SMBUser'], datastore['SMBPass'], datastore['SMBDomain'])  
  
# connect to IPC$  
simple.connect(ipc_share)  
  
# return tree  
return simple.shares[ipc_share]  
end  
  
def do_smb_doublepulsar_probe(tree_id)  
# make doublepulsar knock  
pkt = make_smb_trans2_doublepulsar(tree_id)  
  
sock.put(pkt)  
bytes = sock.get_once  
  
# convert packet to response struct  
pkt = Rex::Proto::SMB::Constants::SMB_TRANS_RES_HDR_PKT.make_struct  
pkt.from_s(bytes[4..-1])  
  
return pkt['SMB'].v['MultiplexID'], pkt['SMB'].v['Signature1'], pkt['SMB'].v['Signature2']  
end  
  
def do_smb_ms17_010_probe(tree_id)  
# request transaction with fid = 0  
pkt = make_smb_trans_ms17_010(tree_id)  
sock.put(pkt)  
bytes = sock.get_once  
  
# convert packet to response struct  
pkt = Rex::Proto::SMB::Constants::SMB_TRANS_RES_HDR_PKT.make_struct  
pkt.from_s(bytes[4..-1])  
  
# convert error code to string  
code = pkt['SMB'].v['ErrorClass']  
smberr = Rex::Proto::SMB::Exceptions::ErrorCode.new  
  
return smberr.get_error(code)  
end  
  
def make_smb_trans2_doublepulsar(tree_id)  
# make a raw transaction packet  
# this one is a trans2 packet, the checker is trans  
pkt = Rex::Proto::SMB::Constants::SMB_TRANS2_PKT.make_struct  
simple.client.smb_defaults(pkt['Payload']['SMB'])  
  
# opcode 0x0e = SESSION_SETUP  
setup = "\x0e\x00\x00\x00"  
setup_count = 1 # 1 word  
trans = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"  
  
# calculate offsets to the SetupData payload  
base_offset = pkt.to_s.length + (setup.length) - 4  
param_offset = base_offset + trans.length  
data_offset = param_offset # + 0  
  
# packet baselines  
pkt['Payload']['SMB'].v['Command'] = Rex::Proto::SMB::Constants::SMB_COM_TRANSACTION2  
pkt['Payload']['SMB'].v['Flags1'] = 0x18  
pkt['Payload']['SMB'].v['MultiplexID'] = 65  
pkt['Payload']['SMB'].v['Flags2'] = 0xc007  
pkt['Payload']['SMB'].v['TreeID'] = tree_id  
pkt['Payload']['SMB'].v['WordCount'] = 14 + setup_count  
pkt['Payload'].v['Timeout'] = 0x00a4d9a6  
pkt['Payload'].v['ParamCountTotal'] = 12  
pkt['Payload'].v['ParamCount'] = 12  
pkt['Payload'].v['ParamCountMax'] = 1  
pkt['Payload'].v['DataCountMax'] = 0  
pkt['Payload'].v['ParamOffset'] = 66  
pkt['Payload'].v['DataOffset'] = 78  
  
pkt['Payload'].v['SetupCount'] = setup_count  
pkt['Payload'].v['SetupData'] = setup  
pkt['Payload'].v['Payload'] = trans  
  
pkt.to_s  
end  
  
def make_smb_trans_ms17_010(tree_id)  
# make a raw transaction packet  
pkt = Rex::Proto::SMB::Constants::SMB_TRANS_PKT.make_struct  
simple.client.smb_defaults(pkt['Payload']['SMB'])  
  
# opcode 0x23 = PeekNamedPipe, fid = 0  
setup = "\x23\x00\x00\x00"  
setup_count = 2 # 2 words  
trans = "\\PIPE\\\x00"  
  
# calculate offsets to the SetupData payload  
base_offset = pkt.to_s.length + (setup.length) - 4  
param_offset = base_offset + trans.length  
data_offset = param_offset # + 0  
  
# packet baselines  
pkt['Payload']['SMB'].v['Command'] = Rex::Proto::SMB::Constants::SMB_COM_TRANSACTION  
pkt['Payload']['SMB'].v['Flags1'] = 0x18  
pkt['Payload']['SMB'].v['Flags2'] = 0x2801 # 0xc803 would unicode  
pkt['Payload']['SMB'].v['TreeID'] = tree_id  
pkt['Payload']['SMB'].v['WordCount'] = 14 + setup_count  
pkt['Payload'].v['ParamCountMax'] = 0xffff  
pkt['Payload'].v['DataCountMax'] = 0xffff  
pkt['Payload'].v['ParamOffset'] = param_offset  
pkt['Payload'].v['DataOffset'] = data_offset  
  
# actual magic: PeekNamedPipe FID=0, \PIPE\  
pkt['Payload'].v['SetupCount'] = setup_count  
pkt['Payload'].v['SetupData'] = setup  
pkt['Payload'].v['Payload'] = trans  
  
pkt.to_s  
end  
end