-
Notifications
You must be signed in to change notification settings - Fork 786
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch '3163-wmi-plugin-dot-py' into develop
- Loading branch information
Showing
6 changed files
with
419 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
import logging | ||
from functools import partial | ||
from pprint import pformat | ||
from typing import Any, Dict, Sequence | ||
from uuid import UUID | ||
|
||
# common imports | ||
from common.credentials import LMHash, NTHash, Password | ||
from common.event_queue import IAgentEventPublisher | ||
from common.types import Event | ||
from common.utils.code_utils import del_key | ||
|
||
# dependencies to get rid of or internalize | ||
from infection_monkey.exploit import IAgentBinaryRepository, IAgentOTPProvider | ||
from infection_monkey.exploit.tools import ( | ||
BruteForceCredentialsProvider, | ||
BruteForceExploiter, | ||
all_exploitation_ports_are_closed, | ||
generate_brute_force_credentials, | ||
secret_type_filter, | ||
) | ||
from infection_monkey.exploit.tools.helpers import get_agent_dst_path | ||
from infection_monkey.i_puppet import ExploiterResultData, TargetHost | ||
from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository | ||
|
||
from .wmi_command_builder import build_wmi_command | ||
from .wmi_options import WMIOptions | ||
|
||
WMI_PORTS = [135] | ||
|
||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
def should_attempt_exploit(host: TargetHost) -> bool: | ||
return not all_exploitation_ports_are_closed(host, WMI_PORTS) | ||
|
||
|
||
class PlaceholderWMIExploitClientFactory: | ||
def __init__(self, *args, **kwargs): | ||
pass | ||
|
||
|
||
class Plugin: | ||
def __init__( | ||
self, | ||
*, | ||
plugin_name: str, | ||
agent_id: UUID, | ||
agent_event_publisher: IAgentEventPublisher, | ||
agent_binary_repository: IAgentBinaryRepository, | ||
propagation_credentials_repository: IPropagationCredentialsRepository, | ||
otp_provider: IAgentOTPProvider, | ||
**kwargs, | ||
): | ||
self._plugin_name = plugin_name | ||
self._agent_id = agent_id | ||
self._agent_event_publisher = agent_event_publisher | ||
self._agent_binary_repository = agent_binary_repository | ||
credentials_generator = partial( | ||
generate_brute_force_credentials, | ||
secret_filter=secret_type_filter([LMHash, NTHash, Password]), | ||
) | ||
self._credentials_provider = BruteForceCredentialsProvider( | ||
propagation_credentials_repository, credentials_generator | ||
) | ||
self._otp_provider = otp_provider | ||
|
||
def run( | ||
self, | ||
*, | ||
host: TargetHost, | ||
servers: Sequence[str], | ||
current_depth: int, | ||
options: Dict[str, Any], | ||
interrupt: Event, | ||
**kwargs, | ||
) -> ExploiterResultData: | ||
# HTTP ports options are hack because they are needed in fingerprinters | ||
del_key(options, "http_ports") | ||
|
||
try: | ||
logger.debug(f"Parsing options: {pformat(options)}") | ||
wmi_options = WMIOptions(**options) | ||
except Exception as err: | ||
msg = f"Failed to parse WMI options: {err}" | ||
logger.exception(msg) | ||
return ExploiterResultData(error_message=msg) | ||
|
||
if not should_attempt_exploit(host): | ||
msg = f"Host {host.ip} has no open WMI ports" | ||
logger.debug(msg) | ||
return ExploiterResultData( | ||
exploitation_success=False, propagation_success=False, error_message=msg | ||
) | ||
|
||
command_builder = partial( | ||
build_wmi_command, | ||
self._agent_id, | ||
servers, | ||
current_depth, | ||
remote_agent_binary_destination_path=get_agent_dst_path(host), | ||
otp_provider=self._otp_provider, | ||
) | ||
wmi_exploit_client_factory = PlaceholderWMIExploitClientFactory( | ||
host, wmi_options, command_builder | ||
) | ||
|
||
brute_force_exploiter = BruteForceExploiter( | ||
self._plugin_name, | ||
self._agent_id, | ||
get_agent_dst_path(host), | ||
wmi_exploit_client_factory, | ||
self._credentials_provider, | ||
self._agent_binary_repository, | ||
self._agent_event_publisher, | ||
{"wmi-exploiter"}, | ||
) | ||
|
||
try: | ||
logger.debug(f"Running WMI exploiter on host {host.ip}") | ||
return brute_force_exploiter.exploit_host(host, interrupt) | ||
except Exception as err: | ||
msg = f"An unexpected exception occurred while attempting to exploit host: {err}" | ||
logger.exception(msg) | ||
return ExploiterResultData(error_message=msg) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
from typing import Sequence | ||
|
||
from infection_monkey.i_puppet import TargetHost | ||
|
||
|
||
def all_exploitation_ports_are_closed(host: TargetHost, exploitation_ports: Sequence[int]) -> bool: | ||
closed_tcp_ports = host.ports_status.tcp_ports.closed | ||
return all([p in closed_tcp_ports for p in exploitation_ports]) |
209 changes: 209 additions & 0 deletions
209
monkey/tests/unit_tests/agent_plugins/exploiters/wmi/test_wmi_plugin.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,209 @@ | ||
from ipaddress import IPv4Address | ||
from threading import Event | ||
from unittest.mock import MagicMock | ||
from uuid import UUID | ||
|
||
import pytest | ||
from agent_plugins.exploiters.wmi.src.plugin import WMI_PORTS, Plugin | ||
|
||
from common import OperatingSystem | ||
from common.types import PortStatus | ||
from infection_monkey.exploit.tools import BruteForceExploiter | ||
from infection_monkey.i_puppet import ( | ||
ExploiterResultData, | ||
PortScanData, | ||
PortScanDataDict, | ||
TargetHost, | ||
TargetHostPorts, | ||
) | ||
from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository | ||
|
||
AGENT_ID = UUID("5c145d4e-ec61-44f7-998e-17477112f50f") | ||
BAD_WMI_OPTIONS_DICT = {"blah": "blah"} | ||
TARGET_IP = IPv4Address("1.1.1.1") | ||
OPEN_WMI_PORTS = TargetHostPorts( | ||
tcp_ports=PortScanDataDict({p: PortScanData(port=p, status=PortStatus.OPEN) for p in WMI_PORTS}) | ||
) | ||
OTHER_PORT = 9999 | ||
EMPTY_TARGET_HOST_PORTS = TargetHostPorts() | ||
SERVERS = ["10.10.10.10"] | ||
EXPLOITER_RESULT_DATA = ExploiterResultData(True, False, error_message="Test error") | ||
|
||
|
||
@pytest.fixture | ||
def target_host() -> TargetHost: | ||
return TargetHost( | ||
ip=TARGET_IP, | ||
operating_system=OperatingSystem.WINDOWS, | ||
ports_status=OPEN_WMI_PORTS, | ||
) | ||
|
||
|
||
@pytest.fixture | ||
def propagation_credentials_repository(): | ||
return MagicMock(spec=IPropagationCredentialsRepository) | ||
|
||
|
||
class ErrorRaisingMockWMIExploiter(BruteForceExploiter): | ||
def __init__(self, *args, **kwargs): | ||
super().__init__(*args, **kwargs) | ||
|
||
def exploit_host(self, *args, **kwargs) -> ExploiterResultData: | ||
raise Exception("Test error") | ||
|
||
|
||
@pytest.fixture | ||
def mock_wmi_exploiter(): | ||
exploiter = MagicMock(spec=BruteForceExploiter) | ||
exploiter.exploit_host.return_value = EXPLOITER_RESULT_DATA | ||
return exploiter | ||
|
||
|
||
@pytest.fixture | ||
def plugin( | ||
monkeypatch, | ||
propagation_credentials_repository: IPropagationCredentialsRepository, | ||
mock_wmi_exploiter: BruteForceExploiter, | ||
) -> Plugin: | ||
monkeypatch.setattr( | ||
"agent_plugins.exploiters.wmi.src.plugin.BruteForceExploiter", | ||
lambda *args, **kwargs: mock_wmi_exploiter, | ||
) | ||
|
||
return Plugin( | ||
plugin_name="WMI", | ||
agent_id=AGENT_ID, | ||
agent_event_publisher=MagicMock(), | ||
agent_binary_repository=MagicMock(), | ||
propagation_credentials_repository=propagation_credentials_repository, | ||
otp_provider=MagicMock(), | ||
) | ||
|
||
|
||
def test_run__fails_on_bad_options(plugin: Plugin, target_host: TargetHost): | ||
result = plugin.run( | ||
host=target_host, | ||
servers=SERVERS, | ||
current_depth=1, | ||
options=BAD_WMI_OPTIONS_DICT, | ||
interrupt=Event(), | ||
) | ||
|
||
assert not result.exploitation_success | ||
assert not result.propagation_success | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"tcp_port_status", | ||
( | ||
PortScanDataDict({OTHER_PORT: PortScanData(port=OTHER_PORT, status=PortStatus.CLOSED)}), | ||
PortScanDataDict({}), | ||
), | ||
) | ||
def test_run__attempts_exploit_if_port_status_unknown( | ||
plugin: Plugin, | ||
mock_wmi_exploiter: BruteForceExploiter, | ||
target_host: TargetHost, | ||
tcp_port_status: PortScanDataDict, | ||
): | ||
host = target_host | ||
host.ports_status.tcp_ports = tcp_port_status | ||
result = plugin.run( | ||
host=host, | ||
servers=SERVERS, | ||
current_depth=1, | ||
options={}, | ||
interrupt=Event(), | ||
) | ||
|
||
mock_wmi_exploiter.exploit_host.assert_called_once() | ||
assert result == EXPLOITER_RESULT_DATA | ||
|
||
|
||
def test_run__attempts_exploit_if_port_status_open( | ||
plugin: Plugin, | ||
mock_wmi_exploiter: BruteForceExploiter, | ||
target_host: TargetHost, | ||
): | ||
host = target_host | ||
host.ports_status.tcp_ports = PortScanDataDict( | ||
{WMI_PORTS[0]: PortScanData(port=WMI_PORTS[0], status=PortStatus.OPEN)} | ||
) | ||
result = plugin.run( | ||
host=host, | ||
servers=SERVERS, | ||
current_depth=1, | ||
options={}, | ||
interrupt=Event(), | ||
) | ||
|
||
mock_wmi_exploiter.exploit_host.assert_called_once() | ||
assert result == EXPLOITER_RESULT_DATA | ||
|
||
|
||
def test_run__skips_exploit_if_port_status_closed( | ||
plugin: Plugin, | ||
mock_wmi_exploiter: BruteForceExploiter, | ||
target_host: TargetHost, | ||
): | ||
host = target_host | ||
host.ports_status.tcp_ports = PortScanDataDict( | ||
{ | ||
WMI_PORTS[0]: PortScanData(port=WMI_PORTS[0], status=PortStatus.CLOSED), | ||
} | ||
) | ||
|
||
result = plugin.run( | ||
host=host, | ||
servers=SERVERS, | ||
current_depth=1, | ||
options={}, | ||
interrupt=Event(), | ||
) | ||
|
||
mock_wmi_exploiter.exploit_host.assert_not_called() | ||
assert result.exploitation_success is False | ||
assert result.propagation_success is False | ||
|
||
|
||
def test_run__returns_exploiter_result_data(plugin: Plugin, target_host: TargetHost): | ||
result = plugin.run( | ||
host=target_host, | ||
servers=SERVERS, | ||
current_depth=1, | ||
options={}, | ||
interrupt=Event(), | ||
) | ||
|
||
assert result == EXPLOITER_RESULT_DATA | ||
|
||
|
||
def test_run__exploit_host_raises_exception( | ||
monkeypatch, | ||
plugin: Plugin, | ||
propagation_credentials_repository: IPropagationCredentialsRepository, | ||
target_host: TargetHost, | ||
): | ||
monkeypatch.setattr( | ||
"agent_plugins.exploiters.wmi.src.plugin.BruteForceExploiter", | ||
ErrorRaisingMockWMIExploiter, | ||
) | ||
|
||
plugin = Plugin( | ||
plugin_name="WMI", | ||
agent_id=AGENT_ID, | ||
agent_event_publisher=MagicMock(), | ||
agent_binary_repository=MagicMock(), | ||
propagation_credentials_repository=propagation_credentials_repository, | ||
otp_provider=MagicMock(), | ||
) | ||
result = plugin.run( | ||
host=target_host, | ||
servers=SERVERS, | ||
current_depth=1, | ||
options={}, | ||
interrupt=Event(), | ||
) | ||
|
||
assert not result.exploitation_success | ||
assert not result.propagation_success |
Oops, something went wrong.