diff --git a/monkey/infection_monkey/credential_collectors/mimikatz_collector/mimikatz_credential_collector.py b/monkey/infection_monkey/credential_collectors/mimikatz_collector/mimikatz_credential_collector.py index e6bc04a31..a39023e0e 100644 --- a/monkey/infection_monkey/credential_collectors/mimikatz_collector/mimikatz_credential_collector.py +++ b/monkey/infection_monkey/credential_collectors/mimikatz_collector/mimikatz_credential_collector.py @@ -2,6 +2,8 @@ import logging from typing import Sequence from common.credentials import Credentials, LMHash, NTHash, Password, Username +from common.event_queue import IEventQueue +from common.events import CredentialsStolenEvent from infection_monkey.i_puppet import ICredentialCollector from infection_monkey.model import USERNAME_PREFIX @@ -11,12 +13,34 @@ from .windows_credentials import WindowsCredentials logger = logging.getLogger(__name__) +MIMIKATZ_CREDENTIAL_COLLECTOR_TAG = "mimikatz-credentials-collector" +T1003_ATTACK_TECHNIQUE_TAG = "attack-t1003" +T1005_ATTACK_TECHNIQUE_TAG = "attack-t1005" + +MIMIKATZ_EVENT_TAGS = frozenset( + ( + MIMIKATZ_CREDENTIAL_COLLECTOR_TAG, + T1003_ATTACK_TECHNIQUE_TAG, + T1005_ATTACK_TECHNIQUE_TAG, + ) +) + + class MimikatzCredentialCollector(ICredentialCollector): + def __init__(self, event_queue: IEventQueue): + self._event_queue = event_queue + def collect_credentials(self, options=None) -> Sequence[Credentials]: logger.info("Attempting to collect windows credentials with pypykatz.") windows_credentials = pypykatz_handler.get_windows_creds() + logger.info(f"Pypykatz gathered {len(windows_credentials)} credentials.") - return MimikatzCredentialCollector._to_credentials(windows_credentials) + + collected_credentials = MimikatzCredentialCollector._to_credentials(windows_credentials) + + self._publish_credentials_stolen_event(collected_credentials) + + return collected_credentials @staticmethod def _to_credentials(windows_credentials: Sequence[WindowsCredentials]) -> Sequence[Credentials]: @@ -49,3 +73,11 @@ class MimikatzCredentialCollector(ICredentialCollector): credentials.append(Credentials(identity, None)) return credentials + + def _publish_credentials_stolen_event(self, collected_credentials: Sequence[Credentials]): + credentials_stolen_event = CredentialsStolenEvent( + tags=MIMIKATZ_EVENT_TAGS, + stolen_credentials=collected_credentials, + ) + + self._event_queue.publish(credentials_stolen_event) diff --git a/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_handler.py b/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_handler.py index 097d59f40..7d8a046f4 100644 --- a/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_handler.py +++ b/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_handler.py @@ -20,12 +20,14 @@ T1003_ATTACK_TECHNIQUE_TAG = "attack-t1003" T1005_ATTACK_TECHNIQUE_TAG = "attack-t1005" T1145_ATTACK_TECHNIQUE_TAG = "attack-t1145" -SSH_COLLECTOR_EVENT_TAG = { - SSH_CREDENTIAL_COLLECTOR_TAG, - T1003_ATTACK_TECHNIQUE_TAG, - T1005_ATTACK_TECHNIQUE_TAG, - T1145_ATTACK_TECHNIQUE_TAG, -} +SSH_COLLECTOR_EVENT_TAGS = frozenset( + ( + SSH_CREDENTIAL_COLLECTOR_TAG, + T1003_ATTACK_TECHNIQUE_TAG, + T1005_ATTACK_TECHNIQUE_TAG, + T1145_ATTACK_TECHNIQUE_TAG, + ) +) def get_ssh_info( @@ -165,7 +167,7 @@ def to_credentials(ssh_info: Iterable[Dict]) -> Sequence[Credentials]: def _publish_credentials_stolen_event(collected_credentials: Credentials, event_queue: IEventQueue): credentials_stolen_event = CredentialsStolenEvent( - tags=frozenset(SSH_COLLECTOR_EVENT_TAG), + tags=SSH_COLLECTOR_EVENT_TAGS, stolen_credentials=[collected_credentials], ) diff --git a/monkey/tests/unit_tests/common/event_queue/test_pypubsub_event_queue.py b/monkey/tests/unit_tests/common/event_queue/test_pypubsub_event_queue.py index 741d9b8ee..006a27916 100644 --- a/monkey/tests/unit_tests/common/event_queue/test_pypubsub_event_queue.py +++ b/monkey/tests/unit_tests/common/event_queue/test_pypubsub_event_queue.py @@ -1,6 +1,6 @@ from dataclasses import dataclass from ipaddress import IPv4Address -from typing import FrozenSet, Union +from typing import Callable, FrozenSet, Union from uuid import UUID import pytest @@ -31,7 +31,13 @@ class TestEvent2(AbstractEvent): tags: FrozenSet = frozenset() -def new_subscriber() -> EventSubscriber: +@pytest.fixture +def event_queue() -> IEventQueue: + return PyPubSubEventQueue(Publisher()) + + +@pytest.fixture +def event_queue_subscriber() -> Callable[[AbstractEvent], None]: def fn(event: AbstractEvent): fn.call_count += 1 fn.call_types.add(event.__class__) @@ -44,68 +50,64 @@ def new_subscriber() -> EventSubscriber: return fn -@pytest.fixture -def subscriber() -> EventSubscriber: - return new_subscriber() - - -@pytest.fixture -def event_queue() -> IEventQueue: - return PyPubSubEventQueue(Publisher()) - - -def test_subscribe_all(event_queue: IEventQueue, subscriber: EventSubscriber): - event_queue.subscribe_all_events(subscriber) +def test_subscribe_all(event_queue: IEventQueue, event_queue_subscriber: EventSubscriber): + event_queue.subscribe_all_events(event_queue_subscriber) event_queue.publish(TestEvent1(tags=frozenset({EVENT_TAG_1, EVENT_TAG_2}))) event_queue.publish(TestEvent1(tags=frozenset({EVENT_TAG_2}))) event_queue.publish(TestEvent1(tags=frozenset({"secret_tag"}))) event_queue.publish(TestEvent2()) - assert subscriber.call_count == 4 - assert TestEvent1 in subscriber.call_types - assert TestEvent2 in subscriber.call_types + assert event_queue_subscriber.call_count == 4 + assert TestEvent1 in event_queue_subscriber.call_types + assert TestEvent2 in event_queue_subscriber.call_types @pytest.mark.parametrize("type_to_subscribe", [TestEvent1, TestEvent2]) -def test_subscribe_types(event_queue: IEventQueue, subscriber: EventSubscriber, type_to_subscribe): - event_queue.subscribe_type(type_to_subscribe, subscriber) +def test_subscribe_types( + event_queue: IEventQueue, event_queue_subscriber: EventSubscriber, type_to_subscribe +): + event_queue.subscribe_type(type_to_subscribe, event_queue_subscriber) event_queue.publish(TestEvent1()) event_queue.publish(TestEvent2()) - assert subscriber.call_count == 1 - assert type_to_subscribe in subscriber.call_types + assert event_queue_subscriber.call_count == 1 + assert type_to_subscribe in event_queue_subscriber.call_types -def test_subscribe_tags_single_type(event_queue: IEventQueue, subscriber: EventSubscriber): - event_queue.subscribe_tag(EVENT_TAG_1, subscriber) +def test_subscribe_tags_single_type( + event_queue: IEventQueue, event_queue_subscriber: EventSubscriber +): + event_queue.subscribe_tag(EVENT_TAG_1, event_queue_subscriber) event_queue.publish(TestEvent1(tags=frozenset({EVENT_TAG_1, EVENT_TAG_2}))) event_queue.publish(TestEvent2(tags=frozenset({EVENT_TAG_2}))) - assert subscriber.call_count == 1 - assert len(subscriber.call_types) == 1 - assert TestEvent1 in subscriber.call_types - assert EVENT_TAG_1 in subscriber.call_tags + assert event_queue_subscriber.call_count == 1 + assert len(event_queue_subscriber.call_types) == 1 + assert TestEvent1 in event_queue_subscriber.call_types + assert EVENT_TAG_1 in event_queue_subscriber.call_tags -def test_subscribe_tags_multiple_types(event_queue: IEventQueue, subscriber: EventSubscriber): - event_queue.subscribe_tag(EVENT_TAG_2, subscriber) +def test_subscribe_tags_multiple_types( + event_queue: IEventQueue, event_queue_subscriber: EventSubscriber +): + event_queue.subscribe_tag(EVENT_TAG_2, event_queue_subscriber) event_queue.publish(TestEvent1(tags=frozenset({EVENT_TAG_1, EVENT_TAG_2}))) event_queue.publish(TestEvent2(tags=frozenset({EVENT_TAG_2}))) - assert subscriber.call_count == 2 - assert len(subscriber.call_types) == 2 - assert TestEvent1 in subscriber.call_types - assert TestEvent2 in subscriber.call_types - assert {EVENT_TAG_1, EVENT_TAG_2}.issubset(subscriber.call_tags) + assert event_queue_subscriber.call_count == 2 + assert len(event_queue_subscriber.call_types) == 2 + assert TestEvent1 in event_queue_subscriber.call_types + assert TestEvent2 in event_queue_subscriber.call_types + assert {EVENT_TAG_1, EVENT_TAG_2}.issubset(event_queue_subscriber.call_tags) -def test_type_tag_collision(event_queue: IEventQueue, subscriber: EventSubscriber): - event_queue.subscribe_type(TestEvent1, subscriber) +def test_type_tag_collision(event_queue: IEventQueue, event_queue_subscriber: EventSubscriber): + event_queue.subscribe_type(TestEvent1, event_queue_subscriber) event_queue.publish(TestEvent2(tags=frozenset({TestEvent1.__name__}))) - assert subscriber.call_count == 0 + assert event_queue_subscriber.call_count == 0 diff --git a/monkey/tests/unit_tests/infection_monkey/credential_collectors/test_mimikatz_collector.py b/monkey/tests/unit_tests/infection_monkey/credential_collectors/test_mimikatz_collector.py index 40d5728dd..12ca9d594 100644 --- a/monkey/tests/unit_tests/infection_monkey/credential_collectors/test_mimikatz_collector.py +++ b/monkey/tests/unit_tests/infection_monkey/credential_collectors/test_mimikatz_collector.py @@ -1,9 +1,15 @@ from typing import Sequence +from unittest.mock import MagicMock import pytest from common.credentials import Credentials, LMHash, NTHash, Password, Username +from common.event_queue import IEventQueue +from common.events import CredentialsStolenEvent from infection_monkey.credential_collectors import MimikatzCredentialCollector +from infection_monkey.credential_collectors.mimikatz_collector.mimikatz_credential_collector import ( # noqa: E501 + MIMIKATZ_EVENT_TAGS, +) from infection_monkey.credential_collectors.mimikatz_collector.windows_credentials import ( WindowsCredentials, ) @@ -18,7 +24,8 @@ def patch_pypykatz(win_creds: [WindowsCredentials], monkeypatch): def collect_credentials() -> Sequence[Credentials]: - return MimikatzCredentialCollector().collect_credentials() + mock_event_queue = MagicMock(spec=IEventQueue) + return MimikatzCredentialCollector(mock_event_queue).collect_credentials() @pytest.mark.parametrize( @@ -110,3 +117,46 @@ def test_pypykatz_result_parsing_no_secrets(monkeypatch): collected_credentials = collect_credentials() assert len(collected_credentials) == 1 assert collected_credentials == expected_credentials + + +def test_mimikatz_credentials_stolen_event_published(monkeypatch): + mock_event_queue = MagicMock(spec=IEventQueue) + patch_pypykatz([], monkeypatch) + + mimikatz_credential_collector = MimikatzCredentialCollector(mock_event_queue) + mimikatz_credential_collector.collect_credentials() + + mock_event_queue.publish.assert_called_once() + + mock_event_queue_call_args = mock_event_queue.publish.call_args[0][0] + + assert isinstance(mock_event_queue_call_args, CredentialsStolenEvent) + + +def test_mimikatz_credentials_stolen_event_tags(monkeypatch): + mock_event_queue = MagicMock(spec=IEventQueue) + patch_pypykatz([], monkeypatch) + + mimikatz_credential_collector = MimikatzCredentialCollector(mock_event_queue) + mimikatz_credential_collector.collect_credentials() + + mock_event_queue_call_args = mock_event_queue.publish.call_args[0][0] + + assert mock_event_queue_call_args.tags == MIMIKATZ_EVENT_TAGS + + +def test_mimikatz_credentials_stolen_event_stolen_credentials(monkeypatch): + mock_event_queue = MagicMock(spec=IEventQueue) + win_creds = [ + WindowsCredentials( + username="user2", password="secret2", lm_hash="0182BD0BD4444BF8FC83B5D9042EED2E" + ), + ] + patch_pypykatz(win_creds, monkeypatch) + + mimikatz_credential_collector = MimikatzCredentialCollector(mock_event_queue) + collected_credentials = mimikatz_credential_collector.collect_credentials() + + mock_event_queue_call_args = mock_event_queue.publish.call_args[0][0] + + assert mock_event_queue_call_args.stolen_credentials == collected_credentials