Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions examples/secretsdump.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -480,4 +487,4 @@ def cleanup(self):
if logging.getLogger().level == logging.DEBUG:
import traceback
traceback.print_exc()
logging.error(e)
logging.error(e)
113 changes: 92 additions & 21 deletions impacket/examples/secretsdump.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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)
Expand All @@ -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':
Expand Down Expand Up @@ -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':
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -2286,27 +2332,48 @@ 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':
userAccountStatus = 'Enabled'
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'
Expand All @@ -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':
Expand Down Expand Up @@ -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:
Expand All @@ -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)
Expand Down Expand Up @@ -3100,4 +3171,4 @@ def getAllowedUsersToReplicate(self):


def _print_helper(*args, **kwargs):
print(args[-1])
print(args[-1])