Machine code improvements. Integration tests work, unit tests fail

pull/44/head
Thorsten Sick 2 years ago
parent a4c979a492
commit 4721bc986c

@ -303,9 +303,13 @@ class Experiment():
if self.machine_needs_caldera(target_1, caldera_attacks):
target_1.install_caldera_service()
target_1.up()
print("before reboot")
target_1.reboot() # Kernel changes on system creation require a reboot
print("after reboot")
needs_reboot = target_1.prime_vulnerabilities()
print("after prime vulns")
needs_reboot |= target_1.prime_sensors()
print("after prime sens")
if needs_reboot:
self.attack_logger.vprint(
f"{CommandlineColors.OKBLUE}rebooting target {tname} ....{CommandlineColors.ENDC}", 1)

@ -9,19 +9,23 @@ import time
import requests
from app.config import MachineConfig
from app.exceptions import ServerError, ConfigurationError
from app.exceptions import ServerError, ConfigurationError, PluginError
from app.pluginmanager import PluginManager
from app.calderacontrol import CalderaControl
from app.interface_sfx import CommandlineColors
from app.attack_log import AttackLog
from plugins.base.machinery import MachineryPlugin
from plugins.base.sensor import SensorPlugin
from plugins.base.vulnerability_plugin import VulnerabilityPlugin
from app.config_verifier import Attacker, Target
from typing import Any, Optional, Union
class Machine():
""" A virtual machine. Attacker or target. Abstracting stuff away. """
def __init__(self, config, attack_logger, calderakey="ADMIN123",):
def __init__(self, config: Union[dict, MachineConfig, Attacker, Target], attack_logger: AttackLog, calderakey: str = "ADMIN123",) -> None:
"""
:param config: The machine configuration as dict
@ -29,14 +33,22 @@ class Machine():
:param calderakey: Key to the caldera controller
"""
self.vm_manager = None
self.attack_logger = None
self.set_attack_logger(attack_logger)
self.vm_manager: Optional[MachineryPlugin] = None
self.attack_logger: AttackLog = attack_logger
self.calderakey: str = calderakey
self.sensors: list[SensorPlugin] = [] # Sensor plugins
self.vulnerabilities: list[VulnerabilityPlugin] = [] # Vulnerability plugins
self.caldera_server: str = ""
if isinstance(config, MachineConfig):
self.config = config
else:
elif isinstance(config, Attacker):
self.config = MachineConfig(config)
elif isinstance(config, Target):
self.config = MachineConfig(config)
else:
print(type(config))
raise ConfigurationError("unknown type")
self.plugin_manager = PluginManager(self.attack_logger)
@ -46,8 +58,6 @@ class Machine():
if self.config.vmcontroller() == "running_vm":
self.__parse_running_vm_config__()
self.caldera_server = None
self.abs_machinepath_external = None
self.abs_machinepath_external = os.path.join(self.vagrantfilepath, self.config.machinepath())
@ -58,13 +68,14 @@ class Machine():
raise ConfigurationError(f"machinepath does not exist: {self.abs_machinepath_external}")
self.load_machine_plugin()
self.caldera_basedir = self.vm_manager.get_playground()
self.calderakey = calderakey
self.sensors = [] # Sensor plugins
self.vulnerabilities = [] # Vulnerability plugins
def __parse_vagrant_config__(self):
if self.vm_manager is None:
raise ConfigurationError("VM manager required")
playground = self.vm_manager.get_playground()
if playground is None:
playground = ""
self.caldera_basedir: str = playground
def __parse_vagrant_config__(self) -> None:
""" Check if a file configured in the config is present """
self.vagrantfilepath = os.path.abspath(self.config.vagrantfilepath())
@ -72,113 +83,152 @@ class Machine():
if not os.path.isfile(self.vagrantfile):
raise ConfigurationError(f"Vagrantfile not existing: {self.vagrantfile}")
def __parse_running_vm_config__(self):
def __parse_running_vm_config__(self) -> None:
""" Check if a file configured in the config is present """
self.vagrantfilepath = os.path.abspath(self.config.vagrantfilepath())
self.vagrantfile = os.path.join(self.vagrantfilepath, "Vagrantfile")
def get_paw(self):
def get_paw(self) -> Optional[str]:
""" Returns the paw of the current machine """
return self.config.caldera_paw()
def get_group(self):
def get_group(self) -> Optional[str]:
""" Returns the group of the current machine """
return self.config.caldera_group()
def destroy(self):
def destroy(self) -> None:
""" Destroys the current machine """
if self.vm_manager is None:
raise ConfigurationError("VM Manager is missing")
self.vm_manager.__call_destroy__()
def create(self, reboot=True):
def create(self, reboot: bool = True) -> None:
""" Create a VM
:param reboot: Reboot the VM during installation. Required if you want to install software
"""
if self.vm_manager is None:
raise ConfigurationError("VM Manager is missing")
self.vm_manager.__call_create__(reboot)
def reboot(self):
def reboot(self) -> None:
""" Reboot a machine """
if self.vm_manager is None:
raise ConfigurationError("VM Manager is missing")
if self.get_os() == "windows":
self.remote_run("shutdown /r")
self.vm_manager.__call_disconnect__()
time.sleep(60) # Shutdown can be slow....
if self.get_os() == "linux":
self.remote_run("reboot")
self.remote_run("sudo reboot", must_succeed=False)
self.vm_manager.__call_disconnect__()
res = None
while not res:
time.sleep(5)
if self.vm_manager is None:
raise ConfigurationError("VM Manager is missing")
res = self.vm_manager.__call_connect__()
self.attack_logger.vprint("Re-connecting....", 3)
if self.attack_logger is not None:
self.attack_logger.vprint("Re-connecting....", 3)
self.attack_logger.vprint(f"The machine {self.vm_manager.get_vm_name()} is back {res.is_connected}", 3)
def up(self): # pylint: disable=invalid-name
def up(self) -> None: # pylint: disable=invalid-name
""" Starts a VM. Creates it if not already created """
if self.vm_manager is None:
raise ConfigurationError("VM Manager is missing")
self.vm_manager.__call_up__()
def halt(self):
def halt(self) -> None:
""" Halts a VM """
if self.vm_manager is None:
raise ConfigurationError("VM Manager is missing")
self.vm_manager.__call_halt__()
def getuser(self):
def getuser(self) -> str:
""" Gets the user of the current VM """
if self.vm_manager is None:
raise ConfigurationError("VM Manager is missing")
return "Result " + str(self.vm_manager.__call_remote_run__("echo $USER"))
def connect(self):
def connect(self) -> Any:
""" command connection. establish it """
if self.vm_manager is None:
raise ConfigurationError("VM Manager is missing")
return self.vm_manager.__call_connect__()
def disconnect(self, connection):
def disconnect(self, connection: Any) -> None:
""" Command connection dis-connect """
self.vm_manager.__call_disconnect__(connection)
if self.vm_manager is None:
raise ConfigurationError("VM Manager is missing")
self.vm_manager.__call_disconnect__()
def remote_run(self, cmd, disown=False):
def remote_run(self, cmd: str, disown: bool = False, must_succeed: bool = False) -> str:
""" Simplifies connect and run
:param cmd: Command to run as shell command
:param disown: run in background
:param must_succeed: Throw an exception if the command being run fails.
"""
if self.vm_manager is None:
raise ConfigurationError("Missing VM Manager")
return self.vm_manager.__call_remote_run__(cmd, disown, must_succeed)
return self.vm_manager.__call_remote_run__(cmd, disown)
def load_machine_plugin(self):
def load_machine_plugin(self) -> None:
""" Loads the matching machine plugin """
for plugin in self.plugin_manager.get_plugins(MachineryPlugin, [self.config.vmcontroller()]):
if not isinstance(plugin, MachineryPlugin):
raise PluginError("Expected Machinery Plugin")
name = plugin.get_name()
self.attack_logger.vprint(f"{CommandlineColors.OKBLUE}Installing machinery: {name}{CommandlineColors.ENDC}", 1)
if self.attack_logger is not None:
self.attack_logger.vprint(f"{CommandlineColors.OKBLUE}Installing machinery: {name}{CommandlineColors.ENDC}", 1)
syscon = {"abs_machinepath_internal": self.abs_machinepath_internal,
"abs_machinepath_external": self.abs_machinepath_external}
plugin.set_sysconf(syscon)
plugin.__call_process_config__(self.config)
self.vm_manager = plugin
self.attack_logger.vprint(f"{CommandlineColors.OKGREEN}Installed machinery: {name}{CommandlineColors.ENDC}",
if self.attack_logger is not None:
self.attack_logger.vprint(f"{CommandlineColors.OKGREEN}Installed machinery: {name}{CommandlineColors.ENDC}",
1)
break
def prime_sensors(self):
def prime_sensors(self) -> bool:
""" Prime sensors from plugins (hard core installs that could require a reboot)
A machine can have several sensors running. Those are defined in a list in the config. This primes the sensors
:result: true if a reboot is required
"""
reboot = False
for plugin in self.plugin_manager.get_plugins(SensorPlugin, self.config.sensors()):
if not isinstance(plugin, SensorPlugin):
raise PluginError("Expected sensor plugin")
name = plugin.get_name()
# if name in self.config.sensors():
self.attack_logger.vprint(f"{CommandlineColors.OKBLUE}Priming sensor: {name}{CommandlineColors.ENDC}", 2)
if self.attack_logger is not None:
self.attack_logger.vprint(f"{CommandlineColors.OKBLUE}Priming sensor: {name}{CommandlineColors.ENDC}", 2)
syscon = {"abs_machinepath_internal": self.abs_machinepath_internal,
"abs_machinepath_external": self.abs_machinepath_external,
}
@ -189,10 +239,11 @@ class Machine():
plugin.setup()
reboot |= plugin.prime()
self.sensors.append(plugin)
self.attack_logger.vprint(f"{CommandlineColors.OKGREEN}Primed sensor: {name}{CommandlineColors.ENDC}", 2)
if self.attack_logger is not None:
self.attack_logger.vprint(f"{CommandlineColors.OKGREEN}Primed sensor: {name}{CommandlineColors.ENDC}", 2)
return reboot
def install_sensors(self):
def install_sensors(self) -> None:
""" Install sensors from plugins
A machine can have several sensors running. Those are defined in a list in the config. This installs the sensors
@ -202,7 +253,8 @@ class Machine():
for plugin in self.get_sensors():
name = plugin.get_name()
self.attack_logger.vprint(f"{CommandlineColors.OKBLUE}Installing sensor: {name}{CommandlineColors.ENDC}", 2)
if self.attack_logger is not None:
self.attack_logger.vprint(f"{CommandlineColors.OKBLUE}Installing sensor: {name}{CommandlineColors.ENDC}", 2)
syscon = {"abs_machinepath_internal": self.abs_machinepath_internal,
"abs_machinepath_external": self.abs_machinepath_external,
}
@ -211,25 +263,28 @@ class Machine():
plugin.process_config(self.config.raw_config.get(name, {})) # plugin specific configuration
plugin.setup()
plugin.install()
self.attack_logger.vprint(f"{CommandlineColors.OKGREEN}Installed sensor: {name}{CommandlineColors.ENDC}", 2)
if self.attack_logger is not None:
self.attack_logger.vprint(f"{CommandlineColors.OKGREEN}Installed sensor: {name}{CommandlineColors.ENDC}", 2)
def get_sensors(self) -> list[SensorPlugin]:
""" Returns a list of running sensors """
return self.sensors
def start_sensors(self):
def start_sensors(self) -> None:
""" Start sensors
A machine can have several sensors running. Those are defined in a list in the config. This starts the sensors
"""
for plugin in self.get_sensors():
self.attack_logger.vprint(f"{CommandlineColors.OKBLUE}Starting sensor: {plugin.get_name()}{CommandlineColors.ENDC}", 2)
if self.attack_logger is not None:
self.attack_logger.vprint(f"{CommandlineColors.OKBLUE}Starting sensor: {plugin.get_name()}{CommandlineColors.ENDC}", 2)
plugin.set_machine_plugin(self.vm_manager)
plugin.start()
self.attack_logger.vprint(f"{CommandlineColors.OKGREEN}Started sensor: {plugin.get_name()}{CommandlineColors.ENDC}", 2)
if self.attack_logger is not None:
self.attack_logger.vprint(f"{CommandlineColors.OKGREEN}Started sensor: {plugin.get_name()}{CommandlineColors.ENDC}", 2)
def stop_sensors(self):
def stop_sensors(self) -> None:
""" Stop sensors
A machine can have several sensors running. Those are defined in a list in the config. This stops the sensors
@ -237,17 +292,20 @@ class Machine():
"""
for plugin in self.get_sensors():
self.attack_logger.vprint(f"{CommandlineColors.OKBLUE}Stopping sensor: {plugin.get_name()}{CommandlineColors.ENDC}", 2)
if self.attack_logger is not None:
self.attack_logger.vprint(f"{CommandlineColors.OKBLUE}Stopping sensor: {plugin.get_name()}{CommandlineColors.ENDC}", 2)
plugin.set_machine_plugin(self.vm_manager)
plugin.stop()
self.attack_logger.vprint(f"{CommandlineColors.OKGREEN}Stopped sensor: {plugin.get_name()}{CommandlineColors.ENDC}", 2)
if self.attack_logger is not None:
self.attack_logger.vprint(f"{CommandlineColors.OKGREEN}Stopped sensor: {plugin.get_name()}{CommandlineColors.ENDC}", 2)
def collect_sensors(self, lootdir):
def collect_sensors(self, lootdir: str) -> list[str]:
""" Collect data from sensors
A machine can have several sensors running. Those are defined in a list in the config. This collects the data from the sensors
:param lootdir: Fresh created directory for loot
:returns: a list of file names to put into the loot zip
"""
machine_specific_path = os.path.join(lootdir, self.config.vmname())
@ -255,27 +313,32 @@ class Machine():
loot_files = []
for plugin in self.get_sensors():
self.attack_logger.vprint(f"{CommandlineColors.OKBLUE}Collecting sensor: {plugin.get_name()}{CommandlineColors.ENDC}", 2)
if self.attack_logger is not None:
self.attack_logger.vprint(f"{CommandlineColors.OKBLUE}Collecting sensor: {plugin.get_name()}{CommandlineColors.ENDC}", 2)
plugin.set_machine_plugin(self.vm_manager)
loot_files += plugin.__call_collect__(machine_specific_path)
self.attack_logger.vprint(f"{CommandlineColors.OKGREEN}Collected sensor: {plugin.get_name()}{CommandlineColors.ENDC}", 2)
if self.attack_logger is not None:
self.attack_logger.vprint(f"{CommandlineColors.OKGREEN}Collected sensor: {plugin.get_name()}{CommandlineColors.ENDC}", 2)
return loot_files
############
def prime_vulnerabilities(self):
def prime_vulnerabilities(self) -> bool:
""" Prime vulnerabilities from plugins (hard core installs that could require a reboot)
A machine can have several vulnerabilities. Those are defined in a list in the config.
:returns: True if a reboot is requires
"""
reboot = False
for plugin in self.plugin_manager.get_plugins(VulnerabilityPlugin, self.config.vulnerabilities()):
if not isinstance(plugin, VulnerabilityPlugin):
raise PluginError("Plugin manager returned wrong plugin type")
name = plugin.get_name()
self.attack_logger.vprint(f"{CommandlineColors.OKBLUE}Priming vulnerability: {name}{CommandlineColors.ENDC}", 2)
if self.attack_logger is not None:
self.attack_logger.vprint(f"{CommandlineColors.OKBLUE}Priming vulnerability: {name}{CommandlineColors.ENDC}", 2)
syscon = {"abs_machinepath_internal": self.abs_machinepath_internal,
"abs_machinepath_external": self.abs_machinepath_external,
}
@ -285,10 +348,11 @@ class Machine():
plugin.setup()
reboot |= plugin.prime()
self.vulnerabilities.append(plugin)
self.attack_logger.vprint(f"{CommandlineColors.OKGREEN}Primed vulnerability: {name}{CommandlineColors.ENDC}", 2)
if self.attack_logger is not None:
self.attack_logger.vprint(f"{CommandlineColors.OKGREEN}Primed vulnerability: {name}{CommandlineColors.ENDC}", 2)
return reboot
def install_vulnerabilities(self):
def install_vulnerabilities(self) -> None:
""" Install vulnerabilities from plugins: The machine is not yet modified ! For that call start_vulnerabilities next
A machine can have several vulnerabilities. Those are defined in a list in the config. This installs the vulnerabilities
@ -296,9 +360,12 @@ class Machine():
"""
for plugin in self.plugin_manager.get_plugins(VulnerabilityPlugin, self.config.vulnerabilities()):
if not isinstance(plugin, VulnerabilityPlugin):
raise PluginError("Plugin manager returned wrong plugin type")
name = plugin.get_name()
self.attack_logger.vprint(f"Configured vulnerabilities: {self.config.vulnerabilities()}", 3)
self.attack_logger.vprint(f"{CommandlineColors.OKBLUE}Installing vulnerability: {name}{CommandlineColors.ENDC}", 2)
if self.attack_logger is not None:
self.attack_logger.vprint(f"Configured vulnerabilities: {self.config.vulnerabilities()}", 3)
self.attack_logger.vprint(f"{CommandlineColors.OKBLUE}Installing vulnerability: {name}{CommandlineColors.ENDC}", 2)
syscon = {"abs_machinepath_internal": self.abs_machinepath_internal,
"abs_machinepath_external": self.abs_machinepath_external}
plugin.set_sysconf(syscon)
@ -312,25 +379,27 @@ class Machine():
""" Returns a list of installed vulnerabilities """
return self.vulnerabilities
def start_vulnerabilities(self):
def start_vulnerabilities(self) -> None:
""" Really install the vulnerabilities on the machine
A machine can have vulnerabilities installed. Those are defined in a list in the config. This starts the vulnerabilities
"""
for plugin in self.get_vulnerabilities():
self.attack_logger.vprint(f"{CommandlineColors.OKBLUE}Activating vulnerability: {plugin.get_name()}{CommandlineColors.ENDC}", 2)
if self.attack_logger is not None:
self.attack_logger.vprint(f"{CommandlineColors.OKBLUE}Activating vulnerability: {plugin.get_name()}{CommandlineColors.ENDC}", 2)
plugin.set_machine_plugin(self.vm_manager)
plugin.start()
def stop_vulnerabilities(self):
def stop_vulnerabilities(self) -> None:
""" Un-install the vulnerabilities on the machine
A machine can have vulnerabilities installed. Those are defined in a list in the config. This stops the vulnerabilities
"""
for plugin in self.get_vulnerabilities():
self.attack_logger.vprint(f"{CommandlineColors.OKBLUE}Uninstalling vulnerability: {plugin.get_name()}{CommandlineColors.ENDC}", 2)
if self.attack_logger is not None:
self.attack_logger.vprint(f"{CommandlineColors.OKBLUE}Uninstalling vulnerability: {plugin.get_name()}{CommandlineColors.ENDC}", 2)
plugin.set_machine_plugin(self.vm_manager)
plugin.stop()
@ -340,6 +409,8 @@ class Machine():
""" Returns the IP of the main ethernet interface of this machine """
# TODO: Find a smarter way to get the ip
if self.vm_manager is None:
raise ConfigurationError("Missing VM Manager")
return self.vm_manager.get_ip()
@ -353,23 +424,31 @@ class Machine():
return self.config.get_nicknames()
def get_playground(self) -> str:
def get_playground(self) -> Optional[str]:
""" Return this machine's playground """
if self.vm_manager is None:
raise ConfigurationError("Missing VM Manager")
return self.vm_manager.get_playground()
def get_machine_path_external(self) -> str:
""" Returns the external path for this machine """
if self.vm_manager is None:
raise ConfigurationError("Missing VM Manager")
return self.vm_manager.get_machine_path_external()
def put(self, src: str, dst: str):
def put(self, src: str, dst: str) -> Any:
""" Send a file to the machine """
if self.vm_manager is None:
raise ConfigurationError("Missing VM Manager")
return self.vm_manager.put(src, dst)
def get(self, src: str, dst: str):
def get(self, src: str, dst: str) -> Any:
""" Get a file from a machine """
if self.vm_manager is None:
raise ConfigurationError("Missing VM Manager")
return self.vm_manager.get(src, dst)
@ -391,14 +470,18 @@ class Machine():
# TODO: Metasploit implant
# options for version: "4.0.0-alpha.2" and "2.8.1"
def install_caldera_server(self, cleanup=False, version="4.0.0-alpha.2"):
def install_caldera_server(self, cleanup: bool = False, version: str = "4.0.0-alpha.2") -> str:
""" Installs the caldera server on the VM
:param cleanup: Remove the old caldera version. Slow but reduces side effects
:param version: Caldera version to use. Check Caldera git for potential branches to use
"""
# https://github.com/mitre/caldera.git
self.attack_logger.vprint(f"{CommandlineColors.OKBLUE}Installing Caldera server {CommandlineColors.ENDC}", 1)
if self.vm_manager is None:
raise ConfigurationError("VM manager missing")
if self.attack_logger is not None:
self.attack_logger.vprint(f"{CommandlineColors.OKBLUE}Installing Caldera server {CommandlineColors.ENDC}", 1)
if cleanup:
cleanupcmd = "rm -rf caldera;"
@ -406,20 +489,25 @@ class Machine():
cleanupcmd = ""
cmd = f"cd {self.caldera_basedir}; {cleanupcmd} git clone https://github.com/mitre/caldera.git --recursive --branch {version}; cd caldera; git checkout {version}; pip3 install -r requirements.txt"
self.attack_logger.vprint(f"{CommandlineColors.OKGREEN}Caldera server installed {CommandlineColors.ENDC}", 1)
if self.attack_logger is not None:
self.attack_logger.vprint(f"{CommandlineColors.OKGREEN}Caldera server installed {CommandlineColors.ENDC}", 1)
res = self.vm_manager.__call_remote_run__(cmd)
return "Result installing caldera server " + str(res)
def wait_for_caldera_server(self, timeout=6):
def wait_for_caldera_server(self, timeout: int = 6) -> bool:
""" Ping caldera server. return as soon as it is responding
:param timeout: timeout in seconds
"""
if self.attack_logger is None:
raise ConfigurationError("Attack logger required")
for i in range(timeout):
time.sleep(10)
caldera_url = "http://" + self.get_ip() + ":8888"
caldera_control = CalderaControl(caldera_url, self.attack_logger, apikey=self.calderakey)
self.attack_logger.vprint(f"{i} Trying to connect to {caldera_url} Caldera API", 3)
if self.attack_logger is not None:
self.attack_logger.vprint(f"{i} Trying to connect to {caldera_url} Caldera API", 3)
try:
caldera_control.list_adversaries()
except requests.exceptions.ConnectionError:
@ -429,20 +517,22 @@ class Machine():
return True
raise ServerError
def start_caldera_server(self):
def start_caldera_server(self) -> None:
""" Start the caldera server on the VM. Required for an attacker VM """
# https://github.com/mitre/caldera.git
self.attack_logger.vprint(f"{CommandlineColors.OKBLUE}Starting Caldera server {CommandlineColors.ENDC}", 1)
if self.attack_logger is not None:
self.attack_logger.vprint(f"{CommandlineColors.OKBLUE}Starting Caldera server {CommandlineColors.ENDC}", 1)
# The pkill was added because the server sometimes gets stuck. And we can not re-create the attacking machines in all cases
self.remote_run(" pkill -f server.py;", disown=False)
self.remote_run(" pkill -f server.py || true;", disown=False)
cmd = f"cd {self.caldera_basedir}; cd caldera ; nohup python3 server.py --insecure &"
self.remote_run(cmd, disown=True)
self.wait_for_caldera_server()
self.attack_logger.vprint(f"{CommandlineColors.OKGREEN}Caldera server started. Confirmed it is running. {CommandlineColors.ENDC}", 1)
if self.attack_logger is not None:
self.attack_logger.vprint(f"{CommandlineColors.OKGREEN}Caldera server started. Confirmed it is running. {CommandlineColors.ENDC}", 1)
def create_start_caldera_client_cmd(self):
def create_start_caldera_client_cmd(self) -> str:
""" Creates a command to start the caldera client """
playground = self.get_playground()
@ -462,20 +552,32 @@ class Machine():
return cmd
def start_caldera_client(self):
def start_caldera_client(self) -> None:
""" Install caldera client. Required on targets """
if self.vm_manager is None:
raise PluginError("Vm manager not available")
name = self.vm_manager.get_vm_name()
self.attack_logger.vprint(f"{CommandlineColors.OKBLUE}Starting Caldera client {name} {CommandlineColors.ENDC}", 1)
if self.attack_logger is not None:
self.attack_logger.vprint(f"{CommandlineColors.OKBLUE}Starting Caldera client {name} {CommandlineColors.ENDC}", 1)
if self.get_os() == "windows":
if self.caldera_server is None:
raise ConfigurationError("Caldera server not set")
url = "http://" + self.caldera_server + ":8888"
if not isinstance(self.attack_logger, AttackLog):
raise ConfigurationError("attack_logger is not of type AttackLog")
if self.abs_machinepath_external is None:
raise ConfigurationError("External machine path not set")
caldera_control = CalderaControl(url, self.attack_logger, apikey=self.calderakey)
caldera_control.fetch_client(platform="windows",
file="sandcat.go",
target_dir=self.abs_machinepath_external,
extension=".go")
dst = self.get_playground()
if self.abs_machinepath_external is None:
raise ConfigurationError("External machine path not set")
src = os.path.join(self.abs_machinepath_external, "caldera_agent.bat")
self.vm_manager.put(src, dst)
src = os.path.join(self.abs_machinepath_external, "splunkd.go") # sandcat.go local name
@ -488,6 +590,10 @@ class Machine():
if self.get_os() == "linux":
dst = self.get_playground()
if self.abs_machinepath_external is None:
raise ConfigurationError("machine_path external not set")
if dst is None:
raise ConfigurationError("Missing playground")
src = os.path.join(self.abs_machinepath_external, "caldera_agent.sh")
self.vm_manager.put(src, dst)
@ -496,14 +602,15 @@ class Machine():
print(cmd)
self.remote_run(cmd, disown=True)
self.attack_logger.vprint(f"{CommandlineColors.OKGREEN}Caldera client started {CommandlineColors.ENDC}", 1)
if self.attack_logger is not None:
self.attack_logger.vprint(f"{CommandlineColors.OKGREEN}Caldera client started {CommandlineColors.ENDC}", 1)
def get_os(self):
def get_os(self) -> str:
""" Returns the OS of the machine """
return self.config.os()
def __wmi_cmd_for_caldera_implant(self):
def __wmi_cmd_for_caldera_implant(self) -> str:
""" Creates a windows specific command to start the caldera implant in background using wmi """
playground = self.get_playground()
@ -511,15 +618,23 @@ class Machine():
playground = playground + "\\"
else:
playground = "%userprofile%\\"
if self.caldera_server is None:
raise ConfigurationError("Caldera server not configured")
url = "http://" + self.caldera_server + ":8888"
res = f'wmic process call create "{playground}splunkd.go -server {url} -group {self.config.caldera_group()} -paw {self.config.caldera_paw()}" '
return res
def __install_caldera_service_cmd(self):
def __install_caldera_service_cmd(self) -> str:
playground = self.get_playground()
if self.abs_machinepath_external is None:
raise ConfigurationError("machine path external is not set")
if self.attack_logger is None:
raise
if self.get_os() == "linux":
return f"""
#!/bin/bash
@ -551,14 +666,18 @@ START {playground}{filename} -server {url} -group {self.config.caldera_group()}
raise Exception # System type unknown
def install_caldera_service(self):
def install_caldera_service(self) -> None:
""" Install the caldera client as a service. For linux targets """
# print("DELETEME ! " + sys._getframe().f_code.co_name)
content = self.__install_caldera_service_cmd()
self.attack_logger.vprint(f"{CommandlineColors.OKBLUE}Installing Caldera service {CommandlineColors.ENDC}", 1)
if self.abs_machinepath_external is None:
raise ConfigurationError("machine path external is not set")
if self.attack_logger is not None:
self.attack_logger.vprint(f"{CommandlineColors.OKBLUE}Installing Caldera service {CommandlineColors.ENDC}", 1)
if self.get_os() == "linux":
filename = os.path.join(self.abs_machinepath_external, "caldera_agent.sh")
@ -566,16 +685,10 @@ START {playground}{filename} -server {url} -group {self.config.caldera_group()}
filename = os.path.join(self.abs_machinepath_external, "caldera_agent.bat")
with open(filename, "wt") as fh:
fh.write(content)
self.attack_logger.vprint(f"{CommandlineColors.OKGREEN}Installed Caldera service {CommandlineColors.ENDC}", 1)
if self.attack_logger is not None:
self.attack_logger.vprint(f"{CommandlineColors.OKGREEN}Installed Caldera service {CommandlineColors.ENDC}", 1)
def set_caldera_server(self, server):
def set_caldera_server(self, server: str) -> None:
""" Set the local caldera server config """
self.caldera_server = server
def set_attack_logger(self, attack_logger):
""" Configure the attack logger for this server
:param attack_logger: The attack logger to set
"""
self.attack_logger = attack_logger

@ -68,13 +68,14 @@ class MachineryPlugin(BasePlugin):
"""
raise NotImplementedError
def remote_run(self, cmd: str, disown: bool = False) -> str:
def remote_run(self, cmd: str, disown: bool = False, must_succeed: bool = False) -> str:
""" Connects to the machine and runs a command there
If you want to use SSH, check out the class SSHFeatures, it is already implemented there
:param cmd: command to run int he machine's shell
:param disown: Send the connection into background
:param must_succeed: Throw an exception if the command being run fails.
:returns: the results as string
"""
@ -88,7 +89,7 @@ class MachineryPlugin(BasePlugin):
"""
raise NotImplementedError
def put(self, src: str, dst: str) -> Any:
def put(self, src: str, dst: Optional[str]) -> Any:
""" Send a file to a machine
If you want to use SSH, check out the class SSHFeatures, it is already implemented there
@ -203,21 +204,22 @@ class MachineryPlugin(BasePlugin):
self.config = config
self.process_config(config.raw_config.__dict__)
def __call_remote_run__(self, cmd: str, disown: bool = False) -> str:
def __call_remote_run__(self, cmd: str, disown: bool = False, must_succeed: bool = False) -> str:
""" Simplifies connect and run
@param cmd: Command to run as shell command
@param disown: run in background
:param cmd: Command to run as shell command
:param disown: run in background
:param must_succeed: Throw an exception if the command being run fails.
"""
return self.remote_run(cmd, disown)
return self.remote_run(cmd, disown, must_succeed)
def __call_disconnect__(self) -> None:
""" Command connection dis-connect """
self.disconnect()
def __call_connect__(self) -> None:
def __call_connect__(self) -> Any:
""" command connection. establish it """
return self.connect()

@ -3,7 +3,7 @@
import os.path
import socket
import time
from typing import Any
from typing import Any, Optional
import paramiko
from fabric import Connection # type: ignore
@ -38,7 +38,7 @@ class SSHFeatures(BasePlugin):
try:
if self.config.os() == "linux":
uhp = self.get_ip()
self.vprint(f"Connecting to {uhp}", 3)
self.vprint(f"SSH connecting to {uhp}", 3)
self.connection = Connection(uhp, connect_timeout=timeout)
if self.config.os() == "windows":
@ -64,11 +64,12 @@ class SSHFeatures(BasePlugin):
self.vprint("SSH network error", 0)
raise NetworkError
def remote_run(self, cmd: str, disown: bool = False) -> str:
def remote_run(self, cmd: str, disown: bool = False, must_succeed: bool = False) -> str:
""" Connects to the machine and runs a command there
:param cmd: The command to execute
:param disown: Send the connection into background
:param must_succeed: Throw an exception if the command being run fails.
:returns: The results as string
"""
@ -82,34 +83,48 @@ class SSHFeatures(BasePlugin):
self.vprint("Disown: " + str(disown), 3)
# self.vprint("Connection: " + self.connection, 1)
result = None
retry = 2
while retry > 0:
retry = 10
while retry >= 0:
do_retry = False
try:
print(f"Running cmd {cmd}")
result = self.connection.run(cmd, disown=disown)
print(result)
# paramiko.ssh_exception.SSHException in the next line is needed for windows openssh
except (paramiko.ssh_exception.NoValidConnectionsError, UnexpectedExit, paramiko.ssh_exception.SSHException) as error:
except (paramiko.ssh_exception.NoValidConnectionsError, paramiko.ssh_exception.SSHException) as error:
if retry <= 0:
raise NetworkError from error
do_retry = True
except UnexpectedExit as error:
if must_succeed:
if retry <= 0:
raise NetworkError from error
do_retry = True
else:
# breakpoint()
break
except Exception as error:
raise NetworkError from error
if do_retry:
time.sleep(5)
self.disconnect()
time.sleep(5)
self.connect()
retry -= 1
self.vprint("Got some SSH errors. Retrying", 2)
self.vprint(f"Got some SSH errors. Retrying {retry}", 2)
else:
break
if result and result.stderr:
self.vprint("Debug: Stderr: " + str(result.stderr.strip()), 0)
return result.stderr.strip()
if result:
return result.stdout.strip()
return ""
def put(self, src: str, dst: str) -> Any:
def put(self, src: str, dst: Optional[str]) -> Any:
""" Send a file to a machine
:param src: source dir

@ -12,6 +12,7 @@ from app.exceptions import ConfigurationError
# from invoke.exceptions import UnexpectedExit
# import paramiko
from plugins.base.ssh_features import SSHFeatures
from typing import Any
# Experiment with paramiko instead of fabric. Seems fabric has some issues with the "put" command to Windows. There seems no fix (just my workarounds). Maybe paramiko is better.
@ -75,7 +76,7 @@ class VagrantPlugin(SSHFeatures, MachineryPlugin):
""" Destroy a machine """
self.v.destroy(vm_name=self.config.vmname())
def connect(self):
def connect(self) -> Any:
""" Connect to a machine. If there is already a connection we keep it """
# For linux we are using Vagrant style
@ -84,8 +85,9 @@ class VagrantPlugin(SSHFeatures, MachineryPlugin):
return self.connection
uhp = self.v.user_hostname_port(vm_name=self.config.vmname())
self.vprint(f"Connecting to {uhp}", 3)
self.vprint(f"Vagrant connecting to {uhp} ({self.get_vm_name()}/{self.config.vmname()})", 3)
self.connection = Connection(uhp, connect_kwargs={"key_filename": self.v.keyfile(vm_name=self.config.vmname())})
self.vprint(f"Vagrant connection: {self.connection.is_connected}", 3)
return self.connection
else:

Loading…
Cancel
Save