From 12c92939c055e36f53e8472a14c6848f0de42507 Mon Sep 17 00:00:00 2001 From: Thorsten Sick Date: Tue, 18 May 2021 09:33:12 +0200 Subject: [PATCH] Caldera now supports jitter and obfuscator from configuration file. Keep in mind: Not all implants support all obfuscators. --- app/attack_log.py | 16 +++- app/calderacontrol.py | 26 +++++- app/config.py | 18 ++++ app/experimentcontrol.py | 42 +++++---- plugins/base/caldera.py | 65 ++++++++++++++ template.yaml | 12 +++ tests/data/attacks_perfect.yaml | 12 +++ tests/data/partial.yaml | 154 ++++++++++++++++++++++++++++++++ tests/test_config.py | 36 ++++++++ 9 files changed, 355 insertions(+), 26 deletions(-) create mode 100644 plugins/base/caldera.py create mode 100644 tests/data/partial.yaml diff --git a/app/attack_log.py b/app/attack_log.py index 000c74c..262b214 100644 --- a/app/attack_log.py +++ b/app/attack_log.py @@ -33,7 +33,7 @@ class AttackLog(): self.log = [] self.verbosity = verbosity - def start_caldera_attack(self, source, paw, group, ability_id, ttp=None, name=None, description=None): # pylint: disable=too-many-arguments + def start_caldera_attack(self, source, paw, group, ability_id, ttp=None, name=None, description=None, obfuscator="default", jitter="default"): # pylint: disable=too-many-arguments """ Mark the start of a caldera attack @param source: source of the attack. Attack IP @@ -43,6 +43,8 @@ class AttackLog(): @param ttp: TTP of the attack (as stated by Caldera internal settings) @param name: Name of the attack. Data source is Caldera internal settings @param description: Descirption of the attack. Caldera is the source + @param obfuscator: C&C obfuscator being used + @param jitter: Jitter being used """ data = {"timestamp": __get_timestamp__(), @@ -55,7 +57,9 @@ class AttackLog(): "ability_id": ability_id, "hunting_tag": __mitre_fix_ttp__(ttp), "name": name or "", - "description": description or "" + "description": description or "", + "obfuscator": obfuscator, + "jitter": jitter } self.log.append(data) @@ -64,7 +68,7 @@ class AttackLog(): # TODO: Add config # TODO: Add results - def stop_caldera_attack(self, source, paw, group, ability_id, ttp=None, name=None, description=None): # pylint: disable=too-many-arguments + def stop_caldera_attack(self, source, paw, group, ability_id, ttp=None, name=None, description=None, obfuscator="default", jitter="default"): # pylint: disable=too-many-arguments """ Mark the end of a caldera attack @param source: source of the attack. Attack IP @@ -74,6 +78,8 @@ class AttackLog(): @param ttp: TTP of the attack (as stated by Caldera internal settings) @param name: Name of the attack. Data source is Caldera internal settings @param description: Descirption of the attack. Caldera is the source + @param obfuscator: C&C obfuscator being used + @param jitter: Jitter being used """ data = {"timestamp": __get_timestamp__(), @@ -86,7 +92,9 @@ class AttackLog(): "ability_id": ability_id, "hunting_tag": __mitre_fix_ttp__(ttp), "name": name or "", - "description": description or "" + "description": description or "", + "obfuscator": obfuscator, + "jitter": jitter } self.log.append(data) diff --git a/app/calderacontrol.py b/app/calderacontrol.py index d91971d..e956aaf 100644 --- a/app/calderacontrol.py +++ b/app/calderacontrol.py @@ -501,6 +501,16 @@ class CalderaControl(): @param ability_id: Ability to run against the target """ + # Tested obfuscators (with sandcat): + # plain-text: worked + # base64: (invalid input on sandcat) + # base64jumble: ? + # caesar: failed + # base64noPadding: worked + # steganopgraphy: ? + obfuscator = self.config.get_caldera_obfuscator() + jitter = self.config.get_caldera_jitter() + adversary_name = "generated_adv__" + str(time.time()) operation_name = "testoperation__" + str(time.time()) @@ -513,12 +523,20 @@ class CalderaControl(): ability_id=ability_id, ttp=self.get_ability(ability_id)[0]["technique_id"], name=self.get_ability(ability_id)[0]["name"], - description=self.get_ability(ability_id)[0]["description"]) + description=self.get_ability(ability_id)[0]["description"], + obfuscator=obfuscator, + jitter=jitter + ) # ##### Create / Run Operation self.attack_logger.vprint(f"New adversary generated. ID: {adid}, ability: {ability_id} group: {group}", 2) - res = self.add_operation(operation_name, advid=adid, group=group) + res = self.add_operation(operation_name, + advid=adid, + group=group, + obfuscator=obfuscator, + jitter=jitter + ) self.attack_logger.vprint(pformat(res), 3) opid = self.get_operation(operation_name)["id"] @@ -573,7 +591,9 @@ class CalderaControl(): ability_id=ability_id, ttp=self.get_ability(ability_id)[0]["technique_id"], name=self.get_ability(ability_id)[0]["name"], - description=self.get_ability(ability_id)[0]["description"] + description=self.get_ability(ability_id)[0]["description"], + obfuscator=obfuscator, + jitter=jitter ) def pretty_print_ability(self, abi): diff --git a/app/config.py b/app/config.py index a2a900c..77a47cb 100644 --- a/app/config.py +++ b/app/config.py @@ -211,6 +211,24 @@ class ExperimentConfig(): return res + def get_caldera_obfuscator(self): + """ Get the caldera configuration. In this case: The obfuscator. Will default to plain-text """ + + try: + res = self.raw_config["caldera_conf"]["obfuscator"] + except KeyError: + return "plain-text" + return res + + def get_caldera_jitter(self): + """ Get the caldera configuration. In this case: Jitter. Will default to 4/8 """ + + try: + res = self.raw_config["caldera_conf"]["jitter"] + except KeyError: + return "4/8" + return res + def get_kali_attacks(self, for_os): """ Get the configured kali attacks to run for a specific OS diff --git a/app/experimentcontrol.py b/app/experimentcontrol.py index c684695..eb376d4 100644 --- a/app/experimentcontrol.py +++ b/app/experimentcontrol.py @@ -30,22 +30,22 @@ class Experiment(): """ self.attacker_1 = None - self.experiment_control = ExperimentConfig(configfile) + self.experiment_config = ExperimentConfig(configfile) self.attack_logger = AttackLog(verbosity) self.__start_attacker() caldera_url = "http://" + self.attacker_1.getip() + ":8888" - caldera_control = CalderaControl(caldera_url, attack_logger=self.attack_logger, config=self.experiment_control) + caldera_control = CalderaControl(caldera_url, attack_logger=self.attack_logger, config=self.experiment_config) # Deleting all currently registered Caldera gents self.attack_logger.vprint(caldera_control.kill_all_agents(), 3) self.attack_logger.vprint(caldera_control.delete_all_agents(), 3) self.starttime = datetime.now().strftime("%Y_%m_%d___%H_%M_%S") - self.lootdir = os.path.join(self.experiment_control.loot_dir(), self.starttime) + 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_control.targets(): + for target_conf in self.experiment_config.targets(): if not target_conf.is_active(): continue @@ -104,26 +104,30 @@ class Experiment(): self.attack_logger.vprint(f"{CommandlineColors.OKBLUE}Running Caldera attacks{CommandlineColors.ENDC}", 1) for target_1 in self.targets: # Run caldera attacks - caldera_attacks = self.experiment_control.get_caldera_attacks(target_1.get_os()) + caldera_attacks = self.experiment_config.get_caldera_attacks(target_1.get_os()) if caldera_attacks: for attack in 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) - caldera_control = CalderaControl("http://" + self.attacker_1.getip() + ":8888", self.attack_logger, config=self.experiment_control) + caldera_control = CalderaControl("http://" + self.attacker_1.getip() + ":8888", self.attack_logger, config=self.experiment_config) - caldera_control.attack(attack_logger=self.attack_logger, paw=target_1.get_paw(), ability_id=attack, group=target_1.get_group()) + caldera_control.attack(attack_logger=self.attack_logger, + paw=target_1.get_paw(), + ability_id=attack, + group=target_1.get_group(), + ) # Moved to fix section below. If fix works: can be removed - # print(f"Pausing before next attack (config: nap_time): {self.experiment_control.get_nap_time()}") - # time.sleep(self.experiment_control.get_nap_time()) + # 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 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_control.get_nap_time()}", 2) - time.sleep(self.experiment_control.get_nap_time()) + 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: running_agents = caldera_control.list_paws_of_running_agents() @@ -143,13 +147,13 @@ class Experiment(): # Run Kali attacks self.attack_logger.vprint(f"{CommandlineColors.OKBLUE}Running Kali attacks{CommandlineColors.ENDC}", 1) for target_1 in self.targets: - kali_attacks = self.experiment_control.get_kali_attacks(target_1.get_os()) + kali_attacks = self.experiment_config.get_kali_attacks(target_1.get_os()) for attack in kali_attacks: # TODO: Work with snapshots self.attack_logger.vprint(f"Attacking machine with PAW: {target_1.get_paw()} with attack: {attack}", 1) - self.attacker_1.kali_attack(attack, target_1.getip(), self.experiment_control) - self.attack_logger.vprint(f"Pausing before next attack (config: nap_time): {self.experiment_control.get_nap_time()}", 3) - time.sleep(self.experiment_control.get_nap_time()) + self.attacker_1.kali_attack(attack, target_1.getip(), self.experiment_config) + 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 Kali attacks{CommandlineColors.ENDC}", 1) @@ -224,19 +228,19 @@ class Experiment(): """ try: - os.makedirs(os.path.abspath(self.experiment_control.loot_dir())) + 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_control.loot_dir())), 3) + 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_control.attacker(0).raw_config, attack_logger=self.attack_logger) + self.attacker_1 = Machine(self.experiment_config.attacker(0).raw_config, attack_logger=self.attack_logger) - if not self.experiment_control.attacker(0).use_existing_machine(): + if not self.experiment_config.attacker(0).use_existing_machine(): try: self.attacker_1.destroy() except subprocess.CalledProcessError: diff --git a/plugins/base/caldera.py b/plugins/base/caldera.py new file mode 100644 index 0000000..8f1eb8b --- /dev/null +++ b/plugins/base/caldera.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +""" Base class for Caldera plugins + +Special for this plugin class: If there is no plugin matching a specified attack-id the system can fallback to default handling. +You only gotta write a plugin if you want some special features +""" + +from plugins.base.plugin_base import BasePlugin + + +class CalderaPlugin(BasePlugin): + """ Class to execute a command on a caldera system targeting another system """ + + # Boilerplate + name = None + description = None + ttp = None + references = None + + required_files = [] + + # TODO: parse results + + def __init__(self): + super().__init__() + self.conf = {} # Plugin specific configuration + self.sysconf = {} # System configuration. common for all plugins + + def teardown(self): + """ Cleanup afterwards """ + pass # pylint: disable=unnecessary-pass + + def run(self, targets): + """ Run the command + + @param targets: A list of targets, ip addresses will do + """ + raise NotImplementedError + + def __execute__(self, targets): + """ Execute the plugin. This is called by the code + + @param targets: A list of targets, ip addresses will do + """ + + self.setup() + self.attack_logger.start_kali_attack(self.machine_plugin.config.vmname(), targets, self.name, ttp=self.get_ttp()) + res = self.run(targets) + self.teardown() + self.attack_logger.stop_kali_attack(self.machine_plugin.config.vmname(), targets, self.name, ttp=self.get_ttp()) + return res + + def get_ttp(self): + """ Returns the ttp of the plugin, please set in boilerplate """ + if self.ttp: + return self.ttp + + raise NotImplementedError + + def get_references(self): + """ Returns the references of the plugin, please set in boilerplate """ + if self.references: + return self.references + + raise NotImplementedError diff --git a/template.yaml b/template.yaml index 494ca02..2ab41e0 100644 --- a/template.yaml +++ b/template.yaml @@ -183,6 +183,18 @@ attacks: # configure the seconds the system idles between the attacks. Makes it slower. But attack and defense logs will be simpler to match nap_time: 5 +### +# Configuration for caldera +caldera_conf: + ### + # The obfuscator to use between the implant and the server. Not all obfuscators are supported by all implants. Existing obfuscators: + # plain-text, base64, base64jumble, caesar, base64noPadding, steganography + obfuscator: plain-text + + ### + # Jitter settings for the implant. it is min/max seconds. The first number has to be smaller. Default is 4/8 + jitter: 4/8 + ### # A list of caldera attacks to run against the targets. caldera_attacks: diff --git a/tests/data/attacks_perfect.yaml b/tests/data/attacks_perfect.yaml index 7a255fb..dea76e7 100644 --- a/tests/data/attacks_perfect.yaml +++ b/tests/data/attacks_perfect.yaml @@ -91,6 +91,18 @@ targets: # For non-vagrant ssh connections a ssh keyfile stored in the machinepath is required. ssh_keyfile: id_rsa.3 +### +# Configuration for caldera +caldera_conf: + ### + # The obfuscator to use between the implant and the server. Not all obfuscators are supported by all implants. Existing obfuscators: + # plain-text, base64, base64jumble, caesar, base64noPadding, steganography + obfuscator: foo-bar + + ### + # Jitter settings for the implant. it is min/max seconds. The first number has to be smaller. Default is 4/8 + jitter: 08/15 + ### # A list of caldera attacks to run against the targets. caldera_attacks: diff --git a/tests/data/partial.yaml b/tests/data/partial.yaml new file mode 100644 index 0000000..11ac422 --- /dev/null +++ b/tests/data/partial.yaml @@ -0,0 +1,154 @@ + +### +# Caldera configuration +caldera: + ### + # API key for caldera. See caldera configuration. Default is ADMIN123 + apikey: ADMIN123 + +### +# Attacks configuration +attackers: + ### + # Configuration for the first attacker. One should normally be enough + attacker: + + ### + # Defining VM controller settings for this machine + vm_controller: + ### + # Type of the VM controller, Options are "vagrant" + type: vagrant + ### + # # path where the vagrantfile is in + vagrantfilepath: systems + + ### + # Name of machine in Vagrantfile + vm_name: attacker + + ### + # machinepath is a path where the machine specific files and logs are stored. Relative to the Vagrantfile path + # and will be mounted internally as /vagrant/ + # If machinepoath is not set AttackX will try "vm_name" + machinepath: attacker1 + + ### + # OS of the VM guest. Options are so far "windows", "linux" + os: linux + + ### + # Do not destroy/create the machine: Set this to "yes". + use_existing_machine: yes + +### +# List of targets +targets: + ### + # Specific target + target1: + vm_controller: + type: vagrant + vagrantfilepath: systems + + vm_name: target1 + os: linux + ### + # Targets need a unique PAW name for caldera + paw: target1 + ### + # Targets need to be in a group for caldera + group: red + + machinepath: target1 + # Do not destroy/create the machine: Set this to "yes". + use_existing_machine: yes + + target2: + #root: systems/target1 + vm_controller: + type: vagrant + vagrantfilepath: systems + + vm_name: target2 + os: windows + paw: target2w + group: red + + machinepath: target2w + + # Do not destroy/create the machine: Set this to "yes". + use_existing_machine: yes + ### + # Optional setting to activate force when halting the machine. Windows guests sometime get stuck + halt_needs_force: yes + + ### + # If SSH without vagrant support is used (Windows !) we need a user name (uppercase) + ssh_user: ATTACKX + + ### + # For non-vagrant ssh connections a ssh keyfile stored in the machinepath is required. + ssh_keyfile: id_rsa.3 + +### +# General attack config +attacks: + ### + # configure the seconds the system idles between the attacks. Makes it slower. But attack and defense logs will be simpler to match + nap_time: 5 + + +## Broken caldera conf +caldera_conf: + foo: bar + +### +# A list of caldera attacks to run against the targets. +caldera_attacks: + ### + # Linux specific attacks. A list of caldera ability IDs + linux: + - "bd527b63-9f9e-46e0-9816-b8434d2b8989" + ### + # Windows specific attacks. A list of caldera ability IDs + windows: + - "bd527b63-9f9e-46e0-9816-b8434d2b8989" + +### +# Kali tool based attacks. Will result in kali commandline tools to be called. Currently supported are: "hydra" +kali_attacks: + ### + # Linux specific attacks, a list + linux: + - hydra + ### + # Windows specific attacks, a list + windows: + - hydra + +### +# Configuration for the kali attack tools +kali_conf: + ### + # Hydra configuration + hydra: + ### + # A list of protocols to brute force against. Supported: "ssh" + protocols: + - ssh + #- ftp + #- ftps + ### + # A file containing potential user names + userfile: users.txt + ### + # A file containing potential passwords + pwdfile: passwords.txt + +### +# Settings for the results being harvested +results: + ### + # The directory the loot will be in + loot_dir: loot \ No newline at end of file diff --git a/tests/test_config.py b/tests/test_config.py index 492edb5..8e3fc3d 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -543,6 +543,42 @@ class TestExperimentConfig(unittest.TestCase): data = ex.kali_conf("hydra") self.assertEqual(data["userfile"], "users.txt") + def test_missing_caldera_config_obfuscator(self): + """ A config file with no caldera config at all """ + + ex = ExperimentConfig("tests/data/basic.yaml") + self.assertEqual(ex.get_caldera_obfuscator(), "plain-text") + + def test_broken_caldera_config_obfuscator(self): + """ A config file with broken caldera config at all """ + + ex = ExperimentConfig("tests/data/partial.yaml") + self.assertEqual(ex.get_caldera_obfuscator(), "plain-text") + + def test_good_caldera_config_obfuscator(self): + """ A config file with broken caldera config at all """ + + ex = ExperimentConfig("tests/data/attacks_perfect.yaml") + self.assertEqual(ex.get_caldera_obfuscator(), "foo-bar") + + def test_missing_caldera_config_jitter(self): + """ A config file with no caldera config at all """ + + ex = ExperimentConfig("tests/data/basic.yaml") + self.assertEqual(ex.get_caldera_jitter(), "4/8") + + def test_broken_caldera_config_jitter(self): + """ A config file with broken caldera config at all """ + + ex = ExperimentConfig("tests/data/partial.yaml") + self.assertEqual(ex.get_caldera_jitter(), "4/8") + + def test_good_caldera_config_jitter(self): + """ A config file with broken caldera config at all """ + + ex = ExperimentConfig("tests/data/attacks_perfect.yaml") + self.assertEqual(ex.get_caldera_jitter(), "08/15") + def test_nap_time(self): """ nap time is set """