Skip to content

Commit 2edaf52

Browse files
authored
Merge pull request #2196 from guardicore/2176-modify-ssh-collector-for-events
2176 modify ssh collector for events
2 parents d09c1a6 + eec48e9 commit 2edaf52

File tree

6 files changed

+116
-62
lines changed

6 files changed

+116
-62
lines changed

monkey/common/events/abstract_event.py

+7-6
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
import time
12
from abc import ABC
2-
from dataclasses import dataclass
3+
from dataclasses import dataclass, field
34
from ipaddress import IPv4Address
45
from typing import FrozenSet, Union
5-
from uuid import UUID
6+
from uuid import UUID, getnode
67

78

89
@dataclass(frozen=True)
@@ -21,7 +22,7 @@ class AbstractEvent(ABC):
2122
:param tags: The set of tags associated with the event
2223
"""
2324

24-
source: UUID
25-
target: Union[UUID, IPv4Address, None]
26-
timestamp: float
27-
tags: FrozenSet[str]
25+
source: UUID = field(default_factory=getnode)
26+
target: Union[UUID, IPv4Address, None] = field(default=None)
27+
timestamp: float = field(default_factory=time.time)
28+
tags: FrozenSet[str] = field(default_factory=frozenset)

monkey/common/events/credentials_stolen_events.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from dataclasses import dataclass
1+
from dataclasses import dataclass, field
22
from typing import Sequence
33

44
from common.credentials import Credentials
@@ -15,4 +15,4 @@ class CredentialsStolenEvent(AbstractEvent):
1515
:param stolen_credentials: The credentials that were stolen by an agent
1616
"""
1717

18-
stolen_credentials: Sequence[Credentials]
18+
stolen_credentials: Sequence[Credentials] = field(default_factory=list)
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import logging
2-
from typing import Dict, Iterable, Sequence
2+
from typing import Sequence
33

4-
from common.credentials import Credentials, SSHKeypair, Username
4+
from common.credentials import Credentials
5+
from common.event_queue import IEventQueue
56
from infection_monkey.credential_collectors.ssh_collector import ssh_handler
67
from infection_monkey.i_puppet import ICredentialCollector
78
from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger
@@ -14,38 +15,13 @@ class SSHCredentialCollector(ICredentialCollector):
1415
SSH keys credential collector
1516
"""
1617

17-
def __init__(self, telemetry_messenger: ITelemetryMessenger):
18+
def __init__(self, telemetry_messenger: ITelemetryMessenger, event_queue: IEventQueue):
1819
self._telemetry_messenger = telemetry_messenger
20+
self._event_queue = event_queue
1921

2022
def collect_credentials(self, _options=None) -> Sequence[Credentials]:
2123
logger.info("Started scanning for SSH credentials")
22-
ssh_info = ssh_handler.get_ssh_info(self._telemetry_messenger)
24+
ssh_info = ssh_handler.get_ssh_info(self._telemetry_messenger, self._event_queue)
2325
logger.info("Finished scanning for SSH credentials")
2426

25-
return SSHCredentialCollector._to_credentials(ssh_info)
26-
27-
@staticmethod
28-
def _to_credentials(ssh_info: Iterable[Dict]) -> Sequence[Credentials]:
29-
ssh_credentials = []
30-
31-
for info in ssh_info:
32-
identity = None
33-
secret = None
34-
35-
if info.get("name", ""):
36-
identity = Username(info["name"])
37-
38-
ssh_keypair = {}
39-
for key in ["public_key", "private_key"]:
40-
if info.get(key) is not None:
41-
ssh_keypair[key] = info[key]
42-
43-
if len(ssh_keypair):
44-
secret = SSHKeypair(
45-
ssh_keypair.get("private_key", ""), ssh_keypair.get("public_key", "")
46-
)
47-
48-
if any([identity, secret]):
49-
ssh_credentials.append(Credentials(identity, secret))
50-
51-
return ssh_credentials
27+
return ssh_handler.to_credentials(ssh_info)

monkey/infection_monkey/credential_collectors/ssh_collector/ssh_handler.py

+63-7
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import glob
22
import logging
33
import os
4-
from typing import Dict, Iterable
4+
from typing import Dict, Iterable, Sequence
55

6+
from common.credentials import Credentials, SSHKeypair, Username
7+
from common.event_queue import IEventQueue
8+
from common.events import CredentialsStolenEvent
69
from common.utils.attack_utils import ScanStatus
710
from infection_monkey.telemetry.attack.t1005_telem import T1005Telem
811
from infection_monkey.telemetry.attack.t1145_telem import T1145Telem
@@ -12,9 +15,22 @@
1215
logger = logging.getLogger(__name__)
1316

1417
DEFAULT_DIRS = ["/.ssh/", "/"]
18+
SSH_CREDENTIAL_COLLECTOR_TAG = "ssh-credentials-collector"
19+
T1003_ATTACK_TECHNIQUE_TAG = "attack-t1003"
20+
T1005_ATTACK_TECHNIQUE_TAG = "attack-t1005"
21+
T1145_ATTACK_TECHNIQUE_TAG = "attack-t1145"
1522

23+
SSH_COLLECTOR_EVENT_TAG = {
24+
SSH_CREDENTIAL_COLLECTOR_TAG,
25+
T1003_ATTACK_TECHNIQUE_TAG,
26+
T1005_ATTACK_TECHNIQUE_TAG,
27+
T1145_ATTACK_TECHNIQUE_TAG,
28+
}
1629

17-
def get_ssh_info(telemetry_messenger: ITelemetryMessenger) -> Iterable[Dict]:
30+
31+
def get_ssh_info(
32+
telemetry_messenger: ITelemetryMessenger, event_queue: IEventQueue
33+
) -> Iterable[Dict]:
1834
# TODO: Remove this check when this is turned into a plugin.
1935
if is_windows_os():
2036
logger.debug(
@@ -23,7 +39,7 @@ def get_ssh_info(telemetry_messenger: ITelemetryMessenger) -> Iterable[Dict]:
2339
return []
2440

2541
home_dirs = _get_home_dirs()
26-
ssh_info = _get_ssh_files(home_dirs, telemetry_messenger)
42+
ssh_info = _get_ssh_files(home_dirs, telemetry_messenger, event_queue)
2743

2844
return ssh_info
2945

@@ -62,9 +78,9 @@ def _get_ssh_struct(name: str, home_dir: str) -> Dict:
6278

6379

6480
def _get_ssh_files(
65-
usr_info: Iterable[Dict], telemetry_messenger: ITelemetryMessenger
81+
user_info: Iterable[Dict], telemetry_messenger: ITelemetryMessenger, event_queue: IEventQueue
6682
) -> Iterable[Dict]:
67-
for info in usr_info:
83+
for info in user_info:
6884
path = info["home_dir"]
6985
for directory in DEFAULT_DIRS:
7086
# TODO: Use PATH
@@ -101,6 +117,11 @@ def _get_ssh_files(
101117
ScanStatus.USED, info["name"], info["home_dir"]
102118
)
103119
)
120+
121+
collected_credentials = to_credentials([info])
122+
_publish_credentials_stolen_event(
123+
collected_credentials, event_queue
124+
)
104125
else:
105126
continue
106127
except (IOError, OSError):
@@ -112,5 +133,40 @@ def _get_ssh_files(
112133
pass
113134
except OSError:
114135
pass
115-
usr_info = [info for info in usr_info if info["private_key"] or info["public_key"]]
116-
return usr_info
136+
user_info = [info for info in user_info if info["private_key"] or info["public_key"]]
137+
return user_info
138+
139+
140+
def to_credentials(ssh_info: Iterable[Dict]) -> Sequence[Credentials]:
141+
ssh_credentials = []
142+
143+
for info in ssh_info:
144+
identity = None
145+
secret = None
146+
147+
if info.get("name", ""):
148+
identity = Username(info["name"])
149+
150+
ssh_keypair = {}
151+
for key in ["public_key", "private_key"]:
152+
if info.get(key) is not None:
153+
ssh_keypair[key] = info[key]
154+
155+
if len(ssh_keypair):
156+
secret = SSHKeypair(
157+
ssh_keypair.get("private_key", ""), ssh_keypair.get("public_key", "")
158+
)
159+
160+
if any([identity, secret]):
161+
ssh_credentials.append(Credentials(identity, secret))
162+
163+
return ssh_credentials
164+
165+
166+
def _publish_credentials_stolen_event(collected_credentials: Credentials, event_queue: IEventQueue):
167+
credentials_stolen_event = CredentialsStolenEvent(
168+
tags=frozenset(SSH_COLLECTOR_EVENT_TAG),
169+
stolen_credentials=[collected_credentials],
170+
)
171+
172+
event_queue.publish(credentials_stolen_event)

monkey/infection_monkey/monkey.py

+29-13
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from pubsub.core import Publisher
1010

1111
import infection_monkey.tunnel as tunnel
12-
from common.event_queue import PyPubSubEventQueue
12+
from common.event_queue import IEventQueue, PyPubSubEventQueue
1313
from common.events import CredentialsStolenEvent
1414
from common.network.network_utils import address_to_ip_port
1515
from common.utils.argparse_types import positive_int
@@ -199,26 +199,26 @@ def _setup(self):
199199
def _build_master(self):
200200
local_network_interfaces = InfectionMonkey._get_local_network_interfaces()
201201

202-
_event_queue = PyPubSubEventQueue(Publisher())
203-
_event_queue.subscribe_type(
204-
CredentialsStolenEvent, add_credentials_from_event_to_propagation_credentials_repository
205-
)
206-
207202
# TODO control_channel and control_client have same responsibilities, merge them
208203
control_channel = ControlChannel(
209204
self._control_client.server_address, GUID, self._control_client.proxies
210205
)
211-
credentials_store = AggregatingPropagationCredentialsRepository(control_channel)
206+
propagation_credentials_repository = AggregatingPropagationCredentialsRepository(
207+
control_channel
208+
)
209+
210+
event_queue = PyPubSubEventQueue(Publisher())
211+
InfectionMonkey._subscribe_events(event_queue, propagation_credentials_repository)
212212

213-
puppet = self._build_puppet(credentials_store)
213+
puppet = self._build_puppet(propagation_credentials_repository, event_queue)
214214

215215
victim_host_factory = self._build_victim_host_factory(local_network_interfaces)
216216

217217
telemetry_messenger = CredentialsInterceptingTelemetryMessenger(
218218
ExploitInterceptingTelemetryMessenger(
219219
self._telemetry_messenger, self._monkey_inbound_tunnel
220220
),
221-
credentials_store,
221+
propagation_credentials_repository,
222222
)
223223

224224
self._master = AutomatedMaster(
@@ -228,7 +228,19 @@ def _build_master(self):
228228
victim_host_factory,
229229
control_channel,
230230
local_network_interfaces,
231-
credentials_store,
231+
propagation_credentials_repository,
232+
)
233+
234+
@staticmethod
235+
def _subscribe_events(
236+
event_queue: IEventQueue,
237+
propagation_credentials_repository: IPropagationCredentialsRepository,
238+
):
239+
event_queue.subscribe_type(
240+
CredentialsStolenEvent,
241+
add_credentials_from_event_to_propagation_credentials_repository(
242+
propagation_credentials_repository
243+
),
232244
)
233245

234246
@staticmethod
@@ -239,7 +251,11 @@ def _get_local_network_interfaces():
239251

240252
return local_network_interfaces
241253

242-
def _build_puppet(self, credentials_store: IPropagationCredentialsRepository) -> IPuppet:
254+
def _build_puppet(
255+
self,
256+
propagation_credentials_repository: IPropagationCredentialsRepository,
257+
event_queue: IEventQueue,
258+
) -> IPuppet:
243259
puppet = Puppet()
244260

245261
puppet.load_plugin(
@@ -249,7 +265,7 @@ def _build_puppet(self, credentials_store: IPropagationCredentialsRepository) ->
249265
)
250266
puppet.load_plugin(
251267
"SSHCollector",
252-
SSHCredentialCollector(self._telemetry_messenger),
268+
SSHCredentialCollector(self._telemetry_messenger, event_queue),
253269
PluginType.CREDENTIAL_COLLECTOR,
254270
)
255271

@@ -281,7 +297,7 @@ def _build_puppet(self, credentials_store: IPropagationCredentialsRepository) ->
281297
)
282298

283299
zerologon_telemetry_messenger = CredentialsInterceptingTelemetryMessenger(
284-
self._telemetry_messenger, credentials_store
300+
self._telemetry_messenger, propagation_credentials_repository
285301
)
286302
zerologon_wrapper = ExploiterWrapper(zerologon_telemetry_messenger, agent_repository)
287303
puppet.load_plugin(

monkey/tests/unit_tests/infection_monkey/credential_collectors/test_ssh_credentials_collector.py

+8-3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import pytest
44

55
from common.credentials import Credentials, SSHKeypair, Username
6+
from common.event_queue import IEventQueue
67
from infection_monkey.credential_collectors import SSHCredentialCollector
78

89

@@ -14,7 +15,7 @@ def patch_telemetry_messenger():
1415
def patch_ssh_handler(ssh_creds, monkeypatch):
1516
monkeypatch.setattr(
1617
"infection_monkey.credential_collectors.ssh_collector.ssh_handler.get_ssh_info",
17-
lambda _: ssh_creds,
18+
lambda _, __: ssh_creds,
1819
)
1920

2021

@@ -23,7 +24,9 @@ def patch_ssh_handler(ssh_creds, monkeypatch):
2324
)
2425
def test_ssh_credentials_empty_results(monkeypatch, ssh_creds, patch_telemetry_messenger):
2526
patch_ssh_handler(ssh_creds, monkeypatch)
26-
collected = SSHCredentialCollector(patch_telemetry_messenger).collect_credentials()
27+
collected = SSHCredentialCollector(
28+
patch_telemetry_messenger, MagicMock(spec=IEventQueue)
29+
).collect_credentials()
2730
assert not collected
2831

2932

@@ -67,5 +70,7 @@ def test_ssh_info_result_parsing(monkeypatch, patch_telemetry_messenger):
6770
Credentials(identity=username3, secret=None),
6871
Credentials(identity=None, secret=ssh_keypair3),
6972
]
70-
collected = SSHCredentialCollector(patch_telemetry_messenger).collect_credentials()
73+
collected = SSHCredentialCollector(
74+
patch_telemetry_messenger, MagicMock(spec=IEventQueue)
75+
).collect_credentials()
7176
assert expected == collected

0 commit comments

Comments
 (0)