forked from p15670423/monkey
Merge pull request #1657 from guardicore/1597-implement-exploitation
1597 implement exploitation
This commit is contained in:
commit
ba5d755dfa
|
@ -16,7 +16,9 @@ class UnknownPluginError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
ExploiterResultData = namedtuple("ExploiterResultData", ["result", "info", "attempts"])
|
ExploiterResultData = namedtuple(
|
||||||
|
"ExploiterResultData", ["success", "info", "attempts", "error_message"]
|
||||||
|
)
|
||||||
PingScanData = namedtuple("PingScanData", ["response_received", "os"])
|
PingScanData = namedtuple("PingScanData", ["response_received", "os"])
|
||||||
PortScanData = namedtuple("PortScanData", ["port", "status", "banner", "service"])
|
PortScanData = namedtuple("PortScanData", ["port", "status", "banner", "service"])
|
||||||
FingerprintData = namedtuple("FingerprintData", ["os_type", "os_version", "services"])
|
FingerprintData = namedtuple("FingerprintData", ["os_type", "os_version", "services"])
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from .ip_scan_results import IPScanResults
|
from .ip_scan_results import IPScanResults
|
||||||
from .ip_scanner import IPScanner
|
from .ip_scanner import IPScanner
|
||||||
|
from .exploiter import Exploiter
|
||||||
from .propagator import Propagator
|
from .propagator import Propagator
|
||||||
from .automated_master import AutomatedMaster
|
from .automated_master import AutomatedMaster
|
||||||
|
|
|
@ -6,18 +6,20 @@ from typing import Any, Callable, Dict, List, Tuple
|
||||||
from infection_monkey.i_control_channel import IControlChannel
|
from infection_monkey.i_control_channel import IControlChannel
|
||||||
from infection_monkey.i_master import IMaster
|
from infection_monkey.i_master import IMaster
|
||||||
from infection_monkey.i_puppet import IPuppet
|
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.messengers.i_telemetry_messenger import ITelemetryMessenger
|
||||||
from infection_monkey.telemetry.post_breach_telem import PostBreachTelem
|
from infection_monkey.telemetry.post_breach_telem import PostBreachTelem
|
||||||
from infection_monkey.telemetry.system_info_telem import SystemInfoTelem
|
from infection_monkey.telemetry.system_info_telem import SystemInfoTelem
|
||||||
from infection_monkey.utils.timer import Timer
|
from infection_monkey.utils.timer import Timer
|
||||||
|
|
||||||
from . import IPScanner, Propagator
|
from . import Exploiter, IPScanner, Propagator
|
||||||
from .threading_utils import create_daemon_thread
|
from .threading_utils import create_daemon_thread
|
||||||
|
|
||||||
CHECK_ISLAND_FOR_STOP_COMMAND_INTERVAL_SEC = 5
|
CHECK_ISLAND_FOR_STOP_COMMAND_INTERVAL_SEC = 5
|
||||||
CHECK_FOR_TERMINATE_INTERVAL_SEC = CHECK_ISLAND_FOR_STOP_COMMAND_INTERVAL_SEC / 5
|
CHECK_FOR_TERMINATE_INTERVAL_SEC = CHECK_ISLAND_FOR_STOP_COMMAND_INTERVAL_SEC / 5
|
||||||
SHUTDOWN_TIMEOUT = 5
|
SHUTDOWN_TIMEOUT = 5
|
||||||
NUM_SCAN_THREADS = 16 # TODO: Adjust this to the optimal number of scan threads
|
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()
|
logger = logging.getLogger()
|
||||||
|
|
||||||
|
@ -27,6 +29,7 @@ class AutomatedMaster(IMaster):
|
||||||
self,
|
self,
|
||||||
puppet: IPuppet,
|
puppet: IPuppet,
|
||||||
telemetry_messenger: ITelemetryMessenger,
|
telemetry_messenger: ITelemetryMessenger,
|
||||||
|
victim_host_factory: VictimHostFactory,
|
||||||
control_channel: IControlChannel,
|
control_channel: IControlChannel,
|
||||||
):
|
):
|
||||||
self._puppet = puppet
|
self._puppet = puppet
|
||||||
|
@ -34,7 +37,10 @@ class AutomatedMaster(IMaster):
|
||||||
self._control_channel = control_channel
|
self._control_channel = control_channel
|
||||||
|
|
||||||
ip_scanner = IPScanner(self._puppet, NUM_SCAN_THREADS)
|
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._stop = threading.Event()
|
||||||
self._master_thread = create_daemon_thread(target=self._run_master_thread)
|
self._master_thread = create_daemon_thread(target=self._run_master_thread)
|
||||||
|
|
|
@ -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()
|
|
@ -14,7 +14,7 @@ from infection_monkey.i_puppet import (
|
||||||
)
|
)
|
||||||
|
|
||||||
from . import IPScanResults
|
from . import IPScanResults
|
||||||
from .threading_utils import create_daemon_thread
|
from .threading_utils import run_worker_threads
|
||||||
|
|
||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
|
|
||||||
|
@ -35,14 +35,7 @@ class IPScanner:
|
||||||
ips.put(ip)
|
ips.put(ip)
|
||||||
|
|
||||||
scan_ips_args = (ips, options, results_callback, stop)
|
scan_ips_args = (ips, options, results_callback, stop)
|
||||||
scan_threads = []
|
run_worker_threads(target=self._scan_ips, args=scan_ips_args, num_workers=self._num_workers)
|
||||||
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()
|
|
||||||
|
|
||||||
def _scan_ips(self, ips: Queue, options: Dict, results_callback: Callback, stop: Event):
|
def _scan_ips(self, ips: Queue, options: Dict, results_callback: Callback, stop: Event):
|
||||||
logger.debug(f"Starting scan thread -- Thread ID: {threading.get_ident()}")
|
logger.debug(f"Starting scan thread -- Thread ID: {threading.get_ident()}")
|
||||||
|
|
|
@ -1,41 +1,60 @@
|
||||||
import logging
|
import logging
|
||||||
from queue import Queue
|
from queue import Queue
|
||||||
from threading import Event, Thread
|
from threading import Event
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
from infection_monkey.i_puppet import FingerprintData, PingScanData, PortScanData, PortStatus
|
from infection_monkey.i_puppet import (
|
||||||
from infection_monkey.model.host import VictimHost
|
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.messengers.i_telemetry_messenger import ITelemetryMessenger
|
||||||
from infection_monkey.telemetry.scan_telem import ScanTelem
|
from infection_monkey.telemetry.scan_telem import ScanTelem
|
||||||
|
|
||||||
from . import IPScanner, IPScanResults
|
from . import Exploiter, IPScanner, IPScanResults
|
||||||
from .threading_utils import create_daemon_thread
|
from .threading_utils import create_daemon_thread
|
||||||
|
|
||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
|
|
||||||
|
|
||||||
class Propagator:
|
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._telemetry_messenger = telemetry_messenger
|
||||||
self._ip_scanner = ip_scanner
|
self._ip_scanner = ip_scanner
|
||||||
|
self._exploiter = exploiter
|
||||||
|
self._victim_host_factory = victim_host_factory
|
||||||
self._hosts_to_exploit = None
|
self._hosts_to_exploit = None
|
||||||
|
|
||||||
def propagate(self, propagation_config: Dict, stop: Event):
|
def propagate(self, propagation_config: Dict, stop: Event):
|
||||||
logger.info("Attempting to propagate")
|
logger.info("Attempting to propagate")
|
||||||
|
|
||||||
|
network_scan_completed = Event()
|
||||||
self._hosts_to_exploit = Queue()
|
self._hosts_to_exploit = Queue()
|
||||||
|
|
||||||
scan_thread = create_daemon_thread(
|
scan_thread = create_daemon_thread(
|
||||||
target=self._scan_network, args=(propagation_config, stop)
|
target=self._scan_network, args=(propagation_config, stop)
|
||||||
)
|
)
|
||||||
exploit_thread = create_daemon_thread(
|
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()
|
scan_thread.start()
|
||||||
exploit_thread.start()
|
exploit_thread.start()
|
||||||
|
|
||||||
scan_thread.join()
|
scan_thread.join()
|
||||||
|
network_scan_completed.set()
|
||||||
|
|
||||||
exploit_thread.join()
|
exploit_thread.join()
|
||||||
|
|
||||||
logger.info("Finished attempting to propagate")
|
logger.info("Finished attempting to propagate")
|
||||||
|
@ -52,7 +71,7 @@ class Propagator:
|
||||||
logger.info("Finished network scan")
|
logger.info("Finished network scan")
|
||||||
|
|
||||||
def _process_scan_results(self, ip: str, scan_results: IPScanResults):
|
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_ping_scan_results(victim_host, scan_results.ping_scan_data)
|
||||||
Propagator._process_tcp_scan_results(victim_host, scan_results.port_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():
|
for service, details in fd.services.items():
|
||||||
victim_host.services.setdefault(service, {}).update(details)
|
victim_host.services.setdefault(service, {}).update(details)
|
||||||
|
|
||||||
def _exploit_targets(self, scan_thread: Thread, stop: Event):
|
def _exploit_hosts(
|
||||||
pass
|
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)
|
||||||
|
)
|
||||||
|
|
|
@ -2,5 +2,16 @@ from threading import Thread
|
||||||
from typing import Callable, Tuple
|
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 = ()):
|
def create_daemon_thread(target: Callable[..., None], args: Tuple = ()):
|
||||||
return Thread(target=target, args=args, daemon=True)
|
return Thread(target=target, args=args, daemon=True)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from infection_monkey.model.host import VictimHost
|
from infection_monkey.model.host import VictimHost
|
||||||
|
from infection_monkey.model.victim_host_factory import VictimHostFactory
|
||||||
|
|
||||||
MONKEY_ARG = "m0nk3y"
|
MONKEY_ARG = "m0nk3y"
|
||||||
DROPPER_ARG = "dr0pp3r"
|
DROPPER_ARG = "dr0pp3r"
|
||||||
|
|
|
@ -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
|
|
@ -280,11 +280,24 @@ class MockPuppet(IPuppet):
|
||||||
"executed_cmds": [],
|
"executed_cmds": [],
|
||||||
}
|
}
|
||||||
successful_exploiters = {
|
successful_exploiters = {
|
||||||
DOT_1: {"PowerShellExploiter": ExploiterResultData(True, info_powershell, attempts)},
|
DOT_1: {
|
||||||
DOT_3: {"SSHExploiter": ExploiterResultData(False, info_ssh, attempts)},
|
"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(
|
def run_payload(
|
||||||
self, name: str, options: Dict, interrupt: threading.Event
|
self, name: str, options: Dict, interrupt: threading.Event
|
||||||
|
|
|
@ -475,6 +475,9 @@ class ConfigService:
|
||||||
formatted_propagation_config["targets"] = ConfigService._format_targets_from_flat_config(
|
formatted_propagation_config["targets"] = ConfigService._format_targets_from_flat_config(
|
||||||
config
|
config
|
||||||
)
|
)
|
||||||
|
formatted_propagation_config[
|
||||||
|
"exploiters"
|
||||||
|
] = ConfigService._format_exploiters_from_flat_config(config)
|
||||||
|
|
||||||
config["propagation"] = formatted_propagation_config
|
config["propagation"] = formatted_propagation_config
|
||||||
|
|
||||||
|
@ -567,3 +570,33 @@ class ConfigService:
|
||||||
config.pop(flat_subnet_scan_list_field, None)
|
config.pop(flat_subnet_scan_list_field, None)
|
||||||
|
|
||||||
return formatted_scan_targets_config
|
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
|
||||||
|
|
|
@ -55,6 +55,7 @@
|
||||||
"ShellShockExploiter",
|
"ShellShockExploiter",
|
||||||
"ElasticGroovyExploiter",
|
"ElasticGroovyExploiter",
|
||||||
"Struts2Exploiter",
|
"Struts2Exploiter",
|
||||||
|
"ZerologonExploiter",
|
||||||
"WebLogicExploiter",
|
"WebLogicExploiter",
|
||||||
"HadoopExploiter",
|
"HadoopExploiter",
|
||||||
"MSSQLExploiter",
|
"MSSQLExploiter",
|
||||||
|
|
|
@ -2,7 +2,7 @@ from infection_monkey.master import AutomatedMaster
|
||||||
|
|
||||||
|
|
||||||
def test_terminate_without_start():
|
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
|
# Test that call to terminate does not raise exception
|
||||||
m.terminate()
|
m.terminate()
|
||||||
|
|
|
@ -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
|
|
@ -1,11 +1,19 @@
|
||||||
from threading import Event
|
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.master import IPScanResults, Propagator
|
||||||
|
from infection_monkey.model import VictimHostFactory
|
||||||
|
from infection_monkey.telemetry.exploit_telem import ExploitTelem
|
||||||
|
|
||||||
empty_fingerprint_data = FingerprintData(None, None, {})
|
empty_fingerprint_data = FingerprintData(None, None, {})
|
||||||
|
|
||||||
dot_1_results = IPScanResults(
|
dot_1_scan_results = IPScanResults(
|
||||||
PingScanData(True, "windows"),
|
PingScanData(True, "windows"),
|
||||||
{
|
{
|
||||||
22: PortScanData(22, PortStatus.CLOSED, None, None),
|
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"),
|
PingScanData(True, "linux"),
|
||||||
{
|
{
|
||||||
22: PortScanData(22, PortStatus.OPEN, "SSH BANNER", "tcp-22"),
|
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),
|
PingScanData(False, None),
|
||||||
{
|
{
|
||||||
22: PortScanData(22, PortStatus.CLOSED, None, None),
|
22: PortScanData(22, PortStatus.CLOSED, None, None),
|
||||||
|
@ -79,19 +87,27 @@ class MockIPScanner:
|
||||||
def scan(self, ips_to_scan, _, results_callback, stop):
|
def scan(self, ips_to_scan, _, results_callback, stop):
|
||||||
for ip in ips_to_scan:
|
for ip in ips_to_scan:
|
||||||
if ip.endswith(".1"):
|
if ip.endswith(".1"):
|
||||||
results_callback(ip, dot_1_results)
|
results_callback(ip, dot_1_scan_results)
|
||||||
elif ip.endswith(".3"):
|
elif ip.endswith(".3"):
|
||||||
results_callback(ip, dot_3_results)
|
results_callback(ip, dot_3_scan_results)
|
||||||
else:
|
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):
|
def test_scan_result_processing(telemetry_messenger_spy):
|
||||||
p = Propagator(telemetry_messenger_spy, MockIPScanner())
|
p = Propagator(telemetry_messenger_spy, MockIPScanner(), StubExploiter(), VictimHostFactory())
|
||||||
p.propagate(
|
p.propagate(
|
||||||
{
|
{
|
||||||
"targets": {"subnet_scan_list": ["10.0.0.1", "10.0.0.2", "10.0.0.3"]},
|
"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(),
|
Event(),
|
||||||
)
|
)
|
||||||
|
@ -119,3 +135,79 @@ def test_scan_result_processing(telemetry_messenger_spy):
|
||||||
assert data["machine"]["os"] == {}
|
assert data["machine"]["os"] == {}
|
||||||
assert data["machine"]["services"] == {}
|
assert data["machine"]["services"] == {}
|
||||||
assert data["machine"]["icmp"] is False
|
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"]
|
||||||
|
|
|
@ -101,8 +101,9 @@ def test_format_config_for_agent__propagation(flat_monkey_config):
|
||||||
ConfigService.format_flat_config_for_agent(flat_monkey_config)
|
ConfigService.format_flat_config_for_agent(flat_monkey_config)
|
||||||
|
|
||||||
assert "propagation" in 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 "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):
|
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 "tcp_target_ports" not in flat_monkey_config
|
||||||
assert "ping_scan_timeout" not in flat_monkey_config
|
assert "ping_scan_timeout" not in flat_monkey_config
|
||||||
assert "finger_classes" 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
|
||||||
|
|
Loading…
Reference in New Issue