mirror of
https://github.com/sploutchy/impacket
synced 2026-06-08 17:32:01 +00:00
9421672ee6
* Added SMB session error handling in getMachineName() (SMB blocked). * Added LDAP session error handling in NTLM authentication (NTLM disabled).
271 lines
12 KiB
Python
Executable File
271 lines
12 KiB
Python
Executable File
#!/usr/bin/env python
|
|
# Impacket - Collection of Python classes for working with network protocols.
|
|
#
|
|
# SECUREAUTH LABS. Copyright (C) 2021 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:
|
|
# This script will gather data about the domain's users and their corresponding email addresses. It will also
|
|
# include some extra information about last logon and last password set attributes.
|
|
# You can enable or disable the the attributes shown in the final table by changing the values in line 184 and
|
|
# headers in line 190.
|
|
# If no entries are returned that means users don't have email addresses specified. If so, you can use the
|
|
# -all-users parameter.
|
|
#
|
|
# Author:
|
|
# Alberto Solino (@agsolino)
|
|
#
|
|
# Reference for:
|
|
# LDAP
|
|
#
|
|
|
|
from __future__ import division
|
|
from __future__ import print_function
|
|
from __future__ import unicode_literals
|
|
import argparse
|
|
import logging
|
|
import sys
|
|
from datetime import datetime
|
|
|
|
from impacket import version
|
|
from impacket.dcerpc.v5.samr import UF_ACCOUNTDISABLE
|
|
from impacket.examples import logger
|
|
from impacket.examples.utils import parse_credentials
|
|
from impacket.ldap import ldap, ldapasn1
|
|
from impacket.smbconnection import SMBConnection, SessionError
|
|
|
|
|
|
class GetADUsers:
|
|
def __init__(self, username, password, domain, cmdLineOptions):
|
|
self.options = cmdLineOptions
|
|
self.__username = username
|
|
self.__password = password
|
|
self.__domain = domain
|
|
self.__target = None
|
|
self.__lmhash = ''
|
|
self.__nthash = ''
|
|
self.__aesKey = cmdLineOptions.aesKey
|
|
self.__doKerberos = cmdLineOptions.k
|
|
#[!] in this script the value of -dc-ip option is self.__kdcIP and the value of -dc-host option is self.__kdcHost
|
|
self.__kdcIP = cmdLineOptions.dc_ip
|
|
self.__kdcHost = cmdLineOptions.dc_host
|
|
self.__requestUser = cmdLineOptions.user
|
|
self.__all = cmdLineOptions.all
|
|
if cmdLineOptions.hashes is not None:
|
|
self.__lmhash, self.__nthash = cmdLineOptions.hashes.split(':')
|
|
|
|
# Create the baseDN
|
|
domainParts = self.__domain.split('.')
|
|
self.baseDN = ''
|
|
for i in domainParts:
|
|
self.baseDN += 'dc=%s,' % i
|
|
# Remove last ','
|
|
self.baseDN = self.baseDN[:-1]
|
|
|
|
# Let's calculate the header and format
|
|
self.__header = ["Name", "Email", "PasswordLastSet", "LastLogon"]
|
|
# Since we won't process all rows at once, this will be fixed lengths
|
|
self.__colLen = [20, 30, 19, 19]
|
|
self.__outputFormat = ' '.join(['{%d:%ds} ' % (num, width) for num, width in enumerate(self.__colLen)])
|
|
|
|
def getMachineName(self, target):
|
|
try:
|
|
s = SMBConnection(target, target)
|
|
s.login('', '')
|
|
except OSError as e:
|
|
if str(e).find('timed out') > 0:
|
|
raise Exception('The connection is timed out. Probably 445/TCP port is closed. Try to specify '
|
|
'corresponding NetBIOS name or FQDN as the value of the -dc-host option')
|
|
else:
|
|
raise
|
|
except SessionError as e:
|
|
if str(e).find('STATUS_NOT_SUPPORTED') > 0:
|
|
raise Exception('The SMB request is not supported. Probably NTLM is disabled. Try to specify '
|
|
'corresponding NetBIOS name or FQDN as the value of the -dc-host option')
|
|
else:
|
|
raise
|
|
except Exception:
|
|
if s.getServerName() == '':
|
|
raise Exception('Error while anonymous logging into %s' % target)
|
|
else:
|
|
s.logoff()
|
|
return s.getServerName()
|
|
|
|
@staticmethod
|
|
def getUnixTime(t):
|
|
t -= 116444736000000000
|
|
t /= 10000000
|
|
return t
|
|
|
|
def processRecord(self, item):
|
|
if isinstance(item, ldapasn1.SearchResultEntry) is not True:
|
|
return
|
|
sAMAccountName = ''
|
|
pwdLastSet = ''
|
|
mail = ''
|
|
lastLogon = 'N/A'
|
|
try:
|
|
for attribute in item['attributes']:
|
|
if str(attribute['type']) == 'sAMAccountName':
|
|
if attribute['vals'][0].asOctets().decode('utf-8').endswith('$') is False:
|
|
# User Account
|
|
sAMAccountName = attribute['vals'][0].asOctets().decode('utf-8')
|
|
elif str(attribute['type']) == 'pwdLastSet':
|
|
if str(attribute['vals'][0]) == '0':
|
|
pwdLastSet = '<never>'
|
|
else:
|
|
pwdLastSet = str(datetime.fromtimestamp(self.getUnixTime(int(str(attribute['vals'][0])))))
|
|
elif str(attribute['type']) == 'lastLogon':
|
|
if str(attribute['vals'][0]) == '0':
|
|
lastLogon = '<never>'
|
|
else:
|
|
lastLogon = str(datetime.fromtimestamp(self.getUnixTime(int(str(attribute['vals'][0])))))
|
|
elif str(attribute['type']) == 'mail':
|
|
mail = str(attribute['vals'][0])
|
|
|
|
print((self.__outputFormat.format(*[sAMAccountName, mail, pwdLastSet, lastLogon])))
|
|
except Exception as e:
|
|
logging.debug("Exception", exc_info=True)
|
|
logging.error('Skipping item, cannot process due to error %s' % str(e))
|
|
pass
|
|
|
|
def run(self):
|
|
if self.__kdcHost is not None:
|
|
self.__target = self.__kdcHost
|
|
else:
|
|
if self.__kdcIP is not None:
|
|
self.__target = self.__kdcIP
|
|
else:
|
|
self.__target = self.__domain
|
|
|
|
if self.__doKerberos:
|
|
logging.info('Getting machine hostname')
|
|
self.__target = self.getMachineName(self.__target)
|
|
|
|
# Connect to LDAP
|
|
try:
|
|
ldapConnection = ldap.LDAPConnection('ldap://%s' % self.__target, self.baseDN, self.__kdcIP)
|
|
if self.__doKerberos is not True:
|
|
ldapConnection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash)
|
|
else:
|
|
ldapConnection.kerberosLogin(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash,
|
|
self.__aesKey, kdcHost=self.__kdcIP)
|
|
except ldap.LDAPSessionError as e:
|
|
if str(e).find('strongerAuthRequired') >= 0:
|
|
# We need to try SSL
|
|
ldapConnection = ldap.LDAPConnection('ldaps://%s' % self.__target, self.baseDN, self.__kdcIP)
|
|
if self.__doKerberos is not True:
|
|
ldapConnection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash)
|
|
else:
|
|
ldapConnection.kerberosLogin(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash,
|
|
self.__aesKey, kdcHost=self.__kdcIP)
|
|
else:
|
|
if str(e).find('NTLMAuthNegotiate') >= 0:
|
|
logging.critical("NTLM negotiation failed. Probably NTLM is disabled. Try to use Kerberos "
|
|
"authentication instead.")
|
|
else:
|
|
if self.__kdcIP is not None and self.__kdcHost is not None:
|
|
logging.critical("If the credentials are valid, check the hostname and IP address of KDC. They "
|
|
"must match exactly each other.")
|
|
raise
|
|
|
|
logging.info('Querying %s for information about domain.' % self.__target)
|
|
# Print header
|
|
print((self.__outputFormat.format(*self.__header)))
|
|
print((' '.join(['-' * itemLen for itemLen in self.__colLen])))
|
|
|
|
# Building the search filter
|
|
if self.__all:
|
|
searchFilter = "(&(sAMAccountName=*)(objectCategory=user)"
|
|
else:
|
|
searchFilter = "(&(sAMAccountName=*)(mail=*)(!(UserAccountControl:1.2.840.113556.1.4.803:=%d))" % UF_ACCOUNTDISABLE
|
|
|
|
if self.__requestUser is not None:
|
|
searchFilter += '(sAMAccountName:=%s))' % self.__requestUser
|
|
else:
|
|
searchFilter += ')'
|
|
|
|
try:
|
|
logging.debug('Search Filter=%s' % searchFilter)
|
|
sc = ldap.SimplePagedResultsControl(size=100)
|
|
ldapConnection.search(searchFilter=searchFilter,
|
|
attributes=['sAMAccountName', 'pwdLastSet', 'mail', 'lastLogon'],
|
|
sizeLimit=0, searchControls = [sc], perRecordCallback=self.processRecord)
|
|
except ldap.LDAPSearchError:
|
|
raise
|
|
|
|
ldapConnection.close()
|
|
|
|
# Process command-line arguments.
|
|
if __name__ == '__main__':
|
|
print((version.BANNER))
|
|
|
|
parser = argparse.ArgumentParser(add_help = True, description = "Queries target domain for users data")
|
|
|
|
parser.add_argument('target', action='store', help='domain[/username[:password]]')
|
|
parser.add_argument('-user', action='store', metavar='username', help='Requests data for specific user ')
|
|
parser.add_argument('-all', action='store_true', help='Return all users, including those with no email '
|
|
'addresses and disabled accounts. When used with -user it '
|
|
'will return user\'s info even if the account is disabled')
|
|
parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output')
|
|
parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON')
|
|
|
|
group = parser.add_argument_group('authentication')
|
|
group.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH')
|
|
group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)')
|
|
group.add_argument('-k', action="store_true", help='Use Kerberos authentication. Grabs credentials from ccache file '
|
|
'(KRB5CCNAME) based on target parameters. If valid credentials '
|
|
'cannot be found, it will use the ones specified in the command '
|
|
'line')
|
|
group.add_argument('-aesKey', action="store", metavar = "hex key", help='AES key to use for Kerberos Authentication '
|
|
'(128 or 256 bits)')
|
|
|
|
group = parser.add_argument_group('connection')
|
|
group.add_argument('-dc-ip', action='store', metavar='ip address', help='IP Address of the domain controller. If '
|
|
'ommited it use the domain part (FQDN) '
|
|
'specified in the target parameter')
|
|
group.add_argument('-dc-host', action='store', metavar='hostname', help='Hostname of the domain controller to use. '
|
|
'If ommited, the domain part (FQDN) '
|
|
'specified in the account parameter will be used')
|
|
|
|
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)
|
|
|
|
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 = parse_credentials(options.target)
|
|
|
|
if domain == '':
|
|
logging.critical('Domain should be specified!')
|
|
sys.exit(1)
|
|
|
|
if password == '' and username != '' and options.hashes is None and options.no_pass is False and options.aesKey is None:
|
|
from getpass import getpass
|
|
password = getpass("Password:")
|
|
|
|
if options.aesKey is not None:
|
|
options.k = True
|
|
|
|
try:
|
|
executer = GetADUsers(username, password, domain, options)
|
|
executer.run()
|
|
except Exception as e:
|
|
if logging.getLogger().level == logging.DEBUG:
|
|
import traceback
|
|
traceback.print_exc()
|
|
logging.error(str(e))
|