diff --git a/monkey/infection_monkey/i_puppet.py b/monkey/infection_monkey/i_puppet.py index 11da6a260..e25d20f53 100644 --- a/monkey/infection_monkey/i_puppet.py +++ b/monkey/infection_monkey/i_puppet.py @@ -16,7 +16,9 @@ class UnknownPluginError(Exception): pass -ExploiterResultData = namedtuple("ExploiterResultData", ["result", "info", "attempts"]) +ExploiterResultData = namedtuple( + "ExploiterResultData", ["success", "info", "attempts", "error_message"] +) PingScanData = namedtuple("PingScanData", ["response_received", "os"]) PortScanData = namedtuple("PortScanData", ["port", "status", "banner", "service"]) FingerprintData = namedtuple("FingerprintData", ["os_type", "os_version", "services"]) diff --git a/monkey/infection_monkey/master/__init__.py b/monkey/infection_monkey/master/__init__.py index fda536194..98ed6db0b 100644 --- a/monkey/infection_monkey/master/__init__.py +++ b/monkey/infection_monkey/master/__init__.py @@ -1,4 +1,5 @@ from .ip_scan_results import IPScanResults from .ip_scanner import IPScanner +from .exploiter import Exploiter from .propagator import Propagator from .automated_master import AutomatedMaster diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index 784046323..ff6af8b43 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -6,18 +6,20 @@ from typing import Any, Callable, Dict, List, Tuple from infection_monkey.i_control_channel import IControlChannel from infection_monkey.i_master import IMaster from infection_monkey.i_puppet import IPuppet +from infection_monkey.model import VictimHostFactory from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger from infection_monkey.telemetry.post_breach_telem import PostBreachTelem from infection_monkey.telemetry.system_info_telem import SystemInfoTelem from infection_monkey.utils.timer import Timer -from . import IPScanner, Propagator +from . import Exploiter, IPScanner, Propagator from .threading_utils import create_daemon_thread CHECK_ISLAND_FOR_STOP_COMMAND_INTERVAL_SEC = 5 CHECK_FOR_TERMINATE_INTERVAL_SEC = CHECK_ISLAND_FOR_STOP_COMMAND_INTERVAL_SEC / 5 SHUTDOWN_TIMEOUT = 5 NUM_SCAN_THREADS = 16 # TODO: Adjust this to the optimal number of scan threads +NUM_EXPLOIT_THREADS = 4 # TODO: Adjust this to the optimal number of exploit threads logger = logging.getLogger() @@ -27,6 +29,7 @@ class AutomatedMaster(IMaster): self, puppet: IPuppet, telemetry_messenger: ITelemetryMessenger, + victim_host_factory: VictimHostFactory, control_channel: IControlChannel, ): self._puppet = puppet @@ -34,7 +37,10 @@ class AutomatedMaster(IMaster): self._control_channel = control_channel ip_scanner = IPScanner(self._puppet, NUM_SCAN_THREADS) - self._propagator = Propagator(self._telemetry_messenger, ip_scanner) + exploiter = Exploiter(self._puppet, NUM_EXPLOIT_THREADS) + self._propagator = Propagator( + self._telemetry_messenger, ip_scanner, exploiter, victim_host_factory + ) self._stop = threading.Event() self._master_thread = create_daemon_thread(target=self._run_master_thread) diff --git a/monkey/infection_monkey/master/exploiter.py b/monkey/infection_monkey/master/exploiter.py new file mode 100644 index 000000000..f1a804ba7 --- /dev/null +++ b/monkey/infection_monkey/master/exploiter.py @@ -0,0 +1,97 @@ +import logging +import queue +import threading +from queue import Queue +from threading import Event +from typing import Callable, Dict, List + +from infection_monkey.i_puppet import ExploiterResultData, IPuppet +from infection_monkey.model import VictimHost + +from .threading_utils import run_worker_threads + +QUEUE_TIMEOUT = 2 + +logger = logging.getLogger() + +ExploiterName = str +Callback = Callable[[ExploiterName, VictimHost, ExploiterResultData], None] + + +class Exploiter: + def __init__(self, puppet: IPuppet, num_workers: int): + self._puppet = puppet + self._num_workers = num_workers + + def exploit_hosts( + self, + exploiter_config: Dict, + hosts_to_exploit: Queue, + results_callback: Callback, + scan_completed: Event, + stop: Event, + ): + # Run vulnerability exploiters before brute force exploiters to minimize the effect of + # account lockout due to invalid credentials + exploiters_to_run = exploiter_config["vulnerability"] + exploiter_config["brute_force"] + logger.debug( + "Agent is configured to run the following exploiters in order: " + f"{','.join([e['name'] for e in exploiters_to_run])}" + ) + + exploit_args = (exploiters_to_run, hosts_to_exploit, results_callback, scan_completed, stop) + run_worker_threads( + target=self._exploit_hosts_on_queue, args=exploit_args, num_workers=self._num_workers + ) + + def _exploit_hosts_on_queue( + self, + exploiters_to_run: List[Dict], + hosts_to_exploit: Queue, + results_callback: Callback, + scan_completed: Event, + stop: Event, + ): + logger.debug(f"Starting exploiter thread -- Thread ID: {threading.get_ident()}") + + while not stop.is_set(): + try: + victim_host = hosts_to_exploit.get(timeout=QUEUE_TIMEOUT) + self._run_all_exploiters(exploiters_to_run, victim_host, results_callback, stop) + except queue.Empty: + if _all_hosts_have_been_processed(scan_completed, hosts_to_exploit): + break + + logger.debug( + f"Exiting exploiter thread -- Thread ID: {threading.get_ident()} -- " + f"stop.is_set(): {stop.is_set()} -- network_scan_completed: " + f"{scan_completed.is_set()}" + ) + + def _run_all_exploiters( + self, + exploiters_to_run: List[Dict], + victim_host: VictimHost, + results_callback: Callback, + stop: Event, + ): + for exploiter in exploiters_to_run: + if stop.is_set(): + break + + exploiter_name = exploiter["name"] + exploiter_results = self._run_exploiter(exploiter_name, victim_host, stop) + results_callback(exploiter_name, victim_host, exploiter_results) + + if exploiter["propagator"] and exploiter_results.success: + break + + def _run_exploiter( + self, exploiter_name: str, victim_host: VictimHost, stop: Event + ) -> ExploiterResultData: + logger.debug(f"Attempting to use {exploiter_name} on {victim_host}") + return self._puppet.exploit_host(exploiter_name, victim_host.ip_addr, {}, stop) + + +def _all_hosts_have_been_processed(scan_completed: Event, hosts_to_exploit: Queue): + return scan_completed.is_set() and hosts_to_exploit.empty() diff --git a/monkey/infection_monkey/master/ip_scanner.py b/monkey/infection_monkey/master/ip_scanner.py index 450ff3006..9e5851e7b 100644 --- a/monkey/infection_monkey/master/ip_scanner.py +++ b/monkey/infection_monkey/master/ip_scanner.py @@ -14,7 +14,7 @@ from infection_monkey.i_puppet import ( ) from . import IPScanResults -from .threading_utils import create_daemon_thread +from .threading_utils import run_worker_threads logger = logging.getLogger() @@ -35,14 +35,7 @@ class IPScanner: ips.put(ip) scan_ips_args = (ips, options, results_callback, stop) - scan_threads = [] - for i in range(0, self._num_workers): - t = create_daemon_thread(target=self._scan_ips, args=scan_ips_args) - t.start() - scan_threads.append(t) - - for t in scan_threads: - t.join() + run_worker_threads(target=self._scan_ips, args=scan_ips_args, num_workers=self._num_workers) def _scan_ips(self, ips: Queue, options: Dict, results_callback: Callback, stop: Event): logger.debug(f"Starting scan thread -- Thread ID: {threading.get_ident()}") diff --git a/monkey/infection_monkey/master/propagator.py b/monkey/infection_monkey/master/propagator.py index 916297110..9d31b94b4 100644 --- a/monkey/infection_monkey/master/propagator.py +++ b/monkey/infection_monkey/master/propagator.py @@ -1,41 +1,60 @@ import logging from queue import Queue -from threading import Event, Thread +from threading import Event from typing import Dict -from infection_monkey.i_puppet import FingerprintData, PingScanData, PortScanData, PortStatus -from infection_monkey.model.host import VictimHost +from infection_monkey.i_puppet import ( + ExploiterResultData, + FingerprintData, + PingScanData, + PortScanData, + PortStatus, +) +from infection_monkey.model import VictimHost, VictimHostFactory +from infection_monkey.telemetry.exploit_telem import ExploitTelem from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger from infection_monkey.telemetry.scan_telem import ScanTelem -from . import IPScanner, IPScanResults +from . import Exploiter, IPScanner, IPScanResults from .threading_utils import create_daemon_thread logger = logging.getLogger() class Propagator: - def __init__(self, telemetry_messenger: ITelemetryMessenger, ip_scanner: IPScanner): + def __init__( + self, + telemetry_messenger: ITelemetryMessenger, + ip_scanner: IPScanner, + exploiter: Exploiter, + victim_host_factory: VictimHostFactory, + ): self._telemetry_messenger = telemetry_messenger self._ip_scanner = ip_scanner + self._exploiter = exploiter + self._victim_host_factory = victim_host_factory self._hosts_to_exploit = None def propagate(self, propagation_config: Dict, stop: Event): logger.info("Attempting to propagate") + network_scan_completed = Event() self._hosts_to_exploit = Queue() scan_thread = create_daemon_thread( target=self._scan_network, args=(propagation_config, stop) ) exploit_thread = create_daemon_thread( - target=self._exploit_targets, args=(scan_thread, stop) + target=self._exploit_hosts, + args=(propagation_config, network_scan_completed, stop), ) scan_thread.start() exploit_thread.start() scan_thread.join() + network_scan_completed.set() + exploit_thread.join() logger.info("Finished attempting to propagate") @@ -52,7 +71,7 @@ class Propagator: logger.info("Finished network scan") def _process_scan_results(self, ip: str, scan_results: IPScanResults): - victim_host = VictimHost(ip) + victim_host = self._victim_host_factory.build_victim_host(ip) Propagator._process_ping_scan_results(victim_host, scan_results.ping_scan_data) Propagator._process_tcp_scan_results(victim_host, scan_results.port_scan_data) @@ -95,5 +114,35 @@ class Propagator: for service, details in fd.services.items(): victim_host.services.setdefault(service, {}).update(details) - def _exploit_targets(self, scan_thread: Thread, stop: Event): - pass + def _exploit_hosts( + self, + propagation_config: Dict, + network_scan_completed: Event, + stop: Event, + ): + logger.info("Exploiting victims") + + exploiter_config = propagation_config["exploiters"] + self._exploiter.exploit_hosts( + exploiter_config, + self._hosts_to_exploit, + self._process_exploit_attempts, + network_scan_completed, + stop, + ) + + logger.info("Finished exploiting victims") + + def _process_exploit_attempts( + self, exploiter_name: str, host: VictimHost, result: ExploiterResultData + ): + if result.success: + logger.info(f"Successfully propagated to {host} using {exploiter_name}") + else: + logger.info( + f"Failed to propagate to {host} using {exploiter_name}: {result.error_message}" + ) + + self._telemetry_messenger.send_telemetry( + ExploitTelem(exploiter_name, host, result.success, result.info, result.attempts) + ) diff --git a/monkey/infection_monkey/master/threading_utils.py b/monkey/infection_monkey/master/threading_utils.py index 56cf4a459..dbcc67984 100644 --- a/monkey/infection_monkey/master/threading_utils.py +++ b/monkey/infection_monkey/master/threading_utils.py @@ -2,5 +2,16 @@ from threading import Thread from typing import Callable, Tuple +def run_worker_threads(target: Callable[..., None], args: Tuple = (), num_workers: int = 2): + worker_threads = [] + for i in range(0, num_workers): + t = create_daemon_thread(target=target, args=args) + t.start() + worker_threads.append(t) + + for t in worker_threads: + t.join() + + def create_daemon_thread(target: Callable[..., None], args: Tuple = ()): return Thread(target=target, args=args, daemon=True) diff --git a/monkey/infection_monkey/model/__init__.py b/monkey/infection_monkey/model/__init__.py index 7c39075be..caf9b6251 100644 --- a/monkey/infection_monkey/model/__init__.py +++ b/monkey/infection_monkey/model/__init__.py @@ -1,4 +1,5 @@ from infection_monkey.model.host import VictimHost +from infection_monkey.model.victim_host_factory import VictimHostFactory MONKEY_ARG = "m0nk3y" DROPPER_ARG = "dr0pp3r" diff --git a/monkey/infection_monkey/model/victim_host_factory.py b/monkey/infection_monkey/model/victim_host_factory.py new file mode 100644 index 000000000..e3ac8d5a7 --- /dev/null +++ b/monkey/infection_monkey/model/victim_host_factory.py @@ -0,0 +1,28 @@ +from infection_monkey.model import VictimHost + + +class VictimHostFactory: + def __init__(self): + pass + + def build_victim_host(self, ip: str): + victim_host = VictimHost(ip) + + # TODO: Reimplement the below logic from the old monkey.py + """ + if self._monkey_tunnel: + self._monkey_tunnel.set_tunnel_for_host(machine) + if self._default_server: + if self._network.on_island(self._default_server): + machine.set_default_server( + get_interface_to_target(machine.ip_addr) + + (":" + self._default_server_port if self._default_server_port else "") + ) + else: + machine.set_default_server(self._default_server) + logger.debug( + f"Default server for machine: {machine} set to {machine.default_server}" + ) + """ + + return victim_host diff --git a/monkey/infection_monkey/puppet/mock_puppet.py b/monkey/infection_monkey/puppet/mock_puppet.py index 5f0389752..204e44ab4 100644 --- a/monkey/infection_monkey/puppet/mock_puppet.py +++ b/monkey/infection_monkey/puppet/mock_puppet.py @@ -280,11 +280,24 @@ class MockPuppet(IPuppet): "executed_cmds": [], } successful_exploiters = { - DOT_1: {"PowerShellExploiter": ExploiterResultData(True, info_powershell, attempts)}, - DOT_3: {"SSHExploiter": ExploiterResultData(False, info_ssh, attempts)}, + DOT_1: { + "PowerShellExploiter": ExploiterResultData(True, info_powershell, attempts, None), + "ZerologonExploiter": ExploiterResultData(False, {}, [], "Zerologon failed"), + "SSHExploiter": ExploiterResultData(False, info_ssh, attempts, "Failed exploiting"), + }, + DOT_3: { + "PowerShellExploiter": ExploiterResultData( + False, info_powershell, attempts, "PowerShell Exploiter Failed" + ), + "SSHExploiter": ExploiterResultData(False, info_ssh, attempts, "Failed exploiting"), + "ZerologonExploiter": ExploiterResultData(True, {}, [], None), + }, } - return successful_exploiters[host][name] + try: + return successful_exploiters[host][name] + except KeyError: + return ExploiterResultData(False, {}, [], f"{name} failed for host {host}") def run_payload( self, name: str, options: Dict, interrupt: threading.Event diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index 2e587444c..a0af1632c 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -475,6 +475,9 @@ class ConfigService: formatted_propagation_config["targets"] = ConfigService._format_targets_from_flat_config( config ) + formatted_propagation_config[ + "exploiters" + ] = ConfigService._format_exploiters_from_flat_config(config) config["propagation"] = formatted_propagation_config @@ -567,3 +570,33 @@ class ConfigService: config.pop(flat_subnet_scan_list_field, None) return formatted_scan_targets_config + + @staticmethod + def _format_exploiters_from_flat_config(config: Dict): + flat_config_exploiter_classes_field = "exploiter_classes" + brute_force_category = "brute_force" + vulnerability_category = "vulnerability" + brute_force_exploiters = { + "MSSQLExploiter", + "PowerShellExploiter", + "SSHExploiter", + "SmbExploiter", + "WmiExploiter", + } + + formatted_exploiters_config = {"brute_force": [], "vulnerability": []} + + for exploiter in sorted(config[flat_config_exploiter_classes_field]): + category = ( + brute_force_category + if exploiter in brute_force_exploiters + else vulnerability_category + ) + + formatted_exploiters_config[category].append( + {"name": exploiter, "propagator": (exploiter != "ZerologonExploiter")} + ) + + config.pop(flat_config_exploiter_classes_field, None) + + return formatted_exploiters_config diff --git a/monkey/tests/data_for_tests/monkey_configs/flat_config.json b/monkey/tests/data_for_tests/monkey_configs/flat_config.json index 977bed817..2840cbbb5 100644 --- a/monkey/tests/data_for_tests/monkey_configs/flat_config.json +++ b/monkey/tests/data_for_tests/monkey_configs/flat_config.json @@ -55,6 +55,7 @@ "ShellShockExploiter", "ElasticGroovyExploiter", "Struts2Exploiter", + "ZerologonExploiter", "WebLogicExploiter", "HadoopExploiter", "MSSQLExploiter", 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 1610e752b..0584ca1cd 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 @@ -2,7 +2,7 @@ from infection_monkey.master import AutomatedMaster def test_terminate_without_start(): - m = AutomatedMaster(None, None, None) + m = AutomatedMaster(None, None, None, None) # Test that call to terminate does not raise exception m.terminate() diff --git a/monkey/tests/unit_tests/infection_monkey/master/test_exploiter.py b/monkey/tests/unit_tests/infection_monkey/master/test_exploiter.py new file mode 100644 index 000000000..5b9297fe6 --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/master/test_exploiter.py @@ -0,0 +1,102 @@ +import logging +from queue import Queue +from threading import Barrier, Event +from unittest.mock import MagicMock + +import pytest + +from infection_monkey.master import Exploiter +from infection_monkey.model import VictimHost +from infection_monkey.puppet.mock_puppet import MockPuppet + +logger = logging.getLogger() + + +@pytest.fixture(autouse=True) +def patch_queue_timeout(monkeypatch): + monkeypatch.setattr("infection_monkey.master.exploiter.QUEUE_TIMEOUT", 0.001) + + +@pytest.fixture +def scan_completed(): + return Event() + + +@pytest.fixture +def stop(): + return Event() + + +@pytest.fixture +def callback(): + return MagicMock() + + +@pytest.fixture +def exploiter_config(): + return { + "brute_force": [ + {"name": "PowerShellExploiter", "propagator": True}, + {"name": "SSHExploiter", "propagator": True}, + ], + "vulnerability": [ + {"name": "ZerologonExploiter", "propagator": False}, + ], + } + + +@pytest.fixture +def hosts(): + return [VictimHost("10.0.0.1"), VictimHost("10.0.0.3")] + + +@pytest.fixture +def hosts_to_exploit(hosts): + q = Queue() + q.put(hosts[0]) + q.put(hosts[1]) + + return q + + +def test_exploiter(exploiter_config, callback, scan_completed, stop, hosts, hosts_to_exploit): + # Set this so that Exploiter() exits once it has processed all victims + scan_completed.set() + + e = Exploiter(MockPuppet(), 2) + e.exploit_hosts(exploiter_config, hosts_to_exploit, callback, scan_completed, stop) + + assert callback.call_count == 5 + host_exploit_combos = set() + + for i in range(0, 5): + victim_host = callback.call_args_list[i][0][0] + exploiter_name = callback.call_args_list[i][0][1] + host_exploit_combos.add((victim_host, exploiter_name)) + + assert ("ZerologonExploiter", hosts[0]) in host_exploit_combos + assert ("PowerShellExploiter", hosts[0]) in host_exploit_combos + assert ("ZerologonExploiter", hosts[1]) in host_exploit_combos + assert ("PowerShellExploiter", hosts[1]) in host_exploit_combos + assert ("SSHExploiter", hosts[1]) in host_exploit_combos + + +def test_stop_after_callback(exploiter_config, callback, scan_completed, stop, hosts_to_exploit): + callback_barrier_count = 2 + + def _callback(*_): + # Block all threads here until 2 threads reach this barrier, then set stop + # and test that neither thread continues to scan. + _callback.barrier.wait() + stop.set() + + _callback.barrier = Barrier(callback_barrier_count) + + stoppable_callback = MagicMock(side_effect=_callback) + + # Intentionally NOT setting scan_completed.set(); _callback() will set stop + + e = Exploiter(MockPuppet(), callback_barrier_count + 2) + e.exploit_hosts(exploiter_config, hosts_to_exploit, stoppable_callback, scan_completed, stop) + + assert stoppable_callback.call_count == 2 diff --git a/monkey/tests/unit_tests/infection_monkey/master/test_propagator.py b/monkey/tests/unit_tests/infection_monkey/master/test_propagator.py index d8f65b54e..4dffdf7e8 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_propagator.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_propagator.py @@ -1,11 +1,19 @@ from threading import Event -from infection_monkey.i_puppet import FingerprintData, PingScanData, PortScanData, PortStatus +from infection_monkey.i_puppet import ( + ExploiterResultData, + FingerprintData, + PingScanData, + PortScanData, + PortStatus, +) from infection_monkey.master import IPScanResults, Propagator +from infection_monkey.model import VictimHostFactory +from infection_monkey.telemetry.exploit_telem import ExploitTelem empty_fingerprint_data = FingerprintData(None, None, {}) -dot_1_results = IPScanResults( +dot_1_scan_results = IPScanResults( PingScanData(True, "windows"), { 22: PortScanData(22, PortStatus.CLOSED, None, None), @@ -19,7 +27,7 @@ dot_1_results = IPScanResults( }, ) -dot_3_results = IPScanResults( +dot_3_scan_results = IPScanResults( PingScanData(True, "linux"), { 22: PortScanData(22, PortStatus.OPEN, "SSH BANNER", "tcp-22"), @@ -42,7 +50,7 @@ dot_3_results = IPScanResults( }, ) -dead_host_results = IPScanResults( +dead_host_scan_results = IPScanResults( PingScanData(False, None), { 22: PortScanData(22, PortStatus.CLOSED, None, None), @@ -79,19 +87,27 @@ class MockIPScanner: def scan(self, ips_to_scan, _, results_callback, stop): for ip in ips_to_scan: if ip.endswith(".1"): - results_callback(ip, dot_1_results) + results_callback(ip, dot_1_scan_results) elif ip.endswith(".3"): - results_callback(ip, dot_3_results) + results_callback(ip, dot_3_scan_results) else: - results_callback(ip, dead_host_results) + results_callback(ip, dead_host_scan_results) + + +class StubExploiter: + def exploit_hosts( + self, hosts_to_exploit, exploiter_config, results_callback, scan_completed, stop + ): + pass def test_scan_result_processing(telemetry_messenger_spy): - p = Propagator(telemetry_messenger_spy, MockIPScanner()) + p = Propagator(telemetry_messenger_spy, MockIPScanner(), StubExploiter(), VictimHostFactory()) p.propagate( { "targets": {"subnet_scan_list": ["10.0.0.1", "10.0.0.2", "10.0.0.3"]}, - "network_scan": {}, + "network_scan": {}, # This is empty since MockIPscanner ignores it + "exploiters": {}, # This is empty since StubExploiter ignores it }, Event(), ) @@ -119,3 +135,79 @@ def test_scan_result_processing(telemetry_messenger_spy): assert data["machine"]["os"] == {} assert data["machine"]["services"] == {} assert data["machine"]["icmp"] is False + + +class MockExploiter: + def exploit_hosts( + self, exploiter_config, hosts_to_exploit, results_callback, scan_completed, stop + ): + hte = [] + for _ in range(0, 2): + hte.append(hosts_to_exploit.get()) + + for host in hte: + if host.ip_addr.endswith(".1"): + results_callback( + "PowerShellExploiter", + host, + ExploiterResultData(True, {}, {}, None), + ) + results_callback( + "SSHExploiter", + host, + ExploiterResultData(False, {}, {}, "SSH FAILED for .1"), + ) + if host.ip_addr.endswith(".2"): + results_callback( + "PowerShellExploiter", + host, + ExploiterResultData(False, {}, {}, "POWERSHELL FAILED for .2"), + ) + results_callback( + "SSHExploiter", + host, + ExploiterResultData(False, {}, {}, "SSH FAILED for .2"), + ) + if host.ip_addr.endswith(".3"): + results_callback( + "PowerShellExploiter", + host, + ExploiterResultData(False, {}, {}, "POWERSHELL FAILED for .3"), + ) + results_callback( + "SSHExploiter", + host, + ExploiterResultData(True, {}, {}, None), + ) + + +def test_exploiter_result_processing(telemetry_messenger_spy): + p = Propagator(telemetry_messenger_spy, MockIPScanner(), MockExploiter(), VictimHostFactory()) + p.propagate( + { + "targets": {"subnet_scan_list": ["10.0.0.1", "10.0.0.2", "10.0.0.3"]}, + "network_scan": {}, # This is empty since MockIPscanner ignores it + "exploiters": {}, # This is empty since MockExploiter ignores it + }, + Event(), + ) + + exploit_telems = [t for t in telemetry_messenger_spy.telemetries if isinstance(t, ExploitTelem)] + assert len(exploit_telems) == 4 + + for t in exploit_telems: + data = t.get_data() + ip = data["machine"]["ip_addr"] + + assert ip.endswith(".1") or ip.endswith(".3") + + if ip.endswith(".1"): + if data["exploiter"].startswith("PowerShell"): + assert data["result"] + else: + assert not data["result"] + elif ip.endswith(".3"): + if data["exploiter"].startswith("PowerShell"): + assert not data["result"] + else: + assert data["result"] diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py index c5e8226ea..09939b2ed 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py @@ -101,8 +101,9 @@ def test_format_config_for_agent__propagation(flat_monkey_config): ConfigService.format_flat_config_for_agent(flat_monkey_config) assert "propagation" in flat_monkey_config - assert "network_scan" in flat_monkey_config["propagation"] assert "targets" in flat_monkey_config["propagation"] + assert "network_scan" in flat_monkey_config["propagation"] + assert "exploiters" in flat_monkey_config["propagation"] def test_format_config_for_agent__propagation_targets(flat_monkey_config): @@ -163,3 +164,31 @@ def test_format_config_for_agent__network_scan(flat_monkey_config): assert "tcp_target_ports" not in flat_monkey_config assert "ping_scan_timeout" not in flat_monkey_config assert "finger_classes" not in flat_monkey_config + + +def test_format_config_for_agent__exploiters(flat_monkey_config): + expected_exploiters_config = { + "brute_force": [ + {"name": "MSSQLExploiter", "propagator": True}, + {"name": "PowerShellExploiter", "propagator": True}, + {"name": "SSHExploiter", "propagator": True}, + {"name": "SmbExploiter", "propagator": True}, + {"name": "WmiExploiter", "propagator": True}, + ], + "vulnerability": [ + {"name": "DrupalExploiter", "propagator": True}, + {"name": "ElasticGroovyExploiter", "propagator": True}, + {"name": "HadoopExploiter", "propagator": True}, + {"name": "ShellShockExploiter", "propagator": True}, + {"name": "Struts2Exploiter", "propagator": True}, + {"name": "WebLogicExploiter", "propagator": True}, + {"name": "ZerologonExploiter", "propagator": False}, + ], + } + ConfigService.format_flat_config_for_agent(flat_monkey_config) + + assert "propagation" in flat_monkey_config + assert "exploiters" in flat_monkey_config["propagation"] + + assert flat_monkey_config["propagation"]["exploiters"] == expected_exploiters_config + assert "exploiter_classes" not in flat_monkey_config