diff --git a/src/primaite/game/agent/actions.py b/src/primaite/game/agent/actions.py index 92b175a9..aa74399e 100644 --- a/src/primaite/game/agent/actions.py +++ b/src/primaite/game/agent/actions.py @@ -1147,6 +1147,48 @@ class RansomwareLaunchC2ServerAction(AbstractAction): 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 TerminalC2ServerAction(AbstractAction): """Action which causes the C2 Server to send a command to the C2 Beacon to execute the terminal command passed.""" @@ -1233,6 +1275,7 @@ class ActionManager: "C2_SERVER_RANSOMWARE_LAUNCH": RansomwareLaunchC2ServerAction, "C2_SERVER_RANSOMWARE_CONFIGURE": RansomwareConfigureC2ServerAction, "C2_SERVER_TERMINAL_COMMAND": TerminalC2ServerAction, + "C2_SERVER_DATA_EXFILTRATE": ExfiltrationC2ServerAction, } """Dictionary which maps action type strings to the corresponding action class.""" diff --git a/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb b/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb index 46fbe886..052136f8 100644 --- a/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb @@ -67,6 +67,7 @@ " - type: C2_SERVER_RANSOMWARE_LAUNCH\n", " - type: C2_SERVER_RANSOMWARE_CONFIGURE\n", " - type: C2_SERVER_TERMINAL_COMMAND\n", + " - type: C2_SERVER_DATA_EXFILTRATE\n", " options:\n", " nodes:\n", " - node_name: web_server\n", @@ -130,10 +131,22 @@ " server_ip_address: 192.168.1.14\n", " payload: ENCRYPT\n", " 6:\n", + " action: C2_SERVER_DATA_EXFILTRATE\n", + " options:\n", + " node_id: 1\n", + " target_file_name: \"database.db\"\n", + " target_folder_name: \"database\"\n", + " exfiltration_folder_name: \"spoils\"\n", + " target_ip_address: 192.168.1.14\n", + " account:\n", + " username: admin\n", + " password: admin \n", + "\n", + " 7:\n", " action: C2_SERVER_RANSOMWARE_LAUNCH\n", " options:\n", " node_id: 1\n", - " 7:\n", + " 8:\n", " action: CONFIGURE_C2_BEACON\n", " options:\n", " node_id: 0\n", @@ -142,7 +155,7 @@ " keep_alive_frequency: 10\n", " masquerade_protocol: TCP\n", " masquerade_port: DNS\n", - " 8:\n", + " 9:\n", " action: CONFIGURE_C2_BEACON\n", " options:\n", " node_id: 0\n", @@ -487,17 +500,88 @@ "ransomware_script.show()" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Command and Control** | C2 Server Actions | C2_SERVER_DATA_EXFILTRATE\n", + "\n", + "Finally, currently the last action available is the ``C2_SERVER_DATA_EXFILTRATE`` which can be used to exfiltrate a target_file on a remote node to the C2 Beacon & Server's host file system via the ``FTP`` services.\n", + "\n", + "This action is indexed as action ``9``. # TODO: Update.\n", + "\n", + "The below yaml snippet shows all the relevant agent options for this action\n", + "\n", + "``` yaml\n", + " action_space:\n", + " action_list:\n", + " ...\n", + " - type: C2_SERVER_DATA_EXFILTRATE\n", + " ...\n", + " options:\n", + " nodes: # Node List\n", + " ...\n", + " - node_name: client_1\n", + " applications: \n", + " - application_name: C2Server\n", + " ...\n", + " action_map:\n", + " 6:\n", + " action: C2_SERVER_DATA_EXFILTRATE\n", + " options:\n", + " node_id: 1\n", + " target_file_name: \"database.db\"\n", + " target_folder_name: \"database\"\n", + " exfiltration_folder_name: \"spoils\"\n", + " target_ip_address: \"192.168.1.14\"\n", + " account:\n", + " username: \"admin\",\n", + " password: \"admin\"\n", + "\n", + "```\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "env.step(6)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "client_1: Computer = env.game.simulation.network.get_node_by_hostname(\"client_1\")\n", + "client_1.software_manager.file_system.show(full=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "web_server: Computer = env.game.simulation.network.get_node_by_hostname(\"web_server\")\n", + "web_server.software_manager.file_system.show(full=True)" + ] + }, { "cell_type": "markdown", "metadata": {}, "source": [ "### **Command and Control** | C2 Server Actions | C2_SERVER_RANSOMWARE_LAUNCH\n", "\n", - "Finally, currently the last action available is the ``C2_SERVER_RANSOMWARE_LAUNCH`` which quite simply launches the ransomware script installed on the same node as the C2 beacon.\n", + "Finally, to the ransomware configuration action, there is also the ``C2_SERVER_RANSOMWARE_LAUNCH`` which quite simply launches the ransomware script installed on the same node as the C2 beacon.\n", "\n", - "This action is indexed as action ``6``.\n", + "This action is indexed as action ``7``.\n", "\n", - "The below yaml snippet shows all the relevant agent options for this actio\n", + "The below yaml snippet shows all the relevant agent options for this action\n", "\n", "``` yaml\n", " action_space:\n", @@ -513,7 +597,7 @@ " - application_name: C2Server\n", " ...\n", " action_map:\n", - " 6:\n", + " 7:\n", " action: C2_SERVER_RANSOMWARE_LAUNCH\n", " options:\n", " node_id: 1\n", @@ -526,7 +610,7 @@ "metadata": {}, "outputs": [], "source": [ - "env.step(6)" + "env.step(7)" ] }, { @@ -1375,7 +1459,7 @@ "metadata": {}, "outputs": [], "source": [ - "env.step(8) # Equivalent of to c2_beacon.configure(c2_server_ip_address=\"192.168.10.22\")\n", + "env.step(9) # Equivalent of to c2_beacon.configure(c2_server_ip_address=\"192.168.10.22\")\n", "env.step(3)\n", "\n", "c2_beacon.show()\n", diff --git a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py index 8a03491e..6fa34fd6 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py @@ -2,16 +2,20 @@ from abc import abstractmethod from enum import Enum from ipaddress import IPv4Address -from typing import Dict, Optional +from typing import Dict, Optional, Union from pydantic import BaseModel, Field, validate_call from primaite.interface.request import RequestResponse +from primaite.simulator.file_system.file_system import FileSystem, Folder from primaite.simulator.network.protocols.masquerade import C2Packet from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.system.applications.application import Application, ApplicationOperatingState from primaite.simulator.system.core.session_manager import Session +from primaite.simulator.system.services.ftp.ftp_client import FTPClient +from primaite.simulator.system.services.ftp.ftp_server import FTPServer +from primaite.simulator.system.services.service import ServiceOperatingState from primaite.simulator.system.software import SoftwareHealthState @@ -24,6 +28,9 @@ class C2Command(Enum): RANSOMWARE_LAUNCH = "Ransomware Launch" "Instructs the c2 beacon to execute the installed ransomware." + DATA_EXFILTRATION = "Data Exfiltration" + "Instructs the c2 beacon to attempt to return a file to the C2 Server." + TERMINAL = "Terminal" "Instructs the c2 beacon to execute the provided terminal command." @@ -80,6 +87,19 @@ class AbstractC2(Application, identifier="AbstractC2"): masquerade_port: Port = Field(default=Port.HTTP) """The currently chosen port that the C2 traffic is masquerading as. Defaults at HTTP.""" + c2_config: _C2_Opts = _C2_Opts() + """ + Holds the current configuration settings of the C2 Suite. + + The C2 beacon initialise this class through it's internal configure method. + + The C2 Server when receiving a keep alive will initialise it's own configuration + to match that of the configuration settings passed in the keep alive through _resolve keep alive. + + If the C2 Beacon is reconfigured then a new keep alive is set which causes the + C2 beacon to reconfigure it's configuration settings. + """ + def _craft_packet( self, c2_payload: C2Payload, c2_command: Optional[C2Command] = None, command_options: Optional[Dict] = {} ) -> C2Packet: @@ -111,19 +131,6 @@ class AbstractC2(Application, identifier="AbstractC2"): ) return constructed_packet - c2_config: _C2_Opts = _C2_Opts() - """ - Holds the current configuration settings of the C2 Suite. - - The C2 beacon initialise this class through it's internal configure method. - - The C2 Server when receiving a keep alive will initialise it's own configuration - to match that of the configuration settings passed in the keep alive through _resolve keep alive. - - If the C2 Beacon is reconfigured then a new keep alive is set which causes the - C2 beacon to reconfigure it's configuration settings. - """ - def describe_state(self) -> Dict: """ Describe the state of the C2 application. @@ -140,6 +147,82 @@ class AbstractC2(Application, identifier="AbstractC2"): kwargs["protocol"] = IPProtocol.TCP super().__init__(**kwargs) + # TODO: We may need to disable the ftp_server/client when using the opposite service. (To test) + @property + def _host_ftp_client(self) -> Optional[FTPClient]: + """Return the FTPClient that is installed C2 Application's host. + + This method confirms that the FTP Client is functional via the ._can_perform_action + method. If the FTP Client service is not in a suitable state (e.g disabled/pause) + then this method will return None. + + (The FTP Client service is installed by default) + + :return: An FTPClient object is successful, else None + :rtype: union[FTPClient, None] + """ + ftp_client: Union[FTPClient, None] = self.software_manager.software.get("FTPClient") + if ftp_client is None: + self.sys_log.warning(f"{self.__class__.__name__}: No FTPClient. Attempting to install.") + self.software_manager.install(FTPClient) + ftp_client = self.software_manager.software.get("FTPClient") + + # Force start if the service is stopped. + if ftp_client.operating_state == ServiceOperatingState.STOPPED: + if not ftp_client.start(): + self.sys_log.warning(f"{self.__class__.__name__}: cannot start the FTP Client.") + + if not ftp_client._can_perform_action(): + self.sys_log.error(f"{self.__class__.__name__}: is unable to use the FTP service on its host.") + return + + return ftp_client + + @property + def _host_ftp_server(self) -> Optional[FTPServer]: + """ + Returns the FTP Server that is installed C2 Application's host. + + If a FTPServer is not installed then this method will attempt to install one. + + :return: An FTPServer object is successful, else None + :rtype: union[FTPServer, None] + """ + ftp_server: Union[FTPServer, None] = self.software_manager.software.get("FTPServer") + if ftp_server is None: + self.sys_log.warning(f"{self.__class__.__name__}:No FTPServer installed. Attempting to install FTPServer.") + self.software_manager.install(FTPServer) + ftp_server = self.software_manager.software.get("FTPServer") + + # Force start if the service is stopped. + if ftp_server.operating_state == ServiceOperatingState.STOPPED: + if not ftp_server.start(): + self.sys_log.warning(f"{self.__class__.__name__}: cannot start the FTP Server.") + + if not ftp_server._can_perform_action(): + self.sys_log.error(f"{self.__class__.__name__}: is unable use FTP Server service on its host.") + return + + return ftp_server + + # Getter property for the get_exfiltration_folder method () + @property + def _host_file_system(self) -> FileSystem: + """Return the C2 Host's filesystem (Used for exfiltration related commands) .""" + host_file_system: FileSystem = self.software_manager.file_system + if host_file_system is None: + self.sys_log.error(f"{self.__class__.__name__}: does not seem to have a file system!") + return host_file_system + + def get_exfiltration_folder(self, folder_name: str) -> Optional[Folder]: + """Return a folder used for storing exfiltrated data. Otherwise returns None.""" + exfiltration_folder: Union[Folder, None] = self._host_file_system.get_folder(folder_name) + if exfiltration_folder is None: + self.sys_log.info(f"{self.__class__.__name__}: Creating a exfiltration folder.") + return self._host_file_system.create_folder(folder_name=folder_name) + + return exfiltration_folder + # Validate call ensures we are only handling Masquerade Packets. @validate_call def _handle_c2_payload(self, payload: C2Packet, session_id: Optional[str] = None) -> bool: @@ -197,7 +280,7 @@ class AbstractC2(Application, identifier="AbstractC2"): """Abstract Method: The C2 Server and the C2 Beacon handle the KEEP ALIVEs differently.""" # from_network_interface=from_network_interface - def receive(self, payload: C2Packet, session_id: Optional[str] = None, **kwargs) -> bool: + def receive(self, payload: any, session_id: Optional[str] = None, **kwargs) -> bool: """Receives masquerade packets. Used by both c2 server and c2 beacon. Defining the `Receive` method so that the application can receive packets via the session manager. @@ -210,6 +293,11 @@ class AbstractC2(Application, identifier="AbstractC2"): :return: Returns a bool if the traffic was received correctly (See _handle_c2_payload.) :rtype: bool """ + if not isinstance(payload, C2Packet): + self.sys_log.warning(f"{self.name}: Payload is not an C2Packet") + self.sys_log.debug(f"{self.name}: {payload}") + return False + return self._handle_c2_payload(payload, session_id) def _send_keep_alive(self, session_id: Optional[str]) -> bool: @@ -352,14 +440,13 @@ class AbstractC2(Application, identifier="AbstractC2"): :return bool: Returns false if connection was lost. Returns True if connection is active or re-established. :rtype bool: """ - super().apply_timestep(timestep=timestep) if ( self.operating_state is ApplicationOperatingState.RUNNING and self.health_state_actual is SoftwareHealthState.GOOD ): self.keep_alive_inactivity += 1 self._confirm_remote_connection(timestep) - return + return super().apply_timestep(timestep=timestep) def _check_connection(self) -> tuple[bool, RequestResponse]: """ diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py index e66bedc5..b3bf1902 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py @@ -135,6 +135,7 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): kwargs["name"] = "C2Beacon" super().__init__(**kwargs) + # Configure is practically setter method for the ``c2.config`` attribute that also ties into the request manager. @validate_call def configure( self, @@ -212,6 +213,7 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): ---------------------|------------------------ RANSOMWARE_CONFIGURE | self._command_ransomware_config() RANSOMWARE_LAUNCH | self._command_ransomware_launch() + DATA_EXFILTRATION | self._command_data_exfiltration() TERMINAL | self._command_terminal() Please see each method individually for further information regarding @@ -249,6 +251,12 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): self.sys_log.info(f"{self.name}: Received a terminal C2 command.") return self._return_command_output(command_output=self._command_terminal(payload), session_id=session_id) + elif command == C2Command.DATA_EXFILTRATION: + self.sys_log.info(f"{self.name}: Received a Data Exfiltration C2 command.") + return self._return_command_output( + command_output=self._command_data_exfiltration(payload), session_id=session_id + ) + else: self.sys_log.error(f"{self.name}: Received an C2 command: {command} but was unable to resolve command.") return self._return_command_output( @@ -313,9 +321,8 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): """ C2 Command: Ransomware Launch. - Creates a request that executes the ransomware script. - This request is then sent to the terminal service in order to be executed. - + Uses the RansomwareScript's public method .attack() to carry out the + ransomware attack and uses the .from_bool method to return a RequestResponse :payload C2Packet: The incoming INPUT command. :type Masquerade Packet: C2Packet. @@ -329,6 +336,119 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): ) return RequestResponse.from_bool(self._host_ransomware_script.attack()) + def _command_data_exfiltration(self, payload: C2Packet) -> RequestResponse: + """ + C2 Command: Data Exfiltration. + + Uses the FTP Client & Server services to perform the data exfiltration. + + Similar to the terminal, the logic of this command is dependant on if a remote_ip + is present within the payload. + + If a payload does contain an IP address then the C2 Beacon will ssh into the target ip + and execute a command which causes the FTPClient service to send a + + target file will be moved from the target IP address onto the C2 Beacon's host + file system. + + However, if no IP is given, then the command will move the target file from this + machine onto the C2 server. (This logic is performed on the C2) + + :payload C2Packet: The incoming INPUT command. + :type Masquerade Packet: C2Packet. + :return: Returns the Request Response returned by the Terminal execute method. + :rtype: Request Response + """ + if self._host_ftp_server is None: + self.sys_log.warning(f"{self.name}: C2 Beacon unable to the FTP Server. Unable to resolve command.") + return RequestResponse( + status="failure", + data={"Reason": "Cannot find any instances of both a FTP Server & Client. Are they installed?"}, + ) + + remote_ip = payload.payload.get("target_ip_address") + target_folder = payload.payload.get("target_folder_name") + dest_folder = payload.payload.get("exfiltration_folder_name") + + # Using the same name for both the target/destination file for clarity. + file_name = payload.payload.get("target_file_name") + + # TODO: Split Remote file extraction and local file extraction into different methods. + # if remote_ip is None: + # self._host_ftp_client.start() + # self.sys_log.info(f"{self.name}: No Remote IP given. Returning target file from local file system.") + # return RequestResponse.from_bool(self._host_ftp_client.send_file( + # dest_ip_address=self.c2_remote_connection, + # src_folder_name=target_folder, + # src_file_name=file_name, + # dest_folder_name=dest_folder, + # dest_file_name=file_name, + # session_id=self.c2_session.uuid + # )) + + # Parsing remote login credentials + given_username = payload.payload.get("username") + given_password = payload.payload.get("password") + + # Setting up the terminal session and the ftp server + terminal_session = self.get_remote_terminal_session( + username=given_username, password=given_password, ip_address=remote_ip + ) + + # Initialising the exfiltration folder. + exfiltration_folder = self.get_exfiltration_folder(dest_folder) + + # Using the terminal to start the FTP Client on the remote machine. + # This can fail if the FTP Client is already enabled. + terminal_session.execute(command=["service", "start", "FTPClient"]) + host_network_interfaces = self.software_manager.node.network_interfaces + local_ip = host_network_interfaces.get(next(iter(host_network_interfaces))).ip_address + # Creating the FTP creation options. + remote_ftp_options = { + "dest_ip_address": str(local_ip), + "src_folder_name": target_folder, + "src_file_name": file_name, + "dest_folder_name": dest_folder, + "dest_file_name": file_name, + } + + # Using the terminal to send the target data back to the C2 Beacon. + remote_ftp_response: RequestResponse = RequestResponse.from_bool( + terminal_session.execute(command=["service", "FTPClient", "send", remote_ftp_options]) + ) + + # Validating that we successfully received the target data. + + if remote_ftp_response.status == "failure": + self.sys_log.warning( + f"{self.name}: Remote connection: {remote_ip} failed to transfer the target data via FTP." + ) + return remote_ftp_response + + if exfiltration_folder.get_file(file_name) is None: + self.sys_log.warning(f"{self.name}: Unable to locate exfiltrated file on local filesystem.") + return RequestResponse( + status="failure", data={"reason": "Unable to locate exfiltrated data on file system."} + ) + + if self._host_ftp_client is None: + self.sys_log.warning(f"{self.name}: C2 Beacon unable to the FTP Server. Unable to resolve command.") + return RequestResponse( + status="failure", + data={"Reason": "Cannot find any instances of both a FTP Server & Client. Are they installed?"}, + ) + + # Sending the transferred target data back to the C2 Server to successfully exfiltrate the data out the network. + return RequestResponse.from_bool( + self._host_ftp_client.send_file( + dest_ip_address=self.c2_remote_connection, + src_folder_name=dest_folder, # TODO: Clarify this - Dest folder has the same name on c2server/c2beacon. + src_file_name=file_name, + dest_folder_name=dest_folder, + dest_file_name=file_name, + ) + ) + def _command_terminal(self, payload: C2Packet) -> RequestResponse: """ C2 Command: Terminal. diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py index f413a4b7..3441a86b 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py @@ -67,6 +67,19 @@ class C2Server(AbstractC2, identifier="C2Server"): """ return self.send_command(given_command=C2Command.RANSOMWARE_LAUNCH, command_options={}) + def _data_exfiltration_action(request: RequestFormat, context: Dict) -> RequestResponse: + """Agent Action - Sends a Data Exfiltration C2Command to the C2 Beacon with the given parameters. + + :param request: Request with one element containing a dict of parameters for the configure method. + :type request: RequestFormat + :param context: additional context for resolving this action, currently unused + :type context: dict + :return: RequestResponse object with a success code reflecting whether the ransomware was launched. + :rtype: RequestResponse + """ + command_payload = request[-1] + return self.send_command(given_command=C2Command.DATA_EXFILTRATION, command_options=command_payload) + def _remote_terminal_action(request: RequestFormat, context: Dict) -> RequestResponse: """Agent Action - Sends a TERMINAL C2Command to the C2 Beacon with the given parameters. @@ -92,6 +105,10 @@ class C2Server(AbstractC2, identifier="C2Server"): name="terminal_command", request_type=RequestType(func=_remote_terminal_action), ) + rm.add_request( + name="exfiltrate", + request_type=RequestType(func=_data_exfiltration_action), + ) return rm def __init__(self, **kwargs): @@ -177,6 +194,7 @@ class C2Server(AbstractC2, identifier="C2Server"): ---------------------|------------------------ RANSOMWARE_CONFIGURE | Configures an installed ransomware script based on the passed parameters. RANSOMWARE_LAUNCH | Launches the installed ransomware script. + DATA_EXFILTRATION | Utilises the FTP Service to exfiltrate data back to the C2 Server. TERMINAL | Executes a command via the terminal installed on the C2 Beacons Host. Currently, these commands leverage the pre-existing capability of other applications. @@ -210,6 +228,14 @@ class C2Server(AbstractC2, identifier="C2Server"): ): return connection_status + if not self._command_setup(given_command, command_options): + self.sys_log.warning( + f"{self.name}: Failed to perform necessary C2 Server setup for given command: {given_command}." + ) + return RequestResponse( + status="failure", data={"Reason": "Failed to perform necessary C2 Server setup for given command."} + ) + self.sys_log.info(f"{self.name}: Attempting to send command {given_command}.") command_packet = self._craft_packet( c2_payload=C2Payload.INPUT, c2_command=given_command, command_options=command_options @@ -232,6 +258,41 @@ class C2Server(AbstractC2, identifier="C2Server"): ) return self.current_command_output + def _command_setup(self, given_command: C2Command, command_options: dict) -> bool: + """ + Performs any necessary C2 Server setup needed to perform certain commands. + + The following table details any C2 Server prequisites for following commands. + + C2 Command | Command Service/Application Requirements + ---------------------|----------------------------------------- + RANSOMWARE_CONFIGURE | N/A + RANSOMWARE_LAUNCH | N/A + DATA_EXFILTRATION | FTP Server & File system folder + TERMINAL | N/A + + Currently, only the data exfiltration command require the C2 Server + to perform any necessary setup. Specifically, the Data Exfiltration command requires + the C2 Server to have an running FTP Server service as well as a folder for + storing any exfiltrated data. + + :param given_command: Any C2 Command. + :type given_command: C2Command. + :param command_options: The relevant command parameters. + :type command_options: Dict + :returns: True the setup was successful, false otherwise. + :rtype: bool + """ + if given_command == C2Command.DATA_EXFILTRATION: # Data exfiltration setup + if self._host_ftp_server is None: + self.sys_log.warning(f"{self.name}: Unable to setup the FTP Server for data exfiltration") + return False + if not self.get_exfiltration_folder(command_options.get("exfiltration_folder_name", "exfil")): + self.sys_log.warning(f"{self.name}: Unable to create a folder for storing exfiltration data.") + return False + + return True + def _confirm_remote_connection(self, timestep: int) -> bool: """Checks the suitability of the current C2 Beacon connection. diff --git a/src/primaite/simulator/system/services/ftp/ftp_client.py b/src/primaite/simulator/system/services/ftp/ftp_client.py index 28a591dd..79216deb 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_client.py +++ b/src/primaite/simulator/system/services/ftp/ftp_client.py @@ -1,8 +1,10 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address -from typing import Optional +from typing import Dict, Optional from primaite import getLogger +from primaite.interface.request import RequestFormat, RequestResponse +from primaite.simulator.core import RequestManager, RequestType from primaite.simulator.file_system.file_system import File from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket, FTPStatusCode from primaite.simulator.network.transmission.network_layer import IPProtocol @@ -28,6 +30,55 @@ class FTPClient(FTPServiceABC): super().__init__(**kwargs) self.start() + def _init_request_manager(self) -> RequestManager: + """ + Initialise the request manager. + + More information in user guide and docstring for SimComponent._init_request_manager. + """ + rm = super()._init_request_manager() + + def _send_data_request(request: RequestFormat, context: Dict) -> RequestResponse: + """ + Request for sending data via the ftp_client using the request options parameters. + + :param request: Request with one element containing a dict of parameters for the send method. + :type request: RequestFormat + :param context: additional context for resolving this action, currently unused + :type context: dict + :return: RequestResponse object with a success code reflecting whether the configuration could be applied. + :rtype: RequestResponse + """ + dest_ip = request[-1].get("dest_ip_address") + dest_ip = None if dest_ip is None else IPv4Address(dest_ip) + + # TODO: Confirm that the default values lead to a safe failure. + src_folder = request[-1].get("src_folder_name", None) + src_file_name = request[-1].get("src_file_name", None) + dest_folder = request[-1].get("dest_folder_name", None) + dest_file_name = request[-1].get("dest_file_name", None) + + if not self.file_system.access_file(folder_name=src_folder, file_name=src_file_name): + self.sys_log.debug( + f"{self.name}: Received a FTP Request to transfer file: {src_file_name} to Remote IP: {dest_ip}." + ) + return RequestResponse( + status="failure", data={"reason": "Unable to locate requested file on local file system."} + ) + + return RequestResponse.from_bool( + self.send_file( + dest_ip_address=dest_ip, + src_folder_name=src_folder, + src_file_name=src_file_name, + dest_folder_name=dest_folder, + dest_file_name=dest_file_name, + ) + ) + + rm.add_request("send", request_type=RequestType(func=_send_data_request)), + return rm + def _process_ftp_command(self, payload: FTPPacket, session_id: Optional[str] = None, **kwargs) -> FTPPacket: """ Process the command in the FTP Packet. diff --git a/tests/conftest.py b/tests/conftest.py index b6375acd..1328bc9c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -462,6 +462,7 @@ def game_and_agent(): {"type": "C2_SERVER_RANSOMWARE_LAUNCH"}, {"type": "C2_SERVER_RANSOMWARE_CONFIGURE"}, {"type": "C2_SERVER_TERMINAL_COMMAND"}, + {"type": "C2_SERVER_DATA_EXFILTRATE"}, ] action_space = ActionManager( diff --git a/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py b/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py index 990c6363..806ce063 100644 --- a/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py +++ b/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py @@ -15,6 +15,8 @@ from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.system.applications.red_applications.c2.c2_beacon import C2Beacon from primaite.simulator.system.applications.red_applications.c2.c2_server import C2Command, C2Server from primaite.simulator.system.services.database.database_service import DatabaseService +from primaite.simulator.system.services.ftp.ftp_client import FTPClient +from primaite.simulator.system.services.ftp.ftp_server import FTPServer from primaite.simulator.system.services.service import ServiceOperatingState @@ -150,3 +152,52 @@ def test_c2_server_ransomware(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyA database_file = server_2.software_manager.file_system.get_file("database", "database.db") assert database_file.health_status == FileSystemItemHealthStatus.CORRUPT + + +def test_c2_server_data_exfiltration(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]): + """Tests that a Red Agent can extract a database.db file via C2 Server actions.""" + game, agent = game_and_agent_fixture + + # Installing a C2 Beacon on server_1 + server_1: Server = game.simulation.network.get_node_by_hostname("server_1") + server_1.software_manager.install(C2Beacon) + + # Installing a database on Server_2 (creates a database.db file.) + server_2: Server = game.simulation.network.get_node_by_hostname("server_2") + server_2.software_manager.install(DatabaseService) + server_2.software_manager.software["DatabaseService"].start() + + # Configuring the C2 to connect to client 1 (C2 Server) + c2_beacon: C2Beacon = server_1.software_manager.software["C2Beacon"] + c2_beacon.configure(c2_server_ip_address=IPv4Address("10.0.1.2")) + c2_beacon.establish() + assert c2_beacon.c2_connection_active == True + + # Selecting a target file to steal: database.db + # Server 2 ip : 10.0.2.3 + database_file = server_2.software_manager.file_system.get_file(folder_name="database", file_name="database.db") + assert database_file is not None + + # C2 Action: Data exfiltrate. + + action = ( + "C2_SERVER_DATA_EXFILTRATE", + { + "node_id": 0, + "target_file_name": "database.db", + "target_folder_name": "database", + "exfiltration_folder_name": "spoils", + "target_ip_address": "10.0.2.3", + "account": { + "username": "admin", + "password": "admin", + }, + }, + ) + agent.store_action(action) + game.step() + + assert server_1.file_system.access_file(folder_name="spoils", file_name="database.db") + + client_1 = game.simulation.network.get_node_by_hostname("client_1") + assert client_1.file_system.access_file(folder_name="spoils", file_name="database.db") diff --git a/tests/integration_tests/system/red_applications/test_c2_suite_integration.py b/tests/integration_tests/system/red_applications/test_c2_suite_integration.py index 904fb449..4d6432f3 100644 --- a/tests/integration_tests/system/red_applications/test_c2_suite_integration.py +++ b/tests/integration_tests/system/red_applications/test_c2_suite_integration.py @@ -22,6 +22,8 @@ from primaite.simulator.system.applications.red_applications.c2.c2_server import from primaite.simulator.system.applications.red_applications.ransomware_script import RansomwareScript from primaite.simulator.system.services.database.database_service import DatabaseService from primaite.simulator.system.services.dns.dns_server import DNSServer +from primaite.simulator.system.services.ftp.ftp_client import FTPClient +from primaite.simulator.system.services.ftp.ftp_server import FTPServer from primaite.simulator.system.services.web_server.web_server import WebServer from tests import TEST_ASSETS_ROOT @@ -497,3 +499,42 @@ def test_c2_suite_yaml(): assert c2_beacon.c2_connection_active is True assert c2_server.c2_connection_active is True + + +def test_c2_suite_file_extraction(basic_network): + """Test that C2 Beacon can successfully exfiltrate a target file.""" + network: Network = basic_network + network, computer_a, c2_server, computer_b, c2_beacon = setup_c2(network) + # Asserting that the c2 beacon has established a c2 connection + assert c2_beacon.c2_connection_active is True + + # Asserting that the c2 server has established a c2 connection. + assert c2_server.c2_connection_active is True + assert c2_server.c2_remote_connection == IPv4Address("192.168.255.2") + + # Creating the target file on computer_c + computer_c: Computer = network.get_node_by_hostname("node_c") + computer_c.file_system.create_folder("important_files") + computer_c.file_system.create_file(file_name="secret.txt", folder_name="important_files") + assert computer_c.file_system.access_file(folder_name="important_files", file_name="secret.txt") + + # Installing an FTP Server on the same node as C2 Beacon via the terminal: + + # Attempting to exfiltrate secret.txt from computer c to the C2 Server + c2_server.send_command( + given_command=C2Command.DATA_EXFILTRATION, + command_options={ + "username": "admin", + "password": "admin", + "target_ip_address": "192.168.255.3", + "target_folder_name": "important_files", + "exfiltration_folder_name": "yoinked_files", + "target_file_name": "secret.txt", + }, + ) + + # Asserting that C2 Beacon has managed to get the file + assert c2_beacon._host_file_system.access_file(folder_name="yoinked_files", file_name="secret.txt") + + # Asserting that the C2 Beacon can relay it back to the C2 Server + assert c2_server._host_file_system.access_file(folder_name="yoinked_files", file_name="secret.txt")