#2689 Initial draft of File exfiltration.
This commit is contained in:
@@ -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."""
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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]:
|
||||
"""
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user