#!/usr/bin/env python # modifications of original script GetAdusers.py from Impacket. # this version returns the list of last seen 24h machines #python list_machines.py TIMATEC.local/fbu -dc-ip 192.168.16.11 #Impacket v0.10.0 - Copyright 2022 SecureAuth Corporation # #Password: #[*] Querying 192.168.16.11 for information about domain. #Name PasswordLastSet LastLogon OperatingSystemVersion OperatingSystem IP Address #-------------------- -------------------------- -------------------------- ---------------------- --------------------------------------- ------------ #DC01 2023-04-30 23:58:00.568324 2023-05-17 09:32:14.370208 10.0 (17763) Windows Server 2019 Standard Evaluation 192.168.16.11 #LAB-FBK-WIN10-X 2023-05-04 18:54:55.470565 2023-05-17 08:01:59.690254 10.0 (19045) Windows 10 Pro #DC02 2023-05-15 14:02:57.771260 2023-05-17 09:23:39.722878 10.0 (17763) Windows Server 2019 Standard Evaluation 192.168.16.12 #WIN10-NOTHING 2023-04-24 17:16:49.914626 2023-05-17 02:56:19.750823 10.0 (18363) Windows 10 Enterprise 192.168.16.4 # # original comment # Impacket - Collection of Python classes for working with network protocols. # # Copyright (C) 2022 Fortra. 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, timedelta 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 import colorama import socket def get_ipv4_by_hostname(hostname): try: # Resolve the hostname to IPv4 address ip_address = socket.gethostbyname(hostname) return ip_address except socket.gaierror: # Handle DNS lookup failure return '' 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", "PasswordLastSet", "LastLogon", "OperatingSystemVersion", "OperatingSystem", "IP Address"] # Since we won't process all rows at once, this will be fixed lengths self.__colLen = [20, 26, 26, 22, 39, 12] 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' operatingSystem = '' operatingSystemVersion= '' ipaddress = '0.0.0.0' last24h = False try: for attribute in item['attributes']: if str(attribute['type']) == 'sAMAccountName': if attribute['vals'][0].asOctets().decode('utf-8').endswith('$') is True: # Computer Account sAMAccountName = attribute['vals'][0].asOctets().decode('utf-8')[:-1] ipaddress = get_ipv4_by_hostname(str(sAMAccountName)) 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]))))) now = datetime.now() time_diff = now - datetime.fromtimestamp(self.getUnixTime(int(str(attribute['vals'][0])))) if time_diff <= timedelta(hours=24): lastLogon = "\033[32m" + str(datetime.fromtimestamp(self.getUnixTime(int(str(attribute['vals'][0])))))+ "\033[0m" last24h = True elif str(attribute['type']) == 'operatingSystem': operatingSystem = str(attribute['vals'][0]) elif str(attribute['type']) == 'operatingSystemVersion': operatingSystemVersion = str(attribute['vals'][0]) if last24h: print((self.__outputFormat.format(*[sAMAccountName, pwdLastSet, lastLogon, operatingSystemVersion, operatingSystem, ipaddress]))) elif self.__all: print((self.__outputFormat.format(*[sAMAccountName, pwdLastSet, lastLogon, operatingSystemVersion, operatingSystem, ipaddress]))) 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 searchFilter = "(&(sAMAccountName=*)(objectCategory=computer)" 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', 'operatingSystem', 'operatingSystemVersion'], sizeLimit=0, searchControls = [sc], perRecordCallback=self.processRecord) except ldap.LDAPSearchError: raise ldapConnection.close() # Process command-line arguments. if __name__ == '__main__': colorama.init() 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 machines even last logon more than 24hours') 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))