diff --git a/examples/secretsdump.py b/examples/secretsdump.py index a881a8cef7..c02a3b218a 100755 --- a/examples/secretsdump.py +++ b/examples/secretsdump.py @@ -174,6 +174,11 @@ def dump(self): if self.__ntdsFile is not None: # Let's grab target's configuration about LM Hashes storage self.__noLMHash = localOperations.checkNoLMHashPolicy() + + # if we are processing a LOCAL adam/lds ditfile, we will calculate the bootkey from it directly at a later stage + elif self.__options.adamlds is True: + bootKey = None + else: import binascii bootKey = binascii.unhexlify(self.__bootkey) @@ -274,7 +279,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) + ldapFilter=self.__ldapFilter, printUserStatus=self.__printUserStatus, isADAMLDS=self.__options.adamlds) try: self.__NTDSHashes.dump() except Exception as e: @@ -357,6 +362,8 @@ def cleanup(self): parser.add_argument('-security', action='store', help='SECURITY hive to parse') parser.add_argument('-sam', action='store', help='SAM hive to parse') parser.add_argument('-ntds', action='store', help='NTDS.DIT file to parse') + parser.add_argument('-adamlds', action='store_true', default=False, help='Indicates that the .dit file to be parsed is an Active Directory ' + 'Application Mode/Lightweight Directory Services (ADAM/LDS) file') parser.add_argument('-resumefile', action='store', help='resume file name to resume NTDS.DIT session dump (only ' 'available to DRSUAPI approach). This file will also be used to keep updating the session\'s ' 'state') @@ -450,7 +457,7 @@ def cleanup(self): sys.exit(1) if remoteName.upper() == 'LOCAL' and username == '': - if options.system is None and options.bootkey is None: + if options.system is None and options.bootkey is None and options.adamlds is None: logging.error('Either the SYSTEM hive or bootkey is required for local parsing, check help') sys.exit(1) else: @@ -480,4 +487,4 @@ def cleanup(self): if logging.getLogger().level == logging.DEBUG: import traceback traceback.print_exc() - logging.error(e) + logging.error(e) \ No newline at end of file diff --git a/impacket/examples/secretsdump.py b/impacket/examples/secretsdump.py index ceb34be89b..08de2cf011 100644 --- a/impacket/examples/secretsdump.py +++ b/impacket/examples/secretsdump.py @@ -1931,6 +1931,9 @@ class SECRET_TYPE: 0xffffff74:'rc4_hmac', } + ROOTPEKLISTPERMUTATION = [2,4,25,9,7,27,5,11] + SCHEMAPEKLISTPERMUTATION = [37,2,17,36,20,11,22,7] + INTERNAL_TO_NAME = dict((v,k) for k,v in NAME_TO_INTERNAL.items()) SAM_NORMAL_USER_ACCOUNT = 0x30000000 @@ -1992,7 +1995,7 @@ def __init__(self, ntdsFile, bootKey, isRemote=False, history=False, noLMHash=Tr useVSSMethod=False, justNTLM=False, pwdLastSet=False, resumeSession=None, outputFileName=None, justUser=None, ldapFilter=None, printUserStatus=False, perSecretCallback = lambda secretType, secret : _print_helper(secret), - resumeSessionMgr=ResumeSessionMgrInFile): + resumeSessionMgr=ResumeSessionMgrInFile, isADAMLDS=False): self.__bootKey = bootKey self.__NTDS = ntdsFile self.__history = history @@ -2015,6 +2018,7 @@ def __init__(self, ntdsFile, bootKey, isRemote=False, history=False, noLMHash=Tr self.__justUser = justUser self.__ldapFilter = ldapFilter self.__perSecretCallback = perSecretCallback + self.__isADAMLDS = isADAMLDS # these are all the columns that we need to get the secrets. # If in the future someone finds other columns containing interesting things please extend ths table. @@ -2041,6 +2045,9 @@ def getResumeSessionFile(self): def __getPek(self): LOG.info('Searching for pekList, be patient') peklist = None + AdamSchemaPekList = None + AdamRootPekList = None + while True: try: record = self.__ESEDB.getNextRow(self.__cursor, filter_tables=self.__filter_tables_usersecret) @@ -2050,14 +2057,51 @@ def __getPek(self): if record is None: break - elif record[self.NAME_TO_INTERNAL['pekList']] is not None: - peklist = unhexlify(record[self.NAME_TO_INTERNAL['pekList']]) - break - elif record[self.NAME_TO_INTERNAL['sAMAccountType']] in self.ACCOUNT_TYPES: + + elif record.get(self.NAME_TO_INTERNAL['pekList']) is not None: + # If we detect a Schema object with a PEKlist, it's a psuedo-PEKlist which must be mixed with + # the pseudo-PEKlist found in the Root object. + if self.__isADAMLDS and record.get(b'ATTm3') == 'Schema': + LOG.debug("Possible ADAM LDS detected based on PEKList attribute in Schma object") + AdamSchemaPekList = unhexlify(record[self.NAME_TO_INTERNAL['pekList']]) + continue + + # If we have a blank ATTm3 (name) record, it's the Root record's psuedo-PEKlist object to be mixed + # with the schema to form the bootkey for ADAMLDS + elif self.__isADAMLDS and record.get(b'ATTm3') == None: + AdamRootPekList = unhexlify(record[self.NAME_TO_INTERNAL['pekList']]) + continue + + # if the PEKlist obeys the standard PEK header format, then it's a real PEKlist, which we will + # later decode with the bootkey to get the decrypted PEKlist. + if record.get(self.NAME_TO_INTERNAL['pekList']).startswith(b"03000000") or record.get(self.NAME_TO_INTERNAL['pekList']).startswith(b"02000000"): + peklist = unhexlify(record[self.NAME_TO_INTERNAL['pekList']]) + + # ADAMLDS accounts do not have sAMAccountType values, and their username values are stored in a very + # generic element "name" (ATTm3), so we must assume that anything with a unicodePwd is an account in this situation + elif (record.get(self.NAME_TO_INTERNAL['sAMAccountType']) is not None and record.get(self.NAME_TO_INTERNAL['sAMAccountType']) in self.ACCOUNT_TYPES) or (self.__isADAMLDS and record.get(self.NAME_TO_INTERNAL['unicodePwd']) is not None): # Okey.. we found some users, but we're not yet ready to process them. # Let's just store them in a temp list self.__tmpUsers.append(record) + # Here we calculate the permutations of root and schema pseudo-peklist to generate the ADAMLDS bootkey value + if self.__isADAMLDS and AdamSchemaPekList is not None and AdamRootPekList is not None: + LOG.debug("The DITfile being processed is an ADAM LDS DITfile.") + bootkey = [] + for i in self.ROOTPEKLISTPERMUTATION: + bootkey.append(AdamRootPekList[i]) + + for i in self.SCHEMAPEKLISTPERMUTATION: + bootkey.append(AdamSchemaPekList[i]) + + # override the bootkey value. + self.__bootKey = bytearray(bootkey) + LOG.debug("Calculated ADAMLDS bootkey: %s" % hexlify(self.__bootKey)) + + elif self.__isADAMLDS and self.__bootKey == b'': + LOG.critical("ADAMLDS ditfile detected, but could not calculate bootkey!") + raise Exception("ADAMLDS ditfile detected, but could not calculate bootkey!") + if peklist is not None: encryptedPekList = self.PEKLIST_ENC(peklist) if encryptedPekList['Header'][:4] == b'\x02\x00\x00\x00': @@ -2139,13 +2183,13 @@ def __decryptSupplementalInfo(self, record, prefixTable=None, keysFile=None, cle haveInfo = False LOG.debug('Entering NTDSHashes.__decryptSupplementalInfo') if self.__useVSSMethod is True: - if record[self.NAME_TO_INTERNAL['supplementalCredentials']] is not None: + if record.get(self.NAME_TO_INTERNAL['supplementalCredentials']) is not None: if len(unhexlify(record[self.NAME_TO_INTERNAL['supplementalCredentials']])) > 24: if record[self.NAME_TO_INTERNAL['userPrincipalName']] is not None: domain = record[self.NAME_TO_INTERNAL['userPrincipalName']].split('@')[-1] - userName = '%s\\%s' % (domain, record[self.NAME_TO_INTERNAL['sAMAccountName']]) + userName = '%s\\%s' % (domain, record.get(self.NAME_TO_INTERNAL['sAMAccountName'])) else: - userName = '%s' % record[self.NAME_TO_INTERNAL['sAMAccountName']] + userName = '%s' % record.get(self.NAME_TO_INTERNAL['sAMAccountName']) cipherText = self.CRYPTED_BLOB(unhexlify(record[self.NAME_TO_INTERNAL['supplementalCredentials']])) if cipherText['Header'][:4] == b'\x13\x00\x00\x00': @@ -2154,6 +2198,8 @@ def __decryptSupplementalInfo(self, record, prefixTable=None, keysFile=None, cle plainText = self.__cryptoCommon.decryptAES(self.__PEK[int(pekIndex[8:10])], cipherText['EncryptedHash'][4:], cipherText['KeyMaterial']) + if self.__isADAMLDS: + LOG.debug(plainText) haveInfo = True else: plainText = self.__removeRC4Layer(cipherText) @@ -2260,7 +2306,7 @@ def __decryptHash(self, record, prefixTable=None, outputFile=None): sid = SAMR_RPC_SID(unhexlify(record[self.NAME_TO_INTERNAL['objectSid']])) rid = sid.formatCanonical().split('-')[-1] - if record[self.NAME_TO_INTERNAL['dBCSPwd']] is not None: + if record.get(self.NAME_TO_INTERNAL['dBCSPwd']) is not None: encryptedLMHash = self.CRYPTED_HASH(unhexlify(record[self.NAME_TO_INTERNAL['dBCSPwd']])) if encryptedLMHash['Header'][:4] == b'\x13\x00\x00\x00': # Win2016 TP4 decryption is different @@ -2275,7 +2321,7 @@ def __decryptHash(self, record, prefixTable=None, outputFile=None): else: LMHash = ntlm.LMOWFv1('', '') - if record[self.NAME_TO_INTERNAL['unicodePwd']] is not None: + if record.get(self.NAME_TO_INTERNAL['unicodePwd']) is not None: encryptedNTHash = self.CRYPTED_HASH(unhexlify(record[self.NAME_TO_INTERNAL['unicodePwd']])) if encryptedNTHash['Header'][:4] == b'\x13\x00\x00\x00': # Win2016 TP4 decryption is different @@ -2286,19 +2332,40 @@ def __decryptHash(self, record, prefixTable=None, outputFile=None): encryptedNTHash['KeyMaterial']) else: tmpNTHash = self.__removeRC4Layer(encryptedNTHash) - NTHash = self.__removeDESLayer(tmpNTHash, rid) + + # ADAMLDS hashes do not have 3DES layers, skip them if this is ADAMLDS ditfile. + if self.__isADAMLDS: + NTHash = tmpNTHash + else: + NTHash = self.__removeDESLayer(tmpNTHash, rid) else: NTHash = ntlm.NTOWFv1('', '') - if record[self.NAME_TO_INTERNAL['userPrincipalName']] is not None: + userName = None + # not all .ditfiles will have userPrincipalName present for user records. + if record.get(self.NAME_TO_INTERNAL['userPrincipalName']) is not None: domain = record[self.NAME_TO_INTERNAL['userPrincipalName']].split('@')[-1] userName = '%s\\%s' % (domain, record[self.NAME_TO_INTERNAL['sAMAccountName']]) - else: - userName = '%s' % record[self.NAME_TO_INTERNAL['sAMAccountName']] + + # Email attribute field (standard)? + elif self.__isADAMLDS and record.get(b"ATTm-2025721505") is not None: + userName = record[b"ATTm-2025721505"] + + # OMF uses a non-standard email attribute field + elif self.__isADAMLDS and record.get(b"ATTm-2038391160") is not None: + userName = record[b"ATTm-2038391160"] + + # this helps us when the type is ADAMLDS and we may not have the other username fields present. + elif self.__isADAMLDS and record.get(self.NAME_TO_INTERNAL['name']) is not None: + userName = record[self.NAME_TO_INTERNAL['name']] + + # final fallback for userName attribute + elif record.get(self.NAME_TO_INTERNAL['sAMAccountName']) is not None: + userName = '%s' % record.get(self.NAME_TO_INTERNAL['sAMAccountName']) if self.__printUserStatus is True: # Enabled / disabled users - if record[self.NAME_TO_INTERNAL['userAccountControl']] is not None: + if record.get(self.NAME_TO_INTERNAL['userAccountControl']) is not None: if '{0:08b}'.format(record[self.NAME_TO_INTERNAL['userAccountControl']])[-2:-1] == '1': userAccountStatus = 'Disabled' elif '{0:08b}'.format(record[self.NAME_TO_INTERNAL['userAccountControl']])[-2:-1] == '0': @@ -2306,7 +2373,7 @@ def __decryptHash(self, record, prefixTable=None, outputFile=None): else: userAccountStatus = 'N/A' - if record[self.NAME_TO_INTERNAL['pwdLastSet']] is not None: + if record.get(self.NAME_TO_INTERNAL['pwdLastSet']) is not None: pwdLastSet = self.__fileTimeToDateTime(record[self.NAME_TO_INTERNAL['pwdLastSet']]) else: pwdLastSet = 'N/A' @@ -2325,14 +2392,14 @@ def __decryptHash(self, record, prefixTable=None, outputFile=None): if self.__history: LMHistory = [] NTHistory = [] - if record[self.NAME_TO_INTERNAL['lmPwdHistory']] is not None: + if record.get(self.NAME_TO_INTERNAL['lmPwdHistory']) is not None: encryptedLMHistory = self.CRYPTED_HISTORY(unhexlify(record[self.NAME_TO_INTERNAL['lmPwdHistory']])) tmpLMHistory = self.__removeRC4Layer(encryptedLMHistory) for i in range(0, len(tmpLMHistory) // 16): LMHash = self.__removeDESLayer(tmpLMHistory[i * 16:(i + 1) * 16], rid) LMHistory.append(LMHash) - if record[self.NAME_TO_INTERNAL['ntPwdHistory']] is not None: + if record.get(self.NAME_TO_INTERNAL['ntPwdHistory']) is not None: encryptedNTHistory = self.CRYPTED_HISTORY(unhexlify(record[self.NAME_TO_INTERNAL['ntPwdHistory']])) if encryptedNTHistory['Header'][:4] == b'\x13\x00\x00\x00': @@ -2547,7 +2614,11 @@ def dump(self): try: self.__decryptHash(record, outputFile=hashesOutputFile) if self.__justNTLM is False: - self.__decryptSupplementalInfo(record, None, keysOutputFile, clearTextOutputFile) + # The struct for supplemental creds on ADAM LDS is very different than other versions and is not reversed yet. + if self.__isADAMLDS: + LOG.debug("Supplemental Credentials info found and decrypted, but is not currently supported.") + #LOG.debug(self.__decryptSupplementalInfo(record, None, keysOutputFile, clearTextOutputFile)) + #self.__decryptSupplementalInfo(record, None, keysOutputFile, clearTextOutputFile) except Exception as e: LOG.debug('Exception', exc_info=True) try: @@ -2571,7 +2642,7 @@ def dump(self): if record is None: break try: - if record[self.NAME_TO_INTERNAL['sAMAccountType']] in self.ACCOUNT_TYPES: + if record.get(self.NAME_TO_INTERNAL['sAMAccountType']) in self.ACCOUNT_TYPES: self.__decryptHash(record, outputFile=hashesOutputFile) if self.__justNTLM is False: self.__decryptSupplementalInfo(record, None, keysOutputFile, clearTextOutputFile) @@ -3100,4 +3171,4 @@ def getAllowedUsersToReplicate(self): def _print_helper(*args, **kwargs): - print(args[-1]) + print(args[-1]) \ No newline at end of file