forked from p15670423/monkey
Agent: Add control client to the agent initialization
This commit is contained in:
parent
133f7f5da1
commit
049eb1b174
|
@ -3,6 +3,7 @@ import logging
|
||||||
import platform
|
import platform
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
from socket import gethostname
|
from socket import gethostname
|
||||||
|
from typing import Mapping, Optional
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from requests.exceptions import ConnectionError
|
from requests.exceptions import ConnectionError
|
||||||
|
@ -23,11 +24,12 @@ logger = logging.getLogger(__name__)
|
||||||
PBA_FILE_DOWNLOAD = "https://%s/api/pba/download/%s"
|
PBA_FILE_DOWNLOAD = "https://%s/api/pba/download/%s"
|
||||||
|
|
||||||
|
|
||||||
class ControlClient(object):
|
class ControlClient:
|
||||||
proxies = {}
|
def __init__(self, server_address: str, proxies: Optional[Mapping[str, str]] = None):
|
||||||
|
self.proxies = {} if not proxies else proxies
|
||||||
|
self.server_address = server_address
|
||||||
|
|
||||||
@staticmethod
|
def wakeup(self, parent=None):
|
||||||
def wakeup(parent=None):
|
|
||||||
if parent:
|
if parent:
|
||||||
logger.debug("parent: %s" % (parent,))
|
logger.debug("parent: %s" % (parent,))
|
||||||
|
|
||||||
|
@ -45,20 +47,19 @@ class ControlClient(object):
|
||||||
"launch_time": agent_process.get_start_time(),
|
"launch_time": agent_process.get_start_time(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if ControlClient.proxies:
|
if self.proxies:
|
||||||
monkey["tunnel"] = ControlClient.proxies.get("https")
|
monkey["tunnel"] = self.proxies.get("https")
|
||||||
|
|
||||||
requests.post( # noqa: DUO123
|
requests.post( # noqa: DUO123
|
||||||
f"https://{WormConfiguration.current_server}/api/agent",
|
f"https://{WormConfiguration.current_server}/api/agent",
|
||||||
data=json.dumps(monkey),
|
data=json.dumps(monkey),
|
||||||
headers={"content-type": "application/json"},
|
headers={"content-type": "application/json"},
|
||||||
verify=False,
|
verify=False,
|
||||||
proxies=ControlClient.proxies,
|
proxies=self.proxies,
|
||||||
timeout=MEDIUM_REQUEST_TIMEOUT,
|
timeout=MEDIUM_REQUEST_TIMEOUT,
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
def find_server(self, default_tunnel=None):
|
||||||
def find_server(default_tunnel=None):
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Trying to wake up with Monkey Island servers list: %r"
|
"Trying to wake up with Monkey Island servers list: %r"
|
||||||
% WormConfiguration.command_servers
|
% WormConfiguration.command_servers
|
||||||
|
@ -73,13 +74,13 @@ class ControlClient(object):
|
||||||
current_server = server
|
current_server = server
|
||||||
|
|
||||||
debug_message = "Trying to connect to server: %s" % server
|
debug_message = "Trying to connect to server: %s" % server
|
||||||
if ControlClient.proxies:
|
if self.proxies:
|
||||||
debug_message += " through proxies: %s" % ControlClient.proxies
|
debug_message += " through proxies: %s" % self.proxies
|
||||||
logger.debug(debug_message)
|
logger.debug(debug_message)
|
||||||
requests.get( # noqa: DUO123
|
requests.get( # noqa: DUO123
|
||||||
f"https://{server}/api?action=is-up",
|
f"https://{server}/api?action=is-up",
|
||||||
verify=False,
|
verify=False,
|
||||||
proxies=ControlClient.proxies,
|
proxies=self.proxies,
|
||||||
timeout=MEDIUM_REQUEST_TIMEOUT,
|
timeout=MEDIUM_REQUEST_TIMEOUT,
|
||||||
)
|
)
|
||||||
WormConfiguration.current_server = current_server
|
WormConfiguration.current_server = current_server
|
||||||
|
@ -92,20 +93,19 @@ class ControlClient(object):
|
||||||
if current_server:
|
if current_server:
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
if ControlClient.proxies:
|
if self.proxies:
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
logger.info("Starting tunnel lookup...")
|
logger.info("Starting tunnel lookup...")
|
||||||
proxy_find = tunnel.find_tunnel(default=default_tunnel)
|
proxy_find = tunnel.find_tunnel(default=default_tunnel)
|
||||||
if proxy_find:
|
if proxy_find:
|
||||||
ControlClient.set_proxies(proxy_find)
|
self.set_proxies(proxy_find)
|
||||||
return ControlClient.find_server()
|
return self.find_server()
|
||||||
else:
|
else:
|
||||||
logger.info("No tunnel found")
|
logger.info("No tunnel found")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@staticmethod
|
def set_proxies(self, proxy_find):
|
||||||
def set_proxies(proxy_find):
|
|
||||||
"""
|
"""
|
||||||
Note: The proxy schema changes between different versions of requests and urllib3,
|
Note: The proxy schema changes between different versions of requests and urllib3,
|
||||||
which causes the machine to not open a tunnel back.
|
which causes the machine to not open a tunnel back.
|
||||||
|
@ -120,12 +120,11 @@ class ControlClient(object):
|
||||||
proxy_address, proxy_port = proxy_find
|
proxy_address, proxy_port = proxy_find
|
||||||
logger.info("Found tunnel at %s:%s" % (proxy_address, proxy_port))
|
logger.info("Found tunnel at %s:%s" % (proxy_address, proxy_port))
|
||||||
if is_windows_os():
|
if is_windows_os():
|
||||||
ControlClient.proxies["https"] = f"http://{proxy_address}:{proxy_port}"
|
self.proxies["https"] = f"http://{proxy_address}:{proxy_port}"
|
||||||
else:
|
else:
|
||||||
ControlClient.proxies["https"] = f"{proxy_address}:{proxy_port}"
|
self.proxies["https"] = f"{proxy_address}:{proxy_port}"
|
||||||
|
|
||||||
@staticmethod
|
def send_telemetry(self, telem_category, json_data: str):
|
||||||
def send_telemetry(telem_category, json_data: str):
|
|
||||||
if not WormConfiguration.current_server:
|
if not WormConfiguration.current_server:
|
||||||
logger.error(
|
logger.error(
|
||||||
"Trying to send %s telemetry before current server is established, aborting."
|
"Trying to send %s telemetry before current server is established, aborting."
|
||||||
|
@ -139,7 +138,7 @@ class ControlClient(object):
|
||||||
data=json.dumps(telemetry),
|
data=json.dumps(telemetry),
|
||||||
headers={"content-type": "application/json"},
|
headers={"content-type": "application/json"},
|
||||||
verify=False,
|
verify=False,
|
||||||
proxies=ControlClient.proxies,
|
proxies=self.proxies,
|
||||||
timeout=MEDIUM_REQUEST_TIMEOUT,
|
timeout=MEDIUM_REQUEST_TIMEOUT,
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
@ -147,8 +146,7 @@ class ControlClient(object):
|
||||||
"Error connecting to control server %s: %s", WormConfiguration.current_server, exc
|
"Error connecting to control server %s: %s", WormConfiguration.current_server, exc
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
def send_log(self, log):
|
||||||
def send_log(log):
|
|
||||||
if not WormConfiguration.current_server:
|
if not WormConfiguration.current_server:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
|
@ -158,7 +156,7 @@ class ControlClient(object):
|
||||||
data=json.dumps(telemetry),
|
data=json.dumps(telemetry),
|
||||||
headers={"content-type": "application/json"},
|
headers={"content-type": "application/json"},
|
||||||
verify=False,
|
verify=False,
|
||||||
proxies=ControlClient.proxies,
|
proxies=self.proxies,
|
||||||
timeout=MEDIUM_REQUEST_TIMEOUT,
|
timeout=MEDIUM_REQUEST_TIMEOUT,
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
@ -166,15 +164,14 @@ class ControlClient(object):
|
||||||
"Error connecting to control server %s: %s", WormConfiguration.current_server, exc
|
"Error connecting to control server %s: %s", WormConfiguration.current_server, exc
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
def load_control_config(self):
|
||||||
def load_control_config():
|
|
||||||
if not WormConfiguration.current_server:
|
if not WormConfiguration.current_server:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
reply = requests.get( # noqa: DUO123
|
reply = requests.get( # noqa: DUO123
|
||||||
f"https://{WormConfiguration.current_server}/api/agent/",
|
f"https://{WormConfiguration.current_server}/api/agent/",
|
||||||
verify=False,
|
verify=False,
|
||||||
proxies=ControlClient.proxies,
|
proxies=self.proxies,
|
||||||
timeout=MEDIUM_REQUEST_TIMEOUT,
|
timeout=MEDIUM_REQUEST_TIMEOUT,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -200,12 +197,11 @@ class ControlClient(object):
|
||||||
)
|
)
|
||||||
raise Exception("Couldn't load from from server's configuration, aborting. %s" % exc)
|
raise Exception("Couldn't load from from server's configuration, aborting. %s" % exc)
|
||||||
|
|
||||||
@staticmethod
|
def create_control_tunnel(self):
|
||||||
def create_control_tunnel():
|
|
||||||
if not WormConfiguration.current_server:
|
if not WormConfiguration.current_server:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
my_proxy = ControlClient.proxies.get("https", "").replace("https://", "")
|
my_proxy = self.proxies.get("https", "").replace("https://", "")
|
||||||
if my_proxy:
|
if my_proxy:
|
||||||
proxy_class = TcpProxy
|
proxy_class = TcpProxy
|
||||||
try:
|
try:
|
||||||
|
@ -224,13 +220,12 @@ class ControlClient(object):
|
||||||
target_port=target_port,
|
target_port=target_port,
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
def get_pba_file(self, filename):
|
||||||
def get_pba_file(filename):
|
|
||||||
try:
|
try:
|
||||||
return requests.get( # noqa: DUO123
|
return requests.get( # noqa: DUO123
|
||||||
PBA_FILE_DOWNLOAD % (WormConfiguration.current_server, filename),
|
PBA_FILE_DOWNLOAD % (WormConfiguration.current_server, filename),
|
||||||
verify=False,
|
verify=False,
|
||||||
proxies=ControlClient.proxies,
|
proxies=self.proxies,
|
||||||
timeout=LONG_REQUEST_TIMEOUT,
|
timeout=LONG_REQUEST_TIMEOUT,
|
||||||
)
|
)
|
||||||
except requests.exceptions.RequestException:
|
except requests.exceptions.RequestException:
|
||||||
|
|
|
@ -89,7 +89,7 @@ class InfectionMonkey:
|
||||||
self._singleton = SystemSingleton()
|
self._singleton = SystemSingleton()
|
||||||
self._opts = self._get_arguments(args)
|
self._opts = self._get_arguments(args)
|
||||||
self._cmd_island_ip, self._cmd_island_port = address_to_ip_port(self._opts.server)
|
self._cmd_island_ip, self._cmd_island_port = address_to_ip_port(self._opts.server)
|
||||||
self._default_server = self._opts.server
|
self.cc_client = ControlClient(self._opts.server)
|
||||||
self._monkey_inbound_tunnel = None
|
self._monkey_inbound_tunnel = None
|
||||||
self._telemetry_messenger = LegacyTelemetryMessengerAdapter()
|
self._telemetry_messenger = LegacyTelemetryMessengerAdapter()
|
||||||
self._current_depth = self._opts.depth
|
self._current_depth = self._opts.depth
|
||||||
|
@ -120,7 +120,6 @@ class InfectionMonkey:
|
||||||
logger.info("Agent is starting...")
|
logger.info("Agent is starting...")
|
||||||
logger.info(f"Agent GUID: {GUID}")
|
logger.info(f"Agent GUID: {GUID}")
|
||||||
|
|
||||||
self._add_default_server_to_config(self._opts.server)
|
|
||||||
self._connect_to_island()
|
self._connect_to_island()
|
||||||
|
|
||||||
# TODO: Reevaluate who is responsible to send this information
|
# TODO: Reevaluate who is responsible to send this information
|
||||||
|
@ -137,27 +136,20 @@ class InfectionMonkey:
|
||||||
self._setup()
|
self._setup()
|
||||||
self._master.start()
|
self._master.start()
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _add_default_server_to_config(default_server: str):
|
|
||||||
if default_server:
|
|
||||||
logger.debug("Added default server: %s" % default_server)
|
|
||||||
WormConfiguration.command_servers.insert(0, default_server)
|
|
||||||
|
|
||||||
def _connect_to_island(self):
|
def _connect_to_island(self):
|
||||||
# Sets island's IP and port for monkey to communicate to
|
# Sets island's IP and port for monkey to communicate to
|
||||||
if self._current_server_is_set():
|
if self._current_server_is_set():
|
||||||
self._default_server = WormConfiguration.current_server
|
logger.debug("Default server set to: %s" % self.cc_client.server_address)
|
||||||
logger.debug("Default server set to: %s" % self._default_server)
|
|
||||||
else:
|
else:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
"Monkey couldn't find server with {} default tunnel.".format(self._opts.tunnel)
|
"Monkey couldn't find server with {} default tunnel.".format(self._opts.tunnel)
|
||||||
)
|
)
|
||||||
|
|
||||||
ControlClient.wakeup(parent=self._opts.parent)
|
self.cc_client.wakeup(parent=self._opts.parent)
|
||||||
ControlClient.load_control_config()
|
self.cc_client.load_control_config()
|
||||||
|
|
||||||
def _current_server_is_set(self) -> bool:
|
def _current_server_is_set(self) -> bool:
|
||||||
if ControlClient.find_server(default_tunnel=self._opts.tunnel):
|
if self.cc_client.find_server(default_tunnel=self._opts.tunnel):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
@ -170,7 +162,7 @@ class InfectionMonkey:
|
||||||
if firewall.is_enabled():
|
if firewall.is_enabled():
|
||||||
firewall.add_firewall_rule()
|
firewall.add_firewall_rule()
|
||||||
|
|
||||||
self._monkey_inbound_tunnel = ControlClient.create_control_tunnel()
|
self._monkey_inbound_tunnel = self.cc_client.create_control_tunnel()
|
||||||
if self._monkey_inbound_tunnel and self._propagation_enabled():
|
if self._monkey_inbound_tunnel and self._propagation_enabled():
|
||||||
self._monkey_inbound_tunnel.start()
|
self._monkey_inbound_tunnel.start()
|
||||||
|
|
||||||
|
@ -184,7 +176,9 @@ class InfectionMonkey:
|
||||||
def _build_master(self):
|
def _build_master(self):
|
||||||
local_network_interfaces = InfectionMonkey._get_local_network_interfaces()
|
local_network_interfaces = InfectionMonkey._get_local_network_interfaces()
|
||||||
|
|
||||||
control_channel = ControlChannel(self._default_server, GUID)
|
# TODO control_channel and control_client have same responsibilities, merge them
|
||||||
|
control_channel = ControlChannel(self.cc_client.server_address, GUID)
|
||||||
|
control_client = self.cc_client
|
||||||
credentials_store = AggregatingCredentialsStore(control_channel)
|
credentials_store = AggregatingCredentialsStore(control_channel)
|
||||||
|
|
||||||
puppet = self._build_puppet(credentials_store)
|
puppet = self._build_puppet(credentials_store)
|
||||||
|
@ -206,6 +200,7 @@ class InfectionMonkey:
|
||||||
control_channel,
|
control_channel,
|
||||||
local_network_interfaces,
|
local_network_interfaces,
|
||||||
credentials_store,
|
credentials_store,
|
||||||
|
control_client,
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -237,7 +232,7 @@ class InfectionMonkey:
|
||||||
puppet.load_plugin("ssh", SSHFingerprinter(), PluginType.FINGERPRINTER)
|
puppet.load_plugin("ssh", SSHFingerprinter(), PluginType.FINGERPRINTER)
|
||||||
|
|
||||||
agent_repository = CachingAgentRepository(
|
agent_repository = CachingAgentRepository(
|
||||||
f"https://{self._default_server}", ControlClient.proxies
|
f"https://{self.cc_client.server_address}", self.cc_client.proxies
|
||||||
)
|
)
|
||||||
exploit_wrapper = ExploiterWrapper(self._telemetry_messenger, agent_repository)
|
exploit_wrapper = ExploiterWrapper(self._telemetry_messenger, agent_repository)
|
||||||
|
|
||||||
|
@ -320,7 +315,9 @@ class InfectionMonkey:
|
||||||
PluginType.POST_BREACH_ACTION,
|
PluginType.POST_BREACH_ACTION,
|
||||||
)
|
)
|
||||||
puppet.load_plugin(
|
puppet.load_plugin(
|
||||||
"CustomPBA", CustomPBA(self._telemetry_messenger), PluginType.POST_BREACH_ACTION
|
"CustomPBA",
|
||||||
|
CustomPBA(self._telemetry_messenger, self.cc_client),
|
||||||
|
PluginType.POST_BREACH_ACTION,
|
||||||
)
|
)
|
||||||
|
|
||||||
puppet.load_plugin("ransomware", RansomwarePayload(), PluginType.PAYLOAD)
|
puppet.load_plugin("ransomware", RansomwarePayload(), PluginType.PAYLOAD)
|
||||||
|
@ -338,7 +335,7 @@ class InfectionMonkey:
|
||||||
)
|
)
|
||||||
|
|
||||||
def _running_on_island(self, local_network_interfaces: List[NetworkInterface]) -> bool:
|
def _running_on_island(self, local_network_interfaces: List[NetworkInterface]) -> bool:
|
||||||
server_ip, _ = address_to_ip_port(self._default_server)
|
server_ip, _ = address_to_ip_port(self.cc_client.server_address)
|
||||||
return server_ip in {interface.address for interface in local_network_interfaces}
|
return server_ip in {interface.address for interface in local_network_interfaces}
|
||||||
|
|
||||||
def _is_another_monkey_running(self):
|
def _is_another_monkey_running(self):
|
||||||
|
@ -363,13 +360,13 @@ class InfectionMonkey:
|
||||||
|
|
||||||
deleted = InfectionMonkey._self_delete()
|
deleted = InfectionMonkey._self_delete()
|
||||||
|
|
||||||
InfectionMonkey._send_log()
|
self._send_log()
|
||||||
|
|
||||||
StateTelem(
|
StateTelem(
|
||||||
is_done=True, version=get_version()
|
is_done=True, version=get_version()
|
||||||
).send() # Signal the server (before closing the tunnel)
|
).send() # Signal the server (before closing the tunnel)
|
||||||
|
|
||||||
InfectionMonkey._close_tunnel()
|
self._close_tunnel()
|
||||||
self._singleton.unlock()
|
self._singleton.unlock()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"An error occurred while cleaning up the monkey agent: {e}")
|
logger.error(f"An error occurred while cleaning up the monkey agent: {e}")
|
||||||
|
@ -384,15 +381,15 @@ class InfectionMonkey:
|
||||||
# maximum depth from the server
|
# maximum depth from the server
|
||||||
return self._current_depth is None or self._current_depth > 0
|
return self._current_depth is None or self._current_depth > 0
|
||||||
|
|
||||||
@staticmethod
|
def _close_tunnel(self):
|
||||||
def _close_tunnel():
|
tunnel_address = (
|
||||||
tunnel_address = ControlClient.proxies.get("https", "").replace("http://", "").split(":")[0]
|
self.cc_client.proxies.get("https", "").replace("http://", "").split(":")[0]
|
||||||
|
)
|
||||||
if tunnel_address:
|
if tunnel_address:
|
||||||
logger.info("Quitting tunnel %s", tunnel_address)
|
logger.info("Quitting tunnel %s", tunnel_address)
|
||||||
tunnel.quit_tunnel(tunnel_address)
|
tunnel.quit_tunnel(tunnel_address)
|
||||||
|
|
||||||
@staticmethod
|
def _send_log(self):
|
||||||
def _send_log():
|
|
||||||
monkey_log_path = get_agent_log_path()
|
monkey_log_path = get_agent_log_path()
|
||||||
if monkey_log_path.is_file():
|
if monkey_log_path.is_file():
|
||||||
with open(monkey_log_path, "r") as f:
|
with open(monkey_log_path, "r") as f:
|
||||||
|
@ -400,7 +397,7 @@ class InfectionMonkey:
|
||||||
else:
|
else:
|
||||||
log = ""
|
log = ""
|
||||||
|
|
||||||
ControlClient.send_log(log)
|
self.cc_client.send_log(log)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _self_delete() -> bool:
|
def _self_delete() -> bool:
|
||||||
|
|
Loading…
Reference in New Issue