mirror of
https://github.com/fortra/impacket
synced 2026-06-08 14:15:13 +00:00
f2765e5f2e
* Add Basic Auth support in exchanger * Update examples/exchanger.py Co-authored-by: alexisbalbachan <alexisbalbachan@gmail.com> --------- Co-authored-by: Thomas Caesar <thomas.caesar@sva.de> Co-authored-by: alexisbalbachan <alexisbalbachan@gmail.com>
1080 lines
41 KiB
Python
Executable File
1080 lines
41 KiB
Python
Executable File
#!/usr/bin/env python
|
|
# 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)
|
|
if options.basic:
|
|
self._rpctransport.set_auth_type(AUTH_BASIC)
|
|
|
|
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__':
|
|
# 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('-ts', action='store_true', help='Adds timestamp to every logging output')
|
|
#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')
|
|
parser.add_argument('-basic', action='store_true', help='Authenticate with Basic Auth instead of NTLM')
|
|
|
|
|
|
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()
|
|
# Init the example's logger theme
|
|
logger.init(options.ts, options.debug)
|
|
|
|
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)")
|