diff --git a/monkey/infection_monkey/credential_collectors/__init__.py b/monkey/infection_monkey/credential_collectors/__init__.py new file mode 100644 index 000000000..a9d22a4c4 --- /dev/null +++ b/monkey/infection_monkey/credential_collectors/__init__.py @@ -0,0 +1,5 @@ +from .credential_components.nt_hash import NTHash +from .credential_components.lm_hash import LMHash +from .credential_components.password import Password +from .credential_components.username import Username +from .mimikatz_collector import MimikatzCredentialCollector diff --git a/monkey/infection_monkey/system_info/windows_cred_collector/__init__.py b/monkey/infection_monkey/credential_collectors/credential_components/__init__.py similarity index 100% rename from monkey/infection_monkey/system_info/windows_cred_collector/__init__.py rename to monkey/infection_monkey/credential_collectors/credential_components/__init__.py diff --git a/monkey/infection_monkey/credential_collectors/credential_components/lm_hash.py b/monkey/infection_monkey/credential_collectors/credential_components/lm_hash.py new file mode 100644 index 000000000..7706540a3 --- /dev/null +++ b/monkey/infection_monkey/credential_collectors/credential_components/lm_hash.py @@ -0,0 +1,9 @@ +from dataclasses import dataclass, field + +from infection_monkey.i_puppet import CredentialType, ICredentialComponent + + +@dataclass(frozen=True) +class LMHash(ICredentialComponent): + credential_type: CredentialType = field(default=CredentialType.LM_HASH, init=False) + lm_hash: str diff --git a/monkey/infection_monkey/credential_collectors/credential_components/nt_hash.py b/monkey/infection_monkey/credential_collectors/credential_components/nt_hash.py new file mode 100644 index 000000000..e6932c4c5 --- /dev/null +++ b/monkey/infection_monkey/credential_collectors/credential_components/nt_hash.py @@ -0,0 +1,9 @@ +from dataclasses import dataclass, field + +from infection_monkey.i_puppet import CredentialType, ICredentialComponent + + +@dataclass(frozen=True) +class NTHash(ICredentialComponent): + credential_type: CredentialType = field(default=CredentialType.NT_HASH, init=False) + nt_hash: str diff --git a/monkey/infection_monkey/credential_collectors/credential_components/password.py b/monkey/infection_monkey/credential_collectors/credential_components/password.py new file mode 100644 index 000000000..701c9fcde --- /dev/null +++ b/monkey/infection_monkey/credential_collectors/credential_components/password.py @@ -0,0 +1,9 @@ +from dataclasses import dataclass, field + +from infection_monkey.i_puppet import CredentialType, ICredentialComponent + + +@dataclass(frozen=True) +class Password(ICredentialComponent): + credential_type: CredentialType = field(default=CredentialType.PASSWORD, init=False) + password: str diff --git a/monkey/infection_monkey/credential_collectors/credential_components/username.py b/monkey/infection_monkey/credential_collectors/credential_components/username.py new file mode 100644 index 000000000..208849061 --- /dev/null +++ b/monkey/infection_monkey/credential_collectors/credential_components/username.py @@ -0,0 +1,9 @@ +from dataclasses import dataclass, field + +from infection_monkey.i_puppet import CredentialType, ICredentialComponent + + +@dataclass(frozen=True) +class Username(ICredentialComponent): + credential_type: CredentialType = field(default=CredentialType.USERNAME, init=False) + username: str diff --git a/monkey/infection_monkey/credential_collectors/mimikatz_collector/__init__.py b/monkey/infection_monkey/credential_collectors/mimikatz_collector/__init__.py new file mode 100644 index 000000000..c6a8f1a91 --- /dev/null +++ b/monkey/infection_monkey/credential_collectors/mimikatz_collector/__init__.py @@ -0,0 +1 @@ +from .mimikatz_credential_collector import MimikatzCredentialCollector 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 new file mode 100644 index 000000000..e1f94c4dd --- /dev/null +++ b/monkey/infection_monkey/credential_collectors/mimikatz_collector/mimikatz_credential_collector.py @@ -0,0 +1,39 @@ +from typing import Iterable + +from infection_monkey.credential_collectors import LMHash, NTHash, Password, Username +from infection_monkey.i_puppet.credential_collection import Credentials, ICredentialCollector + +from . import pypykatz_handler +from .windows_credentials import WindowsCredentials + + +class MimikatzCredentialCollector(ICredentialCollector): + def collect_credentials(self, options=None) -> Iterable[Credentials]: + creds = pypykatz_handler.get_windows_creds() + return MimikatzCredentialCollector._to_credentials(creds) + + @staticmethod + def _to_credentials(win_creds: Iterable[WindowsCredentials]) -> [Credentials]: + all_creds = [] + for win_cred in win_creds: + identities = [] + secrets = [] + if win_cred.username: + identity = Username(win_cred.username) + identities.append(identity) + + if win_cred.password: + password = Password(win_cred.password) + secrets.append(password) + + if win_cred.lm_hash: + lm_hash = LMHash(lm_hash=win_cred.lm_hash) + secrets.append(lm_hash) + + if win_cred.ntlm_hash: + lm_hash = NTHash(nt_hash=win_cred.ntlm_hash) + secrets.append(lm_hash) + + if identities != [] or secrets != []: + all_creds.append(Credentials(identities, secrets)) + return all_creds diff --git a/monkey/infection_monkey/system_info/windows_cred_collector/pypykatz_handler.py b/monkey/infection_monkey/credential_collectors/mimikatz_collector/pypykatz_handler.py similarity index 96% rename from monkey/infection_monkey/system_info/windows_cred_collector/pypykatz_handler.py rename to monkey/infection_monkey/credential_collectors/mimikatz_collector/pypykatz_handler.py index 23bcce771..2b7ceec65 100644 --- a/monkey/infection_monkey/system_info/windows_cred_collector/pypykatz_handler.py +++ b/monkey/infection_monkey/credential_collectors/mimikatz_collector/pypykatz_handler.py @@ -3,9 +3,7 @@ from typing import Any, Dict, List, NewType from pypykatz.pypykatz import pypykatz -from infection_monkey.system_info.windows_cred_collector.windows_credentials import ( - WindowsCredentials, -) +from .windows_credentials import WindowsCredentials CREDENTIAL_TYPES = [ "msv_creds", diff --git a/monkey/infection_monkey/system_info/windows_cred_collector/windows_credentials.py b/monkey/infection_monkey/credential_collectors/mimikatz_collector/windows_credentials.py similarity index 100% rename from monkey/infection_monkey/system_info/windows_cred_collector/windows_credentials.py rename to monkey/infection_monkey/credential_collectors/mimikatz_collector/windows_credentials.py diff --git a/monkey/infection_monkey/i_puppet/__init__.py b/monkey/infection_monkey/i_puppet/__init__.py index c4e6b5b1c..d6422ebc2 100644 --- a/monkey/infection_monkey/i_puppet/__init__.py +++ b/monkey/infection_monkey/i_puppet/__init__.py @@ -10,3 +10,9 @@ from .i_puppet import ( UnknownPluginError, ) from .i_fingerprinter import IFingerprinter +from .credential_collection import ( + Credentials, + CredentialType, + ICredentialCollector, + ICredentialComponent, +) diff --git a/monkey/infection_monkey/i_puppet/credential_collection/__init__.py b/monkey/infection_monkey/i_puppet/credential_collection/__init__.py new file mode 100644 index 000000000..8bfa68b38 --- /dev/null +++ b/monkey/infection_monkey/i_puppet/credential_collection/__init__.py @@ -0,0 +1,4 @@ +from .i_credential_collector import ICredentialCollector +from .credentials import Credentials +from .i_credential_component import ICredentialComponent +from .credential_type import CredentialType diff --git a/monkey/infection_monkey/i_puppet/credential_collection/credential_type.py b/monkey/infection_monkey/i_puppet/credential_collection/credential_type.py new file mode 100644 index 000000000..98e6c0097 --- /dev/null +++ b/monkey/infection_monkey/i_puppet/credential_collection/credential_type.py @@ -0,0 +1,8 @@ +from enum import Enum + + +class CredentialType(Enum): + USERNAME = 1 + PASSWORD = 2 + NT_HASH = 3 + LM_HASH = 4 diff --git a/monkey/infection_monkey/i_puppet/credential_collection/credentials.py b/monkey/infection_monkey/i_puppet/credential_collection/credentials.py new file mode 100644 index 000000000..d5591f6d7 --- /dev/null +++ b/monkey/infection_monkey/i_puppet/credential_collection/credentials.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass +from typing import Tuple + +from .i_credential_component import ICredentialComponent + + +@dataclass(frozen=True) +class Credentials: + identities: Tuple[ICredentialComponent] + secrets: Tuple[ICredentialComponent] diff --git a/monkey/infection_monkey/i_puppet/credential_collection/i_credential_collector.py b/monkey/infection_monkey/i_puppet/credential_collection/i_credential_collector.py new file mode 100644 index 000000000..847cd929d --- /dev/null +++ b/monkey/infection_monkey/i_puppet/credential_collection/i_credential_collector.py @@ -0,0 +1,10 @@ +from abc import ABC, abstractmethod +from typing import Iterable, Mapping, Optional + +from .credentials import Credentials + + +class ICredentialCollector(ABC): + @abstractmethod + def collect_credentials(self, options: Optional[Mapping]) -> Iterable[Credentials]: + pass diff --git a/monkey/infection_monkey/i_puppet/credential_collection/i_credential_component.py b/monkey/infection_monkey/i_puppet/credential_collection/i_credential_component.py new file mode 100644 index 000000000..d1c005886 --- /dev/null +++ b/monkey/infection_monkey/i_puppet/credential_collection/i_credential_component.py @@ -0,0 +1,10 @@ +from abc import ABC, abstractmethod + +from .credential_type import CredentialType + + +class ICredentialComponent(ABC): + @property + @abstractmethod + def credential_type(self) -> CredentialType: + pass diff --git a/monkey/infection_monkey/system_info/windows_cred_collector/mimikatz_cred_collector.py b/monkey/infection_monkey/system_info/windows_cred_collector/mimikatz_cred_collector.py deleted file mode 100644 index ab44d85ea..000000000 --- a/monkey/infection_monkey/system_info/windows_cred_collector/mimikatz_cred_collector.py +++ /dev/null @@ -1,25 +0,0 @@ -from typing import List - -from infection_monkey.system_info.windows_cred_collector import pypykatz_handler -from infection_monkey.system_info.windows_cred_collector.windows_credentials import ( - WindowsCredentials, -) - - -class MimikatzCredentialCollector(object): - @staticmethod - def get_creds(): - creds = pypykatz_handler.get_windows_creds() - return MimikatzCredentialCollector.cred_list_to_cred_dict(creds) - - @staticmethod - def cred_list_to_cred_dict(creds: List[WindowsCredentials]): - cred_dict = {} - for cred in creds: - # TODO: This should be handled by the island, not the agent. There is already similar - # code in monkey_island/cc/models/report/report_dal.py. - # Lets not use "." and "$" in keys, because it will confuse mongo. - # Ideally we should refactor island not to use a dict and simply parse credential list. - key = cred.username.replace(".", ",").replace("$", "") - cred_dict.update({key: cred.to_dict()}) - return cred_dict diff --git a/monkey/infection_monkey/system_info/windows_info_collector.py b/monkey/infection_monkey/system_info/windows_info_collector.py index f3242922e..6285fee0f 100644 --- a/monkey/infection_monkey/system_info/windows_info_collector.py +++ b/monkey/infection_monkey/system_info/windows_info_collector.py @@ -2,7 +2,7 @@ import logging import sys from common.common_consts.system_info_collectors_names import MIMIKATZ_COLLECTOR -from infection_monkey.system_info.windows_cred_collector.mimikatz_cred_collector import ( +from infection_monkey.credential_collectors.windows_cred_collector.mimikatz_cred_collector import ( MimikatzCredentialCollector, ) 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 new file mode 100644 index 000000000..b33d4e097 --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/credential_collectors/test_mimikatz_collector.py @@ -0,0 +1,92 @@ +from typing import List + +import pytest + +from infection_monkey.credential_collectors import ( + LMHash, + MimikatzCredentialCollector, + NTHash, + Password, + Username, +) +from infection_monkey.credential_collectors.mimikatz_collector.windows_credentials import ( + WindowsCredentials, +) +from infection_monkey.i_puppet import Credentials + + +def patch_pypykatz(win_creds: [WindowsCredentials], monkeypatch): + monkeypatch.setattr( + "infection_monkey.credential_collectors" + ".mimikatz_collector.pypykatz_handler.get_windows_creds", + lambda: win_creds, + ) + + +def collect_credentials() -> List[Credentials]: + return list(MimikatzCredentialCollector().collect_credentials()) + + +@pytest.mark.parametrize( + "win_creds", [([WindowsCredentials(username="", password="", ntlm_hash="", lm_hash="")]), ([])] +) +def test_empty_results(monkeypatch, win_creds): + patch_pypykatz(win_creds, monkeypatch) + collected_credentials = collect_credentials() + assert not collected_credentials + + +def test_pypykatz_result_parsing(monkeypatch): + win_creds = [WindowsCredentials(username="user", password="secret", ntlm_hash="", lm_hash="")] + patch_pypykatz(win_creds, monkeypatch) + + username = Username("user") + password = Password("secret") + expected_credentials = Credentials([username], [password]) + + collected_credentials = collect_credentials() + assert len(collected_credentials) == 1 + assert collected_credentials[0] == expected_credentials + + +def test_pypykatz_result_parsing_duplicates(monkeypatch): + win_creds = [ + WindowsCredentials(username="user", password="secret", ntlm_hash="", lm_hash=""), + WindowsCredentials(username="user", password="secret", ntlm_hash="", lm_hash=""), + ] + patch_pypykatz(win_creds, monkeypatch) + + collected_credentials = collect_credentials() + assert len(collected_credentials) == 2 + + +def test_pypykatz_result_parsing_defaults(monkeypatch): + win_creds = [ + WindowsCredentials(username="user2", password="secret2", lm_hash="lm_hash"), + ] + patch_pypykatz(win_creds, monkeypatch) + + # Expected credentials + username = Username("user2") + password = Password("secret2") + lm_hash = LMHash("lm_hash") + expected_credentials = Credentials([username], [password, lm_hash]) + + collected_credentials = collect_credentials() + assert len(collected_credentials) == 1 + assert collected_credentials[0] == expected_credentials + + +def test_pypykatz_result_parsing_no_identities(monkeypatch): + win_creds = [ + WindowsCredentials(username="", password="", ntlm_hash="ntlm_hash", lm_hash="lm_hash"), + ] + patch_pypykatz(win_creds, monkeypatch) + + lm_hash = LMHash("lm_hash") + nt_hash = NTHash("ntlm_hash") + expected_credentials = Credentials([], [lm_hash, nt_hash]) + + collected_credentials = collect_credentials() + assert len(collected_credentials) == 1 + assert collected_credentials[0] == expected_credentials diff --git a/monkey/tests/unit_tests/infection_monkey/system_info/windows_cred_collector/test_pypykatz_handler.py b/monkey/tests/unit_tests/infection_monkey/system_info/windows_cred_collector/test_pypykatz_handler.py index 4d3259e67..9bacb2070 100644 --- a/monkey/tests/unit_tests/infection_monkey/system_info/windows_cred_collector/test_pypykatz_handler.py +++ b/monkey/tests/unit_tests/infection_monkey/system_info/windows_cred_collector/test_pypykatz_handler.py @@ -1,6 +1,6 @@ from unittest import TestCase -from infection_monkey.system_info.windows_cred_collector.pypykatz_handler import ( +from infection_monkey.credential_collectors.mimikatz_collector.pypykatz_handler import ( _get_creds_from_pypykatz_session, )