Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#1505 Add TDO password dump on secretsdump #64

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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
10 changes: 9 additions & 1 deletion examples/secretsdump.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ def __init__(self, remoteName, username='', password='', domain='', options=None
self.__resumeFileName = options.resumefile
self.__canProcessSAMLSA = True
self.__kdcHost = options.dc_ip
self.__dumpTdo = options.dump_tdo
self.__options = options

if options.hashes is not None:
Expand Down Expand Up @@ -274,7 +275,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, dumpTdo=self.__dumpTdo)
try:
self.__NTDSHashes.dump()
except Exception as e:
Expand Down Expand Up @@ -364,6 +365,8 @@ def cleanup(self):
help='base output filename. Extensions will be added for sam, secrets, cached and ntds')
parser.add_argument('-use-vss', action='store_true', default=False,
help='Use the VSS method instead of default DRSUAPI')
parser.add_argument('-dump-tdo', action='store_true', default=False,
help='Try to dump CLEAR trusts password (LOCAL NTDS only)')
parser.add_argument('-rodcNo', action='store', type=int, help='Number of the RODC krbtgt account (only avaiable for Kerb-Key-List approach)')
parser.add_argument('-rodcKey', action='store', help='AES key of the Read Only Domain Controller (only avaiable for Kerb-Key-List approach)')
parser.add_argument('-use-keylist', action='store_true', default=False,
Expand Down Expand Up @@ -437,6 +440,11 @@ def cleanup(self):
# Having this switch on implies not asking for anything else.
options.just_dc = True

if options.dump_tdo is True:
if remoteName.upper() != 'LOCAL':
logging.error('-dump-tdo not compatible with remote target')
sys.exit(1)

if options.use_vss is True and options.resumefile is not None:
logging.error('resuming a previous NTDS.DIT dump session is not supported in VSS mode')
sys.exit(1)
Expand Down
111 changes: 110 additions & 1 deletion impacket/examples/secretsdump.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
from datetime import datetime, timedelta
from struct import unpack, pack
from six import b, PY2
from base64 import b64encode

from impacket import LOG
from impacket import system_errors
Expand Down Expand Up @@ -1867,6 +1868,11 @@ class SECRET_TYPE:
'pekList':b'ATTk590689',
'supplementalCredentials':b'ATTk589949',
'pwdLastSet':b'ATTq589920',


'trustAuthIncoming':b'ATTk589953',
'trustAuthOutgoing':b'ATTk589959',
'trustPartner':b'ATTm589957'
}

NAME_TO_ATTRTYP = {
Expand Down Expand Up @@ -1963,7 +1969,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, dumpTdo=False):
self.__bootKey = bootKey
self.__NTDS = ntdsFile
self.__history = history
Expand All @@ -1979,17 +1985,20 @@ def __init__(self, ntdsFile, bootKey, isRemote=False, history=False, noLMHash=Tr
self.__PEK = list()
self.__cryptoCommon = CryptoCommon()
self.__kerberosKeys = OrderedDict()
self.__tdoSecrets = OrderedDict()
self.__clearTextPwds = OrderedDict()
self.__justNTLM = justNTLM
self.__resumeSession = resumeSessionMgr(resumeSession)
self.__outputFileName = outputFileName
self.__justUser = justUser
self.__ldapFilter = ldapFilter
self.__perSecretCallback = perSecretCallback
self.__dumpTdo = dumpTdo

# 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.
self.__filter_tables_usersecret = {
self.NAME_TO_INTERNAL['objectGUID'] : 1,
self.NAME_TO_INTERNAL['objectSid'] : 1,
self.NAME_TO_INTERNAL['dBCSPwd'] : 1,
self.NAME_TO_INTERNAL['name'] : 1,
Expand All @@ -2004,6 +2013,9 @@ def __init__(self, ntdsFile, bootKey, isRemote=False, history=False, noLMHash=Tr
self.NAME_TO_INTERNAL['supplementalCredentials'] : 1,
self.NAME_TO_INTERNAL['pekList'] : 1,

self.NAME_TO_INTERNAL['trustAuthIncoming'] : 1,
self.NAME_TO_INTERNAL['trustAuthOutgoing'] : 1,
self.NAME_TO_INTERNAL['trustPartner'] : 1,
}

def getResumeSessionFile(self):
Expand Down Expand Up @@ -2105,6 +2117,77 @@ def __fileTimeToDateTime(t):
dt = datetime.fromtimestamp(t)
return dt.strftime("%Y-%m-%d %H:%M")

def __decryptTDOSecret(self, record, way, outputfile=None):

LOG.debug('Entering NTDSHashes.__decryptTDOSecret')
trustAuthTypeList = ["NONE", "NT4OWF","CLEAR","VERSION"]
if way == "In":
val = record[self.NAME_TO_INTERNAL['trustAuthIncoming']]
else:
val = record[self.NAME_TO_INTERNAL['trustAuthOutgoing']]
cipherText = self.CRYPTED_BLOB(unhexlify(val))
plaintext = None
if cipherText['Header'][:4] == b'\x13\x00\x00\x00':
# Win2016 TP4 decryption is different
pekIndex = hexlify(cipherText['Header'])
plainText = self.__cryptoCommon.decryptAES(self.__PEK[int(pekIndex[8:10])],
cipherText['EncryptedHash'][4:],
cipherText['KeyMaterial'])
haveInfo = True
else:
plainText = self.__removeRC4Layer(cipherText)
haveInfo = True
tdoInfo = plainText.hex()
authInfoOffset = unpack('<L', unhexlify(tdoInfo[8:16]))[0]
authInfo = tdoInfo[authInfoOffset*2:]

if(len(authInfo[16:16+8]) != 8):
LOG.debug("Cannot process the TDO... Decryption failed")
LOG.debug('Leaving NTDSHashes.__decryptTDOSecret')
return
trustAuthType = trustAuthTypeList[unpack('<L', unhexlify(authInfo[16:16+8]))[0]]
trustPasswordNT = "Can only process CLEAR AUTH trusts"
trustPassword = ""
if(trustAuthType == "CLEAR"):
trustPassword = authInfo[32:88]
trustPasswordNT = hashlib.new('md4', unhexlify(trustPassword)).digest().hex()
trustPassword = b64encode(unhexlify(trustPassword)).decode()

trustPartner = record[self.NAME_TO_INTERNAL['trustPartner']]
answer = "{} {} {} : NT {} / PLAIN {} ".format(
way,
trustAuthType,
trustPartner,
trustPasswordNT,
trustPassword
)

# Same thing to retrieve the old password
authInfoOffsetOld = unpack('<L', unhexlify(tdoInfo[16:24]))[0]
authInfoOld = tdoInfo[authInfoOffsetOld*2:]
trustPasswordNTOld = "Can only process CLEAR AUTH trusts"
trustPasswordOld = ""
if(trustAuthType == "CLEAR"):
trustPasswordOld = authInfoOld[32:88]
trustPasswordNTOld = hashlib.new('md4', unhexlify(trustPasswordOld)).digest().hex()
trustPasswordOld = b64encode(unhexlify(trustPasswordOld)).decode()

answerOld = "{} {} {} : NT {} / PLAIN {} ".format(
way + " - 1",
trustAuthType,
trustPartner,
trustPasswordNTOld,
trustPasswordOld
)

self.__tdoSecrets[answer] = None
self.__tdoSecrets[answerOld] = None
if outputfile != None:
self.__writeOutput(outputfile, answer + '\n' + answerOld + '\n')
LOG.debug('Leaving NTDSHashes.__decryptTDOSecret')



def __decryptSupplementalInfo(self, record, prefixTable=None, keysFile=None, clearTextFile=None):
# This is based on [MS-SAMR] 2.2.10 Supplemental Credentials Structures
haveInfo = False
Expand All @@ -2117,6 +2200,7 @@ def __decryptSupplementalInfo(self, record, prefixTable=None, keysFile=None, cle
userName = '%s\\%s' % (domain, record[self.NAME_TO_INTERNAL['sAMAccountName']])
else:
userName = '%s' % record[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 Down Expand Up @@ -2464,6 +2548,7 @@ def dump(self):
hashesOutputFile = None
keysOutputFile = None
clearTextOutputFile = None
tdoOutputFile = None

if self.__useVSSMethod is True:
if self.__NTDS is None:
Expand Down Expand Up @@ -2504,6 +2589,9 @@ def dump(self):
if self.__justNTLM is False:
keysOutputFile = openFile(self.__outputFileName+'.ntds.kerberos',mode)
clearTextOutputFile = openFile(self.__outputFileName+'.ntds.cleartext',mode)
if self.__dumpTdo is True:
tdoOutputFile = openFile(self.__outputFileName+'.ntds.tdo', mode)


LOG.info('Dumping Domain Credentials (domain\\uid:rid:lmhash:nthash)')
if self.__useVSSMethod:
Expand Down Expand Up @@ -2542,10 +2630,17 @@ def dump(self):
if record is None:
break
try:
pass
if record[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)

if record[self.NAME_TO_INTERNAL['trustAuthIncoming']] is not None and self.__NTDS is not None and self.__dumpTdo is True:
self.__decryptTDOSecret(record, "In", outputfile=tdoOutputFile)
if record[self.NAME_TO_INTERNAL['trustAuthOutgoing']] is not None and self.__NTDS is not None and self.__dumpTdo is True:
self.__decryptTDOSecret(record, "Out", outputfile=tdoOutputFile)

except Exception as e:
LOG.debug('Exception', exc_info=True)
try:
Expand Down Expand Up @@ -2731,6 +2826,17 @@ def dump(self):

for itemKey in list(self.__clearTextPwds.keys()):
self.__perSecretCallback(NTDSHashes.SECRET_TYPE.NTDS_CLEARTEXT, itemKey)

# Display TDO secret extracted
if len(self.__tdoSecrets) > 0:
if self.__useVSSMethod is True:
LOG.info('TDO secrets from %s ' % self.__NTDS)
else:
LOG.info('TDO extraction only available with NTDS local dumpfile')

for itemKey in list(self.__tdoSecrets.keys()):
self.__perSecretCallback(NTDSHashes.SECRET_TYPE.NTDS, itemKey)

finally:
# Resources cleanup
if hashesOutputFile is not None:
Expand All @@ -2742,6 +2848,9 @@ def dump(self):
if clearTextOutputFile is not None:
clearTextOutputFile.close()

if tdoOutputFile is not None:
tdoOutputFile.close()

self.__resumeSession.endTransaction()

@classmethod
Expand Down