Create_Tasking

Manipulate tasking before it's sent to the agent

create_tasking

All commands must have a create_tasking function with a base case like:

async def create_tasking(self, task: MythicTask) -> MythicTask:
    return task

When an operator types a command in the UI, whatever the operator types (or whatever is populated based on the popup modal) gets sent to this function after the input is parsed and validated by the TaskArguments and CommandParameters functions mentioned in Commands.

It's here that the operator has full control of the task before it gets sent down to an agent. The task is currently in the "preprocessing" stage when this function is executed and allows you to do many things via Remote Procedure Calls (RPC) back to the Mythic server.

Available Context

So, from this create_tasking function, what information do you immediately have available?

  • task.task_id - the task number associated with this task (not the UUID the agent sees, the integer value that operators see)

  • task.agent_task_id - the UUID associated with this task that an agent sees

  • task.original_params - the string of original parameters that was passed to this function before anything was swapped out for default values or edited.

  • task.completed - boolean indicating if the task is marked as completed or not

  • task.operator - the name of the operator that issued the task

  • task.status - the current status of the task (defaults to MythicStatus.Success).

    • Options are MythicStatus.Error, MythicStatus.Completed, MythicStatus.Processed, MythicStatus.Processing

    • If you set the status to Success, then Mythic will set the task to Submitted so that it's ready to be picked up by the agent. If you set it to error, completed, processed, or processing, then the agent won't pick it up.

  • task.callback - information about the task's associated callback. This has a LOT of information, so let's break it down more:

    • task.callback.build_parameters - this is a dictionary of all the build parameters used to generate the payload that the callback is based on. This is the same sort of information from Payload Type Info. So, if you had a build parameter called "version", you could see that associated value with task.callback.build_parameters.['version']

    • task.callback.c2info - this is an array of dictionaries of the different c2 profiles built into the callback. Just like Payload Type Info where you iterate over them, you can do the same here. For example, to see a parameter called "callback_host" in our first c2 profile is task.callback.c2info[0]['callback_host'].

    • task.callback.user is the user for the callback

    • task.callback.host is the hostname of the callback

    • ... These match up with all the fields used for doing the initial callback - Action: Checkin

      • Something to note about integrity_level values - these are based on Windows standard integrity levels where 2 == normal account, 3 == high integrity (or root), and 4 == SYSTEM (but in most cases for operators, 3 and 4 are the same).

  • task.args - access to the associated arguments class for this command that already has all of the values populated and validated. Let's say you have an argument called "remote_path", you can access it via task.args.get_arg("remote_path") .

    • Want to change the value of that to something else? task.args.add_arg("remote_path", "new value").

    • Want to change the value of that to a different type as well? task.args.add_arg("remote_path", 5, ParameterType.Number)

    • Want to add a new argument entirely for this specific instance as part of the JSON response? task.args.add_arg("new key", "new value"). The add_arg functionality will overwrite the value if the key exists, otherwise it'll add a new key with that value. The default ParameterType for args is ParameterType.String, so if you're adding something else, be sure to change the type.

    • You can also remove args task.args.remove_arg("key"), rename args task.args.rename_arg("old key", "new key")

    • You can also get access to the user's commandline as well via task.args.get_commandline()

    • Want to know if an arg is in your args? task.args.has_arg("key")

RPC Functionality

This additional functionality is broken out into separate files that you can import at the top of your Python command file. For each of the below bullets, simply import like from MythicFileRPC import *

  • MythicC2RPC

    • call_c2_func - Call custom functions within C2 Docker containers

  • MythicCryptoRPC

    • encrypt_bytes - Ask Mythic to encrypt a series of bytes with the current callback's encryption information

    • decrypt_bytes - Ask Mythic to decrypt a series of bytes with the current callback's decryption information

  • MythicFileRPC

    • register_file - Register a file in the Mythic database and get back a file UUID. This UUID allows an agent to chunk files as it sends them from Mythic -> agent and allows the file to be tracked by Mythic.

    • get_file_by_name - Search the current operation for a file that hasn't been deleted with the current name, if it exists, return the contents of the file and all metadata about it. This allows you to provide short-hand names on the command-line for an operator, but still translate that into a file UUID that the agent/Mythic can track.

  • MythicPayloadRPC

    • get_payload_by_uuid - Search the current operator for a payload with a specific UUID, if it exists, return it and the contents of the associated file. If there's a command parameter type of "Payload" and the user selects something you don't want or the Payload Type is right, but there's a parameter used that's incompatible with the tasking, this is where you're able to find that information.

    • register_payload_on_host - Register a payload's UUID with a specific host. This informs Mythic that somehow, a specific payload is on a specific host. This allows Mythic to do some back-end auto tabulation and make more complete dialogs when operators then try to link to specific payloads.

    • build_payload_from_template - Given a payload's UUID, ask Mythic to kick off the creation of a new payload based on the parameters and information associated with the given UUID. Since payload creation is an asynchronous task with potentially other Docker containers, there's a bit of a catch here. When doing this, you will get back the UUID of the new payload. You have to then loop within your tasking function to repeatedly call the get_payload_by_uuid function to see when the creation process is done (successfully or if it errored). When creating the payload, if you specify the additional parameter of destination_host, then Mythic will track that the newly created payload exists on that host. This will allow you to automatically populate payloads for doing P2P connections.

    • build_payload_from_parameters - Task Mythic to make a new payload from explicit parameters. When creating the payload, if you specify the additional parameter of destination_host, then Mythic will track that the newly created payload exists on that host. This will allow you to automatically populate payloads for doing P2P connections.

      This includes the payload type, list of commands, build parameters, etc.

      • The C2 profile list takes a specific format:

        • [{ "c2_profile": "HTTP", "c2_profile_parameters": { "param_name": value, "param": value} }]

      • The build parameter list also takes a specific format:

        • [ {"name": "param name", "value": "param value"} ]

    • build_payload_from_MythicPayloadRPCResponse - If you got information about a payload via the get_payload_by_uuid and wanted to make a new payload from that, but with a few adjustments, then this function allows you to do just that. The following is an example of getting a payload based on a template, modifying a c2 parameter value, and tasking a build. When creating the payload, if you specify the additional parameter of destination_host, then Mythic will track that the newly created payload exists on that host. This will allow you to automatically populate payloads for doing P2P connections.

      There are two helper functions on MythicPayloadRPCResponses for adjusting a few key parameters:

      • set_profile_parameter_value - give the name of a c2 profile, parameter name, and a new value - this function makes that replacement or addition.

      • set_build_parameter_value - give the name of a build parameter name and value - this function makes the replacement or addition.

async def create_tasking(self, task: MythicTask) -> MythicTask:
    orig = await MythicPayloadRPC(task).get_payload_by_uuid(task.args.get_arg("template"))
    orig.set_profile_parameter_value("HTTP", "callback_interval", 2)
    gen_resp = await MythicPayloadRPC(task).build_payload_from_MythicPayloadRPCResponse(orig)
    if gen_resp.status == MythicStatus.Success:
        # we know a payload is building, now we want it
        while True:
            resp = await MythicPayloadRPC(task).get_payload_by_uuid(gen_resp.uuid)
            if resp.status == MythicStatus.Success:
                if resp.build_phase == "success":
                    # it's done, so we can register a file for it
                    task.args.add_arg("template", resp.agent_file_id)
                    break
                elif resp.build_phase == "error":
                    raise Exception(
                        "Failed to build new payload: " + resp.error_message
                    )
                else:
                    await asyncio.sleep(1)
    return task
  • MythicResponseRPC

    • user_output - Add a new response that the user can see.

    • update_callback - Update a piece of information about the current callback.

    • register_artifact - Register a new artifact to be tracked within the current operation.

  • MythicSocksRPC

    • start_socks - Ask Mythic to start a new socks instance for this callback on a specific port

    • stop_socks - Ask Mythic to stop the socks instance associated with this callback.

There can only be one socks instance per callback

Example - Upload to Target

from CommandBase import *
from MythicFileRPC import *
import json


class UploadArguments(TaskArguments):
    def __init__(self, command_line):
        super().__init__(command_line)
        self.args = {
            "file": CommandParameter(name="file", type=ParameterType.File),
            "remote_path": CommandParameter(name="remote_path", type=ParameterType.String, description="/remote/path/on/victim.txt")
        }

    async def parse_arguments(self):
        if len(self.command_line) > 0:
            if self.command_line[0] == '{':
                self.load_args_from_json_string(self.command_line)
            else:
                raise ValueError("Missing JSON arguments")
        else:
            raise ValueError("Missing arguments")

...

async def create_tasking(self, task: MythicTask) -> MythicTask:
    original_file_name = json.loads(task.original_params)['file']
    response = await MythicFileRPC(task).register_file(file=task.args.get_arg("file"),
                                                       saved_file_name=original_file_name,
                                                       delete_after_fetch=False)
    if response.status == MythicStatus.Success:
        task.args.add_arg("file", response.agent_file_id)
    else:
        raise Exception("Error from Mythic: " + response.error_message)
    return task

The above example takes in a file from the operator and wants to upload it to a specific remote path on the target. In the UploadArguments class, we define two arguments in self.args, one for the file and one for the remote path. By default, all arguments are required. In the parse_arguments function we expect to get a JSON string of our file and arguments, so if the command_line is <= 0 or doesn't start with a "{", then we know we're not looking at a JSON string, so raise an error.

When it comes to the create_tasking function, we want to register this file in the Mythic database so we can track that the file exists and is being uploaded to a specific remote target. This helps us track artifacts. Taking this exact case an example, when you upload files, 2 things happen:

  1. the task.original_params (this is what the user sees), we'll have the file swapped out with the name of the file they uploaded. So, if we uploaded a file called "bug.jpg", then an example of the task.original_params would be: {"file": "bug.jpg", "remote_path": "/blah/evil.jpg"}.

  2. The actual parameters that get sent down have the base64 of the file contents though, so if you look at task.args.get_arg("file"), you'll get the base64 of the bug.jpg file. This is meant simply as a way to capture the name of the file as well as preventing files from cluttering up the UI for operators.

When we register the file, we want to track the original name of the file as well, this is why we load the original parameters and get the "file" value (which is the name of the file).

Once the file registration is done, we want the agent to be able to pull this down in chunks, so we swap out the base64 of the file with a file UUID instead (remember, if you do task.args.add_arg for an argument that already exists, it just replaces the value).

Last updated