1#!/usr/bin/env python 2# Copyright (c) 2016 CORE Security Technologies 3# 4# This software is provided under under a slightly modified version 5# of the Apache Software License. See the accompanying LICENSE file 6# for more information. 7# 8# Author: 9# Alberto Solino (@agsolino) 10# 11# Description: 12# This script will gather data about the domain's users and their corresponding email addresses. It will also 13# include some extra information about last logon and last password set attributes. 14# You can enable or disable the the attributes shown in the final table by changing the values in line 184 and 15# headers in line 190. 16# If no entries are returned that means users don't have email addresses specified. If so, you can use the 17# -all-users parameter. 18# 19# Reference for: 20# LDAP 21# 22 23 24import argparse 25import logging 26import sys 27from datetime import datetime 28 29from impacket import version 30from impacket.dcerpc.v5.samr import UF_ACCOUNTDISABLE 31from impacket.examples import logger 32from impacket.ldap import ldap, ldapasn1 33from impacket.smbconnection import SMBConnection 34 35 36class GetADUsers: 37 def __init__(self, username, password, domain, cmdLineOptions): 38 self.options = cmdLineOptions 39 self.__username = username 40 self.__password = password 41 self.__domain = domain 42 self.__lmhash = '' 43 self.__nthash = '' 44 self.__aesKey = cmdLineOptions.aesKey 45 self.__doKerberos = cmdLineOptions.k 46 self.__target = None 47 self.__kdcHost = cmdLineOptions.dc_ip 48 self.__requestUser = cmdLineOptions.user 49 self.__all = cmdLineOptions.all 50 if cmdLineOptions.hashes is not None: 51 self.__lmhash, self.__nthash = cmdLineOptions.hashes.split(':') 52 53 # Create the baseDN 54 domainParts = self.__domain.split('.') 55 self.baseDN = '' 56 for i in domainParts: 57 self.baseDN += 'dc=%s,' % i 58 # Remove last ',' 59 self.baseDN = self.baseDN[:-1] 60 61 # Let's calculate the header and format 62 self.__header = ["Name", "Email", "PasswordLastSet", "LastLogon"] 63 # Since we won't process all rows at once, this will be fixed lengths 64 self.__colLen = [20, 30, 19, 19] 65 self.__outputFormat = ' '.join(['{%d:%ds} ' % (num, width) for num, width in enumerate(self.__colLen)]) 66 67 68 69 def getMachineName(self): 70 if self.__kdcHost is not None: 71 s = SMBConnection(self.__kdcHost, self.__kdcHost) 72 else: 73 s = SMBConnection(self.__domain, self.__domain) 74 try: 75 s.login('', '') 76 except Exception: 77 if s.getServerName() == '': 78 raise('Error while anonymous logging into %s' % self.__domain) 79 else: 80 s.logoff() 81 return s.getServerName() 82 83 @staticmethod 84 def getUnixTime(t): 85 t -= 116444736000000000 86 t /= 10000000 87 return t 88 89 def processRecord(self, item): 90 if isinstance(item, ldapasn1.SearchResultEntry) is not True: 91 return 92 sAMAccountName = '' 93 pwdLastSet = '' 94 mail = '' 95 lastLogon = 'N/A' 96 try: 97 for attribute in item['attributes']: 98 if attribute['type'] == 'sAMAccountName': 99 if str(attribute['vals'][0]).endswith('$') is False: 100 # User Account 101 sAMAccountName = str(attribute['vals'][0]) 102 elif attribute['type'] == 'pwdLastSet': 103 if str(attribute['vals'][0]) == '0': 104 pwdLastSet = '<never>' 105 else: 106 pwdLastSet = str(datetime.fromtimestamp(self.getUnixTime(int(str(attribute['vals'][0]))))) 107 elif attribute['type'] == 'lastLogon': 108 if str(attribute['vals'][0]) == '0': 109 lastLogon = '<never>' 110 else: 111 lastLogon = str(datetime.fromtimestamp(self.getUnixTime(int(str(attribute['vals'][0]))))) 112 elif attribute['type'] == 'mail': 113 mail = str(attribute['vals'][0]) 114 115 print(self.__outputFormat.format(*[sAMAccountName, mail, pwdLastSet, lastLogon])) 116 except Exception as e: 117 logging.error('Skipping item, cannot process due to error %s' % str(e)) 118 pass 119 120 def run(self): 121 if self.__doKerberos: 122 self.__target = self.getMachineName() 123 else: 124 if self.__kdcHost is not None: 125 self.__target = self.__kdcHost 126 else: 127 self.__target = self.__domain 128 129 # Connect to LDAP 130 try: 131 ldapConnection = ldap.LDAPConnection('ldap://%s'%self.__target, self.baseDN, self.__kdcHost) 132 if self.__doKerberos is not True: 133 ldapConnection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) 134 else: 135 ldapConnection.kerberosLogin(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash, 136 self.__aesKey, kdcHost=self.__kdcHost) 137 except ldap.LDAPSessionError as e: 138 if str(e).find('strongerAuthRequired') >= 0: 139 # We need to try SSL 140 ldapConnection = ldap.LDAPConnection('ldaps://%s' % self.__target, self.baseDN, self.__kdcHost) 141 if self.__doKerberos is not True: 142 ldapConnection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) 143 else: 144 ldapConnection.kerberosLogin(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash, 145 self.__aesKey, kdcHost=self.__kdcHost) 146 else: 147 raise 148 149 logging.info('Querying %s for information about domain.' % self.__target) 150 # Print header 151 print(self.__outputFormat.format(*self.__header)) 152 print(' '.join(['-' * itemLen for itemLen in self.__colLen])) 153 154 # Building the search filter 155 if self.__all: 156 searchFilter = "(&(sAMAccountName=*)(objectCategory=user)" 157 else: 158 searchFilter = "(&(sAMAccountName=*)(mail=*)(!(UserAccountControl:1.2.840.113556.1.4.803:=%d))" % UF_ACCOUNTDISABLE 159 160 if self.__requestUser is not None: 161 searchFilter += '(sAMAccountName:=%s))' % self.__requestUser 162 else: 163 searchFilter += ')' 164 165 try: 166 logging.debug('Search Filter=%s' % searchFilter) 167 sc = ldap.SimplePagedResultsControl(size=100) 168 resp = ldapConnection.search(searchFilter=searchFilter, 169 attributes=['sAMAccountName', 'pwdLastSet', 'mail', 'lastLogon'], 170 sizeLimit=0, searchControls = [sc], perRecordCallback=self.processRecord) 171 except ldap.LDAPSearchError as e: 172 raise 173 174 ldapConnection.close() 175 176# Process command-line arguments. 177if __name__ == '__main__': 178 # Init the example's logger theme 179 logger.init() 180 print(version.BANNER) 181 182 parser = argparse.ArgumentParser(add_help = True, description = "Queries target domain for users data") 183 184 parser.add_argument('target', action='store', help='domain/username[:password]') 185 parser.add_argument('-user', action='store', metavar='username', help='Requests data for specific user ') 186 parser.add_argument('-all', action='store_true', help='Return all users, including those with no email ' 187 'addresses and disabled accounts. When used with -user it ' 188 'will return user\'s info even if the account is disabled') 189 parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') 190 191 group = parser.add_argument_group('authentication') 192 193 group.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') 194 group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') 195 group.add_argument('-k', action="store_true", help='Use Kerberos authentication. Grabs credentials from ccache file ' 196 '(KRB5CCNAME) based on target parameters. If valid credentials ' 197 'cannot be found, it will use the ones specified in the command ' 198 'line') 199 group.add_argument('-aesKey', action="store", metavar = "hex key", help='AES key to use for Kerberos Authentication ' 200 '(128 or 256 bits)') 201 group.add_argument('-dc-ip', action='store',metavar = "ip address", help='IP Address of the domain controller. If ' 202 'ommited it use the domain part (FQDN) ' 203 'specified in the target parameter') 204 205 if len(sys.argv)==1: 206 parser.print_help() 207 sys.exit(1) 208 209 options = parser.parse_args() 210 211 if options.debug is True: 212 logging.getLogger().setLevel(logging.DEBUG) 213 else: 214 logging.getLogger().setLevel(logging.INFO) 215 216 import re 217 # This is because I'm lazy with regex 218 # ToDo: We need to change the regex to fullfil domain/username[:password] 219 targetParam = options.target+'@' 220 domain, username, password, address = re.compile('(?:(?:([^/@:]*)/)?([^@:]*)(?::([^@]*))?@)?(.*)').match(targetParam).groups('') 221 222 #In case the password contains '@' 223 if '@' in address: 224 password = password + '@' + address.rpartition('@')[0] 225 address = address.rpartition('@')[2] 226 227 if domain is '': 228 logging.critical('Domain should be specified!') 229 sys.exit(1) 230 231 if password == '' and username != '' and options.hashes is None and options.no_pass is False and options.aesKey is None: 232 from getpass import getpass 233 password = getpass("Password:") 234 235 if options.aesKey is not None: 236 options.k = True 237 238 try: 239 executer = GetADUsers(username, password, domain, options) 240 executer.run() 241 except Exception as e: 242 if logging.getLogger().level == logging.DEBUG: 243 import traceback 244 traceback.print_exc() 245 print (str(e)) 246