Merge pull request #2005 from guardicore/1974-refactor-agent-binaries-resource

Island: Refactor agent-binaries resource
This commit is contained in:
ilija-lazoroski 2022-06-09 19:30:21 +02:00 committed by GitHub
commit ae2d212253
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 69 additions and 56 deletions

View File

@ -1,48 +1,40 @@
import logging import logging
from pathlib import Path
from flask import make_response, send_from_directory from flask import make_response, send_file
from monkey_island.cc.repository import AgentRetrievalError, IAgentBinaryRepository
from monkey_island.cc.resources.AbstractResource import AbstractResource from monkey_island.cc.resources.AbstractResource import AbstractResource
from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
AGENTS = {
"linux": "monkey-linux-64",
"windows": "monkey-windows-64.exe",
}
class UnsupportedOSError(Exception):
pass
class AgentBinaries(AbstractResource): class AgentBinaries(AbstractResource):
urls = ["/api/agent-binaries/<string:os>"] urls = ["/api/agent-binaries/<string:os>"]
def __init__(self, agent_binary_repository: IAgentBinaryRepository):
self._agent_binary_repository = agent_binary_repository
# Used by monkey. can't secure. # Used by monkey. can't secure.
def get(self, os): def get(self, os):
"""
Gets the agent binary for the specified OS
:param os: Operating systems. Supported OS are: 'linux' and 'windows'
:return: an agent binary file
"""
try: try:
path = get_agent_executable_path(os) agent_binaries = {
return send_from_directory(path.parent, path.name) "linux": self._agent_binary_repository.get_linux_binary,
except UnsupportedOSError as ex: "windows": self._agent_binary_repository.get_windows_binary,
logger.error(ex) }
return make_response({"error": str(ex)}, 404)
file = agent_binaries[os]()
def get_agent_executable_path(os: str) -> Path: return send_file(file, mimetype="application/octet-stream")
try: except AgentRetrievalError as err:
agent_path = get_executable_full_path(AGENTS[os]) logger.error(err)
logger.debug(f'Local path for {os} executable is "{agent_path}"') return make_response({"error": str(err)}, 500)
if not agent_path.is_file(): except KeyError as err:
logger.error(f"File {agent_path} not found") error_msg = f'No Agents are available for unsupported operating system "{os}": {err}'
logger.error(error_msg)
return agent_path return make_response({"error": error_msg}, 404)
except KeyError:
logger.warning(f"No monkey executables could be found for the host os: {os}")
raise UnsupportedOSError(f'No Agents are available for unsupported operating system "{os}"')
def get_executable_full_path(executable_filename: str) -> Path:
return Path(MONKEY_ISLAND_ABS_PATH) / "cc" / "binaries" / executable_filename

View File

@ -12,6 +12,9 @@ from monkey_island.cc.services.run_local_monkey import LocalMonkeyRunService
class LocalRun(AbstractResource): class LocalRun(AbstractResource):
urls = ["/api/local-monkey"] urls = ["/api/local-monkey"]
def __init__(self, local_monkey_run_service: LocalMonkeyRunService):
self._local_monkey_run_service = local_monkey_run_service
# API Spec: Both of these methods should be separated to their own resources # API Spec: Both of these methods should be separated to their own resources
# API Spec: This should be a REST endpoint, /api/monkeys or something # API Spec: This should be a REST endpoint, /api/monkeys or something
@ -30,7 +33,7 @@ class LocalRun(AbstractResource):
def post(self): def post(self):
body = json.loads(request.data) body = json.loads(request.data)
if body.get("action") == "run": if body.get("action") == "run":
local_run = LocalMonkeyRunService.run_local_monkey() local_run = self._local_monkey_run_service.run_local_monkey()
# API Spec: Feels weird to return "error_text" even when "is_running" is True # API Spec: Feels weird to return "error_text" even when "is_running" is True
return jsonify(is_running=local_run[0], error_text=local_run[1]) return jsonify(is_running=local_run[0], error_text=local_run[1])

View File

@ -26,6 +26,11 @@ AGENT_BINARIES_PATH = Path(MONKEY_ISLAND_ABS_PATH) / "cc" / "binaries"
def initialize_services(data_dir: Path) -> DIContainer: def initialize_services(data_dir: Path) -> DIContainer:
container = DIContainer() container = DIContainer()
# TODO: everything that is build with DI and expects Path in the constructor
# will use the same data_dir. Come up with a better way to inject
# the data_dir in the things that needed
container.register_instance(Path, data_dir)
container.register_instance(AWSInstance, AWSInstance()) container.register_instance(AWSInstance, AWSInstance())
container.register_instance( container.register_instance(
@ -33,10 +38,10 @@ def initialize_services(data_dir: Path) -> DIContainer:
) )
container.register_instance(AWSService, container.resolve(AWSService)) container.register_instance(AWSService, container.resolve(AWSService))
container.register_instance(IAgentBinaryRepository, _build_agent_binary_repository()) container.register_instance(IAgentBinaryRepository, _build_agent_binary_repository())
container.register_instance(LocalMonkeyRunService, container.resolve(LocalMonkeyRunService))
# This is temporary until we get DI all worked out. # This is temporary until we get DI all worked out.
PostBreachFilesService.initialize(container.resolve(IFileRepository)) PostBreachFilesService.initialize(container.resolve(IFileRepository))
LocalMonkeyRunService.initialize(data_dir)
AuthenticationService.initialize(data_dir, JsonFileUserDatastore(data_dir)) AuthenticationService.initialize(data_dir, JsonFileUserDatastore(data_dir))
ReportService.initialize(container.resolve(AWSService)) ReportService.initialize(container.resolve(AWSService))

View File

@ -1,44 +1,58 @@
import logging import logging
import os
import platform import platform
import stat import stat
import subprocess import subprocess
from pathlib import Path from pathlib import Path
from shutil import copyfile from shutil import copyfileobj
from monkey_island.cc.resources.agent_binaries import get_agent_executable_path from monkey_island.cc.repository import AgentRetrievalError, IAgentBinaryRepository
from monkey_island.cc.server_utils.consts import ISLAND_PORT from monkey_island.cc.server_utils.consts import ISLAND_PORT
from monkey_island.cc.services.utils.network_utils import local_ip_addresses from monkey_island.cc.services.utils.network_utils import local_ip_addresses
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
AGENT_NAMES = {"linux": "monkey-linux-64", "windows": "monkey-windows-64.exe"}
class LocalMonkeyRunService: class LocalMonkeyRunService:
DATA_DIR = None def __init__(self, data_dir: Path, agent_binary_repository: IAgentBinaryRepository):
self._data_dir = data_dir
self._agent_binary_repository = agent_binary_repository
# TODO: A number of these services should be instance objects instead of def run_local_monkey(self):
# static/singleton hybrids. At the moment, this requires invasive refactoring that's
# not a priority.
@classmethod
def initialize(cls, data_dir: Path):
cls.DATA_DIR = data_dir
@staticmethod
def run_local_monkey():
# get the monkey executable suitable to run on the server # get the monkey executable suitable to run on the server
operating_system = platform.system().lower()
try: try:
src_path = get_agent_executable_path(platform.system().lower()) agents = {
except Exception as ex: "linux": self._agent_binary_repository.get_linux_binary,
logger.error(f"Error running agent from island: {ex}") "windows": self._agent_binary_repository.get_windows_binary,
return False, str(ex) }
dest_path = LocalMonkeyRunService.DATA_DIR / src_path.name agent_binary = agents[platform.system().lower()]()
except AgentRetrievalError as err:
logger.error(
f"No Agent can be retrieved for the specified operating system"
f'"{operating_system}"'
)
return False, str(err)
except KeyError as err:
logger.error(
f"No Agents are available for unsupported operating system" f'"{operating_system}"'
)
return False, str(err)
except Exception as err:
logger.error(f"Error running agent from island: {err}")
return False, str(err)
dest_path = self._data_dir / AGENT_NAMES[operating_system]
# copy the executable to temp path (don't run the monkey from its current location as it may # copy the executable to temp path (don't run the monkey from its current location as it may
# delete itself) # delete itself)
try: try:
copyfile(src_path, dest_path) with open(dest_path, "wb") as dest_agent:
os.chmod(dest_path, stat.S_IRWXU | stat.S_IRWXG) copyfileobj(agent_binary, dest_agent)
dest_path.chmod(stat.S_IRWXU | stat.S_IRWXG)
except Exception as exc: except Exception as exc:
logger.error("Copy file failed", exc_info=True) logger.error("Copy file failed", exc_info=True)
return False, "Copy file failed: %s" % exc return False, "Copy file failed: %s" % exc
@ -49,7 +63,7 @@ class LocalMonkeyRunService:
port = ISLAND_PORT port = ISLAND_PORT
args = [str(dest_path), "m0nk3y", "-s", f"{ip}:{port}"] args = [str(dest_path), "m0nk3y", "-s", f"{ip}:{port}"]
subprocess.Popen(args, cwd=LocalMonkeyRunService.DATA_DIR) subprocess.Popen(args, cwd=self._data_dir)
except Exception as exc: except Exception as exc:
logger.error("popen failed", exc_info=True) logger.error("popen failed", exc_info=True)
return False, "popen failed: %s" % exc return False, "popen failed: %s" % exc

View File

@ -6,7 +6,6 @@ const sectionOrder = [
'network', 'network',
'island_server', 'island_server',
'exploits', 'exploits',
'dropper',
'classes', 'classes',
'general', 'general',
'testing' 'testing'