Skip to content

Commit f23437b

Browse files
committed
Merge branch '3167-mock-credential-collector' into develop
Issue #3167 PR #3247
2 parents ea2da23 + 2466ffb commit f23437b

File tree

31 files changed

+345
-154
lines changed

31 files changed

+345
-154
lines changed

envs/monkey_zoo/blackbox/test_configurations/credentials_reuse_ssh_key.py

+2-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import dataclasses
22
from typing import Dict, Mapping
33

4-
from common.agent_configuration import AgentConfiguration, PluginConfiguration
4+
from common.agent_configuration import AgentConfiguration
55
from common.credentials import Credentials, Password, Username
66

77
from .noop import noop_test_configuration
@@ -34,10 +34,7 @@ def _add_subnets(agent_configuration: AgentConfiguration) -> AgentConfiguration:
3434

3535

3636
def _add_credentials_collectors(agent_configuration: AgentConfiguration) -> AgentConfiguration:
37-
credentials_collectors = [
38-
PluginConfiguration(name="SSHCollector", options={}),
39-
]
40-
37+
credentials_collectors: Dict[str, Mapping] = {"SSHCollector": {}}
4138
return add_credentials_collectors(
4239
agent_configuration, credentials_collectors=credentials_collectors
4340
)

envs/monkey_zoo/blackbox/test_configurations/depth_1_a.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,9 @@ def _add_subnets(agent_configuration: AgentConfiguration) -> AgentConfiguration:
6464

6565

6666
def _add_credentials_collectors(agent_configuration: AgentConfiguration) -> AgentConfiguration:
67+
credentials_collectors: Dict[str, Mapping] = {"MimikatzCollector": {}}
6768
return add_credentials_collectors(
68-
agent_configuration, [PluginConfiguration(name="MimikatzCollector", options={})]
69+
agent_configuration, credentials_collectors=credentials_collectors
6970
)
7071

7172

envs/monkey_zoo/blackbox/test_configurations/noop.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939

4040
_agent_configuration = AgentConfiguration(
4141
keep_tunnel_open_time=0,
42-
credentials_collectors=[],
42+
credentials_collectors={},
4343
payloads={},
4444
propagation=_propagation_configuration,
4545
)

envs/monkey_zoo/blackbox/test_configurations/utils.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,10 @@ def add_subnets(
4545

4646

4747
def add_credentials_collectors(
48-
agent_configuration: AgentConfiguration, credentials_collectors: Sequence[PluginConfiguration]
48+
agent_configuration: AgentConfiguration, credentials_collectors: Dict[str, Mapping]
4949
) -> AgentConfiguration:
5050
agent_configuration_copy = agent_configuration.copy(deep=True)
51-
agent_configuration_copy.credentials_collectors = tuple(credentials_collectors)
51+
agent_configuration_copy.credentials_collectors = credentials_collectors
5252

5353
return agent_configuration_copy
5454

envs/monkey_zoo/blackbox/test_configurations/wmi_mimikatz.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import dataclasses
22
from typing import Dict, Mapping
33

4-
from common.agent_configuration import AgentConfiguration, PluginConfiguration
4+
from common.agent_configuration import AgentConfiguration
55
from common.credentials import Credentials, Password, Username
66

77
from .noop import noop_test_configuration
@@ -33,8 +33,9 @@ def _add_subnets(agent_configuration: AgentConfiguration) -> AgentConfiguration:
3333

3434

3535
def _add_credentials_collectors(agent_configuration: AgentConfiguration) -> AgentConfiguration:
36+
credentials_collectors: Dict[str, Mapping] = {"MimikatzCollector": {}}
3637
return add_credentials_collectors(
37-
agent_configuration, [PluginConfiguration(name="MimikatzCollector", options={})]
38+
agent_configuration, credentials_collectors=credentials_collectors
3839
)
3940

4041

@@ -48,7 +49,6 @@ def _add_tcp_ports(agent_configuration: AgentConfiguration) -> AgentConfiguratio
4849
test_agent_configuration = _add_subnets(test_agent_configuration)
4950
test_agent_configuration = _add_credentials_collectors(test_agent_configuration)
5051
test_agent_configuration = _add_tcp_ports(test_agent_configuration)
51-
test_agent_configuration = _add_credentials_collectors(test_agent_configuration)
5252

5353
CREDENTIALS = (
5454
Credentials(identity=Username(username="Administrator"), secret=None),

monkey/common/agent_configuration/agent_configuration.py

+7-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
from typing import Dict, Tuple
1+
from typing import Dict
22

33
from pydantic import Field, confloat
44

55
from common.base_models import MutableInfectionMonkeyBaseModel
66

7-
from .agent_sub_configurations import PluginConfiguration, PropagationConfiguration
7+
from .agent_sub_configurations import PropagationConfiguration
88

99

1010
class AgentConfiguration(MutableInfectionMonkeyBaseModel):
@@ -15,9 +15,11 @@ class AgentConfiguration(MutableInfectionMonkeyBaseModel):
1515
"seconds)",
1616
default=30,
1717
)
18-
credentials_collectors: Tuple[PluginConfiguration, ...] = Field(
19-
title="Credentials collectors",
20-
description="Configure options for the attack’s credentials collection stage",
18+
credentials_collectors: Dict[str, Dict] = Field(
19+
title="Enabled credentials collectors",
20+
description="Click on a credentials collector to get more information"
21+
" about it. \n \u26A0 Note that using unsafe options may"
22+
" result in unexpected behavior on the machine.",
2123
)
2224
payloads: Dict[str, Dict] = Field(
2325
title="Payloads", description="Configure payloads that Agents will execute"

monkey/common/agent_configuration/default_agent_configuration.py

+4-7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from copy import deepcopy
2+
from typing import Dict
23

34
from . import AgentConfiguration
45
from .agent_sub_configurations import (
@@ -12,11 +13,7 @@
1213
TCPScanConfiguration,
1314
)
1415

15-
CREDENTIALS_COLLECTORS = ("MimikatzCollector", "SSHCollector")
16-
17-
CREDENTIALS_COLLECTOR_CONFIGURATION = tuple(
18-
PluginConfiguration(name=collector, options={}) for collector in CREDENTIALS_COLLECTORS
19-
)
16+
CREDENTIALS_COLLECTORS: Dict[str, Dict] = {"MimikatzCollector": {}, "SSHCollector": {}}
2017

2118
RANSOMWARE_OPTIONS = {
2219
"encryption": {
@@ -93,10 +90,10 @@
9390

9491
DEFAULT_AGENT_CONFIGURATION = AgentConfiguration(
9592
keep_tunnel_open_time=30,
96-
credentials_collectors=CREDENTIALS_COLLECTOR_CONFIGURATION,
93+
credentials_collectors=CREDENTIALS_COLLECTORS,
9794
payloads=PAYLOAD_CONFIGURATION,
9895
propagation=PROPAGATION_CONFIGURATION,
9996
)
10097

10198
DEFAULT_RANSOMWARE_AGENT_CONFIGURATION = deepcopy(DEFAULT_AGENT_CONFIGURATION)
102-
DEFAULT_RANSOMWARE_AGENT_CONFIGURATION.credentials_collectors = tuple()
99+
DEFAULT_RANSOMWARE_AGENT_CONFIGURATION.credentials_collectors = {}

monkey/infection_monkey/credential_collectors/mimikatz_collector/mimikatz_credential_collector.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def __init__(self, agent_event_queue: IAgentEventQueue, agent_id: AgentID):
3131
self._agent_event_queue = agent_event_queue
3232
self._agent_id = agent_id
3333

34-
def collect_credentials(self, options=None) -> Sequence[Credentials]:
34+
def run(self, options=None, interrupt=None) -> Sequence[Credentials]:
3535
logger.info("Attempting to collect windows credentials with pypykatz.")
3636
windows_credentials = pypykatz_handler.get_windows_creds()
3737

monkey/infection_monkey/credential_collectors/ssh_collector/ssh_credential_collector.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def __init__(self, agent_event_queue: IAgentEventQueue, agent_id: AgentID):
1919
self._agent_event_queue = agent_event_queue
2020
self._agent_id = agent_id
2121

22-
def collect_credentials(self, _options=None) -> Sequence[Credentials]:
22+
def run(self, options=None, interrupt=None) -> Sequence[Credentials]:
2323
logger.info("Started scanning for SSH credentials")
2424
ssh_info = ssh_handler.get_ssh_info(self._agent_event_queue, self._agent_id)
2525
logger.info("Finished scanning for SSH credentials")

monkey/infection_monkey/i_puppet/i_credentials_collector.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
from typing import Mapping, Optional, Sequence
33

44
from common.credentials import Credentials
5+
from common.types import Event
56

67

78
class ICredentialsCollector(ABC):
89
@abstractmethod
9-
def collect_credentials(self, options: Optional[Mapping]) -> Sequence[Credentials]:
10+
def run(self, options: Optional[Mapping], interrupt: Event) -> Sequence[Credentials]:
1011
pass

monkey/infection_monkey/i_puppet/i_puppet.py

+24-26
Original file line numberDiff line numberDiff line change
@@ -27,32 +27,33 @@ def load_plugin(self, plugin_type: AgentPluginType, plugin_name: str, plugin: ob
2727
"""
2828
Loads a plugin into the puppet
2929
30-
:param AgentPluginType plugin_type: The type of plugin being loaded
31-
:param str plugin_name: The plugin class name
32-
:param object plugin: The plugin object to load
30+
:param plugin_type: The type of plugin being loaded
31+
:param plugin_name: The plugin class name
32+
:param plugin: The plugin object to load
3333
"""
3434

3535
@abc.abstractmethod
36-
def run_credentials_collector(self, name: str, options: Dict) -> Sequence[Credentials]:
36+
def run_credentials_collector(
37+
self, name: str, options: Dict, interrupt: Event
38+
) -> Sequence[Credentials]:
3739
"""
3840
Runs a credentials collector
3941
40-
:param str name: The name of the credentials collector to run
41-
:param Dict options: A dictionary containing options that modify the behavior of the
42-
Credentials collector
42+
:param name: The name of the credentials collector to run
43+
:param options: A dictionary containing options that modify the behavior of the
44+
Credentials collector
45+
:param interrupt: An event that can be used to interrupt the credentials collector
4346
:return: A sequence of Credentials that have been collected from the system
44-
:rtype: Sequence[Credentials]
4547
"""
4648

4749
@abc.abstractmethod
4850
def ping(self, host: str, timeout: float) -> PingScanData:
4951
"""
5052
Sends a ping (ICMP packet) to a remote host
5153
52-
:param str host: The domain name or IP address of a host
53-
:param float timeout: The maximum amount of time (in seconds) to wait for a response
54+
:param host: The domain name or IP address of a host
55+
:param timeout: The maximum amount of time (in seconds) to wait for a response
5456
:return: The data collected by attempting to ping the target host
55-
:rtype: PingScanData
5657
"""
5758

5859
@abc.abstractmethod
@@ -84,8 +85,7 @@ def fingerprint(
8485
:param name: The name of the fingerprinter to run
8586
:param host: The domain name or IP address of a host
8687
:param ping_scan_data: Data retrieved from the target host via ICMP
87-
:param port_scan_data: Data retrieved from the target host via a TCP
88-
port scan
88+
:param port_scan_data: Data retrieved from the target host via a TCP port scan
8989
:param options: A dictionary containing options that modify the behavior of the
9090
fingerprinter
9191
:return: Detailed information about the target host
@@ -104,29 +104,27 @@ def exploit_host(
104104
"""
105105
Runs an exploiter against a remote host
106106
107-
:param str name: The name of the exploiter to run
108-
:param TargetHost host: A TargetHost object representing the target to exploit
109-
:param int current_depth: The current propagation depth
107+
:param name: The name of the exploiter to run
108+
:param host: A TargetHost object representing the target to exploit
109+
:param current_depth: The current propagation depth
110110
:param servers: List of socket addresses for victim to connect back to
111-
:param Dict options: A dictionary containing options that modify the behavior of the
112-
exploiter
113-
:param Event interrupt: An `Event` object that signals the exploit to stop
114-
executing and clean itself up.
111+
:param options: A dictionary containing options that modify the behavior of the exploiter
112+
:param interrupt: An `Event` object that signals the exploit to stop executing and clean
113+
itself up.
115114
:raises IncompatibleOperatingSystemError: If an exploiter is not compatible with the target
116115
host's operating system
117-
:return: True if exploitation was successful, False otherwise
118-
:rtype: ExploiterResultData
116+
:return: The result of the exploit attempt
119117
"""
120118

121119
@abc.abstractmethod
122120
def run_payload(self, name: str, options: Dict, interrupt: Event):
123121
"""
124122
Runs a payload
125123
126-
:param str name: The name of the payload to run
127-
:param Dict options: A dictionary containing options that modify the behavior of the payload
128-
:param Event interrupt: An `Event` object that signals the payload to stop
129-
executing and clean itself up.
124+
:param name: The name of the payload to run
125+
:param options: A dictionary containing options that modify the behavior of the payload
126+
:param interrupt: An `Event` object that signals the payload to stop executing and clean
127+
itself up.
130128
"""
131129

132130
@abc.abstractmethod

monkey/infection_monkey/master/automated_master.py

+7-27
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@
22
import multiprocessing
33
import time
44
from ipaddress import IPv4Interface
5-
from typing import Any, Callable, Collection, Dict, List, Optional, Sequence
5+
from typing import Any, Callable, Dict, List, Optional, Sequence
66

77
from egg_timer import EggTimer
88

9-
from common.agent_configuration import PluginConfiguration
109
from infection_monkey.i_control_channel import IControlChannel, IslandCommunicationError
1110
from infection_monkey.i_master import IMaster
1211
from infection_monkey.i_puppet import IPuppet
@@ -127,7 +126,7 @@ def _run_simulation(self):
127126
return
128127

129128
credentials_collector_thread = create_daemon_thread(
130-
target=self._run_plugins_legacy,
129+
target=self._run_plugins,
131130
name="CredentialsCollectorThread",
132131
args=(
133132
config.credentials_collectors,
@@ -156,11 +155,13 @@ def _run_simulation(self):
156155
payload_thread.start()
157156
payload_thread.join()
158157

159-
def _collect_credentials(self, collector: PluginConfiguration):
160-
credentials = self._puppet.run_credentials_collector(collector.name, collector.options)
158+
def _collect_credentials(self, collector_name: str, collector_options: Dict[str, Any]):
159+
credentials = self._puppet.run_credentials_collector(
160+
collector_name, collector_options, self._stop
161+
)
161162

162163
if not credentials:
163-
logger.debug(f"No credentials were collected by {collector}")
164+
logger.debug(f"No credentials were collected by {collector_name}")
164165

165166
def _run_payload(self, name: str, options: Dict):
166167
self._puppet.run_payload(name, options, self._stop)
@@ -186,26 +187,5 @@ def _run_plugins(
186187

187188
logger.info(f"Finished running {plugin_type}s")
188189

189-
def _run_plugins_legacy(
190-
self,
191-
plugins: Collection[PluginConfiguration],
192-
plugin_type: str,
193-
callback: Callable[[Any], None],
194-
):
195-
logger.info(f"Running {plugin_type}s")
196-
logger.debug(f"Found {len(plugins)} {plugin_type}(s) to run")
197-
198-
interrupted_message = f"Received a stop signal, skipping remaining {plugin_type}s"
199-
for p in interruptible_iter(plugins, self._stop, interrupted_message):
200-
try:
201-
callback(p)
202-
except Exception:
203-
logger.exception(
204-
f"Got unhandled exception when running {plugin_type} plugin {p}. "
205-
f"Plugin was passed to {callback}"
206-
)
207-
208-
logger.info(f"Finished running {plugin_type}s")
209-
210190
def cleanup(self):
211191
pass

monkey/infection_monkey/puppet/plugin_registry.py

+11-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import logging
22
from copy import copy
33
from threading import RLock
4-
from typing import Any, Dict
4+
from typing import Dict
55

66
from serpentarium import SingleUsePlugin
77

@@ -42,7 +42,16 @@ def __init__(
4242

4343
self._lock = RLock()
4444

45-
def get_plugin(self, plugin_type: AgentPluginType, plugin_name: str) -> Any:
45+
def get_plugin(self, plugin_type: AgentPluginType, plugin_name: str) -> SingleUsePlugin:
46+
"""
47+
Retrieve a plugin stored in the registry. If the plugin does not exist in the registry, an
48+
attempt will be made to download it from the island.
49+
50+
:param plugin_type: The type of plugin to get.
51+
:param plugin_name: The name of the plugin to get.
52+
:return: A plugin of the given type and name.
53+
:raises UnknownPluginError: If the plugin is not found.
54+
"""
4655
with self._lock:
4756
try:
4857
# Note: The MultiprocessingPluginWrapper is a MultiUsePlugin. The copy() used here

monkey/infection_monkey/puppet/puppet.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,13 @@ def __init__(
4141
def load_plugin(self, plugin_type: AgentPluginType, plugin_name: str, plugin: object) -> None:
4242
self._plugin_registry.load_plugin(plugin_type, plugin_name, plugin)
4343

44-
def run_credentials_collector(self, name: str, options: Dict) -> Sequence[Credentials]:
44+
def run_credentials_collector(
45+
self, name: str, options: Dict, interrupt: Event
46+
) -> Sequence[Credentials]:
4547
credentials_collector = self._plugin_registry.get_plugin(
4648
AgentPluginType.CREDENTIALS_COLLECTOR, name
4749
)
48-
return credentials_collector.collect_credentials(options)
50+
return credentials_collector.run(options=options, interrupt=interrupt)
4951

5052
def ping(self, host: str, timeout: float = CONNECTION_TIMEOUT) -> PingScanData:
5153
return network_scanning.ping(host, timeout, self._agent_event_queue, self._agent_id)

0 commit comments

Comments
 (0)