Skip to content

Commit 5476f60

Browse files
committed
Land rapid7#8271, DOUBLEPULSAR detection for MS17-010
2 parents 6f763a6 + 55f01d3 commit 5476f60

File tree

1 file changed

+98
-12
lines changed

1 file changed

+98
-12
lines changed

modules/auxiliary/scanner/smb/smb_ms17_010.rb

Lines changed: 98 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,17 @@ def initialize(info = {})
2222
If the status returned is "STATUS_INSUFF_SERVER_RESOURCES", the machine does
2323
not have the MS17-010 patch.
2424
25+
If the machine is missing the MS17-010 patch, the module will check for an
26+
existing DoublePulsar (ring 0 shellcode/malware) infection.
27+
2528
This module does not require valid SMB credentials in default server
2629
configurations. It can log on as the user "\" and connect to IPC$.
2730
},
28-
'Author' => [ 'Sean Dillon <[email protected]>' ],
31+
'Author' =>
32+
[
33+
'Sean Dillon <[email protected]>', # @zerosum0x0
34+
'Luke Jennings' # DoublePulsar detection Python code
35+
],
2936
'References' =>
3037
[
3138
[ 'CVE', '2017-0143'],
@@ -35,27 +42,55 @@ def initialize(info = {})
3542
[ 'CVE', '2017-0147'],
3643
[ 'CVE', '2017-0148'],
3744
[ 'MSB', 'MS17-010'],
45+
[ 'URL', 'https://zerosum0x0.blogspot.com/2017/04/doublepulsar-initial-smb-backdoor-ring.html'],
46+
[ 'URL', 'https://github.com/countercept/doublepulsar-detection-script'],
3847
[ 'URL', 'https://technet.microsoft.com/en-us/library/security/ms17-010.aspx']
3948
],
4049
'License' => MSF_LICENSE
4150
))
4251
end
4352

53+
# algorithm to calculate the XOR Key for DoublePulsar knocks
54+
def calculate_doublepulsar_xor_key(s)
55+
x = (2 * s ^ (((s & 0xff00 | (s << 16)) << 8) | (((s >> 16) | s & 0xff0000) >> 8)))
56+
x & 0xffffffff # this line was added just to truncate to 32 bits
57+
end
58+
4459
def run_host(ip)
4560
begin
46-
status = do_smb_probe(ip)
61+
ipc_share = "\\\\#{ip}\\IPC$"
62+
63+
tree_id = do_smb_setup_tree(ipc_share)
64+
vprint_status("Connected to #{ipc_share} with TID = #{tree_id}")
65+
66+
status = do_smb_ms17_010_probe(tree_id)
67+
vprint_status("Received #{status} with FID = 0")
4768

4869
if status == "STATUS_INSUFF_SERVER_RESOURCES"
49-
print_warning("Host is likely VULNERABLE to MS17-010!")
70+
print_good("Host is likely VULNERABLE to MS17-010! (#{simple.client.peer_native_os})")
5071
report_vuln(
5172
host: ip,
5273
name: self.name,
5374
refs: self.references,
54-
info: 'STATUS_INSUFF_SERVER_RESOURCES for FID 0 against IPC$'
75+
info: 'STATUS_INSUFF_SERVER_RESOURCES for FID 0 against IPC$ -- (#{simple.client.peer_native_os})'
5576
)
77+
78+
# vulnerable to MS17-010, check for DoublePulsar infection
79+
code, signature = do_smb_doublepulsar_probe(tree_id)
80+
81+
if code == 0x51
82+
xor_key = calculate_doublepulsar_xor_key(signature).to_s(16).upcase
83+
print_warning("Host is likely INFECTED with DoublePulsar! - XOR Key: #{xor_key}")
84+
report_vuln(
85+
host: ip,
86+
name: "MS17-010 DoublePulsar Infection",
87+
refs: self.references,
88+
info: 'MultiPlexID += 0x10 on Trans2 request - XOR Key: #{xor_key}'
89+
)
90+
end
5691
elsif status == "STATUS_ACCESS_DENIED" or status == "STATUS_INVALID_HANDLE"
5792
# STATUS_ACCESS_DENIED (Windows 10) and STATUS_INVALID_HANDLE (others)
58-
print_good("Host does NOT appear vulnerable.")
93+
print_bad("Host does NOT appear vulnerable.")
5994
else
6095
print_bad("Unable to properly detect if host is vulnerable.")
6196
end
@@ -72,19 +107,34 @@ def run_host(ip)
72107
end
73108
end
74109

75-
def do_smb_probe(ip)
110+
def do_smb_setup_tree(ipc_share)
76111
connect
77112

78113
# logon as user \
79114
simple.login(datastore['SMBName'], datastore['SMBUser'], datastore['SMBPass'], datastore['SMBDomain'])
80115

81116
# connect to IPC$
82-
ipc_share = "\\\\#{ip}\\IPC$"
83117
simple.connect(ipc_share)
84-
tree_id = simple.shares[ipc_share]
85118

86-
print_status("Connected to #{ipc_share} with TID = #{tree_id}")
119+
# return tree
120+
return simple.shares[ipc_share]
121+
end
122+
123+
def do_smb_doublepulsar_probe(tree_id)
124+
# make doublepulsar knock
125+
pkt = make_smb_trans2_doublepulsar(tree_id)
87126

127+
sock.put(pkt)
128+
bytes = sock.get_once
129+
130+
# convert packet to response struct
131+
pkt = Rex::Proto::SMB::Constants::SMB_TRANS_RES_HDR_PKT.make_struct
132+
pkt.from_s(bytes[4..-1])
133+
134+
return pkt['SMB'].v['MultiplexID'], pkt['SMB'].v['Signature1']
135+
end
136+
137+
def do_smb_ms17_010_probe(tree_id)
88138
# request transaction with fid = 0
89139
pkt = make_smb_trans_ms17_010(tree_id)
90140
sock.put(pkt)
@@ -97,10 +147,46 @@ def do_smb_probe(ip)
97147
# convert error code to string
98148
code = pkt['SMB'].v['ErrorClass']
99149
smberr = Rex::Proto::SMB::Exceptions::ErrorCode.new
100-
status = smberr.get_error(code)
101150

102-
print_status("Received #{status} with FID = 0")
103-
status
151+
return smberr.get_error(code)
152+
end
153+
154+
def make_smb_trans2_doublepulsar(tree_id)
155+
# make a raw transaction packet
156+
# this one is a trans2 packet, the checker is trans
157+
pkt = Rex::Proto::SMB::Constants::SMB_TRANS2_PKT.make_struct
158+
simple.client.smb_defaults(pkt['Payload']['SMB'])
159+
160+
# opcode 0x0e = SESSION_SETUP
161+
setup = "\x0e\x00\x00\x00"
162+
setup_count = 1 # 1 word
163+
trans = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
164+
165+
# calculate offsets to the SetupData payload
166+
base_offset = pkt.to_s.length + (setup.length) - 4
167+
param_offset = base_offset + trans.length
168+
data_offset = param_offset # + 0
169+
170+
# packet baselines
171+
pkt['Payload']['SMB'].v['Command'] = Rex::Proto::SMB::Constants::SMB_COM_TRANSACTION2
172+
pkt['Payload']['SMB'].v['Flags1'] = 0x18
173+
pkt['Payload']['SMB'].v['MultiplexID'] = 65
174+
pkt['Payload']['SMB'].v['Flags2'] = 0xc007
175+
pkt['Payload']['SMB'].v['TreeID'] = tree_id
176+
pkt['Payload']['SMB'].v['WordCount'] = 14 + setup_count
177+
pkt['Payload'].v['Timeout'] = 0x00a4d9a6
178+
pkt['Payload'].v['ParamCountTotal'] = 12
179+
pkt['Payload'].v['ParamCount'] = 12
180+
pkt['Payload'].v['ParamCountMax'] = 1
181+
pkt['Payload'].v['DataCountMax'] = 0
182+
pkt['Payload'].v['ParamOffset'] = 66
183+
pkt['Payload'].v['DataOffset'] = 78
184+
185+
pkt['Payload'].v['SetupCount'] = setup_count
186+
pkt['Payload'].v['SetupData'] = setup
187+
pkt['Payload'].v['Payload'] = trans
188+
189+
pkt.to_s
104190
end
105191

106192
def make_smb_trans_ms17_010(tree_id)

0 commit comments

Comments
 (0)