Caldera now supports jitter and obfuscator from configuration file. Keep in mind: Not all implants support all obfuscators.

pull/3/head
Thorsten Sick 3 years ago
parent d88a1625a9
commit 12c92939c0

@ -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)

@ -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):

@ -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

@ -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:

@ -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

@ -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:

@ -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:

@ -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/<name>
# 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

@ -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 """

Loading…
Cancel
Save