#2689 Initial draft of File exfiltration.

This commit is contained in:
Archer Bowen
2024-08-14 19:49:58 +01:00
parent 192ca814e0
commit 6a28f17f1b
9 changed files with 568 additions and 29 deletions

View File

@@ -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."""

View File

@@ -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",

View File

@@ -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]:
"""

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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(

View File

@@ -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")

View File

@@ -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")