mirror of https://github.com/avast/PurpleDome
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
348 lines
16 KiB
Python
348 lines
16 KiB
Python
#!/usr/bin/env python3
|
|
|
|
""" A class to control a whole experiment. From setting up the machines to running the attacks """
|
|
|
|
import os
|
|
import subprocess
|
|
import time
|
|
import zipfile
|
|
import shutil
|
|
from datetime import datetime
|
|
from typing import Optional
|
|
|
|
from app.attack_log import AttackLog
|
|
from app.config import ExperimentConfig
|
|
from app.interface_sfx import CommandlineColors
|
|
from app.exceptions import ServerError
|
|
from app.pluginmanager import PluginManager
|
|
from app.doc_generator import DocGenerator
|
|
from caldera_control import CalderaControl
|
|
from machine_control import Machine
|
|
from plugins.base.attack import AttackPlugin
|
|
|
|
|
|
# TODO: Multi threading at least when starting machines
|
|
|
|
class Experiment():
|
|
""" Class handling experiments """
|
|
|
|
def __init__(self, configfile, verbosity=0, caldera_attacks: list = None):
|
|
"""
|
|
|
|
@param configfile: Path to the configfile to load
|
|
@param verbosity: verbosity level between 0 and 3
|
|
@param caldera_attacks: an optional argument to override caldera attacks in the config file and run just this one caldera attack. A list of caldera ID
|
|
"""
|
|
self.attacker_1: Optional[Machine] = None
|
|
|
|
self.experiment_config = ExperimentConfig(configfile)
|
|
self.attack_logger = AttackLog(verbosity)
|
|
self.plugin_manager = PluginManager(self.attack_logger)
|
|
self.__start_attacker()
|
|
if self.attacker_1 is None:
|
|
raise ServerError
|
|
caldera_url = "http://" + self.attacker_1.get_ip() + ":8888"
|
|
self.caldera_control = CalderaControl(caldera_url, attack_logger=self.attack_logger, config=self.experiment_config)
|
|
# self.caldera_control = CalderaControl("http://" + self.attacker_1.get_ip() + ":8888", self.attack_logger,
|
|
# config=self.experiment_config)
|
|
# Deleting all currently registered Caldera gents
|
|
self.attack_logger.vprint(self.caldera_control.kill_all_agents(), 3)
|
|
self.attack_logger.vprint(self.caldera_control.delete_all_agents(), 3)
|
|
|
|
self.starttime = datetime.now().strftime("%Y_%m_%d___%H_%M_%S")
|
|
self.lootdir = os.path.join(self.experiment_config.loot_dir(), self.starttime)
|
|
os.makedirs(self.lootdir)
|
|
|
|
self.targets = []
|
|
# start target machines
|
|
for target_conf in self.experiment_config.targets():
|
|
if not target_conf.is_active():
|
|
continue
|
|
|
|
tname = target_conf.vmname()
|
|
|
|
self.attack_logger.vprint(f"{CommandlineColors.OKBLUE}preparing target {tname} ....{CommandlineColors.ENDC}", 1)
|
|
target_1 = Machine(target_conf, attack_logger=self.attack_logger)
|
|
target_1.set_caldera_server(self.attacker_1.get_ip())
|
|
try:
|
|
if not target_conf.use_existing_machine():
|
|
target_1.destroy()
|
|
except subprocess.CalledProcessError:
|
|
# Maybe the machine just does not exist yet
|
|
pass
|
|
if self.machine_needs_caldera(target_1, caldera_attacks):
|
|
target_1.install_caldera_service()
|
|
target_1.up()
|
|
target_1.reboot() # Kernel changes on system creation require a reboot
|
|
needs_reboot = target_1.prime_vulnerabilities()
|
|
needs_reboot |= target_1.prime_sensors()
|
|
if needs_reboot:
|
|
self.attack_logger.vprint(
|
|
f"{CommandlineColors.OKBLUE}rebooting target {tname} ....{CommandlineColors.ENDC}", 1)
|
|
target_1.reboot()
|
|
self.attack_logger.vprint(f"{CommandlineColors.OKGREEN}Target is up: {tname} {CommandlineColors.ENDC}", 1)
|
|
self.targets.append(target_1)
|
|
|
|
# Install vulnerabilities
|
|
for a_target in self.targets:
|
|
self.attack_logger.vprint(f"Installing vulnerabilities on {a_target.get_paw()}", 2)
|
|
a_target.install_vulnerabilities()
|
|
a_target.start_vulnerabilities()
|
|
|
|
# Install sensor plugins
|
|
for a_target in self.targets:
|
|
self.attack_logger.vprint(f"Installing sensors on {a_target.get_paw()}", 2)
|
|
a_target.install_sensors()
|
|
a_target.start_sensors()
|
|
|
|
# First start of caldera implants
|
|
at_least_one_caldera_started = False
|
|
for target_1 in self.targets:
|
|
if self.machine_needs_caldera(target_1, caldera_attacks):
|
|
target_1.start_caldera_client()
|
|
self.attack_logger.vprint(f"{CommandlineColors.OKGREEN}Initial start of caldera client: {tname} {CommandlineColors.ENDC}", 1)
|
|
else:
|
|
at_least_one_caldera_started = True
|
|
if at_least_one_caldera_started:
|
|
time.sleep(20) # Wait for all the clients to contact the caldera server
|
|
# TODO: Smarter wait
|
|
|
|
self.attack_logger.vprint(f"{CommandlineColors.OKBLUE}Contacting caldera agents on all targets ....{CommandlineColors.ENDC}", 1)
|
|
# Wait until all targets are registered as Caldera targets
|
|
for target_1 in self.targets:
|
|
running_agents = self.caldera_control.list_paws_of_running_agents()
|
|
self.attack_logger.vprint(f"Agents currently running: {running_agents}", 2)
|
|
while target_1.get_paw() not in running_agents:
|
|
if self.machine_needs_caldera(target_1, caldera_attacks) == 0:
|
|
self.attack_logger.vprint(f"No caldera agent needed for: {target_1.get_paw()} ", 3)
|
|
break
|
|
self.attack_logger.vprint(f"Connecting to caldera {caldera_url}, running agents are: {running_agents}", 3)
|
|
self.attack_logger.vprint(f"Missing agent: {target_1.get_paw()} ...", 3)
|
|
target_1.start_caldera_client()
|
|
self.attack_logger.vprint(f"Restarted caldera agent: {target_1.get_paw()} ...", 3)
|
|
time.sleep(120) # Was 30, but maybe there are timing issues
|
|
running_agents = self.caldera_control.list_paws_of_running_agents()
|
|
self.attack_logger.vprint(f"{CommandlineColors.OKGREEN}Caldera agents reached{CommandlineColors.ENDC}", 1)
|
|
|
|
# Add running machines to log
|
|
for target in self.targets:
|
|
i = target.get_machine_info()
|
|
i["role"] = "target"
|
|
self.attack_logger.add_machine_info(i)
|
|
|
|
i = self.attacker_1.get_machine_info()
|
|
i["role"] = "attacker"
|
|
self.attack_logger.add_machine_info(i)
|
|
|
|
# Attack them
|
|
self.attack_logger.vprint(f"{CommandlineColors.OKBLUE}Running Caldera attacks{CommandlineColors.ENDC}", 1)
|
|
for target_1 in self.targets:
|
|
if caldera_attacks is None:
|
|
# Run caldera attacks
|
|
new_caldera_attacks = self.experiment_config.get_caldera_attacks(target_1.get_os())
|
|
else:
|
|
new_caldera_attacks = caldera_attacks
|
|
if new_caldera_attacks:
|
|
for attack in new_caldera_attacks:
|
|
# TODO: Work with snapshots
|
|
# TODO: If we have several targets in the same group, it is nonsense to attack each one separately. Make this smarter
|
|
self.attack_logger.vprint(f"Attacking machine with PAW: {target_1.get_paw()} with {attack}", 2)
|
|
|
|
it_worked = self.caldera_control.attack(paw=target_1.get_paw(),
|
|
ability_id=attack,
|
|
group=target_1.get_group(),
|
|
target_platform=target_1.get_os()
|
|
)
|
|
|
|
# Moved to fix section below. If fix works: can be removed
|
|
# print(f"Pausing before next attack (config: nap_time): {self.experiment_config.get_nap_time()}")
|
|
# time.sleep(self.experiment_config.get_nap_time())
|
|
|
|
# Fix: Caldera sometimes gets stuck. This is why we better re-start the caldera server and wait till all the implants re-connected
|
|
# Reason: In some scenarios we keep the infra up for hours or days. No re-creation like intended. This can cause Caldera to hick up
|
|
if it_worked:
|
|
self.attack_logger.vprint(f"{CommandlineColors.OKBLUE}Restarting caldera server and waiting for clients to re-connect{CommandlineColors.ENDC}", 1)
|
|
self.attacker_1.start_caldera_server()
|
|
self.attack_logger.vprint(f"Pausing before next attack (config: nap_time): {self.experiment_config.get_nap_time()}", 2)
|
|
time.sleep(self.experiment_config.get_nap_time())
|
|
retries = 100
|
|
for target_system in self.targets:
|
|
if self.machine_needs_caldera(target_system, caldera_attacks) == 0:
|
|
self.attack_logger.vprint(f"No caldera agent needed for: {target_system.get_paw()} ", 3)
|
|
continue
|
|
running_agents = self.caldera_control.list_paws_of_running_agents()
|
|
self.attack_logger.vprint(f"Agents currently connected to the server: {running_agents}", 2)
|
|
while target_system.get_paw() not in running_agents:
|
|
time.sleep(1)
|
|
running_agents = self.caldera_control.list_paws_of_running_agents()
|
|
retries -= 1
|
|
self.attack_logger.vprint(f"Waiting for clients to re-connect ({retries}, {running_agents}) ", 3)
|
|
if retries <= 0:
|
|
raise ServerError
|
|
self.attack_logger.vprint(f"{CommandlineColors.OKGREEN}Restarted caldera server clients re-connected{CommandlineColors.ENDC}", 1)
|
|
# End of fix
|
|
|
|
self.attack_logger.vprint(f"{CommandlineColors.OKGREEN}Finished Caldera attacks{CommandlineColors.ENDC}", 1)
|
|
|
|
# Run plugin based attacks
|
|
self.attack_logger.vprint(f"{CommandlineColors.OKBLUE}Running attack plugins{CommandlineColors.ENDC}", 1)
|
|
for target_1 in self.targets:
|
|
plugin_based_attacks = self.experiment_config.get_plugin_based_attacks(target_1.get_os())
|
|
metasploit_plugins = self.plugin_manager.count_caldera_requirements(AttackPlugin, plugin_based_attacks)
|
|
print(f"Plugins needing metasploit for {target_1.get_paw()} : {metasploit_plugins}")
|
|
for attack in plugin_based_attacks:
|
|
# TODO: Work with snapshots
|
|
self.attack_logger.vprint(f"Attacking machine with PAW: {target_1.get_paw()} with attack: {attack}", 1)
|
|
|
|
self.attack(target_1, attack)
|
|
self.attack_logger.vprint(f"Pausing before next attack (config: nap_time): {self.experiment_config.get_nap_time()}", 3)
|
|
time.sleep(self.experiment_config.get_nap_time())
|
|
|
|
self.attack_logger.vprint(f"{CommandlineColors.OKGREEN}Finished attack plugins{CommandlineColors.ENDC}", 1)
|
|
|
|
# Stop sensor plugins
|
|
# Collect data
|
|
zip_this = []
|
|
for a_target in self.targets:
|
|
a_target.stop_sensors()
|
|
zip_this += a_target.collect_sensors(self.lootdir)
|
|
|
|
# Uninstall vulnerabilities
|
|
for a_target in self.targets:
|
|
self.attack_logger.vprint(f"{CommandlineColors.OKBLUE} Uninstalling vulnerabilities on {a_target.get_paw()} {CommandlineColors.ENDC}", 1)
|
|
a_target.stop_vulnerabilities()
|
|
self.attack_logger.vprint(f"{CommandlineColors.OKGREEN} Done uninstalling vulnerabilities on {a_target.get_paw()} {CommandlineColors.ENDC}", 1)
|
|
|
|
# Stop target machines
|
|
for target_1 in self.targets:
|
|
target_1.halt()
|
|
self.__stop_attacker()
|
|
|
|
self.attack_logger.post_process()
|
|
attack_log_file_path = os.path.join(self.lootdir, "attack.json")
|
|
self.attack_logger.write_json(attack_log_file_path)
|
|
document_generator = DocGenerator()
|
|
document_generator.generate(attack_log_file_path)
|
|
document_generator.compile_documentation()
|
|
zip_this += document_generator.get_outfile_paths()
|
|
self.zip_loot(zip_this)
|
|
|
|
def machine_needs_caldera(self, target, caldera_conf):
|
|
""" Counts the attacks and plugins needing caldera that are registered for this machine """
|
|
|
|
c_cmdline = 0
|
|
if caldera_conf is not None:
|
|
c_cmdline = len(caldera_conf)
|
|
c_conffile = len(self.experiment_config.get_caldera_attacks(target.get_os()))
|
|
plugin_based_attacks = self.experiment_config.get_plugin_based_attacks(target.get_os())
|
|
c_plugins = self.plugin_manager.count_caldera_requirements(AttackPlugin, plugin_based_attacks)
|
|
|
|
print(f"Caldera count: From cmdline: {c_cmdline}, From conf: {c_conffile} from plugins: {c_plugins}")
|
|
|
|
return c_cmdline + c_conffile + c_plugins
|
|
|
|
def attack(self, target, attack):
|
|
""" Pick an attack and run it
|
|
|
|
@param attack: Name of the attack to run
|
|
@param target: IP address of the target
|
|
@returns: The output of the cmdline attacking tool
|
|
"""
|
|
|
|
for plugin in self.plugin_manager.get_plugins(AttackPlugin, [attack]):
|
|
name = plugin.get_name()
|
|
|
|
self.attack_logger.vprint(f"{CommandlineColors.OKBLUE}Running Attack plugin {name}{CommandlineColors.ENDC}", 2)
|
|
plugin.process_config(self.experiment_config.attack_conf(plugin.get_config_section_name()))
|
|
plugin.set_attacker_machine(self.attacker_1)
|
|
plugin.set_sysconf({})
|
|
plugin.set_logger(self.attack_logger)
|
|
plugin.set_caldera(self.caldera_control)
|
|
plugin.connect_metasploit()
|
|
plugin.install()
|
|
|
|
# plugin.__set_logger__(self.attack_logger)
|
|
plugin.__execute__([target])
|
|
|
|
def zip_loot(self, zip_this):
|
|
""" Zip the loot together """
|
|
|
|
filename = os.path.join(self.lootdir, self.starttime + ".zip")
|
|
|
|
self.attack_logger.vprint(f"Creating zip file {filename}", 1)
|
|
|
|
with zipfile.ZipFile(filename, "w") as zfh:
|
|
for a_file in zip_this:
|
|
if a_file != filename:
|
|
self.attack_logger.vprint(a_file, 2)
|
|
zfh.write(a_file)
|
|
|
|
zfh.write(os.path.join(self.lootdir, "attack.json"))
|
|
|
|
# For automation purpose we copy the file into a standard file name
|
|
defaultname = os.path.join(self.lootdir, "..", "most_recent.zip")
|
|
shutil.copyfile(filename, defaultname)
|
|
|
|
# @staticmethod
|
|
# def __get_results_files(root):
|
|
# """ Yields a list of potential result files
|
|
|
|
# @param root: Root dir of the machine to collect data from
|
|
# """
|
|
# # TODO: Properly implement. Get proper root parameter
|
|
|
|
# total = [os.path.join(root, "logstash", "filebeat.json")]
|
|
# for a_file in total:
|
|
# if os.path.exists(a_file):
|
|
# yield a_file
|
|
|
|
# def __clean_result_files(self, root):
|
|
# """ Deletes result files
|
|
|
|
# @param root: Root dir of the machine to collect data from
|
|
# """
|
|
|
|
# TODO: Properly implement. Get proper root parameter
|
|
|
|
# for a_file in self.__get_results_files(root):
|
|
# os.remove(a_file)
|
|
|
|
# def __collect_loot(self, root):
|
|
# """ Collect results into loot dir
|
|
|
|
# @param root: Root dir of the machine to collect data from
|
|
# """
|
|
|
|
# try:
|
|
# os.makedirs(os.path.abspath(self.experiment_config.loot_dir()))
|
|
# except FileExistsError:
|
|
# pass
|
|
# for a_file in self.__get_results_files(root):
|
|
# self.attack_logger.vprint("Copy {} {}".format(a_file, os.path.abspath(self.experiment_config.loot_dir())), 3)
|
|
|
|
def __start_attacker(self):
|
|
""" Start the attacking VM """
|
|
|
|
# Preparing attacker
|
|
self.attacker_1 = Machine(self.experiment_config.attacker(0).raw_config, attack_logger=self.attack_logger)
|
|
|
|
if not self.experiment_config.attacker(0).use_existing_machine():
|
|
try:
|
|
self.attacker_1.destroy()
|
|
except subprocess.CalledProcessError:
|
|
# Machine does not exist
|
|
pass
|
|
self.attacker_1.create(reboot=True)
|
|
self.attacker_1.up()
|
|
self.attacker_1.install_caldera_server(cleanup=False)
|
|
else:
|
|
self.attacker_1.up()
|
|
self.attacker_1.install_caldera_server(cleanup=False)
|
|
|
|
self.attacker_1.start_caldera_server()
|
|
# self.attacker_1.set_attack_logger(self.attack_logger)
|
|
|
|
def __stop_attacker(self):
|
|
""" Stop the attacking VM """
|
|
self.attacker_1.halt()
|