Skip to content

Instantly share code, notes, and snippets.

@k4nfr3
Created May 17, 2023 07:36
Show Gist options
  • Save k4nfr3/89d743a825fe90aa49911895c19b804e to your computer and use it in GitHub Desktop.
Save k4nfr3/89d743a825fe90aa49911895c19b804e to your computer and use it in GitHub Desktop.
#!/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