#!/opt/local/Library/Frameworks/Python.framework/Versions/3.13/bin/python3.13
# Impacket - Collection of Python classes for working with network protocols.
#
# Copyright Fortra, LLC and its affiliated companies 
#
# 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 <mohemiv@gmail.com> / 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]@]<targetName or address>')
    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)")
