From 2d4e603d3e860839c9d5f594ea57a08f47f8400c Mon Sep 17 00:00:00 2001 From: laxa Date: Fri, 15 Mar 2024 15:22:24 +0100 Subject: [PATCH 1/2] Adding support for AzureAD accounts --- diana-msaccountdec.py | 47 +++++++++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/diana-msaccountdec.py b/diana-msaccountdec.py index 211d55e..ae18b65 100755 --- a/diana-msaccountdec.py +++ b/diana-msaccountdec.py @@ -21,14 +21,14 @@ # limitations under the License. """ Windows Microsoft Account DPAPI key decryption utility.""" -import hashlib, optparse, os, sys +import hashlib, optparse, os, sys, hmac from Crypto.Cipher import AES ## pip3 install pycryptodome def checkParams(options, args): if not options.cachedatafile or not options.password: sys.exit('You must provide cleartext password and cachedata file.') if not os.path.isfile(options.cachedatafile): - sys.exit('File not found: {}.'.format(options.cachedatafile)) + sys.exit(f'File not found: {options.cachedatafile}.') return def reverseByte(bByteInput): @@ -84,9 +84,9 @@ def parseDecryptedCache(bClearData, boolVerbose = True): sAccount = bAccount.decode('UTF-16LE') else: return sPassword print('[+] Decoded:') - print(' StableUserID : {} (There should be a SQLite archive at %localappdata%\\ConnectedDevicesPlatform\\{}\\)'.format(sStableUserId, sStableUserId)) - print(' User Account : {}'.format(sAccount)) - print(' XML Cipher : {}'.format(sXML)) + print(f' StableUserID : {sStableUserId} (There should be a SQLite archive at %localappdata%\\ConnectedDevicesPlatform\\{sStableUserId}\\)') + print(f' User Account : {sAccount}') + print(f' XML Cipher : {sXML}') print('') return sPassword @@ -103,7 +103,7 @@ def walkThroughFile(bCacheDataOrg, oCipher): if not b'\x00\x40\x00' in bClearData: continue sPassword = parseDecryptedCache(bClearData, True) if sPassword: - print('[+] Success, use this as your DPAPI cleartext user password:\n {}'.format(sPassword)) + print(f'[+] Success, use this as your DPAPI cleartext user password:\n {sPassword}') break i+=iLength #except Exception as e: print(e) @@ -122,6 +122,7 @@ def walkThroughFile(bCacheDataOrg, oCipher): parser.add_option('--cachedatafile', '-f', metavar='FILE', help=r'CloudAPCache CacheData', default=os.path.join('Windows','System32','config','systemprofile','AppData','Local','Microsoft','Windows','CloudAPCache','MicrosoftAccount','CacheData')) parser.add_option('--password', '-p', metavar='STRING', help=r'Clear Text User Password') parser.add_option('--export', '-e', action="store_true", default=False, metavar='BOOL', help=r'Export a (crackable) Hash, TODO: write Hashcat module') + parser.add_option('--sid', metavar='SID', dest='sid', help=r'SID required for AzureAD accounts') (options, args) = parser.parse_args() @@ -133,24 +134,40 @@ def walkThroughFile(bCacheDataOrg, oCipher): bCacheDataOrg = file.read() file.close() - if not bCacheDataOrg[72:72+4] == b'\x02\x00\x00\x00': + if not bCacheDataOrg[72:72 + 4] == b'\x02\x00\x00\x00': exit('[-] Error: Not a valid Microsoft Live Account CacheData file?') - oCipher = AES.new(bDecryptionKey, AES.MODE_CBC, b'\x00'*16) + oCipher = AES.new(bDecryptionKey, AES.MODE_CBC, b'\x00' * 16) ## First 124 are "static length", from then on length + data bCacheData = bCacheDataOrg[124:] sPassword = None + key = None lstCandidates = getEncryptedData(bCacheData) sToExport = '' for bEncrData in lstCandidates: - if options.export: sToExport += bEncrData[-64:].hex()+"\n" + if options.export: sToExport += bEncrData[-64:].hex() + "\n" bClearData = oCipher.decrypt(bEncrData) ## Since we know the MS Live Emailaddress is in the decoded data, there should be a unicode '@' - if not b'\x00\x40\x00' in bClearData: continue - sPassword = parseDecryptedCache(bClearData, True) - if sPassword: print('[+] Success, use this as the DPAPI cleartext user password:\n {}'.format(sPassword)) - if not sPassword: print('[-] Wrong password?') - if options.export and not sToExport == '': print('\n[+] This should be crackable with PBKDF2-SHA256+AES256 and decrypted should contain bytes "004000":\n{}'.format(sToExport)) + if not b'\x00\x40\x00' in bClearData: + continue + # AzureAD + if b'Version' in bClearData: + if options.sid is None: + sys.exit('[-] AzureAD accoun detected, --sid must be specified') + key = hashlib.sha1(bClearData[0x30:0x70]).digest() + sid = (options.sid + '\0').encode('UTF-16-LE') + key = hmac.new(key, sid, hashlib.sha1).hexdigest() + # Microsoft Account + else: + sPassword = parseDecryptedCache(bClearData, True) + if sPassword: + print(f'[+] Success, use this as the DPAPI cleartext user password:\n {sPassword}') + if key: + print(f'[+] Success, use this key to decrypt masterkeys of the user: 0x{key}') + if not sPassword and not key: + print('[-] Wrong password?') + if options.export and not sToExport == '': + print(f'\n[+] This should be crackable with PBKDF2-SHA256+AES256 and decrypted should contain bytes "004000":\n{sToExport}') - #walkThroughFile(bCacheDataOrg, oCipher) \ No newline at end of file + #walkThroughFile(bCacheDataOrg, oCipher) From 6633b136a5f416f89d55b24dd5bd9a45115522a6 Mon Sep 17 00:00:00 2001 From: laxa Date: Wed, 5 Jun 2024 14:44:41 +0200 Subject: [PATCH 2/2] Improve parsing of CacheData --- diana-msaccountdec.py | 169 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 139 insertions(+), 30 deletions(-) diff --git a/diana-msaccountdec.py b/diana-msaccountdec.py index ae18b65..0927457 100755 --- a/diana-msaccountdec.py +++ b/diana-msaccountdec.py @@ -21,8 +21,71 @@ # limitations under the License. """ Windows Microsoft Account DPAPI key decryption utility.""" -import hashlib, optparse, os, sys, hmac +import hashlib, optparse, os, sys, hmac, struct, binascii, json from Crypto.Cipher import AES ## pip3 install pycryptodome +import dpapick3.eater as eater +from enum import IntEnum +from typing import List + +class DPAPICredKeyBlob(eater.DataStruct): + def __init__(self, raw): + eater.DataStruct.__init__(self, raw) + + def parse(self, data): + self.dwBlobSize = data.eat("L") + self.dwField4 = data.eat("L") + self.dwCredKeyOffset = data.eat("L") + self.dwCredKeySize = data.eat("L") + self.Guid = "%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x" % data.eat("L2H8B") + assert data.ofs == self.dwCredKeyOffset + self.CredKey = data.eat_string(self.dwCredKeySize) + +class CacheNodeType(IntEnum): + PASSWORD = 1 + UNKNOW_TWO = 2 + UNKNOW_THREE = 3 + UNKNOW_FOUR = 4 + PIN = 5 + +class CacheDataNodeHeader(eater.DataStruct): + def __init__(self, raw): + eater.DataStruct.__init__(self, raw) + + def parse(self, data): + self.dwNodeType = data.eat("L") + self.dwCryptoBlobSize = data.eat("L") + self.dwField8 = data.eat("L") + self.dwEncryptedPRTSize = data.eat("L") + self.dwField10 = data.eat("L") + + +class CacheDataNode: + def __init__(self, header : CacheDataNodeHeader): + self._header : CacheDataNodeHeader = header + self._cryptoBlob : bytes = None + self._encryptedPrtBlob : bytes = None + + @property + def cryptoBlob(self): + return self._cryptoBlob + + @cryptoBlob.setter + def cryptoBlob(self, value): + self._cryptoBlob = value + + @property + def encryptedPRTBlob(self): + return self._encryptedPrtBlob + + @encryptedPRTBlob.setter + def encryptedPRTBlob(self, value): + self._encryptedPrtBlob = value + + def is_node_type_password(self) -> bool: + return self._header.dwNodeType == CacheNodeType.PASSWORD + + def is_node_type_pin(self) -> bool: + return self._header.dwNodeType == CacheNodeType.PIN def checkParams(options, args): if not options.cachedatafile or not options.password: @@ -37,19 +100,61 @@ def reverseByte(bByteInput): for x in range(-1, -len(str(sHexInput)), -2): sReversed += sHexInput[x-1] + sHexInput[x] return bytes.fromhex(sReversed) -def getEncryptedData(bLeftoverData): - ## The data is divided into DataLength + Data, the DPAPI data can be the first or even the last - lstReturnData = [] - while len(bLeftoverData) > 0: - iLength = int(reverseByte(bLeftoverData[:4]).hex(), 16) - bData = bLeftoverData[4:4+iLength] - if b'RSA1' in bData: - ## The RSA1 public key does not follow the Length+Data system, just skip over (assuming 256 byte RSA1 modulus) - bLeftoverData = bLeftoverData[4+0x3D8:] +def parse_cache_data(file_path) -> List[CacheDataNode]: + cache_data_node_list = list() + print(f'[+] Parsing CacheData file {file_path}') + with open(file_path, "rb") as f: + file_size = f.seek(0, os.SEEK_END) + f.seek(0, os.SEEK_SET) + # First 4 byte is a version number + (version,) = struct.unpack(" 48: lstReturnData.append(bData) - bLeftoverData = bLeftoverData[4+iLength:] - return lstReturnData + print(f'[+]\tFound blob of size 0x{blob_size:x} (offset = 0x{offset:x}/0x{len(raw_payload):x})') + blob = raw_payload[offset:offset+blob_size] + offset += blob_size + if offset % 4 != 0: + offset += (4 - (offset % 4)) + index_cache_data_node_list = i // 2 + # For each cache node, there is one cryptoBlob and one encryptedPRTBlob + if i % 2 == 0: + cache_data_node_list[index_cache_data_node_list].cryptoBlob = blob + else: + cache_data_node_list[index_cache_data_node_list].encryptedPRTBlob = blob + i += 1 + + return cache_data_node_list def parseDecryptedCache(bClearData, boolVerbose = True): sPassword = None @@ -113,16 +218,14 @@ def walkThroughFile(bCacheDataOrg, oCipher): if __name__ == '__main__': usage = ( 'usage: %prog [options]\n\n' - 'It tries to unlock (decrypt) the Microsoft Account DPAPI password.\n' + 'It tries to unlock (decrypt) the Microsoft Account DPAPI password or Azure AD one.\n' 'NOTE: only works when the *cleartext* password is known\n' - 'NOTE: currently does not support the AzureAD Cache\n' - r' Default Location: Windows\System32\config\systemprofile\AppData\Local\Microsoft\Windows\CloudAPCache\MicrosoftAccount\\Cache\CacheData') + r' Default Location: Windows\System32\config\systemprofile\AppData\Local\Microsoft\Windows\CloudAPCache\[MicrosoftAccount|AzureAD]\\Cache\CacheData') parser = optparse.OptionParser(usage=usage) parser.add_option('--cachedatafile', '-f', metavar='FILE', help=r'CloudAPCache CacheData', default=os.path.join('Windows','System32','config','systemprofile','AppData','Local','Microsoft','Windows','CloudAPCache','MicrosoftAccount','CacheData')) parser.add_option('--password', '-p', metavar='STRING', help=r'Clear Text User Password') parser.add_option('--export', '-e', action="store_true", default=False, metavar='BOOL', help=r'Export a (crackable) Hash, TODO: write Hashcat module') - parser.add_option('--sid', metavar='SID', dest='sid', help=r'SID required for AzureAD accounts') (options, args) = parser.parse_args() @@ -130,34 +233,40 @@ def walkThroughFile(bCacheDataOrg, oCipher): bDecryptionKey = hashlib.pbkdf2_hmac('sha256', options.password.encode('UTF-16LE'), b'', 10000) + candidates = parse_cache_data(options.cachedatafile) file = open(options.cachedatafile,'br') bCacheDataOrg = file.read() file.close() - if not bCacheDataOrg[72:72 + 4] == b'\x02\x00\x00\x00': - exit('[-] Error: Not a valid Microsoft Live Account CacheData file?') - oCipher = AES.new(bDecryptionKey, AES.MODE_CBC, b'\x00' * 16) ## First 124 are "static length", from then on length + data - bCacheData = bCacheDataOrg[124:] sPassword = None key = None - lstCandidates = getEncryptedData(bCacheData) sToExport = '' - for bEncrData in lstCandidates: - if options.export: sToExport += bEncrData[-64:].hex() + "\n" - bClearData = oCipher.decrypt(bEncrData) + for entry in candidates: + if not entry.is_node_type_password(): + continue + if options.export: sToExport += entry.encryptedPRTBlob[-64:].hex() + "\n" + bClearData = oCipher.decrypt(entry.encryptedPRTBlob) ## Since we know the MS Live Emailaddress is in the decoded data, there should be a unicode '@' if not b'\x00\x40\x00' in bClearData: continue # AzureAD if b'Version' in bClearData: - if options.sid is None: - sys.exit('[-] AzureAD accoun detected, --sid must be specified') - key = hashlib.sha1(bClearData[0x30:0x70]).digest() - sid = (options.sid + '\0').encode('UTF-16-LE') - key = hmac.new(key, sid, hashlib.sha1).hexdigest() + version, flags, dword3, raw_dpapi_cred_key_size = struct.unpack("