class NTDSHashes(object): NAME_TO_INTERNAL = { 'uSNCreated': 'ATTq131091', 'uSNChanged': 'ATTq131192', 'name': 'ATTm3', 'objectGUID': 'ATTk589826', 'objectSid': 'ATTr589970', 'userAccountControl': 'ATTj589832', 'primaryGroupID': 'ATTj589922', 'accountExpires': 'ATTq589983', 'loggeronCount': 'ATTj589993', 'sAMAccountName': 'ATTm590045', 'sAMAccountType': 'ATTj590126', 'lastloggeronTimestamp': 'ATTq589876', 'userPrincipalName': 'ATTm590480', 'unicodePwd': 'ATTk589914', 'dBCSPwd': 'ATTk589879', 'ntPwdHistory': 'ATTk589918', 'lmPwdHistory': 'ATTk589984', 'pekList': 'ATTk590689', } INTERNAL_TO_NAME = dict((v, k) for k, v in NAME_TO_INTERNAL.iteritems()) SAM_NORMAL_USER_ACCOUNT = 0x30000000 SAM_MACHINE_ACCOUNT = 0x30000001 SAM_TRUST_ACCOUNT = 0x30000002 ACCOUNT_TYPES = (SAM_NORMAL_USER_ACCOUNT, SAM_MACHINE_ACCOUNT, SAM_TRUST_ACCOUNT) class PEK_KEY(Structure): structure = ( ('Header', '8s=""'), ('KeyMaterial', '16s=""'), ('EncryptedPek', '52s=""'), ) class CRYPTED_HASH(Structure): structure = ( ('Header', '8s=""'), ('KeyMaterial', '16s=""'), ('EncryptedHash', '16s=""'), ) class CRYPTED_HISTORY(Structure): structure = ( ('Header', '8s=""'), ('KeyMaterial', '16s=""'), ('EncryptedHash', ':'), ) def __init__(self, ntds_file, bootKey, history=False, noLMHash=True): self.__bootKey = bootKey self.__ntds_file = ntds_file self.__history = history self.__no_LMhash = noLMHash self.__tmpUsers = list() self.__PEK = None self.__cryptoCommon = CryptoCommon() self.__itemsFound = {} if not self.__ntds_file: return self.__ESEDB = ESENT_DB(self.__ntds_file, isRemote=True) self.__cursor = self.__ESEDB.openTable('datatable') def __getPek(self): logger.info('Searching for pekList, be patient') pek = None while True: record = self.__ESEDB.getNextRow(self.__cursor) if record is None: break elif record[self.NAME_TO_INTERNAL['pekList']] is not None: pek = record[self.NAME_TO_INTERNAL['pekList']].decode('hex') break elif record[self.NAME_TO_INTERNAL[ 'sAMAccountType']] in self.ACCOUNT_TYPES: # 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) if pek is not None: encryptedPek = self.PEK_KEY(pek) md5 = hashlib.new('md5') md5.update(self.__bootKey) for i in range(1000): md5.update(encryptedPek['KeyMaterial']) tmpKey = md5.digest() rc4 = ARC4.new(tmpKey) plainText = rc4.encrypt(encryptedPek['EncryptedPek']) self.__PEK = plainText[36:] def __removeRC4Layer(self, cryptedHash): md5 = hashlib.new('md5') md5.update(self.__PEK) md5.update(cryptedHash['KeyMaterial']) tmpKey = md5.digest() rc4 = ARC4.new(tmpKey) plainText = rc4.encrypt(cryptedHash['EncryptedHash']) return plainText def __removeDESLayer(self, cryptedHash, rid): Key1, Key2 = self.__cryptoCommon.deriveKey(int(rid)) Crypt1 = DES.new(Key1, DES.MODE_ECB) Crypt2 = DES.new(Key2, DES.MODE_ECB) decryptedHash = Crypt1.decrypt(cryptedHash[:8]) + Crypt2.decrypt( cryptedHash[8:]) return decryptedHash def __decryptHash(self, record): logger.debug('Decrypting hash for user: %s' % record[self.NAME_TO_INTERNAL['name']]) sid = SAMR_RPC_SID( record[self.NAME_TO_INTERNAL['objectSid']].decode('hex')) rid = sid.formatCanonical().split('-')[-1] if record[self.NAME_TO_INTERNAL['dBCSPwd']] is not None: encryptedLMHash = self.CRYPTED_HASH( record[self.NAME_TO_INTERNAL['dBCSPwd']].decode('hex')) tmpLMHash = self.__removeRC4Layer(encryptedLMHash) LMHash = self.__removeDESLayer(tmpLMHash, rid) else: LMHash = ntlm.LMOWFv1('', '') encryptedLMHash = None if record[self.NAME_TO_INTERNAL['unicodePwd']] is not None: encryptedNTHash = self.CRYPTED_HASH( record[self.NAME_TO_INTERNAL['unicodePwd']].decode('hex')) tmpNTHash = self.__removeRC4Layer(encryptedNTHash) NTHash = self.__removeDESLayer(tmpNTHash, rid) else: NTHash = ntlm.NTOWFv1('', '') encryptedNTHash = None if record[self.NAME_TO_INTERNAL['userPrincipalName']] is not None: domain = record[self.NAME_TO_INTERNAL['userPrincipalName']].split( '@')[-1] userName = '******' % ( domain, record[self.NAME_TO_INTERNAL['sAMAccountName']]) else: userName = '******' % record[self.NAME_TO_INTERNAL['sAMAccountName']] answer = "%s:%s:%s:%s:::" % (userName, rid, LMHash.encode('hex'), NTHash.encode('hex')) self.__itemsFound[record[self.NAME_TO_INTERNAL['objectSid']].decode( 'hex')] = answer print answer if self.__history: LMHistory = [] NTHistory = [] if record[self.NAME_TO_INTERNAL['lmPwdHistory']] is not None: lmPwdHistory = record[self.NAME_TO_INTERNAL['lmPwdHistory']] encryptedLMHistory = self.CRYPTED_HISTORY(record[ self.NAME_TO_INTERNAL['lmPwdHistory']].decode('hex')) 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: ntPwdHistory = record[self.NAME_TO_INTERNAL['ntPwdHistory']] encryptedNTHistory = self.CRYPTED_HISTORY(record[ self.NAME_TO_INTERNAL['ntPwdHistory']].decode('hex')) tmpNTHistory = self.__removeRC4Layer(encryptedNTHistory) for i in range(0, len(tmpNTHistory) / 16): NTHash = self.__removeDESLayer( tmpNTHistory[i * 16:(i + 1) * 16], rid) NTHistory.append(NTHash) for i, (LMHash, NTHash) in enumerate( map(lambda l, n: (l, n) if l else ('', n), LMHistory[1:], NTHistory[1:])): if self.__no_LMhash: lmhash = ntlm.LMOWFv1('', '').encode('hex') else: lmhash = LMHash.encode('hex') answer = "%s_history%d:%s:%s:%s:::" % ( userName, i, rid, lmhash, NTHash.encode('hex')) self.__itemsFound[ record[self.NAME_TO_INTERNAL['objectSid']].decode('hex') + str(i)] = answer print answer def dumpNTDS(self): if not self.__ntds_file: return logger.info( 'Dumping domain users\' hashes (DOMAIN\\UID:RID:LMhash:NThash), wait..' ) # We start getting rows from the table aiming at reaching # the pekList. If we find users records we stored them # in a temp list for later process self.__getPek() if self.__PEK is not None: logger.info('PEK found and decrypted: 0x%s' % self.__PEK.encode('hex')) logger.info('Reading and decrypting hashes from %s' % self.__ntds_file) # First of all, if we have users already cached, let's decrypt their hashes for record in self.__tmpUsers: self.__decryptHash(record) # Now let's keep moving through the NTDS file and decrypting what we find while True: try: record = self.__ESEDB.getNextRow(self.__cursor) except: logger.error( 'Error while calling getNextRow(), trying the next one' ) continue if record is None: break try: if record[self.NAME_TO_INTERNAL[ 'sAMAccountType']] in self.ACCOUNT_TYPES: self.__decryptHash(record) except Exception, e: try: logger.error('Error while processing row for user %s' % record[self.NAME_TO_INTERNAL['name']]) logger.error(str(e)) except: logger.error('Error while processing row!') logger.error(str(e))
class NTDSHashes: NAME_TO_INTERNAL = { 'uSNCreated':'ATTq131091', 'uSNChanged':'ATTq131192', 'name':'ATTm3', 'objectGUID':'ATTk589826', 'objectSid':'ATTr589970', 'userAccountControl':'ATTj589832', 'primaryGroupID':'ATTj589922', 'accountExpires':'ATTq589983', 'logonCount':'ATTj589993', 'sAMAccountName':'ATTm590045', 'sAMAccountType':'ATTj590126', 'lastLogonTimestamp':'ATTq589876', 'userPrincipalName':'ATTm590480', 'unicodePwd':'ATTk589914', 'dBCSPwd':'ATTk589879', 'ntPwdHistory':'ATTk589918', 'lmPwdHistory':'ATTk589984', 'pekList':'ATTk590689', 'supplementalCredentials':'ATTk589949', 'pwdLastSet':'ATTq589920', } NAME_TO_ATTRTYP = { 'userPrincipalName': 0x90290, 'sAMAccountName': 0x900DD, 'unicodePwd': 0x9005A, 'dBCSPwd': 0x90037, 'ntPwdHistory': 0x9005E, 'lmPwdHistory': 0x900A0, 'supplementalCredentials': 0x9007D, 'objectSid': 0x90092, } ATTRTYP_TO_ATTID = { 'userPrincipalName': '1.2.840.113556.1.4.656', 'sAMAccountName': '1.2.840.113556.1.4.221', 'unicodePwd': '1.2.840.113556.1.4.90', 'dBCSPwd': '1.2.840.113556.1.4.55', 'ntPwdHistory': '1.2.840.113556.1.4.94', 'lmPwdHistory': '1.2.840.113556.1.4.160', 'supplementalCredentials': '1.2.840.113556.1.4.125', 'objectSid': '1.2.840.113556.1.4.146', 'pwdLastSet': '1.2.840.113556.1.4.96', } KERBEROS_TYPE = { 1:'dec-cbc-crc', 3:'des-cbc-md5', 17:'aes128-cts-hmac-sha1-96', 18:'aes256-cts-hmac-sha1-96', 0xffffff74:'rc4_hmac', } INTERNAL_TO_NAME = dict((v,k) for k,v in NAME_TO_INTERNAL.iteritems()) SAM_NORMAL_USER_ACCOUNT = 0x30000000 SAM_MACHINE_ACCOUNT = 0x30000001 SAM_TRUST_ACCOUNT = 0x30000002 ACCOUNT_TYPES = ( SAM_NORMAL_USER_ACCOUNT, SAM_MACHINE_ACCOUNT, SAM_TRUST_ACCOUNT) class PEKLIST_ENC(Structure): structure = ( ('Header','8s=""'), ('KeyMaterial','16s=""'), ('EncryptedPek',':'), ) class PEKLIST_PLAIN(Structure): structure = ( ('Header','32s=""'), ('DecryptedPek',':'), ) class PEK_KEY(Structure): structure = ( ('Header','1s=""'), ('Padding','3s=""'), ('Key','16s=""'), ) class CRYPTED_HASH(Structure): structure = ( ('Header','8s=""'), ('KeyMaterial','16s=""'), ('EncryptedHash','16s=""'), ) class CRYPTED_HISTORY(Structure): structure = ( ('Header','8s=""'), ('KeyMaterial','16s=""'), ('EncryptedHash',':'), ) class CRYPTED_BLOB(Structure): structure = ( ('Header','8s=""'), ('KeyMaterial','16s=""'), ('EncryptedHash',':'), ) def __init__(self, ntdsFile, bootKey, logger, isRemote=False, history=False, noLMHash=True, remoteOps=None, useVSSMethod=False, justNTLM=False, pwdLastSet=False, resumeSession=None, outputFileName=None): self.__bootKey = bootKey self.__logger = logger self.__NTDS = ntdsFile self.__history = history self.__noLMHash = noLMHash self.__useVSSMethod = useVSSMethod self.__remoteOps = remoteOps self.__pwdLastSet = pwdLastSet if self.__NTDS is not None: self.__ESEDB = ESENT_DB(ntdsFile, isRemote = isRemote) self.__cursor = self.__ESEDB.openTable('datatable') self.__tmpUsers = list() self.__PEK = list() self.__cryptoCommon = CryptoCommon() self.__kerberosKeys = OrderedDict() self.__clearTextPwds = OrderedDict() self.__justNTLM = justNTLM self.__savedSessionFile = resumeSession self.__resumeSessionFile = None self.__outputFileName = outputFileName def getResumeSessionFile(self): return self.__resumeSessionFile def __getPek(self): logging.info('Searching for pekList, be patient') peklist = None while True: record = self.__ESEDB.getNextRow(self.__cursor) 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: # 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) if peklist is not None: encryptedPekList = self.PEKLIST_ENC(peklist) md5 = hashlib.new('md5') md5.update(self.__bootKey) for i in range(1000): md5.update(encryptedPekList['KeyMaterial']) tmpKey = md5.digest() rc4 = ARC4.new(tmpKey) decryptedPekList = self.PEKLIST_PLAIN(rc4.encrypt(encryptedPekList['EncryptedPek'])) PEKLen = len(self.PEK_KEY()) for i in range(len( decryptedPekList['DecryptedPek'] ) / PEKLen ): cursor = i * PEKLen pek = self.PEK_KEY(decryptedPekList['DecryptedPek'][cursor:cursor+PEKLen]) logging.info("PEK # %d found and decrypted: %s", i, hexlify(pek['Key'])) self.__PEK.append(pek['Key']) def __removeRC4Layer(self, cryptedHash): md5 = hashlib.new('md5') # PEK index can be found on header of each ciphered blob (pos 8-10) pekIndex = hexlify(cryptedHash['Header']) md5.update(self.__PEK[int(pekIndex[8:10])]) md5.update(cryptedHash['KeyMaterial']) tmpKey = md5.digest() rc4 = ARC4.new(tmpKey) plainText = rc4.encrypt(cryptedHash['EncryptedHash']) return plainText def __removeDESLayer(self, cryptedHash, rid): Key1,Key2 = self.__cryptoCommon.deriveKey(int(rid)) Crypt1 = DES.new(Key1, DES.MODE_ECB) Crypt2 = DES.new(Key2, DES.MODE_ECB) decryptedHash = Crypt1.decrypt(cryptedHash[:8]) + Crypt2.decrypt(cryptedHash[8:]) return decryptedHash def __fileTimeToDateTime(self, t): t -= 116444736000000000 t /= 10000000 if t < 0: return 'never' else: dt = datetime.fromtimestamp(t) return dt.strftime("%Y-%m-%d %H:%M") def __decryptSupplementalInfo(self, record, prefixTable=None, keysFile=None, clearTextFile=None): # This is based on [MS-SAMR] 2.2.10 Supplemental Credentials Structures haveInfo = False if self.__useVSSMethod is True: if record[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 = '******' % (domain, record[self.NAME_TO_INTERNAL['sAMAccountName']]) else: userName = '******' % record[self.NAME_TO_INTERNAL['sAMAccountName']] cipherText = self.CRYPTED_BLOB(unhexlify(record[self.NAME_TO_INTERNAL['supplementalCredentials']])) plainText = self.__removeRC4Layer(cipherText) haveInfo = True else: domain = None userName = None for attr in record['pmsgOut']['V6']['pObjects']['Entinf']['AttrBlock']['pAttr']: try: attId = drsuapi.OidFromAttid(prefixTable, attr['attrTyp']) LOOKUP_TABLE = self.ATTRTYP_TO_ATTID except Exception, e: logging.debug('Failed to execute OidFromAttid with error %s' % e) # Fallbacking to fixed table and hope for the best attId = attr['attrTyp'] LOOKUP_TABLE = self.NAME_TO_ATTRTYP if attId == LOOKUP_TABLE['userPrincipalName']: if attr['AttrVal']['valCount'] > 0: try: domain = ''.join(attr['AttrVal']['pAVal'][0]['pVal']).decode('utf-16le').split('@')[-1] except: domain = None else: domain = None elif attId == LOOKUP_TABLE['sAMAccountName']: if attr['AttrVal']['valCount'] > 0: try: userName = ''.join(attr['AttrVal']['pAVal'][0]['pVal']).decode('utf-16le') except: logging.error('Cannot get sAMAccountName for %s' % record['pmsgOut']['V6']['pNC']['StringName'][:-1]) userName = '******' else: logging.error('Cannot get sAMAccountName for %s' % record['pmsgOut']['V6']['pNC']['StringName'][:-1]) userName = '******' if attId == LOOKUP_TABLE['supplementalCredentials']: if attr['AttrVal']['valCount'] > 0: blob = ''.join(attr['AttrVal']['pAVal'][0]['pVal']) plainText = drsuapi.DecryptAttributeValue(self.__remoteOps.getDrsr(), blob) if len(plainText) > 24: haveInfo = True if domain is not None: userName = '******' % (domain, userName) if haveInfo is True: try: userProperties = samr.USER_PROPERTIES(plainText) except: # On some old w2k3 there might be user properties that don't # match [MS-SAMR] structure, discarding them return propertiesData = userProperties['UserProperties'] for propertyCount in range(userProperties['PropertyCount']): userProperty = samr.USER_PROPERTY(propertiesData) propertiesData = propertiesData[len(userProperty):] # For now, we will only process Newer Kerberos Keys and CLEARTEXT if userProperty['PropertyName'].decode('utf-16le') == 'Primary:Kerberos-Newer-Keys': propertyValueBuffer = unhexlify(userProperty['PropertyValue']) kerbStoredCredentialNew = samr.KERB_STORED_CREDENTIAL_NEW(propertyValueBuffer) data = kerbStoredCredentialNew['Buffer'] for credential in range(kerbStoredCredentialNew['CredentialCount']): keyDataNew = samr.KERB_KEY_DATA_NEW(data) data = data[len(keyDataNew):] keyValue = propertyValueBuffer[keyDataNew['KeyOffset']:][:keyDataNew['KeyLength']] if self.KERBEROS_TYPE.has_key(keyDataNew['KeyType']): answer = "%s:%s:%s" % (userName, self.KERBEROS_TYPE[keyDataNew['KeyType']],hexlify(keyValue)) else: answer = "%s:%s:%s" % (userName, hex(keyDataNew['KeyType']),hexlify(keyValue)) # We're just storing the keys, not printing them, to make the output more readable # This is kind of ugly... but it's what I came up with tonight to get an ordered # set :P. Better ideas welcomed ;) self.__kerberosKeys[answer] = None if keysFile is not None: self.__writeOutput(keysFile, answer + '\n') elif userProperty['PropertyName'].decode('utf-16le') == 'Primary:CLEARTEXT': # [MS-SAMR] 3.1.1.8.11.5 Primary:CLEARTEXT Property # This credential type is the cleartext password. The value format is the UTF-16 encoded cleartext password. answer = "%s:CLEARTEXT:%s" % (userName, unhexlify(userProperty['PropertyValue']).decode('utf-16le')) self.__clearTextPwds[answer] = None if clearTextFile is not None: self.__writeOutput(clearTextFile, answer + '\n') if clearTextFile is not None: clearTextFile.flush() if keysFile is not None: keysFile.flush()
class NTDSHashes: NAME_TO_INTERNAL = { 'uSNCreated': 'ATTq131091', 'uSNChanged': 'ATTq131192', 'name': 'ATTm3', 'objectGUID': 'ATTk589826', 'objectSid': 'ATTr589970', 'userAccountControl': 'ATTj589832', 'primaryGroupID': 'ATTj589922', 'accountExpires': 'ATTq589983', 'logonCount': 'ATTj589993', 'sAMAccountName': 'ATTm590045', 'sAMAccountType': 'ATTj590126', 'lastLogonTimestamp': 'ATTq589876', 'userPrincipalName': 'ATTm590480', 'unicodePwd': 'ATTk589914', 'dBCSPwd': 'ATTk589879', 'ntPwdHistory': 'ATTk589918', 'lmPwdHistory': 'ATTk589984', 'pekList': 'ATTk590689', 'supplementalCredentials': 'ATTk589949', 'pwdLastSet': 'ATTq589920', } NAME_TO_ATTRTYP = { 'userPrincipalName': 0x90290, 'sAMAccountName': 0x900DD, 'unicodePwd': 0x9005A, 'dBCSPwd': 0x90037, 'ntPwdHistory': 0x9005E, 'lmPwdHistory': 0x900A0, 'supplementalCredentials': 0x9007D, 'objectSid': 0x90092, } ATTRTYP_TO_ATTID = { 'userPrincipalName': '1.2.840.113556.1.4.656', 'sAMAccountName': '1.2.840.113556.1.4.221', 'unicodePwd': '1.2.840.113556.1.4.90', 'dBCSPwd': '1.2.840.113556.1.4.55', 'ntPwdHistory': '1.2.840.113556.1.4.94', 'lmPwdHistory': '1.2.840.113556.1.4.160', 'supplementalCredentials': '1.2.840.113556.1.4.125', 'objectSid': '1.2.840.113556.1.4.146', 'pwdLastSet': '1.2.840.113556.1.4.96', } KERBEROS_TYPE = { 1: 'dec-cbc-crc', 3: 'des-cbc-md5', 17: 'aes128-cts-hmac-sha1-96', 18: 'aes256-cts-hmac-sha1-96', 0xffffff74: 'rc4_hmac', } INTERNAL_TO_NAME = dict((v, k) for k, v in NAME_TO_INTERNAL.iteritems()) SAM_NORMAL_USER_ACCOUNT = 0x30000000 SAM_MACHINE_ACCOUNT = 0x30000001 SAM_TRUST_ACCOUNT = 0x30000002 ACCOUNT_TYPES = (SAM_NORMAL_USER_ACCOUNT, SAM_MACHINE_ACCOUNT, SAM_TRUST_ACCOUNT) class PEKLIST_ENC(Structure): structure = ( ('Header', '8s=""'), ('KeyMaterial', '16s=""'), ('EncryptedPek', ':'), ) class PEKLIST_PLAIN(Structure): structure = ( ('Header', '32s=""'), ('DecryptedPek', ':'), ) class PEK_KEY(Structure): structure = ( ('Header', '1s=""'), ('Padding', '3s=""'), ('Key', '16s=""'), ) class CRYPTED_HASH(Structure): structure = ( ('Header', '8s=""'), ('KeyMaterial', '16s=""'), ('EncryptedHash', '16s=""'), ) class CRYPTED_HISTORY(Structure): structure = ( ('Header', '8s=""'), ('KeyMaterial', '16s=""'), ('EncryptedHash', ':'), ) class CRYPTED_BLOB(Structure): structure = ( ('Header', '8s=""'), ('KeyMaterial', '16s=""'), ('EncryptedHash', ':'), ) def __init__(self, ntdsFile, bootKey, logger, isRemote=False, history=False, noLMHash=True, remoteOps=None, useVSSMethod=False, justNTLM=False, pwdLastSet=False, resumeSession=None, outputFileName=None): self.__bootKey = bootKey self.__logger = logger self.__NTDS = ntdsFile self.__history = history self.__noLMHash = noLMHash self.__useVSSMethod = useVSSMethod self.__remoteOps = remoteOps self.__pwdLastSet = pwdLastSet if self.__NTDS is not None: self.__ESEDB = ESENT_DB(ntdsFile, isRemote=isRemote) self.__cursor = self.__ESEDB.openTable('datatable') self.__tmpUsers = list() self.__PEK = list() self.__cryptoCommon = CryptoCommon() self.__kerberosKeys = OrderedDict() self.__clearTextPwds = OrderedDict() self.__justNTLM = justNTLM self.__savedSessionFile = resumeSession self.__resumeSessionFile = None self.__outputFileName = outputFileName def getResumeSessionFile(self): return self.__resumeSessionFile def __getPek(self): logging.info('Searching for pekList, be patient') peklist = None while True: record = self.__ESEDB.getNextRow(self.__cursor) 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: # 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) if peklist is not None: encryptedPekList = self.PEKLIST_ENC(peklist) md5 = hashlib.new('md5') md5.update(self.__bootKey) for i in range(1000): md5.update(encryptedPekList['KeyMaterial']) tmpKey = md5.digest() rc4 = ARC4.new(tmpKey) decryptedPekList = self.PEKLIST_PLAIN( rc4.encrypt(encryptedPekList['EncryptedPek'])) PEKLen = len(self.PEK_KEY()) for i in range(len(decryptedPekList['DecryptedPek']) / PEKLen): cursor = i * PEKLen pek = self.PEK_KEY( decryptedPekList['DecryptedPek'][cursor:cursor + PEKLen]) logging.info("PEK # %d found and decrypted: %s", i, hexlify(pek['Key'])) self.__PEK.append(pek['Key']) def __removeRC4Layer(self, cryptedHash): md5 = hashlib.new('md5') # PEK index can be found on header of each ciphered blob (pos 8-10) pekIndex = hexlify(cryptedHash['Header']) md5.update(self.__PEK[int(pekIndex[8:10])]) md5.update(cryptedHash['KeyMaterial']) tmpKey = md5.digest() rc4 = ARC4.new(tmpKey) plainText = rc4.encrypt(cryptedHash['EncryptedHash']) return plainText def __removeDESLayer(self, cryptedHash, rid): Key1, Key2 = self.__cryptoCommon.deriveKey(int(rid)) Crypt1 = DES.new(Key1, DES.MODE_ECB) Crypt2 = DES.new(Key2, DES.MODE_ECB) decryptedHash = Crypt1.decrypt(cryptedHash[:8]) + Crypt2.decrypt( cryptedHash[8:]) return decryptedHash def __fileTimeToDateTime(self, t): t -= 116444736000000000 t /= 10000000 if t < 0: return 'never' else: dt = datetime.fromtimestamp(t) return dt.strftime("%Y-%m-%d %H:%M") def __decryptSupplementalInfo(self, record, prefixTable=None, keysFile=None, clearTextFile=None): # This is based on [MS-SAMR] 2.2.10 Supplemental Credentials Structures haveInfo = False if self.__useVSSMethod is True: if record[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 = '******' % (domain, record[ self.NAME_TO_INTERNAL['sAMAccountName']]) else: userName = '******' % record[ self.NAME_TO_INTERNAL['sAMAccountName']] cipherText = self.CRYPTED_BLOB( unhexlify(record[ self.NAME_TO_INTERNAL['supplementalCredentials']])) plainText = self.__removeRC4Layer(cipherText) haveInfo = True else: domain = None userName = None for attr in record['pmsgOut']['V6']['pObjects']['Entinf'][ 'AttrBlock']['pAttr']: try: attId = drsuapi.OidFromAttid(prefixTable, attr['attrTyp']) LOOKUP_TABLE = self.ATTRTYP_TO_ATTID except Exception, e: logging.debug( 'Failed to execute OidFromAttid with error %s' % e) # Fallbacking to fixed table and hope for the best attId = attr['attrTyp'] LOOKUP_TABLE = self.NAME_TO_ATTRTYP if attId == LOOKUP_TABLE['userPrincipalName']: if attr['AttrVal']['valCount'] > 0: try: domain = ''.join( attr['AttrVal']['pAVal'][0]['pVal']).decode( 'utf-16le').split('@')[-1] except: domain = None else: domain = None elif attId == LOOKUP_TABLE['sAMAccountName']: if attr['AttrVal']['valCount'] > 0: try: userName = ''.join(attr['AttrVal']['pAVal'][0] ['pVal']).decode('utf-16le') except: logging.error('Cannot get sAMAccountName for %s' % record['pmsgOut']['V6']['pNC'] ['StringName'][:-1]) userName = '******' else: logging.error( 'Cannot get sAMAccountName for %s' % record['pmsgOut']['V6']['pNC']['StringName'][:-1]) userName = '******' if attId == LOOKUP_TABLE['supplementalCredentials']: if attr['AttrVal']['valCount'] > 0: blob = ''.join(attr['AttrVal']['pAVal'][0]['pVal']) plainText = drsuapi.DecryptAttributeValue( self.__remoteOps.getDrsr(), blob) if len(plainText) > 24: haveInfo = True if domain is not None: userName = '******' % (domain, userName) if haveInfo is True: try: userProperties = samr.USER_PROPERTIES(plainText) except: # On some old w2k3 there might be user properties that don't # match [MS-SAMR] structure, discarding them return propertiesData = userProperties['UserProperties'] for propertyCount in range(userProperties['PropertyCount']): userProperty = samr.USER_PROPERTY(propertiesData) propertiesData = propertiesData[len(userProperty):] # For now, we will only process Newer Kerberos Keys and CLEARTEXT if userProperty['PropertyName'].decode( 'utf-16le') == 'Primary:Kerberos-Newer-Keys': propertyValueBuffer = unhexlify( userProperty['PropertyValue']) kerbStoredCredentialNew = samr.KERB_STORED_CREDENTIAL_NEW( propertyValueBuffer) data = kerbStoredCredentialNew['Buffer'] for credential in range( kerbStoredCredentialNew['CredentialCount']): keyDataNew = samr.KERB_KEY_DATA_NEW(data) data = data[len(keyDataNew):] keyValue = propertyValueBuffer[ keyDataNew['KeyOffset']:][:keyDataNew['KeyLength']] if self.KERBEROS_TYPE.has_key(keyDataNew['KeyType']): answer = "%s:%s:%s" % ( userName, self.KERBEROS_TYPE[keyDataNew['KeyType']], hexlify(keyValue)) else: answer = "%s:%s:%s" % (userName, hex(keyDataNew['KeyType']), hexlify(keyValue)) # We're just storing the keys, not printing them, to make the output more readable # This is kind of ugly... but it's what I came up with tonight to get an ordered # set :P. Better ideas welcomed ;) self.__kerberosKeys[answer] = None if keysFile is not None: self.__writeOutput(keysFile, answer + '\n') elif userProperty['PropertyName'].decode( 'utf-16le') == 'Primary:CLEARTEXT': # [MS-SAMR] 3.1.1.8.11.5 Primary:CLEARTEXT Property # This credential type is the cleartext password. The value format is the UTF-16 encoded cleartext password. answer = "%s:CLEARTEXT:%s" % ( userName, unhexlify( userProperty['PropertyValue']).decode('utf-16le')) self.__clearTextPwds[answer] = None if clearTextFile is not None: self.__writeOutput(clearTextFile, answer + '\n') if clearTextFile is not None: clearTextFile.flush() if keysFile is not None: keysFile.flush()
class NTDSHashes: class SECRET_TYPE: NTDS = 0 NTDS_CLEARTEXT = 1 NTDS_KERBEROS = 2 NAME_TO_INTERNAL = { 'uSNCreated': 'ATTq131091', 'uSNChanged': 'ATTq131192', 'name': 'ATTm3', 'objectGUID': 'ATTk589826', 'objectSid': 'ATTr589970', 'userAccountControl': 'ATTj589832', 'primaryGroupID': 'ATTj589922', 'accountExpires': 'ATTq589983', 'logonCount': 'ATTj589993', 'sAMAccountName': 'ATTm590045', 'sAMAccountType': 'ATTj590126', 'lastLogonTimestamp': 'ATTq589876', 'userPrincipalName': 'ATTm590480', 'unicodePwd': 'ATTk589914', 'dBCSPwd': 'ATTk589879', 'ntPwdHistory': 'ATTk589918', 'lmPwdHistory': 'ATTk589984', 'pekList': 'ATTk590689', 'supplementalCredentials': 'ATTk589949', 'pwdLastSet': 'ATTq589920', } INTERNAL_TO_NAME = dict((v, k) for k, v in NAME_TO_INTERNAL.iteritems()) SAM_NORMAL_USER_ACCOUNT = 0x30000000 SAM_MACHINE_ACCOUNT = 0x30000001 SAM_TRUST_ACCOUNT = 0x30000002 ACCOUNT_TYPES = (SAM_NORMAL_USER_ACCOUNT, SAM_MACHINE_ACCOUNT, SAM_TRUST_ACCOUNT) def __init__(self, ntdsFile, isRemote=False): self.__NTDS = ntdsFile try: if self.__NTDS is not None: self.__ESEDB = ESENT_DB(ntdsFile, isRemote=isRemote) self.__cursor = self.__ESEDB.openTable('datatable') except Exception as e: raise e def getNextRecord(self): record = self.__ESEDB.getNextRow(self.__cursor) if record is None: return None, False elif self.NAME_TO_INTERNAL['sAMAccountType'] not in record: raise Exception('InvalidFile') try: if record[self.NAME_TO_INTERNAL[ 'sAMAccountType']] in self.ACCOUNT_TYPES: if record[self.NAME_TO_INTERNAL['sAMAccountName']] is not None: userName = '******' % record[ self.NAME_TO_INTERNAL['sAMAccountName']] else: userName = '******' return None, True if record[self.NAME_TO_INTERNAL['name']] is not None: displayName = '%s' % record[self.NAME_TO_INTERNAL['name']] else: displayName = 'N/A' return None, True if record[self.NAME_TO_INTERNAL['objectGUID']] is not None: objectGuid = '%s' % record[ self.NAME_TO_INTERNAL['objectGUID']] else: objectGuid = 'N/A' return None, True fields = "%s:%s:%s" % (displayName, userName, objectGuid) return fields, True else: return None, True except Exception as e: raise Exception('NextRecordFetchFailed') def finish(self): if self.__NTDS is not None: self.__ESEDB.close()
class NTDSHashes(): NAME_TO_INTERNAL = { 'uSNCreated':'ATTq131091', 'uSNChanged':'ATTq131192', 'name':'ATTm3', 'objectGUID':'ATTk589826', 'objectSid':'ATTr589970', 'userAccountControl':'ATTj589832', 'primaryGroupID':'ATTj589922', 'accountExpires':'ATTq589983', 'logonCount':'ATTj589993', 'sAMAccountName':'ATTm590045', 'sAMAccountType':'ATTj590126', 'lastLogonTimestamp':'ATTq589876', 'userPrincipalName':'ATTm590480', 'unicodePwd':'ATTk589914', 'dBCSPwd':'ATTk589879', 'ntPwdHistory':'ATTk589918', 'lmPwdHistory':'ATTk589984', 'pekList':'ATTk590689', 'supplementalCredentials':'ATTk589949', } KERBEROS_TYPE = { 1:'dec-cbc-crc', 3:'des-cbc-md5', 17:'aes128-cts-hmac-sha1-96', 18:'aes256-cts-hmac-sha1-96', 0xffffff74:'rc4_hmac', } INTERNAL_TO_NAME = dict((v,k) for k,v in NAME_TO_INTERNAL.iteritems()) SAM_NORMAL_USER_ACCOUNT = 0x30000000 SAM_MACHINE_ACCOUNT = 0x30000001 SAM_TRUST_ACCOUNT = 0x30000002 ACCOUNT_TYPES = ( SAM_NORMAL_USER_ACCOUNT, SAM_MACHINE_ACCOUNT, SAM_TRUST_ACCOUNT) class PEK_KEY(Structure): structure = ( ('Header','8s=""'), ('KeyMaterial','16s=""'), ('EncryptedPek','52s=""'), ) class CRYPTED_HASH(Structure): structure = ( ('Header','8s=""'), ('KeyMaterial','16s=""'), ('EncryptedHash','16s=""'), ) class CRYPTED_HISTORY(Structure): structure = ( ('Header','8s=""'), ('KeyMaterial','16s=""'), ('EncryptedHash',':'), ) class CRYPTED_BLOB(Structure): structure = ( ('Header','8s=""'), ('KeyMaterial','16s=""'), ('EncryptedHash',':'), ) def __init__(self, ntdsFile, bootKey, isRemote = False, history = False, noLMHash = True): self.__bootKey = bootKey self.__NTDS = ntdsFile self.__history = history self.__noLMHash = noLMHash if self.__NTDS is not None: self.__ESEDB = ESENT_DB(ntdsFile, isRemote = isRemote) self.__cursor = self.__ESEDB.openTable('datatable') self.__tmpUsers = list() self.__PEK = None self.__cryptoCommon = CryptoCommon() self.__hashesFound = {} self.__kerberosKeys = OrderedDict() def __getPek(self): # logging.info('Searching for pekList, be patient') pek = None while True: record = self.__ESEDB.getNextRow(self.__cursor) if record is None: break elif record[self.NAME_TO_INTERNAL['pekList']] is not None: pek = record[self.NAME_TO_INTERNAL['pekList']].decode('hex') break elif record[self.NAME_TO_INTERNAL['sAMAccountType']] in self.ACCOUNT_TYPES: # 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) if pek is not None: encryptedPek = self.PEK_KEY(pek) md5 = hashlib.new('md5') md5.update(self.__bootKey) for i in range(1000): md5.update(encryptedPek['KeyMaterial']) tmpKey = md5.digest() rc4 = ARC4.new(tmpKey) plainText = rc4.encrypt(encryptedPek['EncryptedPek']) self.__PEK = plainText[36:] def __removeRC4Layer(self, cryptedHash): md5 = hashlib.new('md5') md5.update(self.__PEK) md5.update(cryptedHash['KeyMaterial']) tmpKey = md5.digest() rc4 = ARC4.new(tmpKey) plainText = rc4.encrypt(cryptedHash['EncryptedHash']) return plainText def __removeDESLayer(self, cryptedHash, rid): Key1,Key2 = self.__cryptoCommon.deriveKey(int(rid)) Crypt1 = DES.new(Key1, DES.MODE_ECB) Crypt2 = DES.new(Key2, DES.MODE_ECB) decryptedHash = Crypt1.decrypt(cryptedHash[:8]) + Crypt2.decrypt(cryptedHash[8:]) return decryptedHash def __decryptSupplementalInfo(self, record): # This is based on [MS-SAMR] 2.2.10 Supplemental Credentials Structures if record[self.NAME_TO_INTERNAL['supplementalCredentials']] is not None: if len(record[self.NAME_TO_INTERNAL['supplementalCredentials']].decode('hex')) > 24: if record[self.NAME_TO_INTERNAL['userPrincipalName']] is not None: domain = record[self.NAME_TO_INTERNAL['userPrincipalName']].split('@')[-1] userName = '******' % (domain, record[self.NAME_TO_INTERNAL['sAMAccountName']]) else: userName = '******' % record[self.NAME_TO_INTERNAL['sAMAccountName']] cipherText = self.CRYPTED_BLOB(record[self.NAME_TO_INTERNAL['supplementalCredentials']].decode('hex')) plainText = self.__removeRC4Layer(cipherText) try: userProperties = samr.USER_PROPERTIES(plainText) except: # On some old w2k3 there might be user properties that don't # match [MS-SAMR] structure, discarding them return propertiesData = userProperties['UserProperties'] for propertyCount in range(userProperties['PropertyCount']): userProperty = samr.USER_PROPERTY(propertiesData) propertiesData = propertiesData[len(userProperty):] # For now, we will only process Newer Kerberos Keys. if userProperty['PropertyName'].decode('utf-16le') == 'Primary:Kerberos-Newer-Keys': propertyValueBuffer = userProperty['PropertyValue'].decode('hex') kerbStoredCredentialNew = samr.KERB_STORED_CREDENTIAL_NEW(propertyValueBuffer) data = kerbStoredCredentialNew['Buffer'] for credential in range(kerbStoredCredentialNew['CredentialCount']): keyDataNew = samr.KERB_KEY_DATA_NEW(data) data = data[len(keyDataNew):] keyValue = propertyValueBuffer[keyDataNew['KeyOffset']:][:keyDataNew['KeyLength']] if self.KERBEROS_TYPE.has_key(keyDataNew['KeyType']): answer = "%s:%s:%s" % (userName, self.KERBEROS_TYPE[keyDataNew['KeyType']],keyValue.encode('hex')) else: answer = "%s:%s:%s" % (userName, hex(keyDataNew['KeyType']),keyValue.encode('hex')) # We're just storing the keys, not printing them, to make the output more readable # This is kind of ugly... but it's what I came up with tonight to get an ordered # set :P. Better ideas welcomed ;) self.__kerberosKeys[answer] = None def __decryptHash(self, record): # logging.debug('Decrypting hash for user: %s' % record[self.NAME_TO_INTERNAL['name']]) sid = SAMR_RPC_SID(record[self.NAME_TO_INTERNAL['objectSid']].decode('hex')) rid = sid.formatCanonical().split('-')[-1] if record[self.NAME_TO_INTERNAL['dBCSPwd']] is not None: encryptedLMHash = self.CRYPTED_HASH(record[self.NAME_TO_INTERNAL['dBCSPwd']].decode('hex')) tmpLMHash = self.__removeRC4Layer(encryptedLMHash) LMHash = self.__removeDESLayer(tmpLMHash, rid) else: LMHash = ntlm.LMOWFv1('','') encryptedLMHash = None if record[self.NAME_TO_INTERNAL['unicodePwd']] is not None: encryptedNTHash = self.CRYPTED_HASH(record[self.NAME_TO_INTERNAL['unicodePwd']].decode('hex')) tmpNTHash = self.__removeRC4Layer(encryptedNTHash) NTHash = self.__removeDESLayer(tmpNTHash, rid) else: NTHash = ntlm.NTOWFv1('','') encryptedNTHash = None if record[self.NAME_TO_INTERNAL['userPrincipalName']] is not None: domain = record[self.NAME_TO_INTERNAL['userPrincipalName']].split('@')[-1] userName = '******' % (domain, record[self.NAME_TO_INTERNAL['sAMAccountName']]) else: userName = '******' % record[self.NAME_TO_INTERNAL['sAMAccountName']] answer = "%s:%s:%s:%s:::" % (userName, rid, LMHash.encode('hex'), NTHash.encode('hex')) self.__hashesFound[record[self.NAME_TO_INTERNAL['objectSid']].decode('hex')] = answer # print answer if self.__history: LMHistory = [] NTHistory = [] if record[self.NAME_TO_INTERNAL['lmPwdHistory']] is not None: lmPwdHistory = record[self.NAME_TO_INTERNAL['lmPwdHistory']] encryptedLMHistory = self.CRYPTED_HISTORY(record[self.NAME_TO_INTERNAL['lmPwdHistory']].decode('hex')) 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: ntPwdHistory = record[self.NAME_TO_INTERNAL['ntPwdHistory']] encryptedNTHistory = self.CRYPTED_HISTORY(record[self.NAME_TO_INTERNAL['ntPwdHistory']].decode('hex')) tmpNTHistory = self.__removeRC4Layer(encryptedNTHistory) for i in range(0, len(tmpNTHistory)/16): NTHash = self.__removeDESLayer(tmpNTHistory[i*16:(i+1)*16], rid) NTHistory.append(NTHash) for i, (LMHash, NTHash) in enumerate(map(lambda l,n: (l,n) if l else ('',n), LMHistory[1:], NTHistory[1:])): if self.__noLMHash: lmhash = ntlm.LMOWFv1('','').encode('hex') else: lmhash = LMHash.encode('hex') answer = "%s_history%d:%s:%s:%s:::" % (userName, i, rid, lmhash, NTHash.encode('hex')) self.__hashesFound[record[self.NAME_TO_INTERNAL['objectSid']].decode('hex')+str(i)] = answer # print answer def dump(self): if self.__NTDS is None: # No NTDS.dit file provided return # logging.info('Dumping Domain Credentials (domain\\uid:rid:lmhash:nthash)') # We start getting rows from the table aiming at reaching # the pekList. If we find users records we stored them # in a temp list for later process. self.__getPek() if self.__PEK is not None: # logging.info('Pek found and decrypted: 0x%s' % self.__PEK.encode('hex')) # logging.info('Reading and decrypting hashes from %s ' % self.__NTDS) # First of all, if we have users already cached, let's decrypt their hashes for record in self.__tmpUsers: try: self.__decryptHash(record) self.__decryptSupplementalInfo(record) except Exception, e: try: # logging.error("Error while processing row for user %s" % record[self.NAME_TO_INTERNAL['name']]) # logging.error(str(e)) pass except: # logging.error("Error while processing row!") # logging.error(str(e)) pass # Now let's keep moving through the NTDS file and decrypting what we find while True: try: record = self.__ESEDB.getNextRow(self.__cursor) except: # logging.error('Error while calling getNextRow(), trying the next one') continue if record is None: break try: if record[self.NAME_TO_INTERNAL['sAMAccountType']] in self.ACCOUNT_TYPES: self.__decryptHash(record) self.__decryptSupplementalInfo(record) except Exception, e: try: # logging.error("Error while processing row for user %s" % record[self.NAME_TO_INTERNAL['name']]) # logging.error(str(e)) pass except: # logging.error("Error while processing row!") # logging.error(str(e)) pass