Created
May 17, 2023 07:36
-
-
Save k4nfr3/89d743a825fe90aa49911895c19b804e to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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)) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment