From 15602f18933647206309509488309fb23081755b Mon Sep 17 00:00:00 2001 From: Thorsten Sick Date: Tue, 13 Jul 2021 11:05:33 +0200 Subject: [PATCH 01/13] Config section rename (kali to a more generic one) --- app/config.py | 8 ++++---- app/experimentcontrol.py | 10 +++++----- .../FIN7/local_experiment_config.yaml | 4 ++-- template.yaml | 4 ++-- tests/data/attacker_has_empty_nicknames.yaml | 4 ++-- tests/data/attacks_half.yaml | 4 ++-- tests/data/attacks_missing.yaml | 4 ++-- tests/data/attacks_perfect.yaml | 4 ++-- tests/data/basic.yaml | 4 ++-- tests/data/basic_empty_sensor.yaml | 4 ++-- tests/data/basic_loot_missing.yaml | 4 ++-- tests/data/basic_results_missing.yaml | 4 ++-- tests/data/nap_time_missing.yaml | 4 ++-- tests/test_config.py | 12 ++++++------ 14 files changed, 37 insertions(+), 37 deletions(-) diff --git a/app/config.py b/app/config.py index 27d978d..771c2a8 100644 --- a/app/config.py +++ b/app/config.py @@ -246,17 +246,17 @@ class ExperimentConfig(): return "4/8" return res - def get_kali_attacks(self, for_os): + def get_plugin_based_attacks(self, for_os): """ Get the configured kali attacks to run for a specific OS @param for_os: The os to query the registered attacks for """ - if "kali_attacks" not in self.raw_config: + if "plugin_based_attacks" not in self.raw_config: return [] - if for_os not in self.raw_config["kali_attacks"]: + if for_os not in self.raw_config["plugin_based_attacks"]: return [] - res = self.raw_config["kali_attacks"][for_os] + res = self.raw_config["plugin_based_attacks"][for_os] if res is None: return [] return res diff --git a/app/experimentcontrol.py b/app/experimentcontrol.py index 7b05189..68a3a2e 100644 --- a/app/experimentcontrol.py +++ b/app/experimentcontrol.py @@ -151,18 +151,18 @@ class Experiment(): self.attack_logger.vprint(f"{CommandlineColors.OKGREEN}Finished Caldera attacks{CommandlineColors.ENDC}", 1) # Run Kali attacks - self.attack_logger.vprint(f"{CommandlineColors.OKBLUE}Running Kali attacks{CommandlineColors.ENDC}", 1) + self.attack_logger.vprint(f"{CommandlineColors.OKBLUE}Running attack plugins{CommandlineColors.ENDC}", 1) for target_1 in self.targets: - kali_attacks = self.experiment_config.get_kali_attacks(target_1.get_os()) - for attack in kali_attacks: + plugin_based_attacks = self.experiment_config.get_plugin_based_attacks(target_1.get_os()) + 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.attacker_1.kali_attack(attack, target_1.get_ip(), self.experiment_config) + 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 Kali attacks{CommandlineColors.ENDC}", 1) + self.attack_logger.vprint(f"{CommandlineColors.OKGREEN}Finished attack plugins{CommandlineColors.ENDC}", 1) # Stop sensor plugins # Collect data diff --git a/plugins/default/adversary_emulations/FIN7/local_experiment_config.yaml b/plugins/default/adversary_emulations/FIN7/local_experiment_config.yaml index 12d4c50..9535139 100644 --- a/plugins/default/adversary_emulations/FIN7/local_experiment_config.yaml +++ b/plugins/default/adversary_emulations/FIN7/local_experiment_config.yaml @@ -139,8 +139,8 @@ caldera_conf: ### -# Kali tool based attacks. Will result in kali commandline tools to be called. Currently supported are: "hydra" -kali_attacks: +# Plugin based attacks. Will result in plugins being called +plugin_based_attacks: ### # Linux specific attacks, a list linux: diff --git a/template.yaml b/template.yaml index e487c62..26a72e3 100644 --- a/template.yaml +++ b/template.yaml @@ -239,8 +239,8 @@ caldera_attacks: - "bd527b63-9f9e-46e0-9816-b8434d2b8989" ### -# Kali tool based attacks. Will result in kali commandline tools to be called. Currently supported are: "hydra" -kali_attacks: +# Plugin based attacks. Will result in plugins being called +plugin_based_attacks: ### # Linux specific attacks, a list linux: diff --git a/tests/data/attacker_has_empty_nicknames.yaml b/tests/data/attacker_has_empty_nicknames.yaml index cc42c98..534a24d 100644 --- a/tests/data/attacker_has_empty_nicknames.yaml +++ b/tests/data/attacker_has_empty_nicknames.yaml @@ -121,8 +121,8 @@ caldera_attacks: - "bd527b63-9f9e-46e0-9816-b8434d2b8989" ### -# Kali tool based attacks. Will result in kali commandline tools to be called. Currently supported are: "hydra" -kali_attacks: +# Plugin based attacks. Will result in plugins being called +plugin_based_attacks: ### # Linux specific attacks, a list linux: diff --git a/tests/data/attacks_half.yaml b/tests/data/attacks_half.yaml index 1155c50..1c1fb01 100644 --- a/tests/data/attacks_half.yaml +++ b/tests/data/attacks_half.yaml @@ -109,8 +109,8 @@ caldera_attacks: ## A bug in production was triggered by this half config. Adding a unit test ### -# Kali tool based attacks. Will result in kali commandline tools to be called. Currently supported are: "hydra" -kali_attacks: +# Plugin based attacks. Will result in plugins being called +plugin_based_attacks: ### # Linux specific attacks, a list linux: diff --git a/tests/data/attacks_missing.yaml b/tests/data/attacks_missing.yaml index 377f040..99fed54 100644 --- a/tests/data/attacks_missing.yaml +++ b/tests/data/attacks_missing.yaml @@ -108,8 +108,8 @@ targets: # This is intentionally missing !!!! ### -# Kali tool based attacks. Will result in kali commandline tools to be called. Currently supported are: "hydra" -#kali_attacks: +# Plugin based attacks. Will result in plugins being called +#plugin_based_attacks: ### # Linux specific attacks, a list # linux: diff --git a/tests/data/attacks_perfect.yaml b/tests/data/attacks_perfect.yaml index dea76e7..92d5188 100644 --- a/tests/data/attacks_perfect.yaml +++ b/tests/data/attacks_perfect.yaml @@ -118,8 +118,8 @@ caldera_attacks: - "bar" ### -# Kali tool based attacks. Will result in kali commandline tools to be called. Currently supported are: "hydra" -kali_attacks: +# Plugin based attacks. Will result in plugins being called +plugin_based_attacks: ### # Linux specific attacks, a list linux: diff --git a/tests/data/basic.yaml b/tests/data/basic.yaml index 3331743..9d26f3c 100644 --- a/tests/data/basic.yaml +++ b/tests/data/basic.yaml @@ -111,8 +111,8 @@ caldera_attacks: - "bd527b63-9f9e-46e0-9816-b8434d2b8989" ### -# Kali tool based attacks. Will result in kali commandline tools to be called. Currently supported are: "hydra" -kali_attacks: +# Plugin based attacks. Will result in plugins being called +plugin_based_attacks: ### # Linux specific attacks, a list linux: diff --git a/tests/data/basic_empty_sensor.yaml b/tests/data/basic_empty_sensor.yaml index 9aad123..94cb02b 100644 --- a/tests/data/basic_empty_sensor.yaml +++ b/tests/data/basic_empty_sensor.yaml @@ -111,8 +111,8 @@ caldera_attacks: - "bd527b63-9f9e-46e0-9816-b8434d2b8989" ### -# Kali tool based attacks. Will result in kali commandline tools to be called. Currently supported are: "hydra" -kali_attacks: +# Plugin based attacks. Will result in plugins being called +plugin_based_attacks: ### # Linux specific attacks, a list linux: diff --git a/tests/data/basic_loot_missing.yaml b/tests/data/basic_loot_missing.yaml index 504624b..1f027bd 100644 --- a/tests/data/basic_loot_missing.yaml +++ b/tests/data/basic_loot_missing.yaml @@ -111,8 +111,8 @@ caldera_attacks: - "bd527b63-9f9e-46e0-9816-b8434d2b8989" ### -# Kali tool based attacks. Will result in kali commandline tools to be called. Currently supported are: "hydra" -kali_attacks: +# Plugin based attacks. Will result in plugins being called +plugin_based_attacks: ### # Linux specific attacks, a list linux: diff --git a/tests/data/basic_results_missing.yaml b/tests/data/basic_results_missing.yaml index f659ecc..817623a 100644 --- a/tests/data/basic_results_missing.yaml +++ b/tests/data/basic_results_missing.yaml @@ -111,8 +111,8 @@ caldera_attacks: - "bd527b63-9f9e-46e0-9816-b8434d2b8989" ### -# Kali tool based attacks. Will result in kali commandline tools to be called. Currently supported are: "hydra" -kali_attacks: +# Plugin based attacks. Will result in plugins being called +plugin_based_attacks: ### # Linux specific attacks, a list linux: diff --git a/tests/data/nap_time_missing.yaml b/tests/data/nap_time_missing.yaml index 718aada..3e11860 100644 --- a/tests/data/nap_time_missing.yaml +++ b/tests/data/nap_time_missing.yaml @@ -104,8 +104,8 @@ caldera_attacks: - "bd527b63-9f9e-46e0-9816-b8434d2b8989" ### -# Kali tool based attacks. Will result in kali commandline tools to be called. Currently supported are: "hydra" -kali_attacks: +# Plugin based attacks. Will result in plugins being called +plugin_based_attacks: ### # Linux specific attacks, a list linux: diff --git a/tests/test_config.py b/tests/test_config.py index 35ea34d..7080c52 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -638,28 +638,28 @@ class TestExperimentConfig(unittest.TestCase): ex = ExperimentConfig("tests/data/attacks_missing.yaml") - self.assertEqual(ex.get_kali_attacks("linux"), []) + self.assertEqual(ex.get_plugin_based_attacks("linux"), []) def test_kali_attacks_empty(self): """ zero entries in kali attacks list """ ex = ExperimentConfig("tests/data/attacks_perfect.yaml") - self.assertEqual(ex.get_kali_attacks("missing"), []) + self.assertEqual(ex.get_plugin_based_attacks("missing"), []) def test_kali_attacks_one(self): """ One entry in kali attacks list """ ex = ExperimentConfig("tests/data/attacks_perfect.yaml") - self.assertEqual(ex.get_kali_attacks("linux"), ["hydra"]) + self.assertEqual(ex.get_plugin_based_attacks("linux"), ["hydra"]) def test_kali_attacks_many(self): """ Many entries in kali attacks list """ ex = ExperimentConfig("tests/data/attacks_perfect.yaml") - self.assertEqual(ex.get_kali_attacks("windows"), ["hydra", "medusa", "skylla"]) + self.assertEqual(ex.get_plugin_based_attacks("windows"), ["hydra", "medusa", "skylla"]) def test_caldera_attacks_missing(self): """ caldera attacks entry fully missing from config """ @@ -673,8 +673,8 @@ class TestExperimentConfig(unittest.TestCase): ex = ExperimentConfig("tests/data/attacks_half.yaml") - self.assertEqual(ex.get_kali_attacks("linux"), ["hydra"]) - self.assertEqual(ex.get_kali_attacks("windows"), []) + self.assertEqual(ex.get_plugin_based_attacks("linux"), ["hydra"]) + self.assertEqual(ex.get_plugin_based_attacks("windows"), []) def test_caldera_attacks_half(self): """ caldera attacks entry partially missing from config """ From b2c36de4029616ef00734478268bb110f64ebb83 Mon Sep 17 00:00:00 2001 From: Thorsten Sick Date: Tue, 13 Jul 2021 11:06:04 +0200 Subject: [PATCH 02/13] Automated tests for plugins --- app/pluginmanager.py | 105 ++++++++++++++++++++++++++++++++++++++++++- plugin_manager.py | 25 ++++++++++- tox.ini | 1 + 3 files changed, 128 insertions(+), 3 deletions(-) mode change 100644 => 100755 plugin_manager.py diff --git a/app/pluginmanager.py b/app/pluginmanager.py index 74fdfe9..722e656 100644 --- a/app/pluginmanager.py +++ b/app/pluginmanager.py @@ -10,13 +10,14 @@ from plugins.base.attack import AttackPlugin from plugins.base.machinery import MachineryPlugin from plugins.base.sensor import SensorPlugin from plugins.base.vulnerability_plugin import VulnerabilityPlugin +from app.interface_sfx import CommandlineColors # from app.interface_sfx import CommandlineColors sections = [{"name": "Vulnerabilities", "subclass": VulnerabilityPlugin}, {"name": "Machinery", "subclass": MachineryPlugin}, - {"name": "Kali", + {"name": "Attack", "subclass": AttackPlugin}, {"name": "Sensors", "subclass": SensorPlugin}, @@ -79,6 +80,108 @@ class PluginManager(): print(f"Description: {plugin.get_description()}") print("\t") + def check(self, plugin): + """ Checks a plugin for valid implementation + + @returns: A list of issues + """ + + issues = [] + + # Sensors + if issubclass(type(plugin), SensorPlugin): + # essential methods: collect + if plugin.collect.__func__ is SensorPlugin.collect: + report = f"Method 'collect' not implemented in {plugin.get_name()} in {plugin.plugin_path}" + issues.append(report) + + # Attacks + if issubclass(type(plugin), AttackPlugin): + # essential methods: run + if plugin.run.__func__ is AttackPlugin.run: + report = f"Method 'run' not implemented in {plugin.get_name()} in {plugin.plugin_path}" + issues.append(report) + + # Machinery + if issubclass(type(plugin), MachineryPlugin): + # essential methods: get_ip, get_state, up. halt, create, destroy + if plugin.get_state.__func__ is MachineryPlugin.get_state: + report = f"Method 'get_state' not implemented in {plugin.get_name()} in {plugin.plugin_path}" + issues.append(report) + if plugin.get_ip.__func__ is MachineryPlugin.get_ip: + report = f"Method 'get_ip' not implemented in {plugin.get_name()} in {plugin.plugin_path}" + issues.append(report) + if plugin.up.__func__ is MachineryPlugin.up: + report = f"Method 'up' not implemented in {plugin.get_name()} in {plugin.plugin_path}" + issues.append(report) + if plugin.halt.__func__ is MachineryPlugin.halt: + report = f"Method 'halt' not implemented in {plugin.get_name()} in {plugin.plugin_path}" + issues.append(report) + if plugin.create.__func__ is MachineryPlugin.create: + report = f"Method 'create' not implemented in {plugin.get_name()} in {plugin.plugin_path}" + issues.append(report) + if plugin.destroy.__func__ is MachineryPlugin.destroy: + report = f"Method 'destroy' not implemented in {plugin.get_name()} in {plugin.plugin_path}" + issues.append(report) + + # Vulnerabilities + if issubclass(type(plugin), VulnerabilityPlugin): + # essential methods: start, stop + if plugin.start.__func__ is VulnerabilityPlugin.start: + report = f"Method 'start' not implemented in {plugin.get_name()} in {plugin.plugin_path}" + issues.append(report) + if plugin.stop.__func__ is VulnerabilityPlugin.stop: + report = f"Method 'stop' not implemented in {plugin.get_name()} in {plugin.plugin_path}" + issues.append(report) + + return issues + + def print_check(self): + """ Iterates through all installed plugins and verifies them """ + + # TODO: Identical name + # TODO: identical class name + + names = {} + cnames = {} + + issues = [] + for section in sections: + # print(f'\t\t{section["name"]}') + plugins = self.get_plugins(section["subclass"]) + + for plugin in plugins: + # print(f"Checking: {plugin.get_name()}") + # Check for duplicate names + name = plugin.get_name() + if name in names: + report = f"Name duplication: {name} is used in {names[name]} and {plugin.plugin_path}" + issues.append(report) + self.attack_logger.vprint(f"{CommandlineColors.BACKGROUND_RED}{report}{CommandlineColors.ENDC}", 0) + names[name] = plugin.plugin_path + + # Check for duplicate class names + name = type(plugin).__name__ + if name in cnames: + report = f"Class name duplication: {name} is used in {cnames[name]} and {plugin.plugin_path}" + issues.append(report) + self.attack_logger.vprint(f"{CommandlineColors.BACKGROUND_RED}{report}{CommandlineColors.ENDC}", 0) + cnames[name] = type(plugin) + + # Deep checks + + result = self.check(plugin) + + for r in result: + print(f"* Issue: {r}") + if len(result): + for r in result: + issues.append(r) + self.attack_logger.vprint(f"{CommandlineColors.BACKGROUND_RED}{r}{CommandlineColors.ENDC}", 1) + return issues + + # TODO: Add verify command to verify all plugins (or a specific one) + def print_default_config(self, subclass_name, name): subclass = None diff --git a/plugin_manager.py b/plugin_manager.py old mode 100644 new mode 100755 index e45bdcf..f1d3e2c --- a/plugin_manager.py +++ b/plugin_manager.py @@ -2,6 +2,8 @@ """ Managing plugins """ import argparse +import sys + from app.pluginmanager import PluginManager from app.attack_log import AttackLog @@ -12,6 +14,20 @@ def list_plugins(arguments): attack_logger = AttackLog(arguments.verbose) p = PluginManager(attack_logger) p.print_list() + return 0 + + +def check_plugins(arguments): + """ Check plugins for validity """ + + attack_logger = AttackLog(arguments.verbose) + p = PluginManager(attack_logger) + res = p.print_check() + if len(res): + print("*************************************") + print("Some issues in plugins were found: ") + print("\n".join(res)) + return len(res) def get_default_config(arguments): @@ -29,11 +45,15 @@ def create_parser(): main_parser.add_argument('--verbose', '-v', action='count', default=0) subparsers = main_parser.add_subparsers(help="sub-commands") - # Sub parser for machine creation + # Sub parser for plugin list parser_list = subparsers.add_parser("list", help="list plugins") parser_list.set_defaults(func=list_plugins) # parser_list.add_argument("--configfile", default="experiment.yaml", help="Config file to create from") + # Sub parser for plugin check + parser_list = subparsers.add_parser("check", help="check plugin implementation") + parser_list.set_defaults(func=check_plugins) + parser_default_config = subparsers.add_parser("raw_config", help="print raw default config of the given plugin") parser_default_config.set_defaults(func=get_default_config) parser_default_config.add_argument("subclass_name", help="name of the subclass") @@ -49,4 +69,5 @@ if __name__ == "__main__": args = parser.parse_args() - args.func(args) + exval = args.func(args) + sys.exit(exval) diff --git a/tox.ini b/tox.ini index ef5891f..09b0859 100644 --- a/tox.ini +++ b/tox.ini @@ -51,3 +51,4 @@ commands = bandit -ll -r app/ plugins/ *.py # Linting # pylint *.py # currently off. Needs configuration + python3 ./plugin_manager.py check From 04215b1394f99adf714c3af6cf1496bd84e4f407 Mon Sep 17 00:00:00 2001 From: Thorsten Sick Date: Tue, 13 Jul 2021 11:06:21 +0200 Subject: [PATCH 03/13] Fixed plugins --- plugins/base/sensor.py | 6 +++--- .../metasploit_clearev_t1070/metasploit_clearev_t1070.py | 2 +- .../metasploit_getuid_t1033/metasploit_getuid.py | 2 +- .../metasploit_sysinfo_t1082/metasploit_sysinfo.py | 2 +- tests/data/partial.yaml | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/plugins/base/sensor.py b/plugins/base/sensor.py index 8529048..a7f07ad 100644 --- a/plugins/base/sensor.py +++ b/plugins/base/sensor.py @@ -39,7 +39,7 @@ class SensorPlugin(BasePlugin): """ Install the sensor. Executed on the target. Take the sensor from the share and (maybe) copy it to its destination. Do some setup """ - raise NotImplementedError + raise True def start(self, disown=None): """ Start the sensor. The connection to the client is disowned here. = Sent to background. This keeps the process running. @@ -47,12 +47,12 @@ class SensorPlugin(BasePlugin): @param disown: Send async into background """ - raise NotImplementedError + raise True def stop(self): """ Stop the sensor """ - raise NotImplementedError + raise True def __call_collect__(self, machine_path): """ Generate the data collect command diff --git a/plugins/default/metasploit_attacks/metasploit_clearev_t1070/metasploit_clearev_t1070.py b/plugins/default/metasploit_attacks/metasploit_clearev_t1070/metasploit_clearev_t1070.py index 8cc46c4..e3fb0d0 100644 --- a/plugins/default/metasploit_attacks/metasploit_clearev_t1070/metasploit_clearev_t1070.py +++ b/plugins/default/metasploit_attacks/metasploit_clearev_t1070/metasploit_clearev_t1070.py @@ -6,7 +6,7 @@ from plugins.base.attack import AttackPlugin from app.metasploit import MetasploitInstant -class MetasploitMigratePlugin(AttackPlugin): +class MetasploitClearevPlugin(AttackPlugin): # Boilerplate name = "metasploit_clearev" diff --git a/plugins/default/metasploit_attacks/metasploit_getuid_t1033/metasploit_getuid.py b/plugins/default/metasploit_attacks/metasploit_getuid_t1033/metasploit_getuid.py index 93dde65..f83e7bc 100644 --- a/plugins/default/metasploit_attacks/metasploit_getuid_t1033/metasploit_getuid.py +++ b/plugins/default/metasploit_attacks/metasploit_getuid_t1033/metasploit_getuid.py @@ -6,7 +6,7 @@ from plugins.base.attack import AttackPlugin from app.metasploit import MetasploitInstant -class MetasploitKeyloggingPlugin(AttackPlugin): +class MetasploitGetuidPlugin(AttackPlugin): # Boilerplate name = "metasploit_getuid" diff --git a/plugins/default/metasploit_attacks/metasploit_sysinfo_t1082/metasploit_sysinfo.py b/plugins/default/metasploit_attacks/metasploit_sysinfo_t1082/metasploit_sysinfo.py index 3e1d1aa..71647fa 100644 --- a/plugins/default/metasploit_attacks/metasploit_sysinfo_t1082/metasploit_sysinfo.py +++ b/plugins/default/metasploit_attacks/metasploit_sysinfo_t1082/metasploit_sysinfo.py @@ -6,7 +6,7 @@ from plugins.base.attack import AttackPlugin from app.metasploit import MetasploitInstant -class MetasploitKeyloggingPlugin(AttackPlugin): +class MetasploitSysinfoPlugin(AttackPlugin): # Boilerplate name = "metasploit_sysinfo" diff --git a/tests/data/partial.yaml b/tests/data/partial.yaml index 11ac422..84a076e 100644 --- a/tests/data/partial.yaml +++ b/tests/data/partial.yaml @@ -116,8 +116,8 @@ caldera_attacks: - "bd527b63-9f9e-46e0-9816-b8434d2b8989" ### -# Kali tool based attacks. Will result in kali commandline tools to be called. Currently supported are: "hydra" -kali_attacks: +# Plugin based attacks. Will result in plugins being called +plugin_based_attacks: ### # Linux specific attacks, a list linux: From 279b7e59a352828646c9e2fdd49d09bd1c6bad00 Mon Sep 17 00:00:00 2001 From: Thorsten Sick Date: Tue, 13 Jul 2021 12:03:53 +0200 Subject: [PATCH 04/13] Removed kali config from yaml files. Replaced with a more generic attack_plugin config --- app/calderacontrol.py | 2 -- app/config.py | 5 ++--- app/experimentcontrol.py | 4 +--- .../FIN7/local_experiment_config.yaml | 4 ++-- template.yaml | 4 ++-- tests/data/attacker_has_empty_nicknames.yaml | 4 ++-- tests/data/attacks_half.yaml | 4 ++-- tests/data/attacks_missing.yaml | 4 ++-- tests/data/attacks_perfect.yaml | 4 ++-- tests/data/basic.yaml | 4 ++-- tests/data/basic_empty_sensor.yaml | 4 ++-- tests/data/basic_loot_missing.yaml | 4 ++-- tests/data/basic_results_missing.yaml | 4 ++-- tests/data/nap_time_missing.yaml | 4 ++-- tests/data/partial.yaml | 4 ++-- tests/test_config.py | 18 +++++++++--------- 16 files changed, 36 insertions(+), 41 deletions(-) diff --git a/app/calderacontrol.py b/app/calderacontrol.py index f05985f..37a0e33 100644 --- a/app/calderacontrol.py +++ b/app/calderacontrol.py @@ -15,8 +15,6 @@ from pprint import pprint, pformat # TODO: Ability deserves an own class. -# TODO: Support Stealth settings: "plain-text obfuscation","base64 obfuscation","base64jumble obfuscation","caesar cipher obfuscation","base64noPadding obfuscation","steganography obfuscation" -# TODO: Support Jitter (min/max) # TODO: Support all Caldera agents: "Sandcat (GoLang)","Elasticat (Blue Python/ Elasticsearch)","Manx (Reverse Shell TCP)","Ragdoll (Python/HTML)" class CalderaControl(): diff --git a/app/config.py b/app/config.py index 771c2a8..c28bb8d 100644 --- a/app/config.py +++ b/app/config.py @@ -7,7 +7,6 @@ import yaml from app.exceptions import ConfigurationError -# TODO: Add attack scripts (that will be CACAO in the future !) and plugin config # So the config being read is distributed into several files and they will have different formats (yaml, CACAO) # Currently it is a single file and YAML only. # We want to be independent from file structure or number of config files @@ -213,14 +212,14 @@ class ExperimentConfig(): raise ConfigurationError("results/loot_dir not properly set in configuration") return res - def kali_conf(self, attack): + def attack_conf(self, attack): """ Get kali config for a specific kali attack @param attack: Name of the attack to look up config for """ try: - res = self.raw_config["kali_conf"][attack] + res = self.raw_config["attack_conf"][attack] except KeyError: res = {} if res is None: diff --git a/app/experimentcontrol.py b/app/experimentcontrol.py index 68a3a2e..dd33a1d 100644 --- a/app/experimentcontrol.py +++ b/app/experimentcontrol.py @@ -193,13 +193,11 @@ class Experiment(): @returns: The output of the cmdline attacking tool """ - # TODO: Extend beyond Kali - for plugin in self.plugin_manager.get_plugins(AttackPlugin, [attack]): name = plugin.get_name() self.attack_logger.vprint(f"{CommandlineColors.OKBLUE}Running Kali plugin {name}{CommandlineColors.ENDC}", 2) - plugin.process_config(self.experiment_config.kali_conf(plugin.get_config_section_name())) # TODO: De-kalify + plugin.process_config(self.experiment_config.attack_conf(plugin.get_config_section_name())) plugin.set_attacker_machine(self.attacker_1) plugin.set_logger(self.attack_logger) plugin.set_caldera(self.caldera_control) diff --git a/plugins/default/adversary_emulations/FIN7/local_experiment_config.yaml b/plugins/default/adversary_emulations/FIN7/local_experiment_config.yaml index 9535139..051ce6e 100644 --- a/plugins/default/adversary_emulations/FIN7/local_experiment_config.yaml +++ b/plugins/default/adversary_emulations/FIN7/local_experiment_config.yaml @@ -150,8 +150,8 @@ plugin_based_attacks: - fin7_1 ### -# Configuration for the kali attack tools -kali_conf: +# Configuration for the plugin based attack tools +attack_conf: ### # Hydra configuration hydra: diff --git a/template.yaml b/template.yaml index 26a72e3..5e1cf4c 100644 --- a/template.yaml +++ b/template.yaml @@ -253,8 +253,8 @@ plugin_based_attacks: - nmap ### -# Configuration for the kali attack tools -kali_conf: +# Configuration for the plugin based attack tools +attack_conf: ### # Hydra configuration hydra: diff --git a/tests/data/attacker_has_empty_nicknames.yaml b/tests/data/attacker_has_empty_nicknames.yaml index 534a24d..4585861 100644 --- a/tests/data/attacker_has_empty_nicknames.yaml +++ b/tests/data/attacker_has_empty_nicknames.yaml @@ -133,8 +133,8 @@ plugin_based_attacks: - hydra ### -# Configuration for the kali attack tools -kali_conf: +# Configuration for the plugin based attack tools +attack_conf: ### # Hydra configuration hydra: diff --git a/tests/data/attacks_half.yaml b/tests/data/attacks_half.yaml index 1c1fb01..1676f2b 100644 --- a/tests/data/attacks_half.yaml +++ b/tests/data/attacks_half.yaml @@ -121,8 +121,8 @@ plugin_based_attacks: # - hydra ### -# Configuration for the kali attack tools -kali_conf: +# Configuration for the plugin based attack tools +attack_conf: ### # Hydra configuration hydra: diff --git a/tests/data/attacks_missing.yaml b/tests/data/attacks_missing.yaml index 99fed54..c97adb9 100644 --- a/tests/data/attacks_missing.yaml +++ b/tests/data/attacks_missing.yaml @@ -120,8 +120,8 @@ targets: # - hydra ### -# Configuration for the kali attack tools -kali_conf: +# Configuration for the plugin based attack tools +attack_conf: ### # Hydra configuration hydra: diff --git a/tests/data/attacks_perfect.yaml b/tests/data/attacks_perfect.yaml index 92d5188..bb3978c 100644 --- a/tests/data/attacks_perfect.yaml +++ b/tests/data/attacks_perfect.yaml @@ -132,8 +132,8 @@ plugin_based_attacks: - skylla ### -# Configuration for the kali attack tools -kali_conf: +# Configuration for the plugin based attack tools +attack_conf: ### # Hydra configuration hydra: diff --git a/tests/data/basic.yaml b/tests/data/basic.yaml index 9d26f3c..f292ebb 100644 --- a/tests/data/basic.yaml +++ b/tests/data/basic.yaml @@ -123,8 +123,8 @@ plugin_based_attacks: - hydra ### -# Configuration for the kali attack tools -kali_conf: +# Configuration for the plugin based attack tools +attack_conf: ### # Hydra configuration hydra: diff --git a/tests/data/basic_empty_sensor.yaml b/tests/data/basic_empty_sensor.yaml index 94cb02b..27bc8ca 100644 --- a/tests/data/basic_empty_sensor.yaml +++ b/tests/data/basic_empty_sensor.yaml @@ -123,8 +123,8 @@ plugin_based_attacks: - hydra ### -# Configuration for the kali attack tools -kali_conf: +# Configuration for the plugin based attack tools +attack_conf: ### # Hydra configuration hydra: diff --git a/tests/data/basic_loot_missing.yaml b/tests/data/basic_loot_missing.yaml index 1f027bd..fab76fc 100644 --- a/tests/data/basic_loot_missing.yaml +++ b/tests/data/basic_loot_missing.yaml @@ -123,8 +123,8 @@ plugin_based_attacks: - hydra ### -# Configuration for the kali attack tools -kali_conf: +# Configuration for the plugin based attack tools +attack_conf: ### # Hydra configuration hydra: diff --git a/tests/data/basic_results_missing.yaml b/tests/data/basic_results_missing.yaml index 817623a..42e7180 100644 --- a/tests/data/basic_results_missing.yaml +++ b/tests/data/basic_results_missing.yaml @@ -123,8 +123,8 @@ plugin_based_attacks: - hydra ### -# Configuration for the kali attack tools -kali_conf: +# Configuration for the plugin based attack tools +attack_conf: ### # Hydra configuration hydra: diff --git a/tests/data/nap_time_missing.yaml b/tests/data/nap_time_missing.yaml index 3e11860..202a8b8 100644 --- a/tests/data/nap_time_missing.yaml +++ b/tests/data/nap_time_missing.yaml @@ -116,8 +116,8 @@ plugin_based_attacks: - hydra ### -# Configuration for the kali attack tools -kali_conf: +# Configuration for the plugin based attack tools +attack_conf: ### # Hydra configuration hydra: diff --git a/tests/data/partial.yaml b/tests/data/partial.yaml index 84a076e..5dd41a7 100644 --- a/tests/data/partial.yaml +++ b/tests/data/partial.yaml @@ -128,8 +128,8 @@ plugin_based_attacks: - hydra ### -# Configuration for the kali attack tools -kali_conf: +# Configuration for the plugin based attack tools +attack_conf: ### # Hydra configuration hydra: diff --git a/tests/test_config.py b/tests/test_config.py index 7080c52..9c13028 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -561,26 +561,26 @@ class TestExperimentConfig(unittest.TestCase): ex = ExperimentConfig("tests/data/attacker_has_empty_nicknames.yaml") self.assertEqual(ex._targets[0].get_nicknames(), [1, 2, 3]) - def test_missing_kali_config(self): - """ Getting kali config for a specific attack. Attack missing """ + def test_missing_attack_config(self): + """ Getting attack config for a specific attack. Attack missing """ ex = ExperimentConfig("tests/data/basic.yaml") - self.assertEqual(ex.kali_conf("BOOM"), {}) + self.assertEqual(ex.attack_conf("BOOM"), {}) - def test_working_kali_config(self): - """ Getting kali config for a specific attack """ + def test_working_attack_config(self): + """ Getting attack config for a specific attack """ ex = ExperimentConfig("tests/data/basic.yaml") - data = ex.kali_conf("hydra") + data = ex.attack_conf("hydra") self.assertEqual(data["userfile"], "users.txt") - def test_kali_config_missing_attack_data(self): - """ Getting kali config for a specific attack: Missing """ + def test_attack_config_missing_attack_data(self): + """ Getting attack config for a specific attack: Missing """ ex = ExperimentConfig("tests/data/attacks_missing.yaml") - data = ex.kali_conf("missing") + data = ex.attack_conf("missing") self.assertEqual(data, {}) def test_missing_caldera_config_obfuscator(self): From 47873ba430678b9a972a4978520eccdda7cc6b49 Mon Sep 17 00:00:00 2001 From: Thorsten Sick Date: Tue, 13 Jul 2021 12:04:10 +0200 Subject: [PATCH 05/13] Fixed copy and paste error --- plugins/base/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/base/sensor.py b/plugins/base/sensor.py index a7f07ad..faeae20 100644 --- a/plugins/base/sensor.py +++ b/plugins/base/sensor.py @@ -39,7 +39,7 @@ class SensorPlugin(BasePlugin): """ Install the sensor. Executed on the target. Take the sensor from the share and (maybe) copy it to its destination. Do some setup """ - raise True + return True def start(self, disown=None): """ Start the sensor. The connection to the client is disowned here. = Sent to background. This keeps the process running. @@ -47,12 +47,12 @@ class SensorPlugin(BasePlugin): @param disown: Send async into background """ - raise True + return True def stop(self): """ Stop the sensor """ - raise True + return True def __call_collect__(self, machine_path): """ Generate the data collect command From f93de3f45592ca9547272e45dc509a46aec27e1c Mon Sep 17 00:00:00 2001 From: Thorsten Sick Date: Tue, 13 Jul 2021 12:13:04 +0200 Subject: [PATCH 06/13] TODO cleanup --- app/machinecontrol.py | 2 -- app/metasploit.py | 1 - app/pluginmanager.py | 3 --- experiment_control.py | 1 - plugins/base/plugin_base.py | 3 --- .../default/sensors/linux_filebeat/linux_filebeat_plugin.py | 2 -- 6 files changed, 12 deletions(-) diff --git a/app/machinecontrol.py b/app/machinecontrol.py index bc0b8e0..cbd938a 100644 --- a/app/machinecontrol.py +++ b/app/machinecontrol.py @@ -313,8 +313,6 @@ class Machine(): def get_ip(self): """ Returns the IP of the main ethernet interface of this machine """ - # TODO: Create special code to extract windows IPs - # TODO: Find a smarter way to get the ip return self.vm_manager.get_ip() diff --git a/app/metasploit.py b/app/metasploit.py index c79cf86..9cb5170 100644 --- a/app/metasploit.py +++ b/app/metasploit.py @@ -130,7 +130,6 @@ class Metasploit(): ip = target.get_ip() # Limp on feature if we can not get a name resolution name_resolution_worked = False print(f"Name resolution for {target.get_ip()} failed. Sessions are: {self.get_client().sessions.list}") - # TODO: Try to get the ip address from kali system retries = 100 while retries > 0: diff --git a/app/pluginmanager.py b/app/pluginmanager.py index 722e656..d81d92a 100644 --- a/app/pluginmanager.py +++ b/app/pluginmanager.py @@ -139,9 +139,6 @@ class PluginManager(): def print_check(self): """ Iterates through all installed plugins and verifies them """ - # TODO: Identical name - # TODO: identical class name - names = {} cnames = {} diff --git a/experiment_control.py b/experiment_control.py index 51c27ca..504015b 100644 --- a/experiment_control.py +++ b/experiment_control.py @@ -6,7 +6,6 @@ import argparse from app.experimentcontrol import Experiment -# TODO: Add verbose settings: -v to -vvv # TODO: Name experiments. Name will be written to the log def explain(args): # pylint: disable=unused-argument diff --git a/plugins/base/plugin_base.py b/plugins/base/plugin_base.py index ebf532a..0540530 100644 --- a/plugins/base/plugin_base.py +++ b/plugins/base/plugin_base.py @@ -7,8 +7,6 @@ import yaml from app.exceptions import PluginError import app.exceptions -# TODO: Proper planning and re-building of plugin system. Especially the default config handling should be streamlined. All the plugin types should have a very similar programming interface. - class BasePlugin(): """ Base class for plugins """ @@ -79,7 +77,6 @@ class BasePlugin(): @param config: A dict with system configuration relevant for all plugins """ - # TODO: Verify if it works properly. It should wotk thanks to the new design # self.sysconf["abs_machinepath_internal"] = config["abs_machinepath_internal"] # self.sysconf["abs_machinepath_external"] = config["abs_machinepath_external"] self.load_default_config() diff --git a/plugins/default/sensors/linux_filebeat/linux_filebeat_plugin.py b/plugins/default/sensors/linux_filebeat/linux_filebeat_plugin.py index b25ac89..6a4d6ba 100644 --- a/plugins/default/sensors/linux_filebeat/linux_filebeat_plugin.py +++ b/plugins/default/sensors/linux_filebeat/linux_filebeat_plugin.py @@ -25,8 +25,6 @@ class LinuxFilebeatPlugin(SensorPlugin): def process_templates(self): """ process jinja2 templates of the config files and insert own config """ - # TODO: Implement - env = Environment( loader=FileSystemLoader(self.get_plugin_path(), encoding='utf-8', followlinks=False), autoescape=select_autoescape() From e9cb9d7f4aa888f119d18ecdb1f40f49515063cf Mon Sep 17 00:00:00 2001 From: Thorsten Sick Date: Wed, 14 Jul 2021 14:03:05 +0200 Subject: [PATCH 07/13] Added compiled documentation to shipit --- Makefile | 2 +- doc/Makefile | 6 +++++- doc/source/usage/usage.rst | 4 ---- tools/shipit.py | 1 + 4 files changed, 7 insertions(+), 6 deletions(-) delete mode 100644 doc/source/usage/usage.rst diff --git a/Makefile b/Makefile index 0731361..e8bf497 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ test: tox.ini coverage report; shipit: test - cd doc; make html; cd .. + cd doc; make zip; cd .. git log --pretty="format: %aD %an: %s" > shipit_log.txt python3 tools/shipit.py diff --git a/doc/Makefile b/doc/Makefile index 8dbbbde..dd3d072 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -21,4 +21,8 @@ help: all: html epub latexpdf text man # I want to check the pdf into GIT. Copying it here so I will move it with the usb stick (as the build dir is not moved) - cp build/latex/purpledome.pdf . \ No newline at end of file + cp build/latex/purpledome.pdf . + +zip: html + cd build; zip -r documentation.zip html/ + cp build/documentation.zip . \ No newline at end of file diff --git a/doc/source/usage/usage.rst b/doc/source/usage/usage.rst deleted file mode 100644 index 6d805ef..0000000 --- a/doc/source/usage/usage.rst +++ /dev/null @@ -1,4 +0,0 @@ -***** -Usage -***** - diff --git a/tools/shipit.py b/tools/shipit.py index 15ffe10..639661c 100755 --- a/tools/shipit.py +++ b/tools/shipit.py @@ -32,6 +32,7 @@ globs = ["TODO.md", "doc/source/index.rst", "doc/Makefile", "doc/purpledome.pdf", + "doc/documentation.zip", "doc/source/*/*.rst", "doc/source/_static/*.png", "doc/source/_templates/*", From 4746d77f9c6c3da7f7340f238e3294471f3f1aa7 Mon Sep 17 00:00:00 2001 From: Thorsten Sick Date: Wed, 14 Jul 2021 14:03:38 +0200 Subject: [PATCH 08/13] More unit tests for log --- tests/test_attack_log.py | 82 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/tests/test_attack_log.py b/tests/test_attack_log.py index d463557..d8889ab 100644 --- a/tests/test_attack_log.py +++ b/tests/test_attack_log.py @@ -4,6 +4,8 @@ import unittest from app.attack_log import AttackLog +import app.attack_log +# from unittest.mock import patch, call # from app.exceptions import ConfigurationError # https://docs.python.org/3/library/unittest.html @@ -203,3 +205,83 @@ class TestMachineConfig(unittest.TestCase): self.assertEqual(data[0]["target"], target) self.assertEqual(data[0]["plugin_name"], attack_name) self.assertEqual(data[0]["hunting_tag"], "MITRE_" + ttp) + + def test_file_write_start(self): + """ Starting a file write """ + al = AttackLog() + source = "asource" + target = "a target" + file_name = "a generic filename" + al.start_file_write(source=source, + target=target, + file_name=file_name, + ) + data = al.get_dict() + self.assertEqual(data[0]["event"], "start") + self.assertEqual(data[0]["type"], "dropping_file") + self.assertEqual(data[0]["sub-type"], "by PurpleDome") + self.assertEqual(data[0]["source"], source) + self.assertEqual(data[0]["target"], target) + self.assertEqual(data[0]["file_name"], file_name) + + def test_file_write_stop(self): + """ Stopping a file write """ + al = AttackLog() + source = "asource" + target = "a target" + file_name = "a generic filename" + al.stop_file_write(source=source, + target=target, + file_name=file_name, + ) + data = al.get_dict() + self.assertEqual(data[0]["event"], "stop") + self.assertEqual(data[0]["type"], "dropping_file") + self.assertEqual(data[0]["sub-type"], "by PurpleDome") + self.assertEqual(data[0]["source"], source) + self.assertEqual(data[0]["target"], target) + self.assertEqual(data[0]["file_name"], file_name) + + def test_execute_payload_start(self): + """ Starting a execute payload """ + al = AttackLog() + source = "asource" + target = "a target" + command = "a generic command" + al.start_execute_payload(source=source, + target=target, + command=command, + ) + data = al.get_dict() + self.assertEqual(data[0]["event"], "start") + self.assertEqual(data[0]["type"], "execute_payload") + self.assertEqual(data[0]["sub-type"], "by PurpleDome") + self.assertEqual(data[0]["source"], source) + self.assertEqual(data[0]["target"], target) + self.assertEqual(data[0]["command"], command) + + def test_execute_payload_stop(self): + """ Stopping a execute payload """ + al = AttackLog() + source = "asource" + target = "a target" + command = "a generic command" + al.stop_execute_payload(source=source, + target=target, + command=command, + ) + data = al.get_dict() + self.assertEqual(data[0]["event"], "stop") + self.assertEqual(data[0]["type"], "execute_payload") + self.assertEqual(data[0]["sub-type"], "by PurpleDome") + self.assertEqual(data[0]["source"], source) + self.assertEqual(data[0]["target"], target) + self.assertEqual(data[0]["command"], command) + + def test_mitre_fix_ttp_is_none(self): + """ Testing the mitre ttp fix for ttp being none """ + self.assertEqual(app.attack_log.__mitre_fix_ttp__(None), "") + + def test_mitre_fix_ttp_is_MITRE_SOMETHING(self): + """ Testing the mitre ttp fix for ttp being MITRE_ """ + self.assertEqual(app.attack_log.__mitre_fix_ttp__("MITRE_FOO"), "MITRE_FOO") From c72ab031bb8eeed931d7ebd1c9bad38c6eae7469 Mon Sep 17 00:00:00 2001 From: Thorsten Sick Date: Wed, 14 Jul 2021 14:04:32 +0200 Subject: [PATCH 09/13] More testability by re-structuring log --- app/attack_log.py | 84 ++++++++++++++++++++++++++--------------------- 1 file changed, 46 insertions(+), 38 deletions(-) diff --git a/app/attack_log.py b/app/attack_log.py index 85a9248..62a2e81 100644 --- a/app/attack_log.py +++ b/app/attack_log.py @@ -6,10 +6,6 @@ import json import datetime -def __get_timestamp__(): - return datetime.datetime.now().strftime("%H:%M:%S.%f") - - def __mitre_fix_ttp__(ttp): """ enforce some systematic naming scheme for MITRE TTPs """ @@ -18,8 +14,8 @@ def __mitre_fix_ttp__(ttp): if ttp.startswith("MITRE_"): return ttp - else: - return "MITRE_" + ttp + + return "MITRE_" + ttp class AttackLog(): @@ -33,6 +29,22 @@ class AttackLog(): self.log = [] self.verbosity = verbosity + # TODO. As soon as someone wants custom timestamps, make the format variable + self.datetime_format = "%H:%M:%S.%f" + + def __add_to_log__(self, item: dict): + """ internal command to add a item to the log + + @param item: data chunk to add + """ + + self.log.append(item) + + def __get_timestamp__(self): + """ Get the timestamp to add to the log entries. Currently not configurable """ + + return datetime.datetime.now().strftime(self.datetime_format) + 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 @@ -47,7 +59,7 @@ class AttackLog(): @param jitter: Jitter being used """ - data = {"timestamp": __get_timestamp__(), + data = {"timestamp": self.__get_timestamp__(), "event": "start", "type": "attack", "sub-type": "caldera", @@ -62,7 +74,7 @@ class AttackLog(): "jitter": jitter } - self.log.append(data) + self.__add_to_log__(data) # TODO: Add parameter # TODO: Add config @@ -82,7 +94,7 @@ class AttackLog(): @param jitter: Jitter being used """ - data = {"timestamp": __get_timestamp__(), + data = {"timestamp": self.__get_timestamp__(), "event": "stop", "type": "attack", "sub-type": "caldera", @@ -96,18 +108,17 @@ class AttackLog(): "obfuscator": obfuscator, "jitter": jitter } - self.log.append(data) + self.__add_to_log__(data) - def start_file_write(self, source, target, file_name, ttp=None): + def start_file_write(self, source, target, file_name): """ Mark the start of a file being written to the target (payload !) @param source: source of the attack. Attack IP (empty if written from controller) @param target: Target machine of the attack @param file_name: Name of the file being written - @param ttp: TTP of the attack. From plugin """ - data = {"timestamp": __get_timestamp__(), + data = {"timestamp": self.__get_timestamp__(), "event": "start", "type": "dropping_file", "sub-type": "by PurpleDome", @@ -115,19 +126,18 @@ class AttackLog(): "target": target, "file_name": file_name } - self.log.append(data) + self.__add_to_log__(data) - def stop_file_write(self, source, target, file_name, ttp=None): + def stop_file_write(self, source, target, file_name): """ Mark the stop of a file being written to the target (payload !) @param source: source of the attack. Attack IP (empty if written from controller) @param target: Target machine of the attack @param attack_name: Name of the attack. From plugin @param file_name: Name of the file being written - @param ttp: TTP of the attack. From plugin """ - data = {"timestamp": __get_timestamp__(), + data = {"timestamp": self.__get_timestamp__(), "event": "stop", "type": "dropping_file", "sub-type": "by PurpleDome", @@ -135,18 +145,17 @@ class AttackLog(): "target": target, "file_name": file_name } - self.log.append(data) + self.__add_to_log__(data) - def start_execute_payload(self, source, target, command, ttp=None): + def start_execute_payload(self, source, target, command): """ Mark the start of a payload being executed @param source: source of the attack. Attack IP (empty if written from controller) @param target: Target machine of the attack @param command: Name of the file being written - @param ttp: TTP of the attack. From plugin """ - data = {"timestamp": __get_timestamp__(), + data = {"timestamp": self.__get_timestamp__(), "event": "start", "type": "execute_payload", "sub-type": "by PurpleDome", @@ -154,19 +163,18 @@ class AttackLog(): "target": target, "command": command } - self.log.append(data) + self.__add_to_log__(data) - def stop_execute_payload(self, source, target, command, ttp=None): + def stop_execute_payload(self, source, target, command): """ Mark the stop of a payload being executed @param source: source of the attack. Attack IP (empty if written from controller) @param target: Target machine of the attack @param command: Name of the attack. From plugin @param file_name: Name of the file being written - @param ttp: TTP of the attack. From plugin """ - data = {"timestamp": __get_timestamp__(), + data = {"timestamp": self.__get_timestamp__(), "event": "stop", "type": "execute_payload", "sub-type": "by PurpleDome", @@ -174,7 +182,7 @@ class AttackLog(): "target": target, "command": command } - self.log.append(data) + self.__add_to_log__(data) def start_kali_attack(self, source, target, attack_name, ttp=None): """ Mark the start of a Kali based attack @@ -185,7 +193,7 @@ class AttackLog(): @param ttp: TTP of the attack. From plugin """ - data = {"timestamp": __get_timestamp__(), + data = {"timestamp": self.__get_timestamp__(), "event": "start", "type": "attack", "sub-type": "kali", @@ -194,7 +202,7 @@ class AttackLog(): "kali_name": attack_name, "hunting_tag": __mitre_fix_ttp__(ttp), } - self.log.append(data) + self.__add_to_log__(data) # TODO: Add parameter # TODO: Add config @@ -209,7 +217,7 @@ class AttackLog(): @param ttp: TTP of the attack. From plugin """ - data = {"timestamp": __get_timestamp__(), + data = {"timestamp": self.__get_timestamp__(), "event": "stop", "type": "attack", "sub-type": "kali", @@ -218,7 +226,7 @@ class AttackLog(): "kali_name": attack_name, "hunting_tag": __mitre_fix_ttp__(ttp), } - self.log.append(data) + self.__add_to_log__(data) def start_metasploit_attack(self, source, target, metasploit_command, ttp=None): """ Mark the start of a Metasploit based attack @@ -229,7 +237,7 @@ class AttackLog(): @param ttp: TTP of the attack. From plugin """ - data = {"timestamp": __get_timestamp__(), + data = {"timestamp": self.__get_timestamp__(), "event": "start", "type": "attack", "sub-type": "metasploit", @@ -238,7 +246,7 @@ class AttackLog(): "metasploit_command": metasploit_command, "hunting_tag": __mitre_fix_ttp__(ttp), } - self.log.append(data) + self.__add_to_log__(data) def stop_metasploit_attack(self, source, target, metasploit_command, ttp=None): """ Mark the start of a Metasploit based attack @@ -249,7 +257,7 @@ class AttackLog(): @param ttp: TTP of the attack. From plugin """ - data = {"timestamp": __get_timestamp__(), + data = {"timestamp": self.__get_timestamp__(), "event": "stop", "type": "attack", "sub-type": "metasploit", @@ -258,7 +266,7 @@ class AttackLog(): "metasploit_command": metasploit_command, "hunting_tag": __mitre_fix_ttp__(ttp), } - self.log.append(data) + self.__add_to_log__(data) def start_attack_plugin(self, source, target, plugin_name, ttp=None): """ Mark the start of an attack plugin @@ -269,7 +277,7 @@ class AttackLog(): @param ttp: TTP of the attack. From plugin """ - data = {"timestamp": __get_timestamp__(), + data = {"timestamp": self.__get_timestamp__(), "event": "start", "type": "attack", "sub-type": "attack_plugin", @@ -278,7 +286,7 @@ class AttackLog(): "plugin_name": plugin_name, "hunting_tag": __mitre_fix_ttp__(ttp), } - self.log.append(data) + self.__add_to_log__(data) # TODO: Add parameter # TODO: Add config @@ -293,7 +301,7 @@ class AttackLog(): @param ttp: TTP of the attack. From plugin """ - data = {"timestamp": __get_timestamp__(), + data = {"timestamp": self.__get_timestamp__(), "event": "stop", "type": "attack", "sub-type": "attack_plugin", @@ -302,7 +310,7 @@ class AttackLog(): "plugin_name": plugin_name, "hunting_tag": __mitre_fix_ttp__(ttp), } - self.log.append(data) + self.__add_to_log__(data) def write_json(self, filename): """ Write the json data for this log From 033eb748867bf3e601364769771574c90b67ed7f Mon Sep 17 00:00:00 2001 From: Thorsten Sick Date: Wed, 14 Jul 2021 14:05:20 +0200 Subject: [PATCH 10/13] pylinting --- app/metasploit.py | 1 + machine_control.py | 2 +- metasploit_control.py | 29 ++++++++++++++++------------- plugin_manager.py | 14 +++++++------- requirements.txt | 1 + 5 files changed, 26 insertions(+), 21 deletions(-) diff --git a/app/metasploit.py b/app/metasploit.py index 9cb5170..6577bdd 100644 --- a/app/metasploit.py +++ b/app/metasploit.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +""" Module to control Metasploit and related tools (MSFVenom) on the attack server """ from pymetasploit3.msfrpc import MsfRpcClient # from app.machinecontrol import Machine diff --git a/machine_control.py b/machine_control.py index b704edf..b1780eb 100644 --- a/machine_control.py +++ b/machine_control.py @@ -22,7 +22,7 @@ def create_machines(arguments): attack_logger = AttackLog(arguments.verbose) target_ = Machine(config["targets"]["target1"], attack_logger) - attacker_1 = Machine(config["attackers"]["attacker"]) + attacker_1 = Machine(config["attackers"]["attacker"], attack_logger) print("Got them") diff --git a/metasploit_control.py b/metasploit_control.py index e2e0fd9..4e1f7f6 100644 --- a/metasploit_control.py +++ b/metasploit_control.py @@ -1,3 +1,6 @@ +#!/usr/bin/env python3 +""" Command line tool to interact with metasploit running on the attack server """ + from app.machinecontrol import Machine from app.attack_log import AttackLog from app.metasploit import MSFVenom, Metasploit @@ -6,13 +9,13 @@ from app.metasploit import MSFVenom, Metasploit # For some local tests if __name__ == "__main__": - # msfrpcd -S -P password -u user -f + # msfrpcd -S -P PASSWORD -u USER -f # attacker_ip = "192.168.178.125" # target_ip = "192.168.178.125" # Metasploit RPC - password = "password" - user = "user" + PASSWORD = "PASSWORD" + USER = "USER" attack_logger = AttackLog(2) attacker = Machine({ # "root": "systems/attacker1", @@ -39,14 +42,14 @@ if __name__ == "__main__": target.up() venom = MSFVenom(attacker, target, attack_logger) - payload_type = "linux/x64/meterpreter_reverse_tcp" - print(venom.generate_cmd(payload=payload_type, - architecture="x64", - platform="linux", - # lhost, - format="elf", - outfile="clickme.exe")) - venom.generate_and_deploy(payload=payload_type, + PAYLOAD_TYPE = "linux/x64/meterpreter_reverse_tcp" + print(venom.generate_payload(payload=PAYLOAD_TYPE, + architecture="x64", + platform="linux", + # lhost, + format="elf", + outfile="clickme.exe")) + venom.generate_and_deploy(payload=PAYLOAD_TYPE, architecture="x64", platform="linux", lhost=attacker.get_ip(), @@ -56,8 +59,8 @@ if __name__ == "__main__": # TODO get meterpreter session # TODO simple command to test - metasploit = Metasploit(password, attacker=attacker, username=user) - metasploit.start_exploit_stub_for_external_payload(payload=payload_type) + metasploit = Metasploit(PASSWORD, attacker=attacker, username=USER) + metasploit.start_exploit_stub_for_external_payload(payload=PAYLOAD_TYPE) print(metasploit.meterpreter_execute(["getuid"], 0)) # client = MsfRpcClient('yourpassword', ssl=True) diff --git a/plugin_manager.py b/plugin_manager.py index f1d3e2c..87999be 100755 --- a/plugin_manager.py +++ b/plugin_manager.py @@ -12,8 +12,8 @@ def list_plugins(arguments): """ List plugins """ attack_logger = AttackLog(arguments.verbose) - p = PluginManager(attack_logger) - p.print_list() + plugin_manager = PluginManager(attack_logger) + plugin_manager.print_list() return 0 @@ -21,9 +21,9 @@ def check_plugins(arguments): """ Check plugins for validity """ attack_logger = AttackLog(arguments.verbose) - p = PluginManager(attack_logger) - res = p.print_check() - if len(res): + plugin_manager = PluginManager(attack_logger) + res = plugin_manager.print_check() + if len(res) == 0: print("*************************************") print("Some issues in plugins were found: ") print("\n".join(res)) @@ -34,8 +34,8 @@ def get_default_config(arguments): """ print default config of a specific plugin """ attack_logger = AttackLog(arguments.verbose) - p = PluginManager(attack_logger) - p.print_default_config(arguments.subclass_name, arguments.plugin_name) + plugin_manager = PluginManager(attack_logger) + plugin_manager.print_default_config(arguments.subclass_name, arguments.plugin_name) def create_parser(): diff --git a/requirements.txt b/requirements.txt index 7dafc5e..14e4c13 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,3 +12,4 @@ straight.plugin==1.5.0 sphinxcontrib.asciinema==0.3.2 paramiko==2.7.2 pymetasploit3==1.0.3 +pylint From ab6787241cd31575fde5688476ac841e433638ca Mon Sep 17 00:00:00 2001 From: Thorsten Sick Date: Wed, 14 Jul 2021 16:19:20 +0200 Subject: [PATCH 11/13] pylinting round2 --- app/calderacontrol.py | 21 +++++++++------ app/config.py | 4 +-- app/metasploit.py | 7 ++--- plugins/base/ssh_features.py | 27 ++++++++++--------- .../FIN7/fin7_section1.py | 2 +- .../vm_controller/vagrant/vagrant_plugin.py | 10 +++---- tests/test_calderacontrol.py | 10 ++----- 7 files changed, 41 insertions(+), 40 deletions(-) diff --git a/app/calderacontrol.py b/app/calderacontrol.py index 37a0e33..b1193f2 100644 --- a/app/calderacontrol.py +++ b/app/calderacontrol.py @@ -6,12 +6,12 @@ import json import os import time +from pprint import pprint, pformat import requests import simplejson from app.exceptions import CalderaError from app.interface_sfx import CommandlineColors -from pprint import pprint, pformat # TODO: Ability deserves an own class. @@ -51,7 +51,8 @@ class CalderaControl(): fullurl = self.url + "file/download" request = requests.get(fullurl, headers=header) filename = request.headers["FILENAME"] + extension - open(os.path.join(target_dir, filename), "wb").write(request.content) + with open(os.path.join(target_dir, filename), "wb") as fh: + fh.write(request.content) # print(r.headers) return filename @@ -221,6 +222,10 @@ class CalderaControl(): # ######### Get by id def get_source(self, source_name): + """ Retrieves data source and detailed facts + + @param: The name of the source + """ payload = {"index": "sources", "name": source_name} @@ -343,8 +348,8 @@ class CalderaControl(): facts = [] if parameters is not None: - for k, v in parameters.items(): - facts.append({"trait": k, "value": v}) + for key, value in parameters.items(): + facts.append({"trait": key, "value": value}) payload["facts"] = facts print(payload) @@ -368,8 +373,8 @@ class CalderaControl(): sources_name = "source_" + name self.add_sources(sources_name, parameters) - print("Got:") - print(self.get_source("source_name")) + # To verify: + # print(self.get_source(sources_name)) payload = {"index": "operations", "name": name, @@ -434,8 +439,8 @@ class CalderaControl(): facts = [] if parameters is not None: - for k, v in parameters.items(): - facts.append({"trait": k, "value": v}) + for key, value in parameters.items(): + facts.append({"trait": key, "value": value}) payload["facts"] = facts print(payload) diff --git a/app/config.py b/app/config.py index c28bb8d..2265965 100644 --- a/app/config.py +++ b/app/config.py @@ -208,8 +208,8 @@ class ExperimentConfig(): raise ConfigurationError("results missing in configuration") try: res = self.raw_config["results"]["loot_dir"] - except KeyError: - raise ConfigurationError("results/loot_dir not properly set in configuration") + except KeyError as error: + raise ConfigurationError("results/loot_dir not properly set in configuration") from error return res def attack_conf(self, attack): diff --git a/app/metasploit.py b/app/metasploit.py index 6577bdd..c3af800 100644 --- a/app/metasploit.py +++ b/app/metasploit.py @@ -18,14 +18,16 @@ import os class Metasploit(): - def __init__(self, password, **kwargs): + def __init__(self, password, attack_logger, **kwargs): """ :param password: password for the msfrpcd + :param attack_logger: The attack logger to use for logging/printing :param kwargs: Relevant ones: uri, port, server, username """ self.password = password + self.attack_logger = attack_logger self.username = kwargs.get("username", None) self.kwargs = kwargs self.client = None @@ -353,8 +355,7 @@ class MetasploitInstant(Metasploit): :param attack_logger: The attack logging :param kwargs: Relevant ones: uri, port, server, username """ - super().__init__(password, **kwargs) - self.attack_logger = attack_logger + super().__init__(password, attack_logger, **kwargs) def parse_ps(self, ps_output): d = [] diff --git a/plugins/base/ssh_features.py b/plugins/base/ssh_features.py index 23a45cb..27758e4 100644 --- a/plugins/base/ssh_features.py +++ b/plugins/base/ssh_features.py @@ -15,13 +15,13 @@ class SSHFeatures(BasePlugin): def __init__(self): super().__init__() - self.c = None + self.connection = None def connect(self): """ Connect to a machine """ - if self.c: - return self.c + if self.connection is not None: + return self.connection retries = 10 retry_sleep = 10 @@ -31,7 +31,7 @@ class SSHFeatures(BasePlugin): if self.config.os() == "linux": uhp = self.get_ip() self.vprint(f"Connecting to {uhp}", 3) - self.c = Connection(uhp, connect_timeout=timeout) + self.connection = Connection(uhp, connect_timeout=timeout) if self.config.os() == "windows": args = {} @@ -43,15 +43,15 @@ class SSHFeatures(BasePlugin): self.vprint(args, 3) uhp = self.get_ip() self.vprint(uhp, 3) - self.c = Connection(uhp, connect_timeout=timeout, user=self.config.ssh_user(), connect_kwargs=args) + self.connection = Connection(uhp, connect_timeout=timeout, user=self.config.ssh_user(), connect_kwargs=args) except (paramiko.ssh_exception.SSHException, socket.timeout): self.vprint(f"Failed to connect, will retry {retries} times. Timeout: {timeout}", 0) retries -= 1 timeout += 10 time.sleep(retry_sleep) else: - self.vprint(f"Connection: {self.c}", 3) - return self.c + self.vprint(f"Connection: {self.connection}", 3) + return self.connection self.vprint("SSH network error", 0) raise NetworkError @@ -71,11 +71,12 @@ class SSHFeatures(BasePlugin): self.vprint("Running SSH remote run: " + cmd, 3) self.vprint("Disown: " + str(disown), 3) + # self.vprint("Connection: " + self.connection, 1) result = None retry = 2 while retry > 0: try: - result = self.c.run(cmd, disown=disown) + 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): @@ -113,7 +114,7 @@ class SSHFeatures(BasePlugin): timeout = 30 while retries: try: - res = self.c.put(src, dst) + res = self.connection.put(src, dst) except (paramiko.ssh_exception.SSHException, socket.timeout, UnexpectedExit): self.vprint(f"PUT Failed to connect, will retry {retries} times. Timeout: {timeout}", 3) retries -= 1 @@ -144,7 +145,7 @@ class SSHFeatures(BasePlugin): retry = 2 while retry > 0: try: - res = self.c.get(src, dst) + res = self.connection.get(src, dst) except (paramiko.ssh_exception.NoValidConnectionsError, UnexpectedExit): if retry <= 0: raise NetworkError @@ -163,6 +164,6 @@ class SSHFeatures(BasePlugin): def disconnect(self): """ Disconnect from a machine """ - if self.c: - self.c.close() - self.c = None + if self.connection: + self.connection.close() + self.connection = None diff --git a/plugins/default/adversary_emulations/FIN7/fin7_section1.py b/plugins/default/adversary_emulations/FIN7/fin7_section1.py index 697a623..07c67ba 100644 --- a/plugins/default/adversary_emulations/FIN7/fin7_section1.py +++ b/plugins/default/adversary_emulations/FIN7/fin7_section1.py @@ -32,7 +32,7 @@ class FIN7Plugin(AttackPlugin): if self.metasploit_1: return self.metasploit_1 - self.metasploit_1 = Metasploit(self.metasploit_password, attacker=self.attacker_machine_plugin, username=self.metasploit_user) + self.metasploit_1 = Metasploit(self.metasploit_password, attack_logger=self.attack_logger, attacker=self.attacker_machine_plugin, username=self.metasploit_user) self.metasploit_1.start_exploit_stub_for_external_payload(payload=self.payload_type_1) self.metasploit_1.wait_for_session() return self.metasploit_1 diff --git a/plugins/default/vm_controller/vagrant/vagrant_plugin.py b/plugins/default/vm_controller/vagrant/vagrant_plugin.py index 1917f21..5bf5689 100644 --- a/plugins/default/vm_controller/vagrant/vagrant_plugin.py +++ b/plugins/default/vm_controller/vagrant/vagrant_plugin.py @@ -28,7 +28,7 @@ class VagrantPlugin(SSHFeatures, MachineryPlugin): super().__init__() self.plugin_path = __file__ self.v = None - self.c = None + self.connection = None self.vagrantfilepath = None self.vagrantfile = None self.sysconf = {} @@ -80,13 +80,13 @@ class VagrantPlugin(SSHFeatures, MachineryPlugin): # For linux we are using Vagrant style if self.config.os() == "linux": - if self.c: - return self.c + if self.connection: + return self.connection uhp = self.v.user_hostname_port(vm_name=self.config.vmname()) self.vprint(f"Connecting to {uhp}", 3) - self.c = Connection(uhp, connect_kwargs={"key_filename": self.v.keyfile(vm_name=self.config.vmname())}) - return self.c + self.connection = Connection(uhp, connect_kwargs={"key_filename": self.v.keyfile(vm_name=self.config.vmname())}) + return self.connection else: return super().connect() diff --git a/tests/test_calderacontrol.py b/tests/test_calderacontrol.py index 6ed03b8..7c515ae 100644 --- a/tests/test_calderacontrol.py +++ b/tests/test_calderacontrol.py @@ -269,9 +269,6 @@ class TestExample(unittest.TestCase): "relationships": [], "facts": [] } - exp2 = {"index": "sources", - "name": "source_name" - } exp3 = {"index": "operations", "name": name, "state": state, @@ -288,7 +285,7 @@ class TestExample(unittest.TestCase): with patch.object(self.cc, "__contact_server__", return_value=None) as mock_method: self.cc.add_operation(name, advid, group, state) # mock_method.assert_called_once_with(exp, method="put") - mock_method.assert_has_calls([call(exp1, method="put"), call(exp2), call(exp3, method="put")]) + mock_method.assert_has_calls([call(exp1, method="put"), call(exp3, method="put")]) # add_operation defaults def test_add_operation_defaults(self): @@ -301,9 +298,6 @@ class TestExample(unittest.TestCase): "relationships": [], "facts": [] } - exp2 = {"index": "sources", - "name": "source_name" - } exp3 = {"index": "operations", "name": name, "state": "running", # default @@ -319,7 +313,7 @@ class TestExample(unittest.TestCase): } with patch.object(self.cc, "__contact_server__", return_value=None) as mock_method: self.cc.add_operation(name, advid) - mock_method.assert_has_calls([call(exp1, method="put"), call(exp2), call(exp3, method="put")]) + mock_method.assert_has_calls([call(exp1, method="put"), call(exp3, method="put")]) # add_adversary def test_add_adversary(self): From 12a481da70c25c3645d30a93fe2534862cd845e0 Mon Sep 17 00:00:00 2001 From: Thorsten Sick Date: Thu, 15 Jul 2021 16:43:15 +0200 Subject: [PATCH 12/13] pylinting round3 --- app/metasploit.py | 59 ++++++++++++++++++------------------ app/pluginmanager.py | 11 ++++--- plugins/base/machinery.py | 3 +- plugins/base/sensor.py | 16 +++------- plugins/base/ssh_features.py | 48 +++++++++++++++-------------- 5 files changed, 66 insertions(+), 71 deletions(-) diff --git a/app/metasploit.py b/app/metasploit.py index c3af800..72203cb 100644 --- a/app/metasploit.py +++ b/app/metasploit.py @@ -1,23 +1,25 @@ #!/usr/bin/env python3 """ Module to control Metasploit and related tools (MSFVenom) on the attack server """ +import time +import socket +import os +import random +import requests + from pymetasploit3.msfrpc import MsfRpcClient # from app.machinecontrol import Machine from app.attack_log import AttackLog from app.interface_sfx import CommandlineColors -import time -import socket from app.exceptions import MetasploitError, ServerError -import requests -import random - -import os # https://github.com/DanMcInerney/pymetasploit3 class Metasploit(): + """ Metasploit class for basic Metasploit wrapping """ + def __init__(self, password, attack_logger, **kwargs): """ @@ -42,7 +44,7 @@ class Metasploit(): time.sleep(3) # Waiting for server to start. Or we would get https connection errors when getting the client. def start_exploit_stub_for_external_payload(self, payload='linux/x64/meterpreter_reverse_tcp', exploit='exploit/multi/handler'): - """ + """ Start a metasploit handler and wait for external payload to connect @:returns: res, which contains "job_id" and "uuid" """ @@ -128,18 +130,18 @@ class Metasploit(): # Get_ip can also return a network name. Matching a session needs a real ip name_resolution_worked = True try: - ip = socket.gethostbyname(target.get_ip()) + target_ip = socket.gethostbyname(target.get_ip()) except socket.gaierror: - ip = target.get_ip() # Limp on feature if we can not get a name resolution + target_ip = target.get_ip() # Limp on feature if we can not get a name resolution name_resolution_worked = False print(f"Name resolution for {target.get_ip()} failed. Sessions are: {self.get_client().sessions.list}") retries = 100 while retries > 0: - for k, v in self.get_client().sessions.list.items(): - if v["session_host"] == ip: + for key, value in self.get_client().sessions.list.items(): + if value["session_host"] == target_ip: # print(f"session list: {self.get_client().sessions.list}") - return k + return key time.sleep(1) retries -= 1 @@ -180,12 +182,12 @@ class Metasploit(): shell.write(cmd) time.sleep(delay) retries = 20 - r = "" + shell_result = "" while retries > 0: - r += shell.read() + shell_result += shell.read() time.sleep(0.5) # Command needs time to execute retries -= 1 - res.append(r) + res.append(shell_result) return res @@ -223,6 +225,8 @@ class Metasploit(): class MSFVenom(): + """ Class to remote controll payload generator MSFVenom on the attacker machine """ + def __init__(self, attacker, target, attack_logger: AttackLog): """ @@ -348,23 +352,18 @@ class MetasploitInstant(Metasploit): """ - def __init__(self, password, attack_logger, **kwargs): - """ - - :param password: password for the msfrpcd - :param attack_logger: The attack logging - :param kwargs: Relevant ones: uri, port, server, username - """ - super().__init__(password, attack_logger, **kwargs) - def parse_ps(self, ps_output): - d = [] + """ Parses the data from ps + :param ps_output: Metasploit ps output + :return: A list of dicts + """ + ps_data = [] for line in ps_output.split("\n")[6:]: pieces = line.split(" ") cleaned_pieces = [] - for p in pieces: - if len(p): - cleaned_pieces.append(p) + for piece in pieces: + if len(piece): + cleaned_pieces.append(piece) if len(cleaned_pieces) > 2: rep = {"PID": int(cleaned_pieces[0].strip()), @@ -382,9 +381,9 @@ class MetasploitInstant(Metasploit): rep["User"] = cleaned_pieces[5].strip() if len(cleaned_pieces) >= 7: rep["Path"] = cleaned_pieces[6].strip() - d.append(rep) + ps_data.append(rep) - return d + return ps_data def filter_ps_results(self, data, user=None, name=None, arch=None): """ Filter the process lists for certain diff --git a/app/pluginmanager.py b/app/pluginmanager.py index d81d92a..52b0099 100644 --- a/app/pluginmanager.py +++ b/app/pluginmanager.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 """ Manage plugins """ -import straight.plugin from glob import glob import os @@ -10,6 +9,7 @@ from plugins.base.attack import AttackPlugin from plugins.base.machinery import MachineryPlugin from plugins.base.sensor import SensorPlugin from plugins.base.vulnerability_plugin import VulnerabilityPlugin +import straight.plugin from app.interface_sfx import CommandlineColors # from app.interface_sfx import CommandlineColors @@ -180,15 +180,16 @@ class PluginManager(): # TODO: Add verify command to verify all plugins (or a specific one) def print_default_config(self, subclass_name, name): + """ Pretty prints the default config for this plugin """ subclass = None - for a in sections: - if a["name"] == subclass_name: - subclass = a["subclass"] + for section in sections: + if section["name"] == subclass_name: + subclass = section["subclass"] if subclass is None: print("Use proper subclass. Available subclasses are: ") - "\n- ".join([a for a in sections["name"]]) + "\n- ".join(list(sections["name"])) plugins = self.get_plugins(subclass, [name]) for plugin in plugins: diff --git a/plugins/base/machinery.py b/plugins/base/machinery.py index 5252e84..5849bd3 100644 --- a/plugins/base/machinery.py +++ b/plugins/base/machinery.py @@ -3,11 +3,10 @@ """ Base class for classes to control any kind of machine: vm, bare metal, cloudified """ from enum import Enum - +import os from app.config import MachineConfig from app.interface_sfx import CommandlineColors from plugins.base.plugin_base import BasePlugin -import os class MachineStates(Enum): diff --git a/plugins/base/sensor.py b/plugins/base/sensor.py index faeae20..1f53e74 100644 --- a/plugins/base/sensor.py +++ b/plugins/base/sensor.py @@ -20,28 +20,20 @@ class SensorPlugin(BasePlugin): super().__init__() # pylint:disable=useless-super-delegation self.debugit = False - def set_sysconf(self, config): - """ Set system config - - @param config: A dict with system configuration relevant for all plugins - """ - - super().set_sysconf(config) - - def prime(self): + def prime(self): # pylint: disable=no-self-use """ prime sets hard core configs in the target. You can use it to call everything that permanently alters the OS by settings. If your prime function returns True the machine will be rebooted after prime-ing it. This is very likely what you want. Only use prime if install is not sufficient. """ return False - def install(self): + def install(self): # pylint: disable=no-self-use """ Install the sensor. Executed on the target. Take the sensor from the share and (maybe) copy it to its destination. Do some setup """ return True - def start(self, disown=None): + def start(self, disown=None): # pylint: disable=unused-argument, no-self-use """ Start the sensor. The connection to the client is disowned here. = Sent to background. This keeps the process running. @param disown: Send async into background @@ -49,7 +41,7 @@ class SensorPlugin(BasePlugin): return True - def stop(self): + def stop(self): # pylint: disable=no-self-use """ Stop the sensor """ return True diff --git a/plugins/base/ssh_features.py b/plugins/base/ssh_features.py index 27758e4..fec3aed 100644 --- a/plugins/base/ssh_features.py +++ b/plugins/base/ssh_features.py @@ -1,22 +1,28 @@ #!/usr/bin/env python3 """ A class you can use to add SSH features to you plugin. Useful for vm_controller/machinery classes """ import os.path +import socket +import time +import paramiko from fabric import Connection -from app.exceptions import NetworkError from invoke.exceptions import UnexpectedExit -import paramiko -import time -import socket +from app.exceptions import NetworkError from plugins.base.plugin_base import BasePlugin class SSHFeatures(BasePlugin): + """ A Mixin class to add SSH features to all kind of VM machinery """ def __init__(self): + self.config = None super().__init__() self.connection = None + def get_ip(self): + """ Get the IP of a machine, must be overwritten in the machinery class """ + raise NotImplementedError + def connect(self): """ Connect to a machine """ @@ -79,14 +85,13 @@ class SSHFeatures(BasePlugin): 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): + except (paramiko.ssh_exception.NoValidConnectionsError, UnexpectedExit, paramiko.ssh_exception.SSHException) as error: if retry <= 0: - raise NetworkError - else: - self.disconnect() - self.connect() - retry -= 1 - self.vprint("Got some SSH errors. Retrying", 2) + raise NetworkError from error + self.disconnect() + self.connect() + retry -= 1 + self.vprint("Got some SSH errors. Retrying", 2) else: break @@ -122,8 +127,8 @@ class SSHFeatures(BasePlugin): time.sleep(retry_sleep) self.disconnect() self.connect() - except FileNotFoundError as e: - self.vprint(f"File not found: {e}", 0) + except FileNotFoundError as error: + self.vprint(f"File not found: {error}", 0) break else: return res @@ -146,16 +151,15 @@ class SSHFeatures(BasePlugin): while retry > 0: try: res = self.connection.get(src, dst) - except (paramiko.ssh_exception.NoValidConnectionsError, UnexpectedExit): + except (paramiko.ssh_exception.NoValidConnectionsError, UnexpectedExit) as error: if retry <= 0: - raise NetworkError - else: - self.disconnect() - self.connect() - retry -= 1 - self.vprint("Got some SSH errors. Retrying", 2) - except FileNotFoundError as e: - self.vprint(e, 0) + raise NetworkError from error + self.disconnect() + self.connect() + retry -= 1 + self.vprint("Got some SSH errors. Retrying", 2) + except FileNotFoundError as error: + self.vprint(error, 0) break else: break From 2b6e2a4586e1fff049717a09d634c245e866af46 Mon Sep 17 00:00:00 2001 From: Thorsten Sick Date: Fri, 16 Jul 2021 08:21:00 +0200 Subject: [PATCH 13/13] pylinting round 4 --- app/experimentcontrol.py | 4 ++-- app/pluginmanager.py | 15 +++++++-------- metasploit_control.py | 2 +- plugins/base/attack.py | 20 ++++++++++---------- plugins/base/plugin_base.py | 12 ++++++------ 5 files changed, 26 insertions(+), 27 deletions(-) diff --git a/app/experimentcontrol.py b/app/experimentcontrol.py index dd33a1d..73229fc 100644 --- a/app/experimentcontrol.py +++ b/app/experimentcontrol.py @@ -11,11 +11,11 @@ from datetime import datetime 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 caldera_control import CalderaControl from machine_control import Machine -from app.exceptions import ServerError from plugins.base.attack import AttackPlugin -from app.pluginmanager import PluginManager # TODO: Multi threading at least when starting machines diff --git a/app/pluginmanager.py b/app/pluginmanager.py index 52b0099..aa9b562 100644 --- a/app/pluginmanager.py +++ b/app/pluginmanager.py @@ -167,14 +167,13 @@ class PluginManager(): # Deep checks - result = self.check(plugin) - - for r in result: - print(f"* Issue: {r}") - if len(result): - for r in result: - issues.append(r) - self.attack_logger.vprint(f"{CommandlineColors.BACKGROUND_RED}{r}{CommandlineColors.ENDC}", 1) + results = self.check(plugin) + + if len(results) > 0: + for result in results: + print(f"* Issue: {result}") + issues.append(result) + self.attack_logger.vprint(f"{CommandlineColors.BACKGROUND_RED}{result}{CommandlineColors.ENDC}", 1) return issues # TODO: Add verify command to verify all plugins (or a specific one) diff --git a/metasploit_control.py b/metasploit_control.py index 4e1f7f6..8978454 100644 --- a/metasploit_control.py +++ b/metasploit_control.py @@ -59,7 +59,7 @@ if __name__ == "__main__": # TODO get meterpreter session # TODO simple command to test - metasploit = Metasploit(PASSWORD, attacker=attacker, username=USER) + metasploit = Metasploit(PASSWORD, attack_logger=attack_logger, attacker=attacker, username=USER) metasploit.start_exploit_stub_for_external_payload(payload=PAYLOAD_TYPE) print(metasploit.meterpreter_execute(["getuid"], 0)) # client = MsfRpcClient('yourpassword', ssl=True) diff --git a/plugins/base/attack.py b/plugins/base/attack.py index a6ed6f8..9dea198 100644 --- a/plugins/base/attack.py +++ b/plugins/base/attack.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 """ Base class for Kali plugins """ +import os from plugins.base.plugin_base import BasePlugin from app.exceptions import PluginError, ConfigurationError from app.calderacontrol import CalderaControl # from app.metasploit import MSFVenom, Metasploit -import os class AttackPlugin(BasePlugin): @@ -50,7 +50,7 @@ class AttackPlugin(BasePlugin): """ Cleanup afterwards """ pass # pylint: disable=unnecessary-pass - def attacker_run_cmd(self, command, warn=True, disown=False): + def attacker_run_cmd(self, command, disown=False): """ Execute a command on the attacker @param command: Command to execute @@ -65,7 +65,7 @@ class AttackPlugin(BasePlugin): res = self.attacker_machine_plugin.__call_remote_run__(command, disown=disown) return res - def targets_run_cmd(self, command, warn=True, disown=False): + def targets_run_cmd(self, command, disown=False): """ Execute a command on the target @param command: Command to execute @@ -136,7 +136,7 @@ class AttackPlugin(BasePlugin): """ raise NotImplementedError - def install(self): + def install(self): # pylint: disable=no-self-use """ Install and setup requirements for the attack """ @@ -179,12 +179,12 @@ class AttackPlugin(BasePlugin): @returns: the machine """ - for t in self.targets: - if t.get_name() == name: - return t + for target in self.targets: + if target.get_name() == name: + return target - for t in self.targets: - if name in t.get_nicknames(): - return t + for target in self.targets: + if name in target.get_nicknames(): + return target raise ConfigurationError(f"No matching machine in experiment config for {name}") diff --git a/plugins/base/plugin_base.py b/plugins/base/plugin_base.py index 0540530..b18ae32 100644 --- a/plugins/base/plugin_base.py +++ b/plugins/base/plugin_base.py @@ -41,12 +41,12 @@ class BasePlugin(): """ Set the attack logger for this machine """ self.attack_logger = attack_logger - def process_templates(self): + def process_templates(self): # pylint: disable=no-self-use """ A method you can optionally implement to transfer your jinja2 templates into the files yo want to send to the target. See 'required_files' """ return - def copy_to_attacker_and_defender(self): + def copy_to_attacker_and_defender(self): # pylint: disable=no-self-use """ Copy attacker/defender specific files to the machines """ return @@ -71,7 +71,7 @@ class BasePlugin(): self.machine_plugin = machine_plugin - def set_sysconf(self, config): + def set_sysconf(self, config): # pylint:disable=unused-argument """ Set system config @param config: A dict with system configuration relevant for all plugins @@ -104,7 +104,7 @@ class BasePlugin(): """ Get a file from the machine """ self.machine_plugin.get(src, dst) # nosec - def run_cmd(self, command, warn=True, disown=False): + def run_cmd(self, command, disown=False): """ Execute a command on the vm using the connection @param command: Command to execute @@ -137,7 +137,7 @@ class BasePlugin(): for i in self.alternative_names: res.add(i) - if len(res): + if len(res) > 0: return list(res) raise NotImplementedError @@ -190,7 +190,7 @@ class BasePlugin(): return self.get_name() - def main_path(self): + def main_path(self): # pylint:disable=no-self-use """ Returns the main path of the Purple Dome installation """ app_dir = os.path.dirname(app.exceptions.__file__)