Merged PR 504: Command and Control Full PR
## Summary Implements the Command and Control applications to the quality and capability needed for the TAP001 expansion and lays the foundations for all the features required for TAP002 (Next Release). The C2C suite contains three new applications: **1. Abstract C2** Base class for the C2 Server and the C2 Beacon. Controls the main internal logic of both applications but with a couple of abstract methods which each class defines differently. **2. C2 Server** The C2 Server takes red agent actions and converts the action options into C2 Commands which are then passed to the C2 Beacon. The output of these commands is sent back to the C2 Server and then returned back to the red agent. **3. C2 Beacon** The C2 beacon uses the Terminal and the Ransomware Applications to perform different commands which it receives these commands and executes them and returns the output. The C2 beacon can also be configured by the Red Agent to configure the current networking behaviour. For a much more detailed description please refer to the .rst documentation and the notebook which demonstrate and describe the functionality very explicitly. Lastly the wiki page also provides more information around the design work for this feature. [Command and Control](/Welcome-to-PrimAITE!/Design/[~In-Progress~]/Command-and-Control) Worth noting that some changes were needed that were unseen during the design page but the overall goals of the feature have been accomplished. ## Test process Tested via notebooks and a series of e2e tests. ## Checklist - [x] PR is linked to a **work item** - [x] **acceptance criteria** of linked ticket are met - [x] performed **self-review** of the code - [x] written **tests** for any new functionality added with this PR - [x] updated the **documentation** if this PR changes or adds functionality - [x] written/updated **design docs** if this PR implements new functionality - [x] updated the **change log** - [x] ran **pre-commit** checks for code style - [x] attended to any **TO-DOs** left in the code (One remaining but unsure if it should be handled in this PR) Related work items: #2689, #2720, #2721, #2779
This commit is contained in:
@@ -1071,6 +1071,51 @@ class NodeNetworkServiceReconAction(AbstractAction):
|
||||
]
|
||||
|
||||
|
||||
class ConfigureC2BeaconAction(AbstractAction):
|
||||
"""Action which configures a C2 Beacon based on the parameters given."""
|
||||
|
||||
class _Opts(BaseModel):
|
||||
"""Schema for options that can be passed to this action."""
|
||||
|
||||
c2_server_ip_address: str
|
||||
keep_alive_frequency: int = Field(default=5, ge=1)
|
||||
masquerade_protocol: str = Field(default="TCP")
|
||||
masquerade_port: str = Field(default="HTTP")
|
||||
|
||||
@field_validator(
|
||||
"c2_server_ip_address",
|
||||
"keep_alive_frequency",
|
||||
"masquerade_protocol",
|
||||
"masquerade_port",
|
||||
mode="before",
|
||||
)
|
||||
@classmethod
|
||||
def not_none(cls, v: str, info: ValidationInfo) -> int:
|
||||
"""If None is passed, use the default value instead."""
|
||||
if v is None:
|
||||
return cls.model_fields[info.field_name].default
|
||||
return v
|
||||
|
||||
def __init__(self, manager: "ActionManager", **kwargs) -> None:
|
||||
super().__init__(manager=manager)
|
||||
|
||||
def form_request(self, node_id: int, config: Dict) -> RequestFormat:
|
||||
"""Return the action formatted as a request that can be ingested by the simulation."""
|
||||
node_name = self.manager.get_node_name_by_idx(node_id)
|
||||
if node_name is None:
|
||||
return ["do_nothing"]
|
||||
config = ConfigureC2BeaconAction._Opts(
|
||||
c2_server_ip_address=config["c2_server_ip_address"],
|
||||
keep_alive_frequency=config["keep_alive_frequency"],
|
||||
masquerade_port=config["masquerade_port"],
|
||||
masquerade_protocol=config["masquerade_protocol"],
|
||||
)
|
||||
|
||||
ConfigureC2BeaconAction._Opts.model_validate(config) # check that options adhere to schema
|
||||
|
||||
return ["network", "node", node_name, "application", "C2Beacon", "configure", config.__dict__]
|
||||
|
||||
|
||||
class NodeAccountsChangePasswordAction(AbstractAction):
|
||||
"""Action which changes the password for a user."""
|
||||
|
||||
@@ -1127,6 +1172,79 @@ class NodeSessionsRemoteLogoutAction(AbstractAction):
|
||||
return ["network", "node", node_name, "service", "Terminal", "remote_logoff", remote_ip]
|
||||
|
||||
|
||||
class RansomwareConfigureC2ServerAction(AbstractAction):
|
||||
"""Action which sends a command from the C2 Server to the C2 Beacon which configures a local RansomwareScript."""
|
||||
|
||||
def __init__(self, manager: "ActionManager", **kwargs) -> None:
|
||||
super().__init__(manager=manager)
|
||||
|
||||
def form_request(self, node_id: int, config: Dict) -> RequestFormat:
|
||||
"""Return the action formatted as a request that can be ingested by the simulation."""
|
||||
node_name = self.manager.get_node_name_by_idx(node_id)
|
||||
if node_name is None:
|
||||
return ["do_nothing"]
|
||||
# Using the ransomware scripts model to validate.
|
||||
ConfigureRansomwareScriptAction._Opts.model_validate(config) # check that options adhere to schema
|
||||
return ["network", "node", node_name, "application", "C2Server", "ransomware_configure", config]
|
||||
|
||||
|
||||
class RansomwareLaunchC2ServerAction(AbstractAction):
|
||||
"""Action which causes the C2 Server to send a command to the C2 Beacon to launch the RansomwareScript."""
|
||||
|
||||
def __init__(self, manager: "ActionManager", **kwargs) -> None:
|
||||
super().__init__(manager=manager)
|
||||
|
||||
def form_request(self, node_id: int) -> RequestFormat:
|
||||
"""Return the action formatted as a request that can be ingested by the simulation."""
|
||||
node_name = self.manager.get_node_name_by_idx(node_id)
|
||||
if node_name is None:
|
||||
return ["do_nothing"]
|
||||
# This action currently doesn't require any further configuration options.
|
||||
return ["network", "node", node_name, "application", "C2Server", "ransomware_launch"]
|
||||
|
||||
|
||||
class ExfiltrationC2ServerAction(AbstractAction):
|
||||
"""Action which exfiltrates a target file from a certain node onto the C2 beacon and then the C2 Server."""
|
||||
|
||||
class _Opts(BaseModel):
|
||||
"""Schema for options that can be passed to this action."""
|
||||
|
||||
username: Optional[str]
|
||||
password: Optional[str]
|
||||
target_ip_address: str
|
||||
target_file_name: str
|
||||
target_folder_name: str
|
||||
exfiltration_folder_name: Optional[str]
|
||||
|
||||
def __init__(self, manager: "ActionManager", **kwargs) -> None:
|
||||
super().__init__(manager=manager)
|
||||
|
||||
def form_request(
|
||||
self,
|
||||
node_id: int,
|
||||
account: dict,
|
||||
target_ip_address: str,
|
||||
target_file_name: str,
|
||||
target_folder_name: str,
|
||||
exfiltration_folder_name: Optional[str],
|
||||
) -> RequestFormat:
|
||||
"""Return the action formatted as a request that can be ingested by the simulation."""
|
||||
node_name = self.manager.get_node_name_by_idx(node_id)
|
||||
if node_name is None:
|
||||
return ["do_nothing"]
|
||||
|
||||
command_model = {
|
||||
"target_file_name": target_file_name,
|
||||
"target_folder_name": target_folder_name,
|
||||
"exfiltration_folder_name": exfiltration_folder_name,
|
||||
"target_ip_address": target_ip_address,
|
||||
"username": account["username"],
|
||||
"password": account["password"],
|
||||
}
|
||||
ExfiltrationC2ServerAction._Opts.model_validate(command_model)
|
||||
return ["network", "node", node_name, "application", "C2Server", "exfiltrate", command_model]
|
||||
|
||||
|
||||
class NodeSendRemoteCommandAction(AbstractAction):
|
||||
"""Action which sends a terminal command to a remote node via SSH."""
|
||||
|
||||
@@ -1148,6 +1266,52 @@ class NodeSendRemoteCommandAction(AbstractAction):
|
||||
]
|
||||
|
||||
|
||||
class TerminalC2ServerAction(AbstractAction):
|
||||
"""Action which causes the C2 Server to send a command to the C2 Beacon to execute the terminal command passed."""
|
||||
|
||||
class _Opts(BaseModel):
|
||||
"""Schema for options that can be passed to this action."""
|
||||
|
||||
commands: Union[List[RequestFormat], RequestFormat]
|
||||
ip_address: Optional[str]
|
||||
username: Optional[str]
|
||||
password: Optional[str]
|
||||
|
||||
def __init__(self, manager: "ActionManager", **kwargs) -> None:
|
||||
super().__init__(manager=manager)
|
||||
|
||||
def form_request(self, node_id: int, commands: List, ip_address: Optional[str], account: dict) -> RequestFormat:
|
||||
"""Return the action formatted as a request that can be ingested by the simulation."""
|
||||
node_name = self.manager.get_node_name_by_idx(node_id)
|
||||
if node_name is None:
|
||||
return ["do_nothing"]
|
||||
|
||||
command_model = {
|
||||
"commands": commands,
|
||||
"ip_address": ip_address,
|
||||
"username": account["username"],
|
||||
"password": account["password"],
|
||||
}
|
||||
|
||||
TerminalC2ServerAction._Opts.model_validate(command_model)
|
||||
return ["network", "node", node_name, "application", "C2Server", "terminal_command", command_model]
|
||||
|
||||
|
||||
class RansomwareLaunchC2ServerAction(AbstractAction):
|
||||
"""Action which causes the C2 Server to send a command to the C2 Beacon to launch the RansomwareScript."""
|
||||
|
||||
def __init__(self, manager: "ActionManager", **kwargs) -> None:
|
||||
super().__init__(manager=manager)
|
||||
|
||||
def form_request(self, node_id: int) -> RequestFormat:
|
||||
"""Return the action formatted as a request that can be ingested by the simulation."""
|
||||
node_name = self.manager.get_node_name_by_idx(node_id)
|
||||
if node_name is None:
|
||||
return ["do_nothing"]
|
||||
# This action currently doesn't require any further configuration options.
|
||||
return ["network", "node", node_name, "application", "C2Server", "ransomware_launch"]
|
||||
|
||||
|
||||
class ActionManager:
|
||||
"""Class which manages the action space for an agent."""
|
||||
|
||||
@@ -1199,6 +1363,11 @@ class ActionManager:
|
||||
"CONFIGURE_DATABASE_CLIENT": ConfigureDatabaseClientAction,
|
||||
"CONFIGURE_RANSOMWARE_SCRIPT": ConfigureRansomwareScriptAction,
|
||||
"CONFIGURE_DOSBOT": ConfigureDoSBotAction,
|
||||
"CONFIGURE_C2_BEACON": ConfigureC2BeaconAction,
|
||||
"C2_SERVER_RANSOMWARE_LAUNCH": RansomwareLaunchC2ServerAction,
|
||||
"C2_SERVER_RANSOMWARE_CONFIGURE": RansomwareConfigureC2ServerAction,
|
||||
"C2_SERVER_TERMINAL_COMMAND": TerminalC2ServerAction,
|
||||
"C2_SERVER_DATA_EXFILTRATE": ExfiltrationC2ServerAction,
|
||||
"NODE_ACCOUNTS_CHANGE_PASSWORD": NodeAccountsChangePasswordAction,
|
||||
"SSH_TO_REMOTE": NodeSessionsRemoteLoginAction,
|
||||
"SESSIONS_REMOTE_LOGOFF": NodeSessionsRemoteLogoutAction,
|
||||
|
||||
@@ -27,10 +27,13 @@ from primaite.simulator.network.hardware.nodes.network.router import Router
|
||||
from primaite.simulator.network.hardware.nodes.network.switch import Switch
|
||||
from primaite.simulator.network.hardware.nodes.network.wireless_router import WirelessRouter
|
||||
from primaite.simulator.network.nmne import NMNEConfig
|
||||
from primaite.simulator.network.transmission.network_layer import IPProtocol
|
||||
from primaite.simulator.network.transmission.transport_layer import Port
|
||||
from primaite.simulator.sim_container import Simulation
|
||||
from primaite.simulator.system.applications.application import Application
|
||||
from primaite.simulator.system.applications.database_client import DatabaseClient # noqa: F401
|
||||
from primaite.simulator.system.applications.red_applications.c2.c2_beacon import C2Beacon # noqa: F401
|
||||
from primaite.simulator.system.applications.red_applications.c2.c2_server import C2Server # noqa: F401
|
||||
from primaite.simulator.system.applications.red_applications.data_manipulation_bot import ( # noqa: F401
|
||||
DataManipulationBot,
|
||||
)
|
||||
@@ -453,6 +456,15 @@ class PrimaiteGame:
|
||||
dos_intensity=float(opt.get("dos_intensity", "1.0")),
|
||||
max_sessions=int(opt.get("max_sessions", "1000")),
|
||||
)
|
||||
elif application_type == "C2Beacon":
|
||||
if "options" in application_cfg:
|
||||
opt = application_cfg["options"]
|
||||
new_application.configure(
|
||||
c2_server_ip_address=IPv4Address(opt.get("c2_server_ip_address")),
|
||||
keep_alive_frequency=(opt.get("keep_alive_frequency", 5)),
|
||||
masquerade_protocol=IPProtocol[(opt.get("masquerade_protocol", IPProtocol.TCP))],
|
||||
masquerade_port=Port[(opt.get("masquerade_port", Port.HTTP))],
|
||||
)
|
||||
if "network_interfaces" in node_cfg:
|
||||
for nic_num, nic_cfg in node_cfg["network_interfaces"].items():
|
||||
new_node.connect_nic(NIC(ip_address=nic_cfg["ip_address"], subnet_mask=nic_cfg["subnet_mask"]))
|
||||
|
||||
Reference in New Issue
Block a user