Commands

Command information in python files is expressed via extensions of two classes - CommandBase and TaskArguments.

TaskArguments does two things:

  • defines the parameters that the command needs

  • verifies / parses out the user supplied arguments into their proper components

    • this includes taking user supplied free-form input (like arguments to a sleep command - 10 4) and parsing it into well-defined JSON that's easier for the agent to handle (like {"interval": 10, "jitter": 4})

    • This also includes verifying all the necessary pieces are present. Maybe your command requires a source and destination, but the user only supplied a source. This is where that would be determined and error out for the user. This prevents you from requiring your agent to do that sort of parsing in the agent.

CommandBase defines the metadata about the command as well as any pre-processing functionality that takes place before the final command is ready for the agent to process.

CommandBase

class ScreenshotCommand(CommandBase):
cmd = "screenshot"
needs_admin = False
help_cmd = "screenshot"
description = "Use the built-in CGDisplay API calls to capture the display and send it back over the C2 channel. No need to specify any parameters as the current time will be used as the file name"
version = 1
is_exit = False
is_file_browse = False
file_browse_parameters = ""
is_process_list = False
process_list_parameters = ""
is_download_file = False
download_file_parameters = ""
is_remove_file = False
remove_file_parameters = False
author = ""
parameters = []
attackmapping = ["T1113"]
argument_class = ScreenshotArguments
browser_script = BrowserScript(script_name="screenshot", author="@its_a_feature_")
async def create_tasking(self, task: MythicTask) -> MythicTask:
task.args.command_line += str(datetime.datetime.utcnow())
return task

Creating your own command requires extending this CommandBase class (i.e. class ScreenshotCommand(CommandBase) and providing values for all of the above components.

  • cmd - this is the command name. The name of the class doesn't matter, it's this value that's used to look up the right command at tasking time

  • needs_admin - this is a boolean indicator for if this command requires admin permissions

  • help_cmd - this is the help information presented to the user if they type help [command name] from the main active callbacks page

  • description - this is the description of the command. This is also presented to the user when they type help.

  • version - this is the version of the command you're creating/editing. This allows a helpful way to make sure your commands are up to date and tracking changes

  • is_exit - this boolean indicates if this is the command that can be used to exit the callback. When you select to exit a callback from the dropdown in the UI, this information is used to find which command to task.

  • The rest of the is_x_x and x_x_parameters pairings are used for the file browser or unified process listing to know which commands to task or pull information from for these additional UI features.

  • argument_class - this correlates this command to a specific TaskArguments class for processing/validating arguments

  • attackmapping - this is a list of strings to indicate MITRE ATT&CK mappings. These are in "T1113" format.

  • agent_code_path is automatically populated for you like in building the payload. This allows you to access code files from within commands in case you need to access files, functions, or create new pieces of payloads. This is really useful for a load command so that you can find and read the functions you're wanting to load in.

The one function you have to supply is the create_tasking function which we'll cover after we talk about arguments.

TaskArguments

The TaskArguments class defines the arguments for a command and defines how to parse the user supplied string so that we can verify that all required arguments are supplied.

Task arguments always take in a string command_line from the UI and either keep it as-is or parse it into CommandParameter classes. This is ALWAYS a string for command_line, so even if you use the popup UI and supply your parameters that way, that JSON will get turned into a JSON string that's then passed here. Below is an example of passing arguments to the ls command for the apfell payload.

In self.args we define a dictionary of our arguments and what they should be along with default values if none were provided.

In parse_arguments we parse the user supplied self.command_line into the appropriate arguments. If the user did everything through the UI popup menu, then it should be as easy as self.load_args_from_json_string(self.command_line), the harder part comes when you allow the user to type arguments free-form and then must parse them out into the appropriate pieces.

class LsArguments(TaskArguments):
def __init__(self, command_line):
super().__init__(command_line)
self.args = {
"path": CommandParameter(name="path", type=ParameterType.String, default_value=".")
}
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:
self.add_arg("path", self.command_line)

The main purpose of the TaskArguments class is to manage arguments for a command. It handles parsing the command_line string into CommandParameters, defining the CommandParameters, and providing an easy interface into updating/accessing/adding/removing arguments as needed.

The class must implement the parse_arguments method and define the args dictionary. This parse_arguments method is the one that allows users to supply "short hand" tasking and still parse out the parameters into the required JSON structured input.

When syncing the command with the UI, Mythic goes through each class that extends the CommandBase, looks at the associated argument_class, and parses that class's args dictionary of CommandParameters to create the pop-up in the UI. While the TaskArgument's parse_arguments method simply parses the user supplied input into JSON, it's the CommandParameter's class that actually verifies that every required parameter has a value, that all the values are appropriate, and that default values are supplied if necessary.

CommandParameters

CommandParameters, similar to BuildParameters, provide information for the user via the UI and validates that the values are all supplied and appropriate.

class CommandParameter:
name: str # name of the parameter
type: ParameterType # type of the parameter
description: str # description of the parameter
choices: [any] # choices if the type is ParameterType.ChooseOne or ParameterType.ChooseMultiple
required: bool # if the parameter is required or not
validation_func: callable # a function to validate if the supplied value is good
value: any # the user supplied value
default_value: any # a default value to use if the user didn't supply anything
def __init__(self, name: str,
type: ParameterType,
description: str = "",
choices: [any] = None,
required: bool = True,
default_value: any = None,
validation_func: callable = None,
value: any = None,
supported_agents: [str] = None):
self.name = name
self.type = type
self.description = description
if choices is None:
self.choices = []
else:
self.choices = choices
self.required = required
self.validation_func = validation_func
if value is None:
self.value = default_value
else:
self.value = value
  • name - the name of the parameter. This should match the name in the args dictionary

  • type- this is the parameter type. The valid types are:

    • String

    • Boolean

    • File

    • Array

    • ChooseOne

    • ChooseMultiple

    • Credential_JSON

    • Credential_Account

    • Credential_Realm

    • Credential_Type

    • Credential_Value

    • Number

    • Payload

    • ConnectionInfo

  • description - this is the description of the parameter that's presented to the user when the modal pops up for tasking

  • choices - this is an array of choices if the parameter_type is ChooseOne or ChooseMultiple

  • required - this indicates if this is a required parameter or not. If the parameter is required and there's no default value or the user doesn't supply a value, an exception will be thrown

  • validation_func - this is an additional function you can supply to do additional checks on values to make sure they're valid for the command. If a value isn't valid, an exception should be raised

  • value - this is the final value for the parameter; it'll either be the default_value or the value supplied by the user

  • default_value - this is a value that'll be set if the user doesn't supply a value

  • supported_agents - If your parameter type is Payload then you're expecting to choose from a list of already created payloads so that you can generate a new one. The supported_agents list allows you to narrow down that dropdown field for the user. For example, if you only want to see agents related to the apfell payload type in the dropdown for this parameter of your command, then set supported_agents=["apfell"] when declaring the parameter.

Most command parameters are pretty straight forward - the one that's a bit unique is the File type (where a user is uploading a file as part of the tasking). When you're doing your tasking, this value will be the base64 string of the file uploaded.

Viewing Command Information
Modal popup example in the UI when tasking

Example

Below is an example of a basic command and tasking for a Cat command. This command allows the user to go through the UI and supply a "path" or to do a short-hand and simply type cat /path/to/file on the tasking line. You can see this reflected in the parse_arguments function - checking to see if there's JSON supplied and if so, auto parse that json into the arguments, otherwise take what the user typed and set the path value to that.

from CommandBase import *
import json
class CatArguments(TaskArguments):
args = {
"path": CommandParameter(name="path", type=ParameterType.String, description="path to file (no quotes required)")
}
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:
self.add_arg("path", self.command_line)
else:
raise ValueError("Missing arguments")
class CatCommand(CommandBase):
cmd = "cat"
needs_admin = False
help_cmd = "cat /path/to/file"
description = "Read the contents of a file and display it to the user. No need for quotes and relative paths are fine"
version = 1
is_exit = False
is_file_browse = False
file_browse_parameters = ""
is_process_list = False
process_list_parameters = ""
is_download_file = False
download_file_parameters = ""
is_remove_file = False
remove_file_parameters = False
author = ""
argument_class = CatArguments
attackmapping = ["T1081", "T1106"]
async def create_tasking(self, task: MythicTask) -> MythicTask:
return task
async def process_response(self, response: AgentResponse):
pass

We'll talk about the create_tasking function next