@ -13,7 +13,7 @@ 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 . exceptions import ServerError , CalderaError , MachineError
from app . pluginmanager import PluginManager
from app . doc_generator import DocGenerator
from app . calderacontrol import CalderaControl
@ -26,21 +26,33 @@ from plugins.base.attack import AttackPlugin
class Experiment ( ) :
""" Class handling experiments """
def __init__ ( self , configfile : str , verbosity = 0 , caldera_attacks : list = None ):
def __init__ ( self , configfile : str , verbosity = 0 ):
"""
: 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 . start_time : str = datetime . now ( ) . strftime ( " % Y_ % m_ %d ___ % H_ % M_ % S " ) #: time the experiment started.
self . caldera_control : Optional [ CalderaControl ] = None #: Controller for Caldera interaction
self . loot_dir : str = " loot " #: Directory to store the loot into. Will be fetched from config
self . targets : list [ Machine ] = [ ] #: A list of target machines
self . attacker_1 : Optional [ Machine ] = None #: The attacker machine
self . experiment_config = ExperimentConfig ( configfile )
self . attack_logger = AttackLog ( verbosity )
self . plugin_manager = PluginManager ( self . attack_logger )
def run ( self , caldera_attacks : list = None ) :
"""
Run the experiment
: 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
: return :
"""
self . __start_attacker ( )
if self . attacker_1 is None :
raise ServerError
raise MachineError( " Attacker not initialised " )
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,
@ -49,92 +61,88 @@ class Experiment():
self . attack_logger . vprint ( self . caldera_control . kill_all_agents ( ) , 3 )
self . attack_logger . vprint ( self . caldera_control . delete_all_agents ( ) , 3 )
self . start time = datetime . now ( ) . strftime ( " % Y_ % m_ %d ___ % H_ % M_ % S " )
self . loot dir = os . path . join ( self . experiment_config . loot_dir ( ) , self . start time)
os . makedirs ( self . loot dir)
self . start _ time = datetime . now ( ) . strftime ( " % Y_ % m_ %d ___ % H_ % M_ % S " )
self . loot _ dir = os . path . join ( self . experiment_config . loot_dir ( ) , self . start _ time)
os . makedirs ( self . loot _ dir)
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 )
self . start_target_machines ( caldera_attacks )
# 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 ( )
self . install_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 ( )
self . install_sensor_plugins ( )
# 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 . first_start_of_caldera_implants ( caldera_attacks )
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 . wait_until_all_targets_have_caldera_implants ( caldera_url , caldera_attacks )
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 )
self . add_running_machines_to_log ( )
# Attack them
self . run_caldera_attacks ( caldera_attacks )
# Run plugin based attacks
self . run_plugin_attacks ( )
# Stop sensor plugins
# Collect data
zip_this = [ ]
for a_target in self . targets :
a_target . stop_sensors ( )
zip_this + = a_target . collect_sensors ( self . loot_dir )
# 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 . loot_dir , " 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 run_plugin_attacks ( self ) :
""" 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 )
def run_caldera_attacks ( self , caldera_attacks : Optional [ list [ str ] ] = None ) :
""" Run caldera based attacks
: param caldera_attacks : An optional list of caldera attack ids as string
"""
self . attack_logger . vprint ( f " { CommandlineColors . OKBLUE } Running Caldera attacks { CommandlineColors . ENDC } " , 1 )
for target_1 in self . targets :
if caldera_attacks is None :
@ -147,7 +155,8 @@ class Experiment():
# 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 )
if self . caldera_control is None :
raise CalderaError ( " Caldera control not initialised " )
it_worked = self . caldera_control . attack ( paw = target_1 . get_paw ( ) ,
ability_id = attack ,
group = target_1 . get_group ( ) ,
@ -161,12 +170,20 @@ class Experiment():
# 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 . attack_logger . vprint (
f " { CommandlineColors . OKBLUE } Restarting caldera server and waiting for clients to re-connect { CommandlineColors . ENDC } " ,
1 )
if self . attacker_1 is None :
raise MachineError ( " attacker not initialised " )
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 )
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 . caldera_control is None :
raise CalderaError ( " Caldera is not initialised " )
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
@ -176,63 +193,133 @@ class Experiment():
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 )
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 )
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 )
def add_running_machines_to_log ( self ) :
""" Add machine infos for targets and attacker to the 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 )
def wait_until_all_targets_have_caldera_implants ( self , caldera_url : str , caldera_attacks : Optional [ list [ str ] ] = None ) :
"""
: param caldera_attacks : a list of command line defined caldera attacks
: param caldera_url : URL of the caldera server
"""
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 )
if self . caldera_control is None :
raise CalderaError ( " Caldera is not initialised " )
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 ( 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 ( ) )
def first_start_of_caldera_implants ( self , caldera_attacks : Optional [ list [ str ] ] = None ) :
""" Start caldera implant on the targets
self . attack_logger . vprint ( f " { CommandlineColors . OKGREEN } Finished attack plugins { CommandlineColors . ENDC } " , 1 )
: param caldera_attacks : a list of command line defined caldera attacks
"""
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: { target_1 . get_name ( ) } { 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
# Stop sensor plugins
# Collect data
zip_this = [ ]
def install_sensor_plugins ( self ) :
""" Installs sensor plugins on the targets
"""
for a_target in self . targets :
a_target . stop_sensors ( )
zip_this + = a_target . collect_sensors ( self . lootdir )
self . attack_logger . vprint ( f " Installing sensors on { a_target . get_paw ( ) } " , 2 )
a_target . install_sensors ( )
a_target . start_sensors ( )
# Uninstall vulnerabilities
def install_vulnerabilities ( self ) :
""" Install vulnerabilities on the targets
"""
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 )
self . attack_logger . vprint ( f " I nstalling vulnerabilities on { a_target . get_paw ( ) } ", 2 )
a_target . install _vulnerabilities( )
a_target . start_vulnerabilities ( )
# Stop target machines
for target_1 in self . targets :
target_1 . halt ( )
self . __stop_attacker ( )
def start_target_machines ( self , caldera_attacks : Optional [ list [ str ] ] = None ) :
""" Start target machines
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 )
: param caldera_attacks : Caldera attacks as defined on the command line
"""
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 )
if target_1 is None :
raise MachineError ( " Creating target machine failed " )
if self . attacker_1 is None :
raise MachineError ( " Creating attacker machine failed " )
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 )
def machine_needs_caldera ( self , target , caldera_conf ) :
""" Counts the attacks and plugins needing caldera that are registered for this machine """
def machine_needs_caldera ( self , target , caldera_from_cmdline : Optional [ list [ str ] ] = None ) - > int :
""" Counts the attacks and plugins needing caldera that are registered for this machine
: param target : Target machine we will check the config file for assigned caldera attacks for
: param caldera_from_cmdline : Caldera attacks listed on the commandline
: returns : the number of caldera attacks planned for this machine
"""
c_cmdline = 0
if caldera_conf is not None :
c_cmdline = len ( caldera_conf )
if caldera_ from_cmdline is not None :
c_cmdline = len ( caldera_ from_cmdline )
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 )
@ -264,10 +351,13 @@ class Experiment():
# plugin.__set_logger__(self.attack_logger)
plugin . __execute__ ( [ target ] )
def zip_loot ( self , zip_this ) :
""" Zip the loot together """
def zip_loot ( self , zip_this : list [ str ] ) :
""" Zip the loot together
: param zip_this : A list of file paths to add to the zip file
"""
filename = os . path . join ( self . lootdir , self . starttime + " .zip " )
filename = os . path . join ( self . loot _ dir, self . start _ time + " .zip " )
self . attack_logger . vprint ( f " Creating zip file { filename } " , 1 )
@ -277,11 +367,11 @@ class Experiment():
self . attack_logger . vprint ( a_file , 2 )
zfh . write ( a_file )
zfh . write ( os . path . join ( self . loot dir, " attack.json " ) )
zfh . write ( os . path . join ( self . loot _ dir, " attack.json " ) )
# For automation purpose we copy the file into a standard file name
default name = os . path . join ( self . loot dir, " .. " , " most_recent.zip " )
shutil . copyfile ( filename , default name)
default _ name = os . path . join ( self . loot _ dir, " .. " , " most_recent.zip " )
shutil . copyfile ( filename , default _ name)
def __start_attacker ( self ) :
""" Start the attacking VM """
@ -289,6 +379,9 @@ class Experiment():
# Preparing attacker
self . attacker_1 = Machine ( self . experiment_config . attacker ( 0 ) . raw_config , attack_logger = self . attack_logger )
if self . attacker_1 is None :
raise ServerError
if not self . experiment_config . attacker ( 0 ) . use_existing_machine ( ) :
try :
self . attacker_1 . destroy ( )
@ -303,6 +396,9 @@ class Experiment():
self . attacker_1 . install_caldera_server ( cleanup = False )
self . attacker_1 . start_caldera_server ( )
if self . attacker_1 is None :
raise ServerError
# self.attacker_1.set_attack_logger(self.attack_logger)
def __stop_attacker ( self ) :