Command Transform Customization

This section describes how to create custom command transforms

What are command transforms?

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.

Where are command transforms?

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.

Command Transform Features

The CommandTransformOperation class is as follows:

class CommandTransformOperation:
    def __init__(self, file_mapping):
        self.file_mapping = file_mapping
        # file mapping is an array of lists where:
        #  index 0 is the name of the associated parameter
        #  index 1 should be set as None to indicate a file still needs to be created
        #  index 2 is the name of the file to be created when written to disk on Apfell (just filename, no path)
        #  index 3 is a boolean to indicate if the file should be deleted after an agent pulls it down (True = delete after pull down)
        # if you want to create your own registered file on the back-end as a result of your transform
        #    simply add an entry to the self.file_mapping list with the above data and make sure the corresponding
        #    dictionary entry in the parameters is the base64 version of the bytes of the file you want to write
        self.saved_dict = {}
        self.saved_array = []
    # These commands take in the parameters of the Task, do something to them, and returns the params that will be used
    # Each transform can optionally take in a parameter to help it do its tasks

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:

File Mapping

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:

{
    "my_file_parameter": "base64 file bytes",
    "my_other_param": "user typed info here"
}

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:

  1. Name of the parameter that contains a file (my_file_parameter in this case)

  2. Indication of if the file has been registered in Apfell yet (will be None in this case)

  3. 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)

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

Adding files to the file mapping

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:

  1. Add the new parameter to the parameter JSON with the base64 of the new file

  2. 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] .

Examples

Let's look at some examples of transforms:

Adding new parameter JSON key and value

async def swap_shortnames(self, task_params: str, parameter: None) -> str:
    # sets a flag to swap parameters that end in _id with filenames if the current value exists as a file name
    import json
    try:
        params = json.loads(task_params)
        params['swap_shortnames'] = True
        return json.dumps(params)
    except Exception as e:
        print("can't add swap_shortnames field since it's not json")
    return task_params

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

Editing file_mapping values

async def convert_to_file_id_param_name(self, task_params: str, parameter: None) -> str:
    import json
    try:
        params = json.loads(task_params)
        params['file_id'] = params['file']
        del params['file']
        for file_update in self.file_mapping:
            if file_update[0] == 'file':
                file_update[0] = 'file_id'
                file_update[3] = True
        return json.dumps(params)
    except Exception as e:
        raise(e)

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

Last updated