mirror of
https://github.com/fortra/impacket
synced 2026-06-08 14:15:13 +00:00
028f0724fe
* Added SAM history parsing to secretsdump.py and secretsdump.py * Update impacket/examples/regsecrets.py Co-authored-by: Gabriel Gonzalez <gabriel.gonzalez@fortra.com> * Update impacket/examples/secretsdump.py Co-authored-by: Gabriel Gonzalez <gabriel.gonzalez@fortra.com> * Update impacket/examples/secretsdump.py Co-authored-by: Gabriel Gonzalez <gabriel.gonzalez@fortra.com> * Update impacket/examples/regsecrets.py Co-authored-by: Gabriel Gonzalez <gabriel.gonzalez@fortra.com> * Update help message for -history argument Clarified help message for -history argument to specify NTDS and SAM hashes. * Clarify -history argument help text Updated help text for the -history argument to clarify that it dumps NTDS and SAM hashes. --------- Co-authored-by: Gabriel Gonzalez <gabriel.gonzalez@fortra.com>
1111 lines
47 KiB
Python
1111 lines
47 KiB
Python
import hashlib
|
|
import ntpath
|
|
import logging
|
|
import time
|
|
import re
|
|
import json
|
|
import codecs
|
|
from datetime import datetime
|
|
from struct import unpack, pack
|
|
from six import b
|
|
|
|
from impacket import ntlm
|
|
from impacket.ese import getUnixTime
|
|
from impacket import LOG
|
|
from impacket.dcerpc.v5 import transport, rrp, scmr, wkst
|
|
from impacket.crypto import transformKey
|
|
from impacket.system_errors import ERROR_NO_MORE_ITEMS
|
|
from impacket.structure import hexdump
|
|
from impacket.krb5 import constants
|
|
from impacket.krb5.crypto import string_to_key
|
|
|
|
from impacket.examples.secretsdump import CryptoCommon, _print_helper, DOMAIN_ACCOUNT_F, SAM_KEY_DATA, SAM_KEY_DATA_AES, ARC4, DES, AES, USER_ACCOUNT_V, SAM_HASH, SAM_HASH_AES, LSA_SECRET_XP, HMAC, MD4, MD5, LSA_SECRET, LSA_SECRET_BLOB, NL_RECORD, DPAPI_SYSTEM, NTDSHashes
|
|
from binascii import unhexlify, hexlify
|
|
|
|
# Helper to create files for exporting
|
|
def openFile(fileName, mode='w+', openFileFunc=None):
|
|
if openFileFunc is not None:
|
|
return openFileFunc(fileName, mode)
|
|
else:
|
|
return codecs.open(fileName, mode, encoding='utf-8')
|
|
|
|
class RemoteOperations:
|
|
def __init__(self, smbConnection, doKerberos, kdcHost=None):
|
|
self.__smbConnection = smbConnection
|
|
if self.__smbConnection is not None:
|
|
self.__smbConnection.setTimeout(5 * 60)
|
|
self.__serviceName = 'RemoteRegistry'
|
|
self.__stringBindingWinReg = r'ncacn_np:445[\pipe\winreg]'
|
|
self.__rrp = None
|
|
self.__regHandle = None
|
|
|
|
self.__samr = None
|
|
|
|
self.__drsr = None
|
|
self.__doKerberos = doKerberos
|
|
self.__kdcHost = kdcHost
|
|
|
|
self.__bootKey = b''
|
|
self.__disabled = False
|
|
self.__shouldStop = False
|
|
self.__started = False
|
|
|
|
self.__stringBindingSvcCtl = r'ncacn_np:445[\pipe\svcctl]'
|
|
self.__scmr = None
|
|
self.__tmpServiceName = None
|
|
self.__serviceDeleted = False
|
|
|
|
|
|
def __connectSvcCtl(self):
|
|
rpc = transport.DCERPCTransportFactory(self.__stringBindingSvcCtl)
|
|
rpc.set_smb_connection(self.__smbConnection)
|
|
self.__scmr = rpc.get_dce_rpc()
|
|
self.__scmr.connect()
|
|
self.__scmr.bind(scmr.MSRPC_UUID_SCMR)
|
|
|
|
def __connectWinReg(self):
|
|
rpc = transport.DCERPCTransportFactory(self.__stringBindingWinReg)
|
|
rpc.set_smb_connection(self.__smbConnection)
|
|
self.__rrp = rpc.get_dce_rpc()
|
|
self.__rrp.connect()
|
|
self.__rrp.bind(rrp.MSRPC_UUID_RRP)
|
|
|
|
def getMachineKerberosSalt(self):
|
|
"""
|
|
Returns Kerberos salt for the current connection if
|
|
we have the correct information
|
|
"""
|
|
if self.__smbConnection.getServerName() == '':
|
|
host, _ = self.getMachineNameAndDomain()
|
|
remoteName = self.__smbConnection.getRemoteName()
|
|
# Check if remoteName is FQDN, otherwise it will likely be the hostname only and we can't build the salt
|
|
if remoteName.lower().startswith(f"{host.lower()}."):
|
|
domain = ".".join(remoteName.split(".")[1:])
|
|
else:
|
|
return b''
|
|
else:
|
|
host = self.__smbConnection.getServerName()
|
|
domain = self.__smbConnection.getServerDNSDomainName()
|
|
LOG.debug(f"[Secretsdump][getMachineKerberosSalt] Host: {host} / Domain FQDN: {domain}")
|
|
salt = b'%shost%s.%s' % (domain.upper().encode('utf-8'), host.lower().encode('utf-8'), domain.lower().encode('utf-8'))
|
|
return salt
|
|
|
|
def getMachineNameAndDomain(self):
|
|
if self.__smbConnection.getServerName() == '':
|
|
# No serverName.. this is either because we're doing Kerberos
|
|
# or not receiving that data during the login process.
|
|
# Let's try getting it through RPC
|
|
rpc = transport.DCERPCTransportFactory(r'ncacn_np:445[\pipe\wkssvc]')
|
|
rpc.set_smb_connection(self.__smbConnection)
|
|
dce = rpc.get_dce_rpc()
|
|
dce.connect()
|
|
dce.bind(wkst.MSRPC_UUID_WKST)
|
|
resp = wkst.hNetrWkstaGetInfo(dce, 100)
|
|
dce.disconnect()
|
|
return resp['WkstaInfo']['WkstaInfo100']['wki100_computername'][:-1], resp['WkstaInfo']['WkstaInfo100'][
|
|
'wki100_langroup'][:-1]
|
|
else:
|
|
return self.__smbConnection.getServerName(), self.__smbConnection.getServerDomain()
|
|
|
|
def getDefaultLoginAccount(self):
|
|
try:
|
|
ans = rrp.hBaseRegOpenKey(self.__rrp, self.__regHandle, r'SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon')
|
|
keyHandle = ans['phkResult']
|
|
dataType, dataValue = rrp.hBaseRegQueryValue(self.__rrp, keyHandle, 'DefaultUserName')
|
|
username = dataValue[:-1]
|
|
dataType, dataValue = rrp.hBaseRegQueryValue(self.__rrp, keyHandle, 'DefaultDomainName')
|
|
domain = dataValue[:-1]
|
|
rrp.hBaseRegCloseKey(self.__rrp, keyHandle)
|
|
if len(domain) > 0:
|
|
return '%s\\%s' % (domain, username)
|
|
else:
|
|
return username
|
|
except:
|
|
return None
|
|
|
|
def getServiceAccount(self, serviceName):
|
|
try:
|
|
# Open the service
|
|
ans = scmr.hROpenServiceW(self.__scmr, self.__scManagerHandle, serviceName)
|
|
serviceHandle = ans['lpServiceHandle']
|
|
resp = scmr.hRQueryServiceConfigW(self.__scmr, serviceHandle)
|
|
account = resp['lpServiceConfig']['lpServiceStartName'][:-1]
|
|
scmr.hRCloseServiceHandle(self.__scmr, serviceHandle)
|
|
if account.startswith('.\\'):
|
|
account = account[2:]
|
|
return account
|
|
except Exception as e:
|
|
# Don't log if history service is not found, that should be normal
|
|
if serviceName.endswith("_history") is False:
|
|
LOG.error(e)
|
|
return None
|
|
|
|
def __checkServiceStatus(self):
|
|
# Open SC Manager
|
|
ans = scmr.hROpenSCManagerW(self.__scmr)
|
|
self.__scManagerHandle = ans['lpScHandle']
|
|
# Now let's open the service
|
|
ans = scmr.hROpenServiceW(self.__scmr, self.__scManagerHandle, self.__serviceName)
|
|
self.__serviceHandle = ans['lpServiceHandle']
|
|
# Let's check its status
|
|
ans = scmr.hRQueryServiceStatus(self.__scmr, self.__serviceHandle)
|
|
if ans['lpServiceStatus']['dwCurrentState'] == scmr.SERVICE_STOPPED:
|
|
LOG.info('Service %s is in stopped state'% self.__serviceName)
|
|
self.__shouldStop = True
|
|
self.__started = False
|
|
elif ans['lpServiceStatus']['dwCurrentState'] == scmr.SERVICE_RUNNING:
|
|
LOG.debug('Service %s is already running'% self.__serviceName)
|
|
self.__shouldStop = False
|
|
self.__started = True
|
|
else:
|
|
raise Exception('Unknown service state 0x%x - Aborting' % ans['CurrentState'])
|
|
|
|
# Let's check its configuration if service is stopped, maybe it's disabled :s
|
|
if self.__started is False:
|
|
ans = scmr.hRQueryServiceConfigW(self.__scmr,self.__serviceHandle)
|
|
if ans['lpServiceConfig']['dwStartType'] == 0x4:
|
|
LOG.info('Service %s is disabled, enabling it'% self.__serviceName)
|
|
self.__disabled = True
|
|
scmr.hRChangeServiceConfigW(self.__scmr, self.__serviceHandle, dwStartType = 0x3)
|
|
LOG.info('Starting service %s' % self.__serviceName)
|
|
scmr.hRStartServiceW(self.__scmr,self.__serviceHandle)
|
|
time.sleep(1)
|
|
|
|
def enableRegistry(self):
|
|
self.__connectSvcCtl()
|
|
self.__checkServiceStatus()
|
|
self.__connectWinReg()
|
|
|
|
def __restore(self):
|
|
# First of all stop the service if it was originally stopped
|
|
if self.__shouldStop is True:
|
|
LOG.info('Stopping service %s' % self.__serviceName)
|
|
scmr.hRControlService(self.__scmr, self.__serviceHandle, scmr.SERVICE_CONTROL_STOP)
|
|
if self.__disabled is True:
|
|
LOG.info('Restoring the disabled state for service %s' % self.__serviceName)
|
|
scmr.hRChangeServiceConfigW(self.__scmr, self.__serviceHandle, dwStartType = 0x4)
|
|
if self.__serviceDeleted is False and self.__tmpServiceName is not None:
|
|
# Check again the service we created does not exist, starting a new connection
|
|
# Why?.. Hitting CTRL+C might break the whole existing DCE connection
|
|
try:
|
|
rpc = transport.DCERPCTransportFactory(r'ncacn_np:%s[\pipe\svcctl]' % self.__smbConnection.getRemoteHost())
|
|
if hasattr(rpc, 'set_credentials'):
|
|
# This method exists only for selected protocol sequences.
|
|
rpc.set_credentials(*self.__smbConnection.getCredentials())
|
|
rpc.set_kerberos(self.__doKerberos, self.__kdcHost)
|
|
self.__scmr = rpc.get_dce_rpc()
|
|
self.__scmr.connect()
|
|
self.__scmr.bind(scmr.MSRPC_UUID_SCMR)
|
|
# Open SC Manager
|
|
ans = scmr.hROpenSCManagerW(self.__scmr)
|
|
self.__scManagerHandle = ans['lpScHandle']
|
|
# Now let's open the service
|
|
resp = scmr.hROpenServiceW(self.__scmr, self.__scManagerHandle, self.__tmpServiceName)
|
|
service = resp['lpServiceHandle']
|
|
scmr.hRDeleteService(self.__scmr, service)
|
|
scmr.hRControlService(self.__scmr, service, scmr.SERVICE_CONTROL_STOP)
|
|
scmr.hRCloseServiceHandle(self.__scmr, service)
|
|
scmr.hRCloseServiceHandle(self.__scmr, self.__serviceHandle)
|
|
scmr.hRCloseServiceHandle(self.__scmr, self.__scManagerHandle)
|
|
rpc.disconnect()
|
|
except Exception as e:
|
|
# If service is stopped it'll trigger an exception
|
|
# If service does not exist it'll trigger an exception
|
|
# So. we just wanna be sure we delete it, no need to
|
|
# show this exception message
|
|
pass
|
|
|
|
def finish(self):
|
|
if self.__regHandle is not None:
|
|
rrp.hBaseRegCloseKey(self.__rrp, self.__regHandle)
|
|
self.__restore()
|
|
if self.__rrp is not None:
|
|
self.__rrp.disconnect()
|
|
if self.__drsr is not None:
|
|
self.__drsr.disconnect()
|
|
if self.__samr is not None:
|
|
self.__samr.disconnect()
|
|
if self.__scmr is not None:
|
|
try:
|
|
self.__scmr.disconnect()
|
|
except Exception as e:
|
|
if str(e).find('STATUS_INVALID_PARAMETER') >= 0:
|
|
pass
|
|
else:
|
|
raise
|
|
|
|
def getBootKey(self):
|
|
bootKey = b''
|
|
self.openHKLMHandle()
|
|
for key in ['JD','Skew1','GBG','Data']:
|
|
LOG.debug('Retrieving class info for %s'% key)
|
|
ans = rrp.hBaseRegOpenKey(self.__rrp, self.__regHandle, 'SYSTEM\\CurrentControlSet\\Control\\Lsa\\%s' % key)
|
|
keyHandle = ans['phkResult']
|
|
ans = rrp.hBaseRegQueryInfoKey(self.__rrp, keyHandle)
|
|
bootKey = bootKey + b(ans['lpClassOut'][:-1])
|
|
rrp.hBaseRegCloseKey(self.__rrp, keyHandle)
|
|
|
|
transforms = [ 8, 5, 4, 2, 11, 9, 13, 3, 0, 6, 1, 12, 14, 10, 15, 7 ]
|
|
|
|
bootKey = unhexlify(bootKey)
|
|
|
|
for i in range(len(bootKey)):
|
|
self.__bootKey += bootKey[transforms[i]:transforms[i]+1]
|
|
|
|
LOG.info('Target system bootKey: 0x%s' % hexlify(self.__bootKey).decode('utf-8'))
|
|
|
|
return self.__bootKey
|
|
|
|
def openHKLMHandle(self):
|
|
if self.__regHandle is None:
|
|
ans = rrp.hOpenLocalMachine(self.__rrp)
|
|
self.__regHandle = ans['phKey']
|
|
|
|
def retrieveSubKey(self, subKey, value, throttle=0):
|
|
LOG.debug(f'[{datetime.now()}] Retrieving {subKey}\\{value}')
|
|
self.openHKLMHandle()
|
|
try:
|
|
ans = rrp.hBaseRegOpenKey(self.__rrp, self.__regHandle, subKey, dwOptions=rrp.REG_OPTION_BACKUP_RESTORE | rrp.REG_OPTION_OPEN_LINK,
|
|
samDesired=None)
|
|
except:
|
|
raise Exception(f"Can't open {subKey} subKey")
|
|
keyHandle = ans['phkResult']
|
|
value = rrp.hBaseRegQueryValue(self.__rrp, ans['phkResult'], value)
|
|
rrp.hBaseRegCloseKey(self.__rrp, keyHandle)
|
|
time.sleep(throttle)
|
|
return value
|
|
|
|
def enumSubKey(self, subKey, throttle=0):
|
|
LOG.debug(f'[{datetime.now()}] Enumerating keys {subKey}')
|
|
self.openHKLMHandle()
|
|
try:
|
|
ans = rrp.hBaseRegOpenKey(self.__rrp, self.__regHandle, subKey, dwOptions=rrp.REG_OPTION_BACKUP_RESTORE | rrp.REG_OPTION_OPEN_LINK,
|
|
samDesired=None)
|
|
except:
|
|
raise Exception(f"Can't open {subKey} subKey")
|
|
keyHandle = ans['phkResult']
|
|
i = 0
|
|
values = []
|
|
while True:
|
|
try:
|
|
key = rrp.hBaseRegEnumKey(self.__rrp, keyHandle, i)
|
|
i += 1
|
|
values.append(key['lpNameOut'][:-1])
|
|
except Exception:
|
|
break
|
|
rrp.hBaseRegCloseKey(self.__rrp, keyHandle)
|
|
time.sleep(throttle)
|
|
return values
|
|
|
|
def enumValues(self, subKey:str, throttle=0) -> dict:
|
|
LOG.debug(f'[{datetime.now()}] Enumerating values {subKey}')
|
|
self.openHKLMHandle()
|
|
try:
|
|
ans = rrp.hBaseRegOpenKey(self.__rrp, self.__regHandle, subKey, dwOptions=rrp.REG_OPTION_BACKUP_RESTORE | rrp.REG_OPTION_OPEN_LINK,
|
|
samDesired=None)
|
|
except:
|
|
raise Exception(f"Can't open {subKey} subKey")
|
|
keyHandle = ans['phkResult']
|
|
i = 0
|
|
values = dict()
|
|
while True:
|
|
try:
|
|
ans2 = rrp.hBaseRegEnumValue(self.__rrp, keyHandle, i)
|
|
lp_value_name = ans2['lpValueNameOut'][:-1]
|
|
if len(lp_value_name) == 0:
|
|
lp_value_name = '(Default)'
|
|
lp_type = ans2['lpType']
|
|
lp_data = b''.join(ans2['lpData'])
|
|
values[lp_value_name] = self.__parse_lp_data(lp_type, lp_data)
|
|
i += 1
|
|
except rrp.DCERPCSessionError as e:
|
|
if e.get_error_code() == ERROR_NO_MORE_ITEMS:
|
|
break
|
|
time.sleep(throttle)
|
|
return values
|
|
|
|
def __parse_lp_data(self, valueType, valueData):
|
|
try:
|
|
if valueType == rrp.REG_SZ or valueType == rrp.REG_EXPAND_SZ:
|
|
if type(valueData) is int:
|
|
return None
|
|
else:
|
|
return valueData.decode('utf-16le')[:-1]
|
|
elif valueType == rrp.REG_BINARY:
|
|
return valueData
|
|
elif valueType == rrp.REG_DWORD:
|
|
return unpack('<L', valueData)[0]
|
|
elif valueType == rrp.REG_QWORD:
|
|
return unpack('<Q', valueData)[0]
|
|
elif valueType == rrp.REG_NONE:
|
|
return valueData
|
|
elif valueType == rrp.REG_MULTI_SZ:
|
|
return valueData.decode('utf-16le')[:-2]
|
|
else:
|
|
print("Unknown Type 0x%x!" % valueType)
|
|
hexdump(valueData)
|
|
except Exception as e:
|
|
logging.debug('Exception thrown when printing reg value %s' % str(e))
|
|
print('Invalid data')
|
|
pass
|
|
|
|
class SAMHashes():
|
|
def __init__(self, bootKey, perSecretCallback = lambda secret: _print_helper(secret), remoteOps:RemoteOperations=None, history=False, throttle=0):
|
|
self.__remoteOps = remoteOps
|
|
self.__hashedBootKey = b''
|
|
self.__bootKey = bootKey
|
|
self.__cryptoCommon = CryptoCommon()
|
|
self.__itemsFound = {}
|
|
self.__perSecretCallback = perSecretCallback
|
|
self.__history = history
|
|
self.__historyItems = []
|
|
self.__throttle = throttle
|
|
|
|
def MD5(self, data):
|
|
md5 = hashlib.new('md5')
|
|
md5.update(data)
|
|
return md5.digest()
|
|
|
|
def getHBootKey(self):
|
|
LOG.debug('Calculating HashedBootKey from SAM')
|
|
QWERTY = b"!@#$%^&*()qwertyUIOPAzxcvbnmQQQQQQQQQQQQ)(*@&%\0"
|
|
DIGITS = b"0123456789012345678901234567890123456789\0"
|
|
|
|
F = self.__remoteOps.retrieveSubKey(r'SAM\SAM\Domains\Account', 'F', throttle=self.__throttle)[1]
|
|
|
|
domainData = DOMAIN_ACCOUNT_F(F)
|
|
|
|
if domainData['Key0'][0:1] == b'\x01':
|
|
samKeyData = SAM_KEY_DATA(domainData['Key0'])
|
|
|
|
rc4Key = self.MD5(samKeyData['Salt'] + QWERTY + self.__bootKey + DIGITS)
|
|
rc4 = ARC4.new(rc4Key)
|
|
self.__hashedBootKey = rc4.encrypt(samKeyData['Key'] + samKeyData['CheckSum'])
|
|
|
|
# Verify key with checksum
|
|
checkSum = self.MD5( self.__hashedBootKey[:16] + DIGITS + self.__hashedBootKey[:16] + QWERTY)
|
|
|
|
if checkSum != self.__hashedBootKey[16:]:
|
|
raise Exception('hashedBootKey CheckSum failed, Syskey startup password probably in use! :(')
|
|
|
|
elif domainData['Key0'][0:1] == b'\x02':
|
|
# This is Windows 2016 TP5 on in theory (it is reported that some W10 and 2012R2 might behave this way also)
|
|
samKeyData = SAM_KEY_DATA_AES(domainData['Key0'])
|
|
|
|
self.__hashedBootKey = self.__cryptoCommon.decryptAES(self.__bootKey,
|
|
samKeyData['Data'][:samKeyData['DataLen']], samKeyData['Salt'])
|
|
|
|
def __decryptHash(self, rid, cryptedHash, constant, newStyle = False):
|
|
# Section 2.2.11.1.1 Encrypting an NT or LM Hash Value with a Specified Key
|
|
# plus hashedBootKey stuff
|
|
Key1,Key2 = self.__cryptoCommon.deriveKey(rid)
|
|
|
|
Crypt1 = DES.new(Key1, DES.MODE_ECB)
|
|
Crypt2 = DES.new(Key2, DES.MODE_ECB)
|
|
|
|
if newStyle is False:
|
|
rc4Key = self.MD5( self.__hashedBootKey[:0x10] + pack("<L", rid) + constant )
|
|
rc4 = ARC4.new(rc4Key)
|
|
key = rc4.encrypt(cryptedHash['Hash'])
|
|
else:
|
|
key = self.__cryptoCommon.decryptAES(self.__hashedBootKey[:0x10], cryptedHash['Hash'], cryptedHash['Salt'])[:16]
|
|
|
|
decryptedHash = Crypt1.decrypt(key[:8]) + Crypt2.decrypt(key[8:])
|
|
|
|
return decryptedHash
|
|
|
|
def __unwrap_history_block(self, rid_int, block16):
|
|
key1, key2 = self.__cryptoCommon.deriveKey(rid_int)
|
|
crypt1 = DES.new(key1, DES.MODE_ECB)
|
|
crypt2 = DES.new(key2, DES.MODE_ECB)
|
|
return crypt1.decrypt(block16[:8]) + crypt2.decrypt(block16[8:16])
|
|
|
|
def __decrypt_history_entries_aes(self, rid_int, entries):
|
|
out = []
|
|
key = self.__hashedBootKey[:0x10]
|
|
for salt, enc in entries:
|
|
if not enc or (len(enc) % 16) != 0:
|
|
continue
|
|
if not salt or len(salt) != 16:
|
|
continue
|
|
cipher = AES.new(key, AES.MODE_CBC, iv=salt)
|
|
plain = cipher.decrypt(enc)
|
|
for off in range(0, len(plain), 16):
|
|
block = plain[off:off + 16]
|
|
if len(block) < 16:
|
|
break
|
|
out.append(self.__unwrap_history_block(rid_int, block))
|
|
return out
|
|
|
|
def __scan_v_for_aes_entries(self, vdata):
|
|
entries = []
|
|
length = len(vdata)
|
|
offset = 0x100 if length > 0x200 else 0
|
|
while offset + 20 <= length:
|
|
salt = vdata[offset:offset + 16]
|
|
data_len = int.from_bytes(vdata[offset + 16:offset + 20], 'little', signed=False)
|
|
if data_len == 0 or data_len > 0x2000:
|
|
offset += 4
|
|
continue
|
|
data_off = offset + 20
|
|
if data_off + data_len > length or (data_len % 16) != 0:
|
|
offset += 4
|
|
continue
|
|
enc = vdata[data_off:data_off + data_len]
|
|
entries.append((salt, enc))
|
|
offset = data_off + data_len
|
|
if offset % 4:
|
|
offset += (4 - (offset % 4))
|
|
return entries
|
|
|
|
def __decode_aes_history_block(self, rid_int, data, offset, length):
|
|
if offset <= 0 or length <= 0:
|
|
return []
|
|
end = offset + length
|
|
if end > len(data):
|
|
return []
|
|
|
|
blob = data[offset:end]
|
|
if len(blob) < 24:
|
|
return []
|
|
|
|
try:
|
|
record = SAM_HASH_AES(blob)
|
|
except Exception:
|
|
LOG.debug('Failed to parse SAM_HASH_AES history block at 0x%x (len=%d)', offset, length, exc_info=True)
|
|
return []
|
|
|
|
enc = record['Hash']
|
|
if not enc:
|
|
return []
|
|
|
|
enc_len = len(enc) - (len(enc) % 16)
|
|
if enc_len <= 0:
|
|
return []
|
|
|
|
enc = enc[:enc_len]
|
|
return self.__decrypt_history_entries_aes(rid_int, [(record['Salt'], enc)])
|
|
|
|
def __extract_local_history(self, rid_int, new_style, user_account):
|
|
lm_const = b"LMPASSWORDHISTORY\0"
|
|
nt_const = b"NTPASSWORDHISTORY\0"
|
|
|
|
result = {'lm': [], 'nt': []}
|
|
meta = user_account['Unknown15']
|
|
data = user_account['Data']
|
|
|
|
lm_offset = int.from_bytes(meta[0:4], 'little', signed=False)
|
|
lm_length = int.from_bytes(meta[4:8], 'little', signed=False)
|
|
nt_offset = int.from_bytes(meta[12:16], 'little', signed=False)
|
|
nt_length = int.from_bytes(meta[16:20], 'little', signed=False)
|
|
|
|
if not new_style:
|
|
LOG.debug('Skipping old-style history for RID %d; RC4/DES path disabled', rid_int)
|
|
return result
|
|
|
|
lm_entries = self.__decode_aes_history_block(rid_int, data, lm_offset, lm_length)
|
|
nt_entries = self.__decode_aes_history_block(rid_int, data, nt_offset, nt_length)
|
|
|
|
if not lm_entries and lm_length:
|
|
blob = data[lm_offset:lm_offset + lm_length]
|
|
lm_entries = self.__decrypt_history_entries_aes(rid_int, self.__scan_v_for_aes_entries(blob))
|
|
if not nt_entries and nt_length:
|
|
blob = data[nt_offset:nt_offset + nt_length]
|
|
nt_entries = self.__decrypt_history_entries_aes(rid_int, self.__scan_v_for_aes_entries(blob))
|
|
|
|
result['lm'] = nt_entries
|
|
result['nt'] = lm_entries
|
|
|
|
if not result['lm'] and not result['nt'] and lm_length == 0 and nt_length == 0:
|
|
fallback_entries = self.__scan_v_for_aes_entries(data)
|
|
if fallback_entries:
|
|
result['nt'] = self.__decrypt_history_entries_aes(rid_int, fallback_entries)
|
|
|
|
return result
|
|
|
|
def dump(self):
|
|
NTPASSWORD = b"NTPASSWORD\0"
|
|
LMPASSWORD = b"LMPASSWORD\0"
|
|
|
|
LOG.info('Dumping local SAM hashes (uid:rid:lmhash:nthash)')
|
|
self.getHBootKey()
|
|
self.__historyItems = []
|
|
|
|
usersKey = r'SAM\SAM\Domains\Account\Users'
|
|
|
|
# Enumerate all the RIDs
|
|
rids = self.__remoteOps.enumSubKey(usersKey, throttle=self.__throttle)
|
|
# Remove the Names item
|
|
try:
|
|
rids.remove('Names')
|
|
except:
|
|
pass
|
|
|
|
empty_lm_hex = hexlify(ntlm.LMOWFv1('', '')).decode('utf-8')
|
|
empty_nt_hex = hexlify(ntlm.NTOWFv1('', '')).decode('utf-8')
|
|
|
|
for rid in rids:
|
|
userAccount = USER_ACCOUNT_V(self.__remoteOps.retrieveSubKey(ntpath.join(usersKey, rid), 'V', throttle=self.__throttle)[1])
|
|
rid = int(rid, 16)
|
|
|
|
V = userAccount['Data']
|
|
|
|
userName = V[userAccount['NameOffset']:userAccount['NameOffset']+userAccount['NameLength']].decode('utf-16le')
|
|
|
|
if userAccount['NTHashLength'] == 0:
|
|
logging.error('SAM hashes extraction for user %s failed. The account doesn\'t have hash information.' % userName)
|
|
continue
|
|
|
|
encNTHash = b''
|
|
if V[userAccount['NTHashOffset']:][2:3] == b'\x01':
|
|
# Old Style hashes
|
|
newStyle = False
|
|
if userAccount['LMHashLength'] == 20:
|
|
encLMHash = SAM_HASH(V[userAccount['LMHashOffset']:][:userAccount['LMHashLength']])
|
|
if userAccount['NTHashLength'] == 20:
|
|
encNTHash = SAM_HASH(V[userAccount['NTHashOffset']:][:userAccount['NTHashLength']])
|
|
else:
|
|
# New Style hashes
|
|
newStyle = True
|
|
if userAccount['LMHashLength'] == 24:
|
|
encLMHash = SAM_HASH_AES(V[userAccount['LMHashOffset']:][:userAccount['LMHashLength']])
|
|
encNTHash = SAM_HASH_AES(V[userAccount['NTHashOffset']:][:userAccount['NTHashLength']])
|
|
|
|
LOG.debug('NewStyle hashes is: %s' % newStyle)
|
|
if userAccount['LMHashLength'] >= 20:
|
|
lmHash = self.__decryptHash(rid, encLMHash, LMPASSWORD, newStyle)
|
|
else:
|
|
lmHash = b''
|
|
|
|
if encNTHash != b'':
|
|
ntHash = self.__decryptHash(rid, encNTHash, NTPASSWORD, newStyle)
|
|
else:
|
|
ntHash = b''
|
|
|
|
if lmHash == b'':
|
|
lmHash = ntlm.LMOWFv1('','')
|
|
if ntHash == b'':
|
|
ntHash = ntlm.NTOWFv1('','')
|
|
|
|
answer = "%s:%d:%s:%s:::" % (userName, rid, hexlify(lmHash).decode('utf-8'), hexlify(ntHash).decode('utf-8'))
|
|
self.__itemsFound[rid] = answer
|
|
self.__perSecretCallback(answer)
|
|
|
|
if self.__history:
|
|
try:
|
|
history = self.__extract_local_history(rid, newStyle, userAccount)
|
|
lm_hist = history.get('lm', [])
|
|
nt_hist = history.get('nt', [])
|
|
while lm_hist and nt_hist and lm_hist[-1] == nt_hist[-1]:
|
|
lm_hist.pop()
|
|
nt_hist.pop()
|
|
LOG.debug('History lengths for %s (RID %d): lm=%d nt=%d', userName, rid,
|
|
len(lm_hist), len(nt_hist))
|
|
count = max(len(lm_hist), len(nt_hist))
|
|
for idx in range(count):
|
|
lm_val = lm_hist[idx] if idx < len(lm_hist) else b''
|
|
nt_val = nt_hist[idx] if idx < len(nt_hist) else b''
|
|
lm_hex = hexlify(lm_val).decode('utf-8') if lm_val else empty_lm_hex
|
|
nt_hex = hexlify(nt_val).decode('utf-8') if nt_val else empty_nt_hex
|
|
if lm_hex == empty_lm_hex and nt_hex == empty_nt_hex:
|
|
continue
|
|
history_line = f"{userName}_history{idx}:{rid}:{lm_hex}:{nt_hex}:::"
|
|
self.__historyItems.append(history_line)
|
|
self.__perSecretCallback(history_line)
|
|
except Exception as exc:
|
|
LOG.error('SAM history parsing failed for RID %d: %s', rid, exc, exc_info=True)
|
|
|
|
def export(self, baseFileName, openFileFunc = None):
|
|
if len(self.__itemsFound) > 0:
|
|
items = sorted(self.__itemsFound)
|
|
fileName = baseFileName + '.sam'
|
|
fd = openFile(fileName, openFileFunc=openFileFunc)
|
|
for item in items:
|
|
fd.write(self.__itemsFound[item] + '\n')
|
|
for line in self.__historyItems:
|
|
fd.write(line + '\n')
|
|
fd.close()
|
|
return fileName
|
|
|
|
class LSASecrets():
|
|
UNKNOWN_USER = '(Unknown User)'
|
|
class SECRET_TYPE:
|
|
LSA = 0
|
|
LSA_HASHED = 1
|
|
LSA_RAW = 2
|
|
LSA_KERBEROS = 3
|
|
|
|
def __init__(self, bootKey, remoteOps:RemoteOperations=None, history=False,
|
|
perSecretCallback=lambda secretType, secret: _print_helper(secret), throttle=0):
|
|
self.__bootKey = bootKey
|
|
self.__LSAKey = b''
|
|
self.__NKLMKey = b''
|
|
self.__vistaStyle = True
|
|
self.__cryptoCommon = CryptoCommon()
|
|
self.__remoteOps = remoteOps
|
|
self.__cachedItems = []
|
|
self.__secretItems = []
|
|
self.__perSecretCallback = perSecretCallback
|
|
self.__history = history
|
|
self.__throttle = throttle
|
|
|
|
def MD5(self, data):
|
|
md5 = hashlib.new('md5')
|
|
md5.update(data)
|
|
return md5.digest()
|
|
|
|
def __sha256(self, key, value, rounds=1000):
|
|
sha = hashlib.sha256()
|
|
sha.update(key)
|
|
for i in range(rounds):
|
|
sha.update(value)
|
|
return sha.digest()
|
|
|
|
def __decryptSecret(self, key, value):
|
|
# [MS-LSAD] Section 5.1.2
|
|
plainText = b''
|
|
|
|
encryptedSecretSize = unpack('<I', value[:4])[0]
|
|
value = value[len(value)-encryptedSecretSize:]
|
|
|
|
key0 = key
|
|
for i in range(0, len(value), 8):
|
|
cipherText = value[:8]
|
|
tmpStrKey = key0[:7]
|
|
tmpKey = transformKey(tmpStrKey)
|
|
Crypt1 = DES.new(tmpKey, DES.MODE_ECB)
|
|
plainText += Crypt1.decrypt(cipherText)
|
|
key0 = key0[7:]
|
|
value = value[8:]
|
|
# AdvanceKey
|
|
if len(key0) < 7:
|
|
key0 = key[len(key0):]
|
|
|
|
secret = LSA_SECRET_XP(plainText)
|
|
return secret['Secret']
|
|
|
|
def __decryptHash(self, key, value, iv):
|
|
hmac_md5 = HMAC.new(key,iv,MD5)
|
|
rc4key = hmac_md5.digest()
|
|
|
|
rc4 = ARC4.new(rc4key)
|
|
data = rc4.encrypt(value)
|
|
return data
|
|
|
|
def __decryptLSA(self, value):
|
|
if self.__vistaStyle is True:
|
|
# ToDo: There could be more than one LSA Keys
|
|
record = LSA_SECRET(value)
|
|
tmpKey = self.__sha256(self.__bootKey, record['EncryptedData'][:32])
|
|
plainText = self.__cryptoCommon.decryptAES(tmpKey, record['EncryptedData'][32:])
|
|
record = LSA_SECRET_BLOB(plainText)
|
|
self.__LSAKey = record['Secret'][52:][:32]
|
|
|
|
else:
|
|
md5 = hashlib.new('md5')
|
|
md5.update(self.__bootKey)
|
|
for i in range(1000):
|
|
md5.update(value[60:76])
|
|
tmpKey = md5.digest()
|
|
rc4 = ARC4.new(tmpKey)
|
|
plainText = rc4.decrypt(value[12:60])
|
|
self.__LSAKey = plainText[0x10:0x20]
|
|
|
|
def __getLSASecretKey(self):
|
|
LOG.debug('Decrypting LSA Key')
|
|
# Let's try the key post XP
|
|
value = self.__remoteOps.retrieveSubKey(r'SECURITY\Policy\PolEKList', '', throttle=self.__throttle)
|
|
if value is None:
|
|
LOG.debug('PolEKList not found, trying PolSecretEncryptionKey')
|
|
# Second chance
|
|
value = self.__remoteOps.retrieveSubKey(r'SECURITY\Policy\PolSecretEncryptionKey', '', throttle=self.__throttle)
|
|
self.__vistaStyle = False
|
|
if value is None:
|
|
# No way :(
|
|
return None
|
|
|
|
self.__decryptLSA(value[1])
|
|
|
|
def __getNLKMSecret(self):
|
|
LOG.debug('Decrypting NL$KM')
|
|
value = self.__remoteOps.retrieveSubKey(r'SECURITY\Policy\Secrets\NL$KM\CurrVal', '', throttle=self.__throttle)
|
|
if value is None:
|
|
raise Exception("Couldn't get NL$KM value")
|
|
if self.__vistaStyle is True:
|
|
record = LSA_SECRET(value[1])
|
|
tmpKey = self.__sha256(self.__LSAKey, record['EncryptedData'][:32])
|
|
self.__NKLMKey = self.__cryptoCommon.decryptAES(tmpKey, record['EncryptedData'][32:])
|
|
else:
|
|
self.__NKLMKey = self.__decryptSecret(self.__LSAKey, value[1])
|
|
|
|
def __pad(self, data):
|
|
if (data & 0x3) > 0:
|
|
return data + (data & 0x3)
|
|
else:
|
|
return data
|
|
|
|
def dumpCachedHashes(self):
|
|
|
|
LOG.info('Dumping cached domain logon information (domain/username:hash)')
|
|
|
|
# Let's first see if there are cached entries
|
|
values = self.__remoteOps.enumValues(r'SECURITY\Cache', throttle=self.__throttle)
|
|
if values is None:
|
|
# No cache entries
|
|
return
|
|
try:
|
|
# Remove unnecessary value
|
|
del values['NL$Control']
|
|
except:
|
|
pass
|
|
|
|
iterationCount = 10240
|
|
|
|
if values.get('NL$IterationCount', None):
|
|
record = values.get('NL$IterationCount')
|
|
del values['NL$IterationCount']
|
|
|
|
if record > 10240:
|
|
iterationCount = record & 0xfffffc00
|
|
else:
|
|
iterationCount = record * 1024
|
|
|
|
self.__getLSASecretKey()
|
|
self.__getNLKMSecret()
|
|
LOG.debug(f'LsaSecretKey: 0x{hexlify(self.__LSAKey).decode()}')
|
|
LOG.debug(f'NKLM Secret: 0x{hexlify(self.__NKLMKey).decode()}')
|
|
|
|
for key, value in values.items():
|
|
LOG.debug(f'Looking into {key}')
|
|
record = NL_RECORD(value)
|
|
if record['IV'] != 16 * b'\x00':
|
|
#if record['UserLength'] > 0:
|
|
if record['Flags'] & 1 == 1:
|
|
# Encrypted
|
|
if self.__vistaStyle is True:
|
|
plainText = self.__cryptoCommon.decryptAES(self.__NKLMKey[16:32], record['EncryptedData'], record['IV'])
|
|
else:
|
|
plainText = self.__decryptHash(self.__NKLMKey, record['EncryptedData'], record['IV'])
|
|
pass
|
|
else:
|
|
# Plain! Until we figure out what this is, we skip it
|
|
#plainText = record['EncryptedData']
|
|
continue
|
|
encHash = plainText[:0x10]
|
|
plainText = plainText[0x48:]
|
|
userName = plainText[:record['UserLength']].decode('utf-16le')
|
|
plainText = plainText[self.__pad(record['UserLength']) + self.__pad(record['DomainNameLength']):]
|
|
domainLong = plainText[:self.__pad(record['DnsDomainNameLength'])].decode('utf-16le')
|
|
timestamp = datetime.utcfromtimestamp(getUnixTime(record['LastWrite']))
|
|
|
|
if self.__vistaStyle is True:
|
|
answer = "%s/%s:$DCC2$%s#%s#%s: (%s)" % (domainLong, userName, iterationCount, userName, hexlify(encHash).decode('utf-8'), timestamp)
|
|
else:
|
|
answer = "%s/%s:%s:%s: (%s)" % (domainLong, userName, hexlify(encHash).decode('utf-8'), userName, timestamp)
|
|
|
|
self.__cachedItems.append(answer)
|
|
self.__perSecretCallback(LSASecrets.SECRET_TYPE.LSA_HASHED, answer)
|
|
|
|
def __printSecret(self, name, secretItem):
|
|
# Based on [MS-LSAD] section 3.1.1.4
|
|
|
|
# First off, let's discard NULL secrets.
|
|
if len(secretItem) == 0:
|
|
LOG.debug('Discarding secret %s, NULL Data' % name)
|
|
return
|
|
|
|
# We might have secrets with zero
|
|
if secretItem.startswith(b'\x00\x00'):
|
|
LOG.debug('Discarding secret %s, all zeros' % name)
|
|
return
|
|
|
|
upperName = name.upper()
|
|
|
|
LOG.info('%s ' % name)
|
|
|
|
secret = ''
|
|
|
|
if upperName.startswith('_SC_'):
|
|
# Service name, a password might be there
|
|
# Let's first try to decode the secret
|
|
try:
|
|
strDecoded = secretItem.decode('utf-16le')
|
|
except:
|
|
pass
|
|
else:
|
|
# We have to get the account the service
|
|
# runs under
|
|
if hasattr(self.__remoteOps, 'getServiceAccount'):
|
|
account = self.__remoteOps.getServiceAccount(name[4:])
|
|
if account is None:
|
|
secret = self.UNKNOWN_USER + ':'
|
|
else:
|
|
secret = "%s:" % account
|
|
else:
|
|
# We don't support getting this info for local targets at the moment
|
|
secret = self.UNKNOWN_USER + ':'
|
|
secret += strDecoded
|
|
|
|
elif upperName.startswith('DEFAULTPASSWORD'):
|
|
# defaults password for winlogon
|
|
# Let's first try to decode the secret
|
|
try:
|
|
strDecoded = secretItem.decode('utf-16le')
|
|
except:
|
|
pass
|
|
else:
|
|
# We have to get the account this password is for
|
|
if hasattr(self.__remoteOps, 'getDefaultLoginAccount'):
|
|
account = self.__remoteOps.getDefaultLoginAccount()
|
|
if account is None:
|
|
secret = self.UNKNOWN_USER + ':'
|
|
else:
|
|
secret = "%s:" % account
|
|
else:
|
|
# We don't support getting this info for local targets at the moment
|
|
secret = self.UNKNOWN_USER + ':'
|
|
secret += strDecoded
|
|
|
|
elif upperName.startswith('ASPNET_WP_PASSWORD'):
|
|
try:
|
|
strDecoded = secretItem.decode('utf-16le')
|
|
except:
|
|
pass
|
|
else:
|
|
secret = 'ASPNET: %s' % strDecoded
|
|
|
|
elif upperName.startswith('DPAPI_SYSTEM'):
|
|
# Decode the DPAPI Secrets
|
|
dpapi = DPAPI_SYSTEM(secretItem)
|
|
secret = "dpapi_machinekey:0x{0}\ndpapi_userkey:0x{1}".format( hexlify(dpapi['MachineKey']).decode('latin-1'),
|
|
hexlify(dpapi['UserKey']).decode('latin-1'))
|
|
elif upperName.startswith('$MACHINE.ACC'):
|
|
# compute MD4 of the secret.. yes.. that is the nthash? :-o
|
|
md4 = MD4.new()
|
|
md4.update(secretItem)
|
|
if hasattr(self.__remoteOps, 'getMachineNameAndDomain'):
|
|
machine, domain = self.__remoteOps.getMachineNameAndDomain()
|
|
printname = "%s\\%s$" % (domain, machine)
|
|
secret = "%s\\%s$:%s:%s:::" % (domain, machine, hexlify(ntlm.LMOWFv1('','')).decode('utf-8'),
|
|
hexlify(md4.digest()).decode('utf-8'))
|
|
else:
|
|
printname = "$MACHINE.ACC"
|
|
secret = "$MACHINE.ACC: %s:%s" % (hexlify(ntlm.LMOWFv1('','')).decode('utf-8'),
|
|
hexlify(md4.digest()).decode('utf-8'))
|
|
# Attempt to calculate and print Kerberos keys
|
|
if not self.__printMachineKerberos(secretItem, printname):
|
|
LOG.debug('Could not calculate machine account Kerberos keys, only printing plain password (hex encoded)')
|
|
# Always print plaintext anyway since this may be needed for some popular usecases
|
|
extrasecret = "%s:plain_password_hex:%s" % (printname, hexlify(secretItem).decode('utf-8'))
|
|
self.__secretItems.append(extrasecret)
|
|
self.__perSecretCallback(LSASecrets.SECRET_TYPE.LSA, extrasecret)
|
|
|
|
elif re.match(r'^L\$_SQSA_(S-[0-9]-[0-9]-([0-9])+-([0-9])+-([0-9])+-([0-9])+-([0-9])+)$', upperName) is not None:
|
|
# Decode stored security questions
|
|
sid = re.search(r'^L\$_SQSA_(S-[0-9]-[0-9]-([0-9])+-([0-9])+-([0-9])+-([0-9])+-([0-9])+)$', upperName).group(1)
|
|
try:
|
|
strDecoded = secretItem.decode('utf-16le')
|
|
strDecoded = json.loads(strDecoded)
|
|
except Exception as e:
|
|
pass
|
|
else:
|
|
output = []
|
|
if strDecoded['version'] == 1:
|
|
if len(strDecoded['questions']) != 0:
|
|
output.append(" - Version : %d" % strDecoded['version'])
|
|
for qk in strDecoded['questions']:
|
|
output.append(" | Question: %s" % qk['question'])
|
|
output.append(" | |--> Answer: %s" % qk['answer'])
|
|
output = '\n'.join(output)
|
|
secret = 'Security questions for user %s: \n%s' % (sid, output)
|
|
else:
|
|
secret = 'Empty security questions for user %s.' % sid
|
|
else:
|
|
LOG.warning("Unknown SQSA version (%s), please open an issue with the following data so we can add a parser for it." % str(strDecoded['version']))
|
|
LOG.warning("Don't forget to remove sensitive content before sending the data in a Github issue.")
|
|
secret = json.dumps(strDecoded, indent=4)
|
|
|
|
elif re.match('^SCM:{([0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12})}', upperName) is not None:
|
|
# Decode stored service password
|
|
sid = re.search('^SCM:{([0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12})}', upperName).group(1)
|
|
try:
|
|
password = secretItem.decode('utf-16le').rstrip('\x00')
|
|
except:
|
|
pass
|
|
else:
|
|
secret = 'Password of service %s: %s' % (sid, password)
|
|
|
|
elif re.match(r'^L\$([0-9A-Z]{3})-PRV-([0-9A-F]{32})$', upperName) is not None:
|
|
# Decode stored OpenGPG private key
|
|
keyid = re.search(r'^L\$([0-9A-Z]{3})-PRV-([0-9A-F]{32})$', upperName).group(2)
|
|
try:
|
|
b64key = secretItem.decode('utf-16le')
|
|
except:
|
|
pass
|
|
else:
|
|
secret = 'OpenGPG private key %s: \n%s' % (keyid, b64key)
|
|
elif re.match(r'^L\$([0-9A-Z]{3})-PUB-([0-9A-F]{32})$', upperName) is not None:
|
|
# Decode stored OpenGPG public key
|
|
keyid = re.search(r'^L\$([0-9A-Z]{3})-PUB-([0-9A-F]{32})$', upperName).group(2)
|
|
try:
|
|
b64key = secretItem.decode('utf-16le')
|
|
except:
|
|
pass
|
|
else:
|
|
secret = 'OpenGPG public key %s: \n%s' % (keyid, b64key)
|
|
|
|
if secret != '':
|
|
printableSecret = secret
|
|
self.__secretItems.append(secret)
|
|
self.__perSecretCallback(LSASecrets.SECRET_TYPE.LSA, printableSecret)
|
|
else:
|
|
# Default print, hexdump
|
|
printableSecret = '%s:%s' % (name, hexlify(secretItem).decode('utf-8'))
|
|
self.__secretItems.append(printableSecret)
|
|
# If we're using the default callback (ourselves), we print the hex representation. If not, the
|
|
# user will need to decide what to do.
|
|
if self.__module__ == self.__perSecretCallback.__module__:
|
|
hexdump(secretItem)
|
|
self.__perSecretCallback(LSASecrets.SECRET_TYPE.LSA_RAW, printableSecret)
|
|
|
|
def __printMachineKerberos(self, rawsecret, machinename):
|
|
# Attempt to create Kerberos keys from machine account (if possible)
|
|
if hasattr(self.__remoteOps, 'getMachineKerberosSalt'):
|
|
salt = self.__remoteOps.getMachineKerberosSalt()
|
|
if salt == b'':
|
|
return False
|
|
else:
|
|
allciphers = [
|
|
int(constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value),
|
|
int(constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value),
|
|
int(constants.EncryptionTypes.des_cbc_md5.value)
|
|
]
|
|
# Ok, so the machine account password is in raw UTF-16, BUT can contain any amount
|
|
# of invalid unicode characters.
|
|
# This took me (Dirk-jan) way too long to figure out, but apparently Microsoft
|
|
# implicitly replaces those when converting utf-16 to utf-8.
|
|
# When we use the same method we get the valid password -> key mapping :)
|
|
rawsecret = rawsecret.decode('utf-16-le', 'replace').encode('utf-8', 'replace')
|
|
for etype in allciphers:
|
|
try:
|
|
key = string_to_key(etype, rawsecret, salt, None)
|
|
except Exception:
|
|
LOG.debug('Exception', exc_info=True)
|
|
raise
|
|
typename = NTDSHashes.KERBEROS_TYPE[etype]
|
|
secret = "%s:%s:%s" % (machinename, typename, hexlify(key.contents).decode('utf-8'))
|
|
self.__secretItems.append(secret)
|
|
self.__perSecretCallback(LSASecrets.SECRET_TYPE.LSA_KERBEROS, secret)
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
def dumpSecrets(self):
|
|
LOG.info('Dumping LSA Secrets')
|
|
|
|
keys = self.__remoteOps.enumSubKey(r'SECURITY\Policy\Secrets', throttle=self.__throttle)
|
|
|
|
if keys is None:
|
|
# No entries
|
|
return
|
|
try:
|
|
# This key has no use for LSASecrets and is only used for MSCache decryption
|
|
keys.remove('NL$KM')
|
|
except:
|
|
pass
|
|
|
|
try:
|
|
# Remove unnecessary value
|
|
keys.remove('NL$Control')
|
|
except:
|
|
pass
|
|
|
|
if self.__LSAKey == b'':
|
|
self.__getLSASecretKey()
|
|
|
|
for key in keys:
|
|
LOG.debug(f'Looking into {key}')
|
|
valueTypeList = ['CurrVal']
|
|
# Check if old LSA secrets values are also need to be shown
|
|
if self.__history:
|
|
valueTypeList.append('OldVal')
|
|
|
|
for valueType in valueTypeList:
|
|
try:
|
|
value = self.__remoteOps.retrieveSubKey(f'SECURITY\\Policy\\Secrets\\{key}\\{valueType}', '', throttle=self.__throttle)
|
|
except Exception as e:
|
|
if logging.getLogger().level == logging.DEBUG:
|
|
import traceback
|
|
traceback.print_exc()
|
|
print(str(e))
|
|
continue
|
|
if value is not None and value[1] != 0:
|
|
if self.__vistaStyle is True:
|
|
record = LSA_SECRET(value[1])
|
|
tmpKey = self.__sha256(self.__LSAKey, record['EncryptedData'][:32])
|
|
plainText = self.__cryptoCommon.decryptAES(tmpKey, record['EncryptedData'][32:])
|
|
record = LSA_SECRET_BLOB(plainText)
|
|
secret = record['Secret']
|
|
else:
|
|
secret = self.__decryptSecret(self.__LSAKey, value[1])
|
|
|
|
# If this is an OldVal secret, let's append '_history' to be able to distinguish it and
|
|
# also be consistent with NTDS history
|
|
if valueType == 'OldVal':
|
|
key += '_history'
|
|
self.__printSecret(key, secret)
|
|
|
|
def getSecret(self, key):
|
|
LOG.info(f'Dumping LSA secret: {key}')
|
|
|
|
if self.__LSAKey == b'':
|
|
self.__getLSASecretKey()
|
|
|
|
valueTypeList = ['CurrVal']
|
|
# Check if old LSA secrets values are also need to be shown
|
|
if self.__history:
|
|
valueTypeList.append('OldVal')
|
|
|
|
for valueType in valueTypeList:
|
|
try:
|
|
value = self.__remoteOps.retrieveSubKey(f'SECURITY\\Policy\\Secrets\\{key}\\{valueType}', '', throttle=self.__throttle)
|
|
except Exception as e:
|
|
if logging.getLogger().level == logging.DEBUG:
|
|
import traceback
|
|
traceback.print_exc()
|
|
print(str(e))
|
|
continue
|
|
if value is not None and value[1] != 0:
|
|
if self.__vistaStyle is True:
|
|
record = LSA_SECRET(value[1])
|
|
tmpKey = self.__sha256(self.__LSAKey, record['EncryptedData'][:32])
|
|
plainText = self.__cryptoCommon.decryptAES(tmpKey, record['EncryptedData'][32:])
|
|
record = LSA_SECRET_BLOB(plainText)
|
|
secret = record['Secret']
|
|
else:
|
|
secret = self.__decryptSecret(self.__LSAKey, value[1])
|
|
|
|
# If this is an OldVal secret, let's append '_history' to be able to distinguish it and
|
|
# also be consistent with NTDS history
|
|
if valueType == 'OldVal':
|
|
key += '_history'
|
|
return secret
|
|
|
|
def exportSecrets(self, baseFileName, openFileFunc = None):
|
|
if len(self.__secretItems) > 0:
|
|
fileName = baseFileName + '.secrets'
|
|
fd = openFile(fileName, openFileFunc=openFileFunc)
|
|
for item in self.__secretItems:
|
|
fd.write(item + '\n')
|
|
fd.close()
|
|
return fileName
|
|
|
|
def exportCached(self, baseFileName, openFileFunc = None):
|
|
if len(self.__cachedItems) > 0:
|
|
fileName = baseFileName + '.cached'
|
|
fd = openFile(fileName, openFileFunc=openFileFunc)
|
|
for item in self.__cachedItems:
|
|
fd.write(item + '\n')
|
|
fd.close()
|
|
return fileName
|