From 93d0bb6cd21ae324209d239f47a06a4b0e188d91 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 13 Dec 2021 16:53:07 -0500 Subject: [PATCH 01/13] Agent: Add a placeholder VictimHostFactory The AutomatedMaster will need access to the monkey's tunnel, IP addresses, and default server in order to properly configure the victim host. The VictimHostFactory can abstract these dependencies away and handle these details on behalf of the AutomatedMaster. --- .../master/automated_master.py | 4 ++- monkey/infection_monkey/master/propagator.py | 12 ++++++-- monkey/infection_monkey/model/__init__.py | 1 + .../model/victim_host_factory.py | 28 +++++++++++++++++++ .../master/test_automated_master.py | 2 +- .../master/test_propagator.py | 3 +- 6 files changed, 44 insertions(+), 6 deletions(-) create mode 100644 monkey/infection_monkey/model/victim_host_factory.py diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index 784046323..57b8f52b2 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -6,6 +6,7 @@ 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 @@ -27,6 +28,7 @@ class AutomatedMaster(IMaster): self, puppet: IPuppet, telemetry_messenger: ITelemetryMessenger, + victim_host_factory: VictimHostFactory, control_channel: IControlChannel, ): self._puppet = puppet @@ -34,7 +36,7 @@ class AutomatedMaster(IMaster): self._control_channel = control_channel ip_scanner = IPScanner(self._puppet, NUM_SCAN_THREADS) - self._propagator = Propagator(self._telemetry_messenger, ip_scanner) + self._propagator = Propagator(self._telemetry_messenger, ip_scanner, 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/propagator.py b/monkey/infection_monkey/master/propagator.py index 916297110..78e08a98d 100644 --- a/monkey/infection_monkey/master/propagator.py +++ b/monkey/infection_monkey/master/propagator.py @@ -4,7 +4,7 @@ from threading import Event, Thread from typing import Dict from infection_monkey.i_puppet import FingerprintData, PingScanData, PortScanData, PortStatus -from infection_monkey.model.host import VictimHost +from infection_monkey.model import VictimHost, VictimHostFactory from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger from infection_monkey.telemetry.scan_telem import ScanTelem @@ -15,9 +15,15 @@ logger = logging.getLogger() class Propagator: - def __init__(self, telemetry_messenger: ITelemetryMessenger, ip_scanner: IPScanner): + def __init__( + self, + telemetry_messenger: ITelemetryMessenger, + ip_scanner: IPScanner, + victim_host_factory: VictimHostFactory, + ): self._telemetry_messenger = telemetry_messenger self._ip_scanner = ip_scanner + self._victim_host_factory = victim_host_factory self._hosts_to_exploit = None def propagate(self, propagation_config: Dict, stop: Event): @@ -52,7 +58,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) 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/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_propagator.py b/monkey/tests/unit_tests/infection_monkey/master/test_propagator.py index d8f65b54e..941f17a6c 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_propagator.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_propagator.py @@ -2,6 +2,7 @@ from threading import Event from infection_monkey.i_puppet import FingerprintData, PingScanData, PortScanData, PortStatus from infection_monkey.master import IPScanResults, Propagator +from infection_monkey.model import VictimHostFactory empty_fingerprint_data = FingerprintData(None, None, {}) @@ -87,7 +88,7 @@ class MockIPScanner: def test_scan_result_processing(telemetry_messenger_spy): - p = Propagator(telemetry_messenger_spy, MockIPScanner()) + p = Propagator(telemetry_messenger_spy, MockIPScanner(), VictimHostFactory()) p.propagate( { "targets": {"subnet_scan_list": ["10.0.0.1", "10.0.0.2", "10.0.0.3"]}, From 09305bca4c44a46f6d54a2576d2a52eb99f7e229 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 13 Dec 2021 20:04:26 -0500 Subject: [PATCH 02/13] Island: Reformat "exploiter" config options before sending to Agent --- monkey/monkey_island/cc/services/config.py | 33 +++++++++++++++++++ .../monkey_configs/flat_config.json | 1 + .../monkey_island/cc/services/test_config.py | 31 ++++++++++++++++- 3 files changed, 64 insertions(+), 1 deletion(-) 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/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 From eb7612d80dafbefdac32c2073dab1566b0de1074 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 14 Dec 2021 08:49:44 -0500 Subject: [PATCH 03/13] Agent: Rename result -> success in ExploiterResultData --- monkey/infection_monkey/i_puppet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/i_puppet.py b/monkey/infection_monkey/i_puppet.py index 11da6a260..78a0ea83f 100644 --- a/monkey/infection_monkey/i_puppet.py +++ b/monkey/infection_monkey/i_puppet.py @@ -16,7 +16,7 @@ class UnknownPluginError(Exception): pass -ExploiterResultData = namedtuple("ExploiterResultData", ["result", "info", "attempts"]) +ExploiterResultData = namedtuple("ExploiterResultData", ["success", "info", "attempts"]) PingScanData = namedtuple("PingScanData", ["response_received", "os"]) PortScanData = namedtuple("PortScanData", ["port", "status", "banner", "service"]) FingerprintData = namedtuple("FingerprintData", ["os_type", "os_version", "services"]) From 1e02286b2a996d42d62be9a6d62b10c549d40851 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 14 Dec 2021 12:18:50 -0500 Subject: [PATCH 04/13] Agent: Add "error_message" to ExploiterResultData --- monkey/infection_monkey/i_puppet.py | 4 +++- monkey/infection_monkey/puppet/mock_puppet.py | 8 ++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/i_puppet.py b/monkey/infection_monkey/i_puppet.py index 78a0ea83f..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", ["success", "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/puppet/mock_puppet.py b/monkey/infection_monkey/puppet/mock_puppet.py index 5f0389752..fe21f4cb0 100644 --- a/monkey/infection_monkey/puppet/mock_puppet.py +++ b/monkey/infection_monkey/puppet/mock_puppet.py @@ -280,8 +280,12 @@ 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) + }, + DOT_3: { + "SSHExploiter": ExploiterResultData(False, info_ssh, attempts, "Failed exploiting") + }, } return successful_exploiters[host][name] From 3394629cb275e6cfa361b7e109a44d41dc2bf54f Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 14 Dec 2021 14:22:46 -0500 Subject: [PATCH 05/13] Agent: Run exploiters from AutomatedMaster --- monkey/infection_monkey/master/__init__.py | 1 + .../master/automated_master.py | 8 +- monkey/infection_monkey/master/exploiter.py | 107 +++++++++++++++++ monkey/infection_monkey/master/propagator.py | 52 ++++++++- monkey/infection_monkey/puppet/mock_puppet.py | 10 +- .../infection_monkey/master/test_exploiter.py | 102 ++++++++++++++++ .../master/test_propagator.py | 109 ++++++++++++++++-- 7 files changed, 371 insertions(+), 18 deletions(-) create mode 100644 monkey/infection_monkey/master/exploiter.py create mode 100644 monkey/tests/unit_tests/infection_monkey/master/test_exploiter.py 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 57b8f52b2..ff6af8b43 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -12,13 +12,14 @@ 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() @@ -36,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, victim_host_factory) + 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..3f732ffa3 --- /dev/null +++ b/monkey/infection_monkey/master/exploiter.py @@ -0,0 +1,107 @@ +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 create_daemon_thread + +QUEUE_TIMEOUT = 2 + +logger = logging.getLogger() + +ExploiterName = str +Callback = Callable[[VictimHost, ExploiterName, 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) + + # TODO: This functionality is also used in IPScanner and can be generalized. Extract it. + exploiter_threads = [] + for i in range(0, self._num_workers): + t = create_daemon_thread(target=self._exploit_hosts_on_queue, args=exploit_args) + t.start() + exploiter_threads.append(t) + + for t in exploiter_threads: + t.join() + + 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) + or stop.is_set() + ): + 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/propagator.py b/monkey/infection_monkey/master/propagator.py index 78e08a98d..24d5fb8f0 100644 --- a/monkey/infection_monkey/master/propagator.py +++ b/monkey/infection_monkey/master/propagator.py @@ -3,12 +3,19 @@ from queue import Queue from threading import Event, Thread from typing import Dict -from infection_monkey.i_puppet import FingerprintData, PingScanData, PortScanData, PortStatus +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() @@ -19,29 +26,35 @@ class Propagator: 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=(scan_thread, 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") @@ -101,5 +114,34 @@ 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, + scan_thread: Thread, + propagation_config: Dict, + network_scan_completed: Event, + stop: Event, + ): + logger.info("Exploiting victims") + + exploiter_config = propagation_config["exploiters"] + self._exploiter.exploit_hosts( + self._hosts_to_exploit, + exploiter_config, + 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("Successfully propagated to {host} using {exploiter_name}") + else: + logger.info(result.error_message) + + self._telemetry_messenger.send_telemetry( + ExploitTelem(exploiter_name, host, result.success, result.info, result.attempts) + ) diff --git a/monkey/infection_monkey/puppet/mock_puppet.py b/monkey/infection_monkey/puppet/mock_puppet.py index fe21f4cb0..64c247170 100644 --- a/monkey/infection_monkey/puppet/mock_puppet.py +++ b/monkey/infection_monkey/puppet/mock_puppet.py @@ -281,10 +281,16 @@ class MockPuppet(IPuppet): } successful_exploiters = { DOT_1: { - "PowerShellExploiter": ExploiterResultData(True, info_powershell, attempts, None) + "PowerShellExploiter": ExploiterResultData(True, info_powershell, attempts, None), + "ZerologonExploiter": ExploiterResultData(False, {}, [], "Zerologon failed"), + "SSHExploiter": ExploiterResultData(False, info_ssh, attempts, "Failed exploiting"), }, DOT_3: { - "SSHExploiter": ExploiterResultData(False, info_ssh, attempts, "Failed exploiting") + "PowerShellExploiter": ExploiterResultData( + False, info_powershell, attempts, "PowerShell Exploiter Failed" + ), + "SSHExploiter": ExploiterResultData(False, info_ssh, attempts, "Failed exploiting"), + "ZerologonExploiter": ExploiterResultData(True, {}, [], None), }, } 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 941f17a6c..de44f40f4 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_propagator.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_propagator.py @@ -1,12 +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), @@ -20,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"), @@ -43,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), @@ -80,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(), VictimHostFactory()) + 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(), ) @@ -120,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, hosts_to_exploit, exploiter_config, 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"] From bda192eba98dd298cb54fef19ec8d434d7a4b1b0 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 14 Dec 2021 15:18:50 -0500 Subject: [PATCH 06/13] Agent: Extract run_worker_threads() from IPScanner and Exploiter --- monkey/infection_monkey/master/exploiter.py | 15 ++++----------- monkey/infection_monkey/master/ip_scanner.py | 11 ++--------- monkey/infection_monkey/master/threading_utils.py | 11 +++++++++++ 3 files changed, 17 insertions(+), 20 deletions(-) diff --git a/monkey/infection_monkey/master/exploiter.py b/monkey/infection_monkey/master/exploiter.py index 3f732ffa3..0dfc8869f 100644 --- a/monkey/infection_monkey/master/exploiter.py +++ b/monkey/infection_monkey/master/exploiter.py @@ -8,7 +8,7 @@ from typing import Callable, Dict, List from infection_monkey.i_puppet import ExploiterResultData, IPuppet from infection_monkey.model import VictimHost -from .threading_utils import create_daemon_thread +from .threading_utils import run_worker_threads QUEUE_TIMEOUT = 2 @@ -40,16 +40,9 @@ class Exploiter: ) exploit_args = (exploiters_to_run, hosts_to_exploit, results_callback, scan_completed, stop) - - # TODO: This functionality is also used in IPScanner and can be generalized. Extract it. - exploiter_threads = [] - for i in range(0, self._num_workers): - t = create_daemon_thread(target=self._exploit_hosts_on_queue, args=exploit_args) - t.start() - exploiter_threads.append(t) - - for t in exploiter_threads: - t.join() + run_worker_threads( + target=self._exploit_hosts_on_queue, args=exploit_args, num_workers=self._num_workers + ) def _exploit_hosts_on_queue( self, 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/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) From b466a17f7690350532f0fd5f3af4721f5955f980 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 14 Dec 2021 15:29:43 -0500 Subject: [PATCH 07/13] Agent: Remove scan_thread from Propagator._exploit_hosts() arguments --- monkey/infection_monkey/master/propagator.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/master/propagator.py b/monkey/infection_monkey/master/propagator.py index 24d5fb8f0..0b42d345b 100644 --- a/monkey/infection_monkey/master/propagator.py +++ b/monkey/infection_monkey/master/propagator.py @@ -1,6 +1,6 @@ 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 ( @@ -46,7 +46,7 @@ class Propagator: ) exploit_thread = create_daemon_thread( target=self._exploit_hosts, - args=(scan_thread, propagation_config, network_scan_completed, stop), + args=(propagation_config, network_scan_completed, stop), ) scan_thread.start() @@ -116,7 +116,6 @@ class Propagator: def _exploit_hosts( self, - scan_thread: Thread, propagation_config: Dict, network_scan_completed: Event, stop: Event, From da61451947c73a01a05edc7a0fc6c30021a6da9d Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 14 Dec 2021 15:30:18 -0500 Subject: [PATCH 08/13] Agent: Fix order of arguments to Exploiter.exploit_hosts() --- monkey/infection_monkey/master/propagator.py | 2 +- .../tests/unit_tests/infection_monkey/master/test_propagator.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/master/propagator.py b/monkey/infection_monkey/master/propagator.py index 0b42d345b..33fc826ea 100644 --- a/monkey/infection_monkey/master/propagator.py +++ b/monkey/infection_monkey/master/propagator.py @@ -124,8 +124,8 @@ class Propagator: exploiter_config = propagation_config["exploiters"] self._exploiter.exploit_hosts( - self._hosts_to_exploit, exploiter_config, + self._hosts_to_exploit, self._process_exploit_attempts, network_scan_completed, stop, 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 de44f40f4..4dffdf7e8 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_propagator.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_propagator.py @@ -139,7 +139,7 @@ def test_scan_result_processing(telemetry_messenger_spy): class MockExploiter: def exploit_hosts( - self, hosts_to_exploit, exploiter_config, results_callback, scan_completed, stop + self, exploiter_config, hosts_to_exploit, results_callback, scan_completed, stop ): hte = [] for _ in range(0, 2): From 6c1caa1af4a3a7e4296050f2c77eae9f6d601eea Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 14 Dec 2021 15:31:34 -0500 Subject: [PATCH 09/13] Agent: Improve log message for failed propagation --- monkey/infection_monkey/master/propagator.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/master/propagator.py b/monkey/infection_monkey/master/propagator.py index 33fc826ea..ef3bf92ea 100644 --- a/monkey/infection_monkey/master/propagator.py +++ b/monkey/infection_monkey/master/propagator.py @@ -139,7 +139,9 @@ class Propagator: if result.success: logger.info("Successfully propagated to {host} using {exploiter_name}") else: - logger.info(result.error_message) + 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) From 4b3984dbd72d26c6d34ff653f35c8f076f8174ac Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 14 Dec 2021 15:32:10 -0500 Subject: [PATCH 10/13] Agent: Add default return value in MockPuppet.exploit_host() --- monkey/infection_monkey/puppet/mock_puppet.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/puppet/mock_puppet.py b/monkey/infection_monkey/puppet/mock_puppet.py index 64c247170..204e44ab4 100644 --- a/monkey/infection_monkey/puppet/mock_puppet.py +++ b/monkey/infection_monkey/puppet/mock_puppet.py @@ -294,7 +294,10 @@ class MockPuppet(IPuppet): }, } - 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 From fc767e207468783652575b3b1be752b2a3074720 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 15 Dec 2021 06:48:30 -0500 Subject: [PATCH 11/13] Agent: Add missing "f" to f-string Co-authored-by: Shreya Malviya --- monkey/infection_monkey/master/propagator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/master/propagator.py b/monkey/infection_monkey/master/propagator.py index ef3bf92ea..9d31b94b4 100644 --- a/monkey/infection_monkey/master/propagator.py +++ b/monkey/infection_monkey/master/propagator.py @@ -137,7 +137,7 @@ class Propagator: self, exploiter_name: str, host: VictimHost, result: ExploiterResultData ): if result.success: - logger.info("Successfully propagated to {host} using {exploiter_name}") + 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}" From f1b55b70c2900a61aa61e51170802b15048b39d0 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 15 Dec 2021 08:10:00 -0500 Subject: [PATCH 12/13] Agent: Remove redundant check for stop in Exploiter --- monkey/infection_monkey/master/exploiter.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/monkey/infection_monkey/master/exploiter.py b/monkey/infection_monkey/master/exploiter.py index 0dfc8869f..383fc6fe3 100644 --- a/monkey/infection_monkey/master/exploiter.py +++ b/monkey/infection_monkey/master/exploiter.py @@ -59,10 +59,7 @@ class Exploiter: 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) - or stop.is_set() - ): + if _all_hosts_have_been_processed(scan_completed, hosts_to_exploit): break logger.debug( From a6bb81e4733728fab0349d27d64c931fcc7a5e83 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 15 Dec 2021 08:34:21 -0500 Subject: [PATCH 13/13] Agent: Fix order of Exploiter Callback type hint arguments --- monkey/infection_monkey/master/exploiter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/master/exploiter.py b/monkey/infection_monkey/master/exploiter.py index 383fc6fe3..f1a804ba7 100644 --- a/monkey/infection_monkey/master/exploiter.py +++ b/monkey/infection_monkey/master/exploiter.py @@ -15,7 +15,7 @@ QUEUE_TIMEOUT = 2 logger = logging.getLogger() ExploiterName = str -Callback = Callable[[VictimHost, ExploiterName, ExploiterResultData], None] +Callback = Callable[[ExploiterName, VictimHost, ExploiterResultData], None] class Exploiter: