Command Transform Customization
This section describes how to create custom command transforms
Last updated
This section describes how to create custom command transforms
Last updated
Command transforms are a pre-processing step that can be applied to any task before it's ready for an agent to pull down the task. There can be any number of command transforms applied in a row to any task as long as the final result is a string.The final output of a command transform is a new value to the task's parameter
value.
Command transforms are located in the "Manage Operations" -> "Transform Management" page.
If you need to create or modify transforms, this is where you'll go. For command transforms specifically, you'll find them at the top portion.
Once you're ready to apply them to an actual command, go to "Manage Operations" -> "Payload Management" -> Edit Commands -> Select your command from the top dropdown -> go to the Command Transforms
section.
The CommandTransformOperation
class is as follows:
This class is instantiated each time there are transforms associated with a command when tasking is issued. When doing transforms, you can save information between each transform in your sequence either in the self.saved_dict
for named parameters or in the self.saved_array
with positional information. The more interesting piece is the self.file_mapping
portion:
When a task has a parameter of type file
, the user is prompted to select a file to upload. This data is stored in the JSON representation of the task that's used on the Apfell server. For example, if the command has two parameters - my_file_parameter
of type file
and my_other_param
of type string
. The parameter
field of the task will initially look like:
The Apfell server recognizes that my_file_parameter
is of type file and that it hasn't been registered in Apfell's database yet, so it tracks it in a file_mapping
. This file_mapping
is simply a list of lists where each entry contains the following:
Name of the parameter that contains a file (my_file_parameter
in this case)
Indication of if the file has been registered in Apfell yet (will be None
in this case)
Name of the file to be created when this data is written to disk (typically is the name of the file that was uploaded, but doesn't have to be)
Indication of if this file should be deleted from disk once it's retrieved by an agent (default is false
). This allows us to register one-off files that don't need to persist (like new module loads) by setting this to true
or keeping these files around for future reference (like actually uploading files to disk on target).
This information is passed to the command transform in the class' self.file_mapping
field. If you want to modify the contents of a file (such as obfuscating it), you can locate the appropriate parameter in the parameters field, base64 decode the file, do your modifications, base64 encode it, and re-assign it to the parameter.
Because these command transforms run within your payload type's associated docker container (or the external
docker container), your transforms have access to everything in that docker container. This means you can pre-load your docker container with necessary static files (such as .NET CLRs, necessary DLLs, command specific libraries, etc) that your agent might need, but don't want want to upload through the UI each time.
If you want to register additional files in the file mapping to be registered within Apfell for your agent to pull down, it's a pretty simple process. The use case here would be, for example, taking the user's task input to generate a task specific executable or library that the agent needs to pull down and execute. There's just two things that you need to do:
Add the new parameter to the parameter JSON with the base64 of the new file
Track that new parameter in the self.file_mapping
.
The first piece is highly command and payload_type specific, but you generate your new file or read a static file from your docker container, base64 encode it, and add it to your parameter's dictionary.
The second piece just requires adding a new entry to the file mapping list: ["your new parameter name", None, "the name of the file that should be written on disk", True]
.
Let's look at some examples of transforms:
Always start with async def transform_name(self, task_params: str, parameter: (type here)) -> str:
. This is simply the function definition that'll be called.
Always import any necessary packages (such as JSON) and surround your function in a try
except
block. Your tasking parameters always comes in as a string, so if it's actually JSON, you'll need to parse it out json.loads(task_params)
. You can add a new element in python's dictionary by simply using the new key, such as params['swap_shortnames'] = True
and either the key and value will be added, or if the key exists, the value will be modified. In the end, always return a string of the new parameters: return json.dumps(params)
.
This example has a file
parameter that is of type file
, but wants to swap that to be file_id
instead. So, the first thing is to adjust the parameters by converting it from a string to a dictionary. Then making a new field, file_id
that has the same contents of file
, and removing the old file
parameter from the dictionary.
Lastly, the file_mapping needs to be updated because it's still looking for the file
keywords in the parameter's JSON dictionary for the contents of the file to write to disk. So, we loop through the elements in self.file_mapping
until we find one where the named parameter (index 0) matches file
. We update it to be file_id
instead (to reflect our changes to the parameters) and also mark the file to be removed once it's fetched by an agent file_update[3] = True
. As usual, we return the final result as a string return json.dumps(params)
.