Skip to content

Commit

Permalink
Merge branch '3163-wmi-plugin-dot-py' into develop
Browse files Browse the repository at this point in the history
Issue #3163
PR #3266
  • Loading branch information
mssalvatore committed Apr 27, 2023
2 parents 5e8988d + 2bce52d commit ed940f9
Show file tree
Hide file tree
Showing 6 changed files with 419 additions and 6 deletions.
17 changes: 11 additions & 6 deletions monkey/agent_plugins/exploiters/smb/src/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
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
Expand All @@ -14,7 +15,9 @@
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
Expand All @@ -25,13 +28,11 @@
from .smb_remote_access_client import SMB_PORTS
from .smb_remote_access_client_factory import SMBRemoteAccessClientFactory


def should_attempt_exploit(host: TargetHost) -> bool:
closed_tcp_ports = host.ports_status.tcp_ports.closed
return not all([p in closed_tcp_ports for p in SMB_PORTS])
logger = logging.getLogger(__name__)


logger = logging.getLogger(__name__)
def should_attempt_exploit(host: TargetHost) -> bool:
return not all_exploitation_ports_are_closed(host, SMB_PORTS)


class Plugin:
Expand All @@ -50,8 +51,12 @@ def __init__(
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, generate_brute_force_credentials
propagation_credentials_repository, credentials_generator
)
self._otp_provider = otp_provider

Expand Down
126 changes: 126 additions & 0 deletions monkey/agent_plugins/exploiters/wmi/src/plugin.py
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)
1 change: 1 addition & 0 deletions monkey/infection_monkey/exploit/tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@
from .i_remote_access_client_factory import IRemoteAccessClientFactory
from .brute_force_credentials_provider import BruteForceCredentialsProvider
from .brute_force_exploiter import BruteForceExploiter
from .utils import all_exploitation_ports_are_closed
8 changes: 8 additions & 0 deletions monkey/infection_monkey/exploit/tools/utils.py
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])
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
Loading

0 comments on commit ed940f9

Please sign in to comment.