diff --git a/monkey/monkey_island/cc/services/telemetry/processing/credentials/__init__.py b/monkey/monkey_island/cc/services/telemetry/processing/credentials/__init__.py index e69de29bb..034f2e83b 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/credentials/__init__.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/credentials/__init__.py @@ -0,0 +1 @@ +from .credentials import Credentials diff --git a/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials.py b/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials.py new file mode 100644 index 000000000..5cb169ae4 --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Sequence, Mapping, Any + + +@dataclass(frozen=True) +class Credentials: + identities: Sequence[Mapping] + secrets: Sequence[Mapping] + monkey_guid: str + + @staticmethod + def from_mapping(cred_dict: Mapping[str, Any], monkey_guid: str) -> Credentials: + return Credentials( + identities=cred_dict["identities"], + secrets=cred_dict["secrets"], + monkey_guid=monkey_guid, + ) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials_parser.py b/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials_parser.py index 60264993d..9df47f91d 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials_parser.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials_parser.py @@ -1,31 +1,34 @@ import logging +from itertools import chain from typing import Mapping from common.common_consts.credential_component_type import CredentialComponentType +from .credentials import Credentials from .identities.username_processor import process_username from .secrets.lm_hash_processor import process_lm_hash from .secrets.nt_hash_processor import process_nt_hash from .secrets.password_processor import process_password +from .secrets.ssh_key_processor import process_ssh_key logger = logging.getLogger(__name__) -SECRET_PROCESSORS = { - CredentialComponentType.PASSWORD: process_password, - CredentialComponentType.NT_HASH: process_nt_hash, +CREDENTIAL_COMPONENT_PROCESSORS = { CredentialComponentType.LM_HASH: process_lm_hash, -} - -IDENTITY_PROCESSORS = { + CredentialComponentType.NT_HASH: process_nt_hash, + CredentialComponentType.PASSWORD: process_password, + CredentialComponentType.SSH_KEYPAIR: process_ssh_key, CredentialComponentType.USERNAME: process_username, } -def parse_credentials(credentials: Mapping): - for credential in credentials["data"]: - for identity in credential["identities"]: - credential_type = CredentialComponentType[identity["credential_type"]] - IDENTITY_PROCESSORS[credential_type](identity) - for secret in credential["secrets"]: - credential_type = CredentialComponentType[secret["credential_type"]] - SECRET_PROCESSORS[credential_type](secret) +def parse_credentials(telemetry_dict: Mapping): + credentials = [ + Credentials.from_mapping(credential, telemetry_dict["monkey_guid"]) + for credential in telemetry_dict["data"] + ] + + for credential in credentials: + for cred_comp in chain(credential.identities, credential.secrets): + credential_type = CredentialComponentType[cred_comp["credential_type"]] + CREDENTIAL_COMPONENT_PROCESSORS[credential_type](cred_comp, credential) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/credentials/identities/username_processor.py b/monkey/monkey_island/cc/services/telemetry/processing/credentials/identities/username_processor.py index 79b09901b..1b2febdb9 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/credentials/identities/username_processor.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/credentials/identities/username_processor.py @@ -1,5 +1,8 @@ +from typing import Mapping + from monkey_island.cc.services.config import ConfigService +from monkey_island.cc.services.telemetry.processing.credentials import Credentials -def process_username(username: dict): +def process_username(username: Mapping, _: Credentials): ConfigService.creds_add_username(username["username"]) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/lm_hash_processor.py b/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/lm_hash_processor.py index 7c5d5f3fa..4939c81bf 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/lm_hash_processor.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/lm_hash_processor.py @@ -1,5 +1,8 @@ +from typing import Mapping + from monkey_island.cc.services.config import ConfigService +from monkey_island.cc.services.telemetry.processing.credentials import Credentials -def process_lm_hash(lm_hash: dict): +def process_lm_hash(lm_hash: Mapping, _: Credentials): ConfigService.creds_add_lm_hash(lm_hash["lm_hash"]) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/nt_hash_processor.py b/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/nt_hash_processor.py index e29e2eef0..82f82af89 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/nt_hash_processor.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/nt_hash_processor.py @@ -1,5 +1,8 @@ +from typing import Mapping + from monkey_island.cc.services.config import ConfigService +from monkey_island.cc.services.telemetry.processing.credentials import Credentials -def process_nt_hash(nt_hash: dict): +def process_nt_hash(nt_hash: Mapping, _: Credentials): ConfigService.creds_add_ntlm_hash(nt_hash["nt_hash"]) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/password_processor.py b/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/password_processor.py index 6d3331db6..6df5a33ce 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/password_processor.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/password_processor.py @@ -1,5 +1,8 @@ +from typing import Mapping + from monkey_island.cc.services.config import ConfigService +from monkey_island.cc.services.telemetry.processing.credentials import Credentials -def process_password(password: dict): +def process_password(password: Mapping, _: Credentials): ConfigService.creds_add_password(password["password"]) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/ssh_key_processor.py b/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/ssh_key_processor.py new file mode 100644 index 000000000..0273732da --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/ssh_key_processor.py @@ -0,0 +1,49 @@ +from typing import Mapping + +from monkey_island.cc.models import Monkey +from monkey_island.cc.server_utils.encryption import get_datastore_encryptor +from monkey_island.cc.services.config import ConfigService +from monkey_island.cc.services.telemetry.processing.credentials import Credentials + + +class SSHKeyProcessingError(ValueError): + def __init__(self, msg=""): + self.msg = f"Error while processing ssh keypair: {msg}" + super().__init__(self.msg) + + +def process_ssh_key(keypair: Mapping, credentials: Credentials): + if len(credentials.identities) != 1: + raise SSHKeyProcessingError( + f"SSH credentials have {len(credentials.identities)} users associated with it!" + ) + + if not _contains_both_keys(keypair): + raise SSHKeyProcessingError("Private or public key missing") + + # TODO investigate if IP is needed at all + ip = Monkey.get_single_monkey_by_guid(credentials.monkey_guid).ip_addresses[0] + username = credentials.identities[0]["username"] + + encrypted_keys = _encrypt_ssh_keys(keypair) + + ConfigService.ssh_add_keys( + user=username, + public_key=encrypted_keys["public_key"], + private_key=encrypted_keys["private_key"], + ip=ip, + ) + + +def _contains_both_keys(ssh_key: Mapping) -> bool: + try: + return ssh_key["public_key"] and ssh_key["private_key"] + except KeyError: + return False + + +def _encrypt_ssh_keys(ssh_key: Mapping) -> Mapping: + encrypted_keys = {} + for field in ["public_key", "private_key"]: + encrypted_keys[field] = get_datastore_encryptor().encrypt(ssh_key[field]) + return encrypted_keys diff --git a/monkey/monkey_island/cc/services/telemetry/processing/processing.py b/monkey/monkey_island/cc/services/telemetry/processing/processing.py index 0dd93aab1..abea5dc38 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/processing.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/processing.py @@ -13,16 +13,16 @@ from monkey_island.cc.services.telemetry.processing.tunnel import process_tunnel logger = logging.getLogger(__name__) TELEMETRY_CATEGORY_TO_PROCESSING_FUNC = { - TelemCategoryEnum.CREDENTIALS: parse_credentials, - TelemCategoryEnum.TUNNEL: process_tunnel_telemetry, - TelemCategoryEnum.STATE: process_state_telemetry, - TelemCategoryEnum.EXPLOIT: process_exploit_telemetry, - TelemCategoryEnum.SCAN: process_scan_telemetry, - TelemCategoryEnum.POST_BREACH: process_post_breach_telemetry, - TelemCategoryEnum.AWS_INFO: process_aws_telemetry, # `lambda *args, **kwargs: None` is a no-op. - TelemCategoryEnum.TRACE: lambda *args, **kwargs: None, TelemCategoryEnum.ATTACK: lambda *args, **kwargs: None, + TelemCategoryEnum.AWS_INFO: process_aws_telemetry, + TelemCategoryEnum.CREDENTIALS: parse_credentials, + TelemCategoryEnum.EXPLOIT: process_exploit_telemetry, + TelemCategoryEnum.POST_BREACH: process_post_breach_telemetry, + TelemCategoryEnum.SCAN: process_scan_telemetry, + TelemCategoryEnum.STATE: process_state_telemetry, + TelemCategoryEnum.TRACE: lambda *args, **kwargs: None, + TelemCategoryEnum.TUNNEL: process_tunnel_telemetry, } diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/conftest.py b/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/conftest.py new file mode 100644 index 000000000..d2891678e --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/conftest.py @@ -0,0 +1,23 @@ +from datetime import datetime + +import mongoengine +import pytest + +from monkey_island.cc.services.config import ConfigService + + +@pytest.fixture +def fake_mongo(monkeypatch): + mongo = mongoengine.connection.get_connection() + monkeypatch.setattr("monkey_island.cc.services.config.mongo", mongo) + config = ConfigService.get_default_config() + ConfigService.update_config(config, should_encrypt=True) + + +CREDENTIAL_TELEM_TEMPLATE = { + "monkey_guid": "272405690278083", + "telem_category": "credentials", + "timestamp": datetime(2022, 2, 18, 11, 51, 15, 338953), + "command_control_channel": {"src": "10.2.2.251", "dst": "10.2.2.251:5000"}, + "data": None, +} diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_credential_processing.py b/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_credential_processing.py index c39082e83..2ad17431d 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_credential_processing.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_credential_processing.py @@ -1,9 +1,10 @@ from copy import deepcopy -from datetime import datetime import dpath.util -import mongoengine import pytest +from tests.unit_tests.monkey_island.cc.services.telemetry.processing.credentials.conftest import ( + CREDENTIAL_TELEM_TEMPLATE, +) from common.config_value_paths import ( LM_HASH_LIST_PATH, @@ -16,20 +17,21 @@ from monkey_island.cc.services.telemetry.processing.credentials.credentials_pars parse_credentials, ) -CREDENTIAL_TELEM_TEMPLATE = { - "monkey_guid": "272405690278083", - "telem_category": "credentials", - "timestamp": datetime(2022, 2, 18, 11, 51, 15, 338953), - "command_control_channel": {"src": "10.2.2.251", "dst": "10.2.2.251:5000"}, - "data": None, -} - fake_username = "m0nk3y_user" cred_telem_usernames = deepcopy(CREDENTIAL_TELEM_TEMPLATE) cred_telem_usernames["data"] = [ {"identities": [{"username": fake_username, "credential_type": "USERNAME"}], "secrets": []} ] +fake_special_username = "$m0nk3y.user" +cred_telem_special_usernames = deepcopy(CREDENTIAL_TELEM_TEMPLATE) +cred_telem_special_usernames["data"] = [ + { + "identities": [{"username": fake_special_username, "credential_type": "USERNAME"}], + "secrets": [], + } +] + fake_nt_hash = "c1c58f96cdf212b50837bc11a00be47c" fake_lm_hash = "299BD128C1101FD6" fake_password = "trytostealthis" @@ -49,24 +51,22 @@ cred_empty_telem = deepcopy(CREDENTIAL_TELEM_TEMPLATE) cred_empty_telem["data"] = [{"identities": [], "secrets": []}] -@pytest.fixture -def fake_mongo(monkeypatch): - mongo = mongoengine.connection.get_connection() - monkeypatch.setattr("monkey_island.cc.services.config.mongo", mongo) - config = ConfigService.get_default_config() - ConfigService.update_config(config, should_encrypt=True) - return mongo - - -@pytest.mark.usefixtures("uses_database") -def test_cred_username_parsing(fake_mongo): +@pytest.mark.usefixtures("uses_database", "fake_mongo") +def test_cred_username_parsing(): parse_credentials(cred_telem_usernames) config = ConfigService.get_config(should_decrypt=True) assert fake_username in dpath.util.get(config, USER_LIST_PATH) -@pytest.mark.usefixtures("uses_database") -def test_cred_telemetry_parsing(fake_mongo): +@pytest.mark.usefixtures("uses_database", "fake_mongo") +def test_cred_special_username_parsing(): + parse_credentials(cred_telem_special_usernames) + config = ConfigService.get_config(should_decrypt=True) + assert fake_special_username in dpath.util.get(config, USER_LIST_PATH) + + +@pytest.mark.usefixtures("uses_database", "fake_mongo") +def test_cred_telemetry_parsing(): parse_credentials(cred_telem) config = ConfigService.get_config(should_decrypt=True) assert fake_username in dpath.util.get(config, USER_LIST_PATH) @@ -75,8 +75,8 @@ def test_cred_telemetry_parsing(fake_mongo): assert fake_password in dpath.util.get(config, PASSWORD_LIST_PATH) -@pytest.mark.usefixtures("uses_database") -def test_empty_cred_telemetry_parsing(fake_mongo): +@pytest.mark.usefixtures("uses_database", "fake_mongo") +def test_empty_cred_telemetry_parsing(): default_config = deepcopy(ConfigService.get_config(should_decrypt=True)) default_usernames = dpath.util.get(default_config, USER_LIST_PATH) default_nt_hashes = dpath.util.get(default_config, NTLM_HASH_LIST_PATH) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_ssh_key_processing.py b/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_ssh_key_processing.py new file mode 100644 index 000000000..2d012c4ee --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_ssh_key_processing.py @@ -0,0 +1,54 @@ +from copy import deepcopy + +import dpath.util +import pytest +from tests.unit_tests.monkey_island.cc.services.telemetry.processing.credentials.conftest import ( + CREDENTIAL_TELEM_TEMPLATE, +) + +from common.config_value_paths import SSH_KEYS_PATH, USER_LIST_PATH +from monkey_island.cc.models import Monkey +from monkey_island.cc.services.config import ConfigService +from monkey_island.cc.services.telemetry.processing.credentials.credentials_parser import ( + parse_credentials, +) + +fake_monkey_guid = "272405690278083" +fake_ip_address = "192.168.56.1" + +fake_private_key = "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAACmFlczI1N\n" +fake_partial_secret = {"private_key": fake_private_key, "credential_type": "SSH_KEYPAIR"} + +fake_username = "ubuntu" +fake_public_key = ( + "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC1u2+50OFRnzOGHpWo69" + "tc02oMXudeML7pOl7rqXLmdxuj monkey@krk-wpas5" +) +fake_secret_full = { + "private_key": fake_private_key, + "public_key": fake_public_key, + "credential_type": "SSH_KEYPAIR", +} +fake_identity = {"username": fake_username, "credential_type": "USERNAME"} + +ssh_telem = deepcopy(CREDENTIAL_TELEM_TEMPLATE) +ssh_telem["data"] = [{"identities": [fake_identity], "secrets": [fake_secret_full]}] + + +@pytest.fixture +def insert_fake_monkey(): + monkey = Monkey(guid=fake_monkey_guid, ip_addresses=[fake_ip_address]) + monkey.save() + + +@pytest.mark.usefixtures("uses_encryptor", "uses_database", "fake_mongo", "insert_fake_monkey") +def test_ssh_credential_parsing(): + parse_credentials(ssh_telem) + config = ConfigService.get_config(should_decrypt=True) + ssh_keypairs = dpath.util.get(config, SSH_KEYS_PATH) + assert len(ssh_keypairs) == 1 + assert ssh_keypairs[0]["private_key"] == fake_private_key + assert ssh_keypairs[0]["public_key"] == fake_public_key + assert ssh_keypairs[0]["user"] == fake_username + assert ssh_keypairs[0]["ip"] == fake_ip_address + assert fake_username in dpath.util.get(config, USER_LIST_PATH)