Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1597 implement exploitation #1657

Merged
merged 13 commits into from
Dec 15, 2021
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion monkey/infection_monkey/i_puppet.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ class UnknownPluginError(Exception):
pass


ExploiterResultData = namedtuple("ExploiterResultData", ["result", "info", "attempts"])
ExploiterResultData = namedtuple(
"ExploiterResultData", ["success", "info", "attempts", "error_message"]
)
PingScanData = namedtuple("PingScanData", ["response_received", "os"])
PortScanData = namedtuple("PortScanData", ["port", "status", "banner", "service"])
FingerprintData = namedtuple("FingerprintData", ["os_type", "os_version", "services"])
Expand Down
1 change: 1 addition & 0 deletions monkey/infection_monkey/master/__init__.py
Original file line number Diff line number Diff line change
@@ -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
10 changes: 8 additions & 2 deletions monkey/infection_monkey/master/automated_master.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,20 @@
from infection_monkey.i_control_channel import IControlChannel
from infection_monkey.i_master import IMaster
from infection_monkey.i_puppet import IPuppet
from infection_monkey.model import VictimHostFactory
from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger
from infection_monkey.telemetry.post_breach_telem import PostBreachTelem
from infection_monkey.telemetry.system_info_telem import SystemInfoTelem
from infection_monkey.utils.timer import Timer

from . import IPScanner, Propagator
from . import Exploiter, IPScanner, Propagator
from .threading_utils import create_daemon_thread

CHECK_ISLAND_FOR_STOP_COMMAND_INTERVAL_SEC = 5
CHECK_FOR_TERMINATE_INTERVAL_SEC = CHECK_ISLAND_FOR_STOP_COMMAND_INTERVAL_SEC / 5
SHUTDOWN_TIMEOUT = 5
NUM_SCAN_THREADS = 16 # TODO: Adjust this to the optimal number of scan threads
NUM_EXPLOIT_THREADS = 4 # TODO: Adjust this to the optimal number of exploit threads

logger = logging.getLogger()

Expand All @@ -27,14 +29,18 @@ def __init__(
self,
puppet: IPuppet,
telemetry_messenger: ITelemetryMessenger,
victim_host_factory: VictimHostFactory,
control_channel: IControlChannel,
):
self._puppet = puppet
self._telemetry_messenger = telemetry_messenger
self._control_channel = control_channel

ip_scanner = IPScanner(self._puppet, NUM_SCAN_THREADS)
self._propagator = Propagator(self._telemetry_messenger, ip_scanner)
exploiter = Exploiter(self._puppet, NUM_EXPLOIT_THREADS)
self._propagator = Propagator(
self._telemetry_messenger, ip_scanner, exploiter, victim_host_factory
)

self._stop = threading.Event()
self._master_thread = create_daemon_thread(target=self._run_master_thread)
Expand Down
100 changes: 100 additions & 0 deletions monkey/infection_monkey/master/exploiter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
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[[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)
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)
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
Comment on lines +79 to +80
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same consideration as before: each layer of logic contains the same

if stop.is_set():
    return

This logic is not specific to the Exploiter, it's an exception logic. Maybe there's a way to raise an exception when stop.is_set() instead. Maybe this can be solved with an interface/re-usable decorator

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A decorator won't work because the Event needs to be available on import, but we could potentially add a utility function that handles this. We're going to need it for the plugins as well.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the moment I'm going to leave it. Each loop is using it in a slightly different way, or has slightly different requirements. As we accumulate more, similar loops, we can find the common patterns and make them reusable. At the moment I think it may be too early to do so.


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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it's worth considering creating the stop event in the agent. Then, puppet and master will be created with it. This way we wouldn't need to pass stop through the parameters everywhere. This would de-couple stopping logic from execution logic a bit, for better or worse.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would leak too much information into the agent, give the agent the ability to stop parts of the master that it shouldn't, and limit how the master is able to control functionality.

The stop signal is something that the master can control per-run of the Propagator. The master controls the Propagator, the Propagator controls the scanner and exploiter. Giving the higher-level abstractions the ability to skip layers and control the lower-level abstractions increases complexity and coupling.



def _all_hosts_have_been_processed(scan_completed: Event, hosts_to_exploit: Queue):
return scan_completed.is_set() and hosts_to_exploit.empty()
11 changes: 2 additions & 9 deletions monkey/infection_monkey/master/ip_scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
)

from . import IPScanResults
from .threading_utils import create_daemon_thread
from .threading_utils import run_worker_threads

logger = logging.getLogger()

Expand All @@ -35,14 +35,7 @@ def scan(self, ips_to_scan: List[str], options: Dict, results_callback: Callback
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()}")
Expand Down
67 changes: 58 additions & 9 deletions monkey/infection_monkey/master/propagator.py
Original file line number Diff line number Diff line change
@@ -1,41 +1,60 @@
import logging
from queue import Queue
from threading import Event, Thread
from threading import Event
from typing import Dict

from infection_monkey.i_puppet import FingerprintData, PingScanData, PortScanData, PortStatus
from infection_monkey.model.host import VictimHost
from infection_monkey.i_puppet import (
ExploiterResultData,
FingerprintData,
PingScanData,
PortScanData,
PortStatus,
)
from infection_monkey.model import VictimHost, VictimHostFactory
from infection_monkey.telemetry.exploit_telem import ExploitTelem
from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger
from infection_monkey.telemetry.scan_telem import ScanTelem

from . import IPScanner, IPScanResults
from . import Exploiter, IPScanner, IPScanResults
from .threading_utils import create_daemon_thread

logger = logging.getLogger()


class Propagator:
def __init__(self, telemetry_messenger: ITelemetryMessenger, ip_scanner: IPScanner):
def __init__(
self,
telemetry_messenger: ITelemetryMessenger,
ip_scanner: IPScanner,
exploiter: Exploiter,
victim_host_factory: VictimHostFactory,
):
self._telemetry_messenger = telemetry_messenger
self._ip_scanner = ip_scanner
self._exploiter = exploiter
self._victim_host_factory = victim_host_factory
self._hosts_to_exploit = None

def propagate(self, propagation_config: Dict, stop: Event):
logger.info("Attempting to propagate")

network_scan_completed = Event()
self._hosts_to_exploit = Queue()

scan_thread = create_daemon_thread(
target=self._scan_network, args=(propagation_config, stop)
)
exploit_thread = create_daemon_thread(
target=self._exploit_targets, args=(scan_thread, stop)
target=self._exploit_hosts,
args=(propagation_config, network_scan_completed, stop),
)

scan_thread.start()
exploit_thread.start()

scan_thread.join()
network_scan_completed.set()

exploit_thread.join()

logger.info("Finished attempting to propagate")
Expand All @@ -52,7 +71,7 @@ def _scan_network(self, propagation_config: Dict, stop: Event):
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)
Expand Down Expand Up @@ -95,5 +114,35 @@ def _process_fingerprinter_results(victim_host: VictimHost, fingerprint_data: Fi
for service, details in fd.services.items():
victim_host.services.setdefault(service, {}).update(details)

def _exploit_targets(self, scan_thread: Thread, stop: Event):
pass
def _exploit_hosts(
self,
propagation_config: Dict,
network_scan_completed: Event,
stop: Event,
):
logger.info("Exploiting victims")

exploiter_config = propagation_config["exploiters"]
self._exploiter.exploit_hosts(
exploiter_config,
self._hosts_to_exploit,
self._process_exploit_attempts,
network_scan_completed,
stop,
)

logger.info("Finished exploiting victims")

def _process_exploit_attempts(
self, exploiter_name: str, host: VictimHost, result: ExploiterResultData
):
if result.success:
logger.info(f"Successfully propagated to {host} using {exploiter_name}")
else:
logger.info(
f"Failed to propagate to {host} using {exploiter_name}: {result.error_message}"
)

self._telemetry_messenger.send_telemetry(
ExploitTelem(exploiter_name, host, result.success, result.info, result.attempts)
)
11 changes: 11 additions & 0 deletions monkey/infection_monkey/master/threading_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,16 @@
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)
1 change: 1 addition & 0 deletions monkey/infection_monkey/model/__init__.py
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
28 changes: 28 additions & 0 deletions monkey/infection_monkey/model/victim_host_factory.py
Original file line number Diff line number Diff line change
@@ -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
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will get done when we integrate the automated master with 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
19 changes: 16 additions & 3 deletions monkey/infection_monkey/puppet/mock_puppet.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,11 +280,24 @@ def exploit_host(
"executed_cmds": [],
}
successful_exploiters = {
DOT_1: {"PowerShellExploiter": ExploiterResultData(True, info_powershell, attempts)},
DOT_3: {"SSHExploiter": ExploiterResultData(False, info_ssh, attempts)},
DOT_1: {
"PowerShellExploiter": ExploiterResultData(True, info_powershell, attempts, None),
"ZerologonExploiter": ExploiterResultData(False, {}, [], "Zerologon failed"),
"SSHExploiter": ExploiterResultData(False, info_ssh, attempts, "Failed exploiting"),
},
DOT_3: {
"PowerShellExploiter": ExploiterResultData(
False, info_powershell, attempts, "PowerShell Exploiter Failed"
),
"SSHExploiter": ExploiterResultData(False, info_ssh, attempts, "Failed exploiting"),
"ZerologonExploiter": ExploiterResultData(True, {}, [], None),
},
}

return successful_exploiters[host][name]
try:
return successful_exploiters[host][name]
except KeyError:
return ExploiterResultData(False, {}, [], f"{name} failed for host {host}")

def run_payload(
self, name: str, options: Dict, interrupt: threading.Event
Expand Down
Loading