From df0174eacba28e051532757910699f10b242d0e7 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 19 Nov 2021 16:46:48 -0500 Subject: [PATCH 1/4] Agent: Add IPuppet --- monkey/infection_monkey/i_puppet.py | 88 +++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 monkey/infection_monkey/i_puppet.py diff --git a/monkey/infection_monkey/i_puppet.py b/monkey/infection_monkey/i_puppet.py new file mode 100644 index 000000000..46181f509 --- /dev/null +++ b/monkey/infection_monkey/i_puppet.py @@ -0,0 +1,88 @@ +import abc +import threading +from collections import namedtuple +from enum import Enum +from typing import Dict, Optional, Tuple + + +class PortStatus(Enum): + OPEN = 1 + CLOSED = 2 + + +PortScanData = namedtuple("PortScanData", ["port", "status", "banner", "service"]) + + +class IPuppet(metaclass=abc.ABCMeta): + @abc.abstractmethod + def run_sys_info_collector(self, name: str) -> Dict: + """ + Runs a system info collector + :param str name: The name of the system info collector to run + :return: A dictionary containing the information collected from the system + :rtype: Dict + """ + + @abc.abstractmethod + def run_pba(self, name: str, options: Dict) -> None: + """ + Runs a post-breach action (PBA) + :param str name: The name of the post-breach action to run + :param Dict options: A dictionary containing options that modify the behavior of the PBA + """ + + @abc.abstractmethod + def ping(self, host: str) -> Tuple[bool, Optional[str]]: + """ + Sends a ping (ICMP packet) to a remote host + :param str host: The domain name or IP address of a host + :return: A tuple that contains whether or not the host responded and the host's inferred + operating system + :rtype: Tuple[bool, Optional[str]] + """ + + @abc.abstractmethod + def scan_tcp_port(self, host: str, port: int) -> PortScanData: + """ + Scans a TCP port on a remote host + :param str host: The domain name or IP address of a host + :param int port: A TCP port number to scan + :return: The data collected by scanning the provided host:port combination + :rtype: PortScanData + """ + + @abc.abstractmethod + def fingerprint(self, name: str, host: str) -> Dict: + """ + Runs a fingerprinter against a remote host + :param str name: The name of the fingerprinter to run + :param str host: The domain name or IP address of a host + :return: A dictionary containing the information collected by the fingerprinter + :rtype: Dict + """ + + @abc.abstractmethod + def exploit_host(self, name: str, host: str, options: dict, interrupt: threading.Event) -> bool: + """ + Runs an exploiter against a remote host + :param str name: The name of the exploiter to run + :param str host: The domain name or IP address of a host + :param Dict options: A dictionary containing options that modify the behavior of the + exploiter + :return: True if exploitation was successful, False otherwise + :rtype: bool + """ + + @abc.abstractmethod + def run_payload(self, name: str, options: dict, interrupt: threading.Event) -> None: + """ + Runs a payload + :param str name: The name of the payload to run + :param Dict options: A dictionary containing options that modify the behavior of the payload + """ + + @abc.abstractmethod + def cleanup(self) -> None: + """ + Revert any changes made to the system by the puppet. + """ From 4fc484cd8d519f2eaf545d7da2c8e615a0e6c127 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 22 Nov 2021 12:04:52 -0500 Subject: [PATCH 2/4] Agent: Add a preliminary MockPuppet implementation --- monkey/infection_monkey/puppet/__init__.py | 0 monkey/infection_monkey/puppet/mock_puppet.py | 225 ++++++++++++++++++ vulture_allowlist.py | 8 + 3 files changed, 233 insertions(+) create mode 100644 monkey/infection_monkey/puppet/__init__.py create mode 100644 monkey/infection_monkey/puppet/mock_puppet.py diff --git a/monkey/infection_monkey/puppet/__init__.py b/monkey/infection_monkey/puppet/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/infection_monkey/puppet/mock_puppet.py b/monkey/infection_monkey/puppet/mock_puppet.py new file mode 100644 index 000000000..b35d168b7 --- /dev/null +++ b/monkey/infection_monkey/puppet/mock_puppet.py @@ -0,0 +1,225 @@ +import logging +import threading +from typing import Dict, Optional, Tuple + +from infection_monkey.i_puppet import IPuppet, PortScanData, PortStatus + +DOT_1 = "10.0.0.1" +DOT_2 = "10.0.0.2" +DOT_3 = "10.0.0.3" +DOT_4 = "10.0.0.4" + +logger = logging.getLogger() + + +class MockPuppet(IPuppet): + def run_sys_info_collector(self, name: str) -> Dict: + logger.debug(f"run_sys_info_collector({name})") + # TODO: More collectors + if name == "LinuxInfoCollector": + return { + "credentials": {}, + "network_info": { + "networks": [ + {"addr": "10.0.0.7", "netmask": "255.255.255.0"}, + {"addr": "10.45.31.103", "netmask": "255.255.255.0"}, + {"addr": "192.168.33.241", "netmask": "255.255.0.0"}, + ] + }, + "ssh_info": [ + { + "name": "m0nk3y", + "home_dir": "/home/m0nk3y", + "public_key": "ssh-rsa " + "AAAAB3NzaC1yc2EAAAADAQABAAABAQCqhqTJfcrAbTUPzQ+Ou9bhQjmP29jRBz00BAdvNu77Y1SwM/+wETxapv7QPG55oc04Y5qR1KaItcwz3Prh7Qe/ohP/I2mIhP5tDRNfYHxXaGtj58wQhFrkrUhERVvEvwyvb97RWPAtAJjWT8+S6ASjjvyUNHulFIjJ0Yptlj2fboeh1eETDQ4FKfofpgwmab110ct2500FOtY1MWqFgpRvV0EX8WgJoscQ5FnsJAn6Ueb3DnsrIDq1LtK1rmxGSiZwpgOCwvyC1FFfHeP+cfpPsS+G9pBSYm2VqR42QL1BJL1pm4wFPVrBDmzORVQRf35k6agL7loRlfmAt28epDi1 ubuntu@test\n", # noqa: E501 + "private_key": "-----BEGIN RSA PRIVATE KEY-----\n" + "MIIEpAIBAAKCAQEAqoakyX3KwG01D80PjrvW4UI5j9vY0Qc9NAQHbzbu+2NUsDP/\n" + "sBE8Wqb+0DxueaHNOGOakdSmiLXMM9z64e0Hv6IT/yNpiIT+bQ0TX2B8V2hrY+fM\n" + "Ew0OBSn6H6YMJmm9ddHLdudNBTrWNTFqhYKUb1dBF/FoCaLHEORZ7CQJ+lHm9w57\n" + "KyA6tS7Sta5sRkomcKYDgsL8gtRRXx3j/nH6T7EvhvaQUmJtlakeNkC9QSS9aZuM\n" + "snegLvVSlHVmKe8SjD0YAF7g9HH/vm0R2jYTYSArslw4mUZMjTcAQ/XBeDHDkNZq\n" + "x9ECzXdeZhXCXlKcadC+kNp+yT4MwkHAjid6AyalSDJ+9k3QRaI6ItxofWJhnZdB\n" + "RxQtnkJNOZCMKqwxmxUweX7AyShT1KdBdkw0VzkY0O3VUgdR9IzQu73eME5Qr4LM\n" + "5x+rFy0EggHkzCXecviDDQ/SJZEDR4yE0SCxwY0GxVfDdvM6aoLK7wLfu0hG+hjO\n" + "ewXmOAECgYEA4yA14atxKYWf8tAJnmH+IJi1nuiyBoaKJh9nGulGTFVpugytkfdy\n" + "omGYsvlSJd6x4KPM2nXuSD9uvS0ZDeHDXbPJcFAPscghwwIekunQigECgYEAwDRl\n" + "QOhBx8PpicbRmoEe06zb+gRNTYTnvcHgkJN275pqTn1hIAdQSGnyuyWdCN6CU8cg\n" + "p7ecLbCujAstim4H8LG6xMv8jBgVeBKclKEEy9IpvMZ/DGOdUS4/RMWkdVbcFFHZ\n" + "57gycmFwgN7ZFXdMkuCCZi2KCa4jX54G1VNX0+k64cLV8lgQXvVyl9QdvBkt8NqB\n" + "Zoce2vfDrFkUHoxQmAl2jvn8925KkAdga4Zj+zvLgmcryxCFZnA6IvxaoHzrUSxO\n" + "HpuEdCFek/4gyhXPbYQO99ZtOjx0mXwZVqRaEA1kvhX3+PjoPRO2wgBLXVNyb+P5\n" + "5Bxfk6XI40UAUSYv6XQlfIQj0xz/YfSkWbOwTJOShgMbJtiZVFuZ2YcEjSYXzNtv\n" + "WBM0+05OGqjxdyI+qpjHqrZVWN9WvvkH0gJz+zvcorygINMnuSjpNCw4nipXHaud\n" + "LbiqWK42eTmVSiFH+pH+YwVaTatc0RfQ7OP218GD8dtkTgw2JFOzbA==\n" + "-----END RSA PRIVATE KEY-----\n", + "known_hosts": "|1|pERVcy3opIGJnp7HVTpeA0FmuEY=|L64j7430lwkSFrmcn49Nf8YEsLc= " # noqa: E501 + "ssh-rsa " + "AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==\n" # noqa: E501 + "|1|DXEyHSAtnxSSWb4z6XLaxHJL/aM=|zjIBopXOz1GB9hbdpVcYsHY+eSU= " + "ssh-rsa " + "AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==\n" # noqa: E501 + "10.197.94.221 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBL3o1lUn7mZ6HNKDlkFJH9lvFIOXpTH62XkxM7wKXeZbKUy1BKnx2Jkkpv6736XnbFNkUHSnPlCAYDBqsH4nr28=\n" # noqa: E501 + "|1|kVjsp1IWhGMsWfrbQuhLUABrNMk=|xKCh+yr8mPEyCLZ2/E5bC8bjvw0= " + "ecdsa-sha2-nistp256 " + "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBL3o1lUn7mZ6HNKDlkFJH9lvFIOXpTH62XkxM7wKXeZbKUy1BKnx2Jkkpv6736XnbFNkUHSnPlCAYDBqsH4nr28=\n" # noqa: E501 + "other_host,fd42:5289:fddc:ffdf:216:3eff:fe5b:9114 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBL3o1lUn7mZ6HNKDlkFJH9lvFIOXpTH62XkxM7wKXeZbKUy1BKnx2Jkkpv6736XnbFNkUHSnPlCAYDBqsH4nr28=\n" # noqa: E501 + "|1|S6K6SneX+l7xTM1gNLvDAAzj4gs=|cSOIX6qf5YuIe2aw/KmUrM2ye/c= " + "ecdsa-sha2-nistp256 " + "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBL3o1lUn7mZ6HNKDlkFJH9lvFIOXpTH62XkxM7wKXeZbKUy1BKnx2Jkkpv6736XnbFNkUHSnPlCAYDBqsH4nr28=\n", # noqa: E501 + } + ], + } + if name == "ProcessListCollector": + return { + "process_list": { + 1: { + "cmdline": "/sbin/init", + "full_image_path": "/sbin/init", + "name": "systemd", + "pid": 1, + "ppid": 0, + }, + 65: { + "cmdline": "/lib/systemd/systemd-journald", + "full_image_path": "/lib/systemd/systemd-journald", + "name": "systemd-journald", + "pid": 65, + "ppid": 1, + }, + 84: { + "cmdline": "/lib/systemd/systemd-udevd", + "full_image_path": "/lib/systemd/systemd-udevd", + "name": "systemd-udevd", + "pid": 84, + "ppid": 1, + }, + 192: { + "cmdline": "/lib/systemd/systemd-networkd", + "full_image_path": "/lib/systemd/systemd-networkd", + "name": "systemd-networkd", + "pid": 192, + "ppid": 1, + }, + 17749: { + "cmdline": "-zsh", + "full_image_path": "/bin/zsh", + "name": "zsh", + "pid": 17749, + "ppid": 17748, + }, + 18392: { + "cmdline": "/home/ubuntu/venvs/monkey/bin/python " "monkey_island.py", + "full_image_path": "/usr/bin/python3.7", + "name": "python", + "pid": 18392, + "ppid": 17502, + }, + 18400: { + "cmdline": "/home/ubuntu/git/monkey/monkey/monkey_island/bin/mongodb/bin/mongod " # noqa: E501 + "--dbpath /home/ubuntu/.monkey_island/db", + "full_image_path": "/home/ubuntu/git/monkey/monkey/monkey_island/bin/mongodb/bin/mongod", # noqa: E501 + "name": "mongod", + "pid": 18400, + "ppid": 18392, + }, + 26535: { + "cmdline": "ACCESS DENIED", + "full_image_path": "null", + "name": "null", + "pid": 26535, + "ppid": 26469, + }, + 29291: { + "cmdline": "python infection_monkey.py m0nk3y -s " "localhost:5000", + "full_image_path": "/usr/bin/python3.7", + "name": "python", + "pid": 29291, + "ppid": 17749, + }, + } + } + + return {} + + def run_pba(self, name: str, options: dict) -> None: + logger.debug(f"run_pba({name}, {options})") + return None + + def ping(self, host: str) -> Tuple[bool, Optional[str]]: + logger.debug(f"run_ping({host})") + if host == DOT_1: + return (True, "windows") + + if host == DOT_2: + return (False, None) + + if host == DOT_3: + return (True, "Linux") + + if host == DOT_4: + return (False, None) + + return (False, None) + + def scan_tcp_port(self, host: str, port: int) -> PortScanData: + logger.debug(f"run_scan_tcp_port({host}, {port})") + dot_1_results = { + 22: PortScanData(22, PortStatus.CLOSED, None, None), + 445: PortScanData(445, PortStatus.OPEN, "SMB BANNER", "tcp-445"), + 3389: PortScanData(3389, PortStatus.OPEN, "", "tcp-3389"), + } + dot_3_results = { + 22: PortScanData(22, PortStatus.OPEN, "SSH BANNER", "tcp-22"), + 443: PortScanData(443, PortStatus.OPEN, "HTTPS BANNER", "tcp-443"), + 3389: PortScanData(3389, PortStatus.CLOSED, "", None), + } + + if host == DOT_1: + return dot_1_results.get(port, _get_empty_results(port)) + + if host == DOT_3: + return dot_3_results.get(port, _get_empty_results(port)) + + return _get_empty_results(port) + + def fingerprint(self, name: str, host: str) -> Dict: + logger.debug(f"fingerprint({name}, {host})") + dot_1_results = { + "SMBFinger": { + "os": {"type": "windows", "version": "vista"}, + "services": {"tcp-445": {"name": "SSH", "os": "linux"}}, + } + } + + dot_3_results = { + "SSHFinger": {"os": "linux", "services": {"tcp-22": {"name": "SSH"}}}, + "HTTPFinger": { + "services": {"tcp-https": {"name": "http", "data": ("SERVER_HEADERS", DOT_3)}} + }, + } + + if host == DOT_1: + return dot_1_results.get(name, {}) + + if host == DOT_3: + return dot_3_results.get(name, {}) + + return {} + + def exploit_host(self, name: str, host: str, options: dict, interrupt: threading.Event) -> bool: + logger.debug(f"exploit_hosts({name}, {host}, {options})") + successful_exploiters = {DOT_1: {"PowerShellExploiter"}, DOT_3: {"SSHExploiter"}} + + return name in successful_exploiters.get(host, {}) + + def run_payload(self, name: str, options: dict, interrupt: threading.Event) -> None: + logger.debug(f"run_payload({name}, {options})") + return None + + def cleanup(self) -> None: + print("Cleanup called!") + pass + + +def _get_empty_results(port: int): + return PortScanData(port, False, None, None) diff --git a/vulture_allowlist.py b/vulture_allowlist.py index b57fe73ab..f2c6edf3a 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -201,3 +201,11 @@ environment # unused variable (monkey/monkey_island/cc/models/monkey.py:59) _.instance_name # unused attribute (monkey/common/cloud/azure/azure_instance.py:35) _.instance_name # unused attribute (monkey/common/cloud/azure/azure_instance.py:64) GCPHandler # unused function (envs/monkey_zoo/blackbox/test_blackbox.py:57) + +# TODO: Reevaluate these as the agent refactor progresses +run_sys_info_collector +ping +scan_tcp_port +fingerprint +interrupt +MockPuppet From 6e6c3f6133e8d6a9b190a0beb7e770d4b18f7064 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 23 Nov 2021 06:32:31 -0500 Subject: [PATCH 3/4] Agent: Fix capitalization of Dict type hints in IPuppet --- monkey/infection_monkey/i_puppet.py | 4 ++-- monkey/infection_monkey/puppet/mock_puppet.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/monkey/infection_monkey/i_puppet.py b/monkey/infection_monkey/i_puppet.py index 46181f509..d44f827eb 100644 --- a/monkey/infection_monkey/i_puppet.py +++ b/monkey/infection_monkey/i_puppet.py @@ -62,7 +62,7 @@ class IPuppet(metaclass=abc.ABCMeta): """ @abc.abstractmethod - def exploit_host(self, name: str, host: str, options: dict, interrupt: threading.Event) -> bool: + def exploit_host(self, name: str, host: str, options: Dict, interrupt: threading.Event) -> bool: """ Runs an exploiter against a remote host :param str name: The name of the exploiter to run @@ -74,7 +74,7 @@ class IPuppet(metaclass=abc.ABCMeta): """ @abc.abstractmethod - def run_payload(self, name: str, options: dict, interrupt: threading.Event) -> None: + def run_payload(self, name: str, options: Dict, interrupt: threading.Event) -> None: """ Runs a payload :param str name: The name of the payload to run diff --git a/monkey/infection_monkey/puppet/mock_puppet.py b/monkey/infection_monkey/puppet/mock_puppet.py index b35d168b7..910c3a636 100644 --- a/monkey/infection_monkey/puppet/mock_puppet.py +++ b/monkey/infection_monkey/puppet/mock_puppet.py @@ -141,7 +141,7 @@ class MockPuppet(IPuppet): return {} - def run_pba(self, name: str, options: dict) -> None: + def run_pba(self, name: str, options: Dict) -> None: logger.debug(f"run_pba({name}, {options})") return None @@ -206,13 +206,13 @@ class MockPuppet(IPuppet): return {} - def exploit_host(self, name: str, host: str, options: dict, interrupt: threading.Event) -> bool: + def exploit_host(self, name: str, host: str, options: Dict, interrupt: threading.Event) -> bool: logger.debug(f"exploit_hosts({name}, {host}, {options})") successful_exploiters = {DOT_1: {"PowerShellExploiter"}, DOT_3: {"SSHExploiter"}} return name in successful_exploiters.get(host, {}) - def run_payload(self, name: str, options: dict, interrupt: threading.Event) -> None: + def run_payload(self, name: str, options: Dict, interrupt: threading.Event) -> None: logger.debug(f"run_payload({name}, {options})") return None From a4a9de6a8d73fe077fde0a4047220e2a85c43abb Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 23 Nov 2021 07:16:06 -0500 Subject: [PATCH 4/4] Agent: Add a timeout parameter to scan_tcp_port() --- monkey/infection_monkey/i_puppet.py | 3 ++- monkey/infection_monkey/puppet/mock_puppet.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/i_puppet.py b/monkey/infection_monkey/i_puppet.py index d44f827eb..c10731d8f 100644 --- a/monkey/infection_monkey/i_puppet.py +++ b/monkey/infection_monkey/i_puppet.py @@ -42,11 +42,12 @@ class IPuppet(metaclass=abc.ABCMeta): """ @abc.abstractmethod - def scan_tcp_port(self, host: str, port: int) -> PortScanData: + def scan_tcp_port(self, host: str, port: int, timeout: int) -> PortScanData: """ Scans a TCP port on a remote host :param str host: The domain name or IP address of a host :param int port: A TCP port number to scan + :param int timeout: The maximum amount of time (in seconds) to wait for a response :return: The data collected by scanning the provided host:port combination :rtype: PortScanData """ diff --git a/monkey/infection_monkey/puppet/mock_puppet.py b/monkey/infection_monkey/puppet/mock_puppet.py index 910c3a636..0652c109b 100644 --- a/monkey/infection_monkey/puppet/mock_puppet.py +++ b/monkey/infection_monkey/puppet/mock_puppet.py @@ -161,8 +161,8 @@ class MockPuppet(IPuppet): return (False, None) - def scan_tcp_port(self, host: str, port: int) -> PortScanData: - logger.debug(f"run_scan_tcp_port({host}, {port})") + def scan_tcp_port(self, host: str, port: int, timeout: int = 3) -> PortScanData: + logger.debug(f"run_scan_tcp_port({host}, {port}, {timeout})") dot_1_results = { 22: PortScanData(22, PortStatus.CLOSED, None, None), 445: PortScanData(445, PortStatus.OPEN, "SMB BANNER", "tcp-445"),