Files
Angerona/documentation-payload/Cazalla/developing-commands.md
T
2026-01-26 09:35:31 +00:00

20 KiB

+++ title = "Developing Commands" chapter = false weight = 20 pre = "3. " +++

Developing New Commands for Cazalla

This guide provides a step-by-step walkthrough for adding new commands to the Cazalla agent. Understanding this process is essential for extending Cazalla's capabilities.

📋 Overview

Adding a new command to Cazalla requires coordination across multiple components:

  1. Python Command Definition - Defines the command interface in Mythic
  2. Translator Mapping - Maps command names to hex codes for agent communication
  3. C Agent Implementation - Implements the actual command logic
  4. Command Dispatcher Registration - Registers the handler in the command dispatcher
  5. Builder Configuration - Enables command selection during payload creation

🏗️ Architecture Overview

graph LR
    A[Python Command] --> B[Translator]
    B --> C[C Agent Handler]
    C --> D[Command Dispatcher]
    D --> E[Builder Mapping]
    
    style A fill:#e1f5ff
    style B fill:#fff4e1
    style C fill:#e8f5e9
    style D fill:#fce4ec
    style E fill:#f3e5f5

Data Flow:

  • Mythic UI → Python command receives task
  • Python → Translator converts to binary format with hex code
  • Agent → C handler processes command and returns response
  • Translator → Converts response back to Mythic format

📝 Step-by-Step Guide

Step 1: Create Python Command File

Create a new Python file in Payload_Type/cazalla/cazalla/agent_functions/ named after your command (e.g., mycommand.py).

1.1 Command Arguments Class

Define the argument parsing logic:

from mythic_container.MythicCommandBase import *
from mythic_container.MythicRPC import *

class MyCommandArguments(TaskArguments):
    def __init__(self, command_line, **kwargs):
        super().__init__(command_line, **kwargs)
        self.args = [
            CommandParameter(
                name="param1",
                type=ParameterType.String,  # or ParameterType.Number, etc.
                description="Description of parameter",
                parameter_group_info=[ParameterGroupInfo(
                    required=True,  # or False
                    ui_position=1
                )]
            ),
            # Add more parameters as needed
        ]

    async def parse_arguments(self):
        """Parse command-line arguments"""
        if len(self.command_line) == 0:
            raise ValueError("Parameter required")
        # Parse and validate arguments
        self.add_arg("param1", self.command_line)

    async def parse_dictionary(self, dictionary):
        """Parse dictionary arguments (from UI)"""
        self.load_args_from_dictionary(dictionary)

1.2 Command Class

Define the main command class:

class MyCommandCommand(CommandBase):
    cmd = "mycommand"  # Command name (lowercase, no spaces)
    needs_admin = False  # Set to True if command requires admin privileges
    help_cmd = "mycommand <param1>"
    description = "Detailed description of what the command does"
    version = 1
    author = "YourName"
    argument_class = MyCommandArguments
    attackmapping = ["T1059"]  # MITRE ATT&CK technique IDs
    
    attributes = CommandAttributes(
        supported_os=[SupportedOS.Windows],
        builtin=False,  # Set to True if command should always be included
        suggested_command=False  # Set to True to suggest in UI
    )

    async def opsec_pre(self, taskData: PTTaskMessageAllData) -> PTTaskOPSECPreTaskMessageResponse:
        """Optional: OPSEC check before task creation"""
        # Add warnings about detection risks
        return PTTaskOPSECPreTaskMessageResponse(
            TaskID=taskData.Task.ID,
            Success=True,
            OpsecPreBlocked=False,
            OpsecPreBypassRole="operator",
            OpsecPreMessage="OPSEC warning message here"
        )

    async def opsec_post(self, taskData: PTTaskMessageAllData) -> PTTaskOPSECPostTaskMessageResponse:
        """Optional: OPSEC check after task creation"""
        return PTTaskOPSECPostTaskMessageResponse(
            TaskID=taskData.Task.ID,
            Success=True,
            OpsecPostBlocked=False,
            OpsecPostBypassRole="operator",
            OpsecPostMessage="Post-execution warning"
        )

    async def create_go_tasking(self, taskData: PTTaskMessageAllData) -> PTTaskCreateTaskingMessageResponse:
        """Process task creation - called when task is created"""
        response = PTTaskCreateTaskingMessageResponse(
            TaskID=taskData.Task.ID,
            Success=True
        )
        # Add any pre-processing logic here
        # Access arguments: taskData.args.get_arg("param1")
        return response

    async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse:
        """Process agent response - called when agent sends response"""
        resp = PTTaskProcessResponseMessageResponse(
            TaskID=task.Task.ID,
            Success=True
        )
        # Process response data here if needed
        return resp

1.3 Key Attributes

  • cmd: Command name used in Mythic (must match translator mapping)
  • builtin:
    • True = Always included in payload (e.g., exit, sleep)
    • False = Can be selected during payload creation
  • needs_admin: Set to True if command requires administrator privileges
  • attackmapping: List of MITRE ATT&CK technique IDs (e.g., ["T1059"])

Step 2: Add Command to Translator

Edit Payload_Type/cazalla/translator/commands_from_c2.py and add your command to the commands dictionary:

commands = {
    # ... existing commands ...
    "mycommand": {"hex_code": 0x99, "input_type": "string"},
}

Important Notes:

  • hex_code: Choose an unused hex code (0x00-0xFF). Check comandos.h for existing codes
  • input_type:
    • "string" - String parameter
    • "int" - Integer parameter
    • None - No parameters
    • Special types: "bof_special", "assembly_special", "socks_special" (see existing commands)

Hex Code Allocation:

  • 0x00-0x1F: Reserved for core commands
  • 0x20-0x5F: File system and standard commands
  • 0x60-0x6F: Network tunneling commands
  • 0x70-0x7F: Code execution commands
  • 0x80+: Control commands

Step 3: Implement C Agent Handler

3.1 Define Command Constant

Edit Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.h and add:

#define MYCOMMAND_CMD  0x99  // Must match hex_code from translator

3.2 Implement Handler Function

Create or edit the appropriate C file (e.g., SistemadeFicheros.c for file operations, or create a new file):

#include "cazalla.h"
#include "comandos.h"
#include "paquete.h"

void MyCommandHandler(PAnalizador analizadorTarea) {
    _inf("===== PROCESSING MYCOMMAND_CMD (0x%02X) =====", MYCOMMAND_CMD);
    
    // Read task UUID (first 36 bytes)
    SIZE_T uuidLen = 36;
    PCHAR taskUuid = getString(analizadorTarea, &uuidLen);
    if (!taskUuid || uuidLen != 36) {
        _err("Error reading task UUID");
        if (analizadorTarea) liberarAnalizador(analizadorTarea);
        return;
    }
    
    // Read parameters based on input_type
    // For string input_type:
    SIZE_T param1Len = 0;
    PCHAR param1 = getString(analizadorTarea, &param1Len);
    
    // For int input_type:
    // UINT32 param1 = (UINT32)getInt32(analizadorTarea);
    
    // Implement your command logic here
    // ...
    
    // Create response packet
    PPaquete respuesta = nuevoPaquete(POST_RESPONSE, TRUE);  // TRUE adds agent UUID
    addString(respuesta, taskUuid, FALSE);  // Add task UUID
    
    // Add output to response
    PackageAddFormatPrintf(respuesta, FALSE, "Command executed successfully\n");
    PackageAddFormatPrintf(respuesta, FALSE, "Parameter: %.*s\n", (int)param1Len, param1);
    
    // Optional: Add artifacts (file writes, process creates, etc.)
    // addArtifact(respuesta, "File Write", "C:\\path\\to\\file");
    
    // Optional: Add credentials if discovered
    // addCredential(respuesta, "plaintext", "DOMAIN", "password", "username");
    
    // Send response
    Analizador* resp = mandarPaquete(respuesta);
    liberarPaquete(respuesta);
    if (resp) liberarAnalizador(resp);
    
    if (analizadorTarea) liberarAnalizador(analizadorTarea);
    _inf("===== MYCOMMAND_CMD COMPLETED =====");
}

3.3 Key C Functions

  • getString(analizador, &len): Read string from buffer
  • getInt32(analizador): Read 32-bit integer
  • getByte(analizador): Read single byte
  • getBytes(analizador, &len): Read byte array
  • nuevoPaquete(type, add_uuid): Create response packet
  • addString(paquete, str, copy): Add string to packet
  • PackageAddFormatPrintf(paquete, copy, format, ...): Add formatted output
  • addArtifact(paquete, type, data): Add artifact (file write, process create, etc.)
  • addCredential(paquete, type, realm, credential, account): Add credential
  • mandarPaquete(paquete): Send packet to Mythic
  • liberarPaquete(paquete): Free packet memory
  • liberarAnalizador(analizador): Free analyzer memory

Step 4: Register Handler in Command Dispatcher

Edit Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.c and add your command to the dispatch chain in handleGetTasking():

#ifdef ENABLE_MYCOMMAND_CMD
        else if (tarea == MYCOMMAND_CMD) {
            _inf("===== PROCESSING MYCOMMAND_CMD (0x%02X) =====", tarea);
            MyCommandHandler(analizadorTarea);
            _inf("===== MYCOMMAND_CMD COMPLETED =====");
        }
#endif

Placement:

  • Add after SLEEP_CMD (which always closes with })
  • Before the final else block for unknown commands
  • Use else if (not } else if) to maintain the chain

Important: The command handler must be wrapped in #ifdef ENABLE_MYCOMMAND_CMD to support conditional compilation.

Step 5: Add to Builder Mapping

Edit Payload_Type/cazalla/cazalla/agent_functions/builder.py and add your command to the command_to_define dictionary (around line 209):

command_to_define = {
    # ... existing commands ...
    "mycommand": "ENABLE_MYCOMMAND_CMD",
}

This maps the Python command name to the C preprocessor define name.

Step 6: Update Includes (if needed)

If your command requires a new header file:

In comandos.h:

#ifdef ENABLE_MYCOMMAND_CMD
#include "mycommand.h"
#endif

In comandos.c:

#ifdef ENABLE_MYCOMMAND_CMD
#include "mycommand.h"
#endif

Step 7: Add Forward Declaration (if needed)

If your handler function is defined in a separate file, add a forward declaration in comandos.c:

#ifdef ENABLE_MYCOMMAND_CMD
void MyCommandHandler(PAnalizador analizadorTarea);
#endif

🔍 Complete Example: Adding a "ping" Command

Let's walk through a complete example of adding a simple ping command that sends a message back to Mythic.

Example 1: Python Command File (ping.py)

from mythic_container.MythicCommandBase import *
from mythic_container.MythicRPC import *

class PingArguments(TaskArguments):
    def __init__(self, command_line, **kwargs):
        super().__init__(command_line, **kwargs)
        self.args = [
            CommandParameter(
                name="message",
                type=ParameterType.String,
                description="Message to echo back",
                parameter_group_info=[ParameterGroupInfo(required=False)]
            ),
        ]

    async def parse_arguments(self):
        if len(self.command_line) > 0:
            self.add_arg("message", self.command_line)
        else:
            self.add_arg("message", "pong")

    async def parse_dictionary(self, dictionary):
        self.load_args_from_dictionary(dictionary)

class PingCommand(CommandBase):
    cmd = "ping"
    needs_admin = False
    help_cmd = "ping [message]"
    description = "Echo a message back to verify agent communication"
    version = 1
    author = "YourName"
    argument_class = PingArguments
    attackmapping = []
    
    attributes = CommandAttributes(
        supported_os=[SupportedOS.Windows],
        builtin=False
    )

    async def create_go_tasking(self, taskData: PTTaskMessageAllData) -> PTTaskCreateTaskingMessageResponse:
        response = PTTaskCreateTaskingMessageResponse(
            TaskID=taskData.Task.ID,
            Success=True
        )
        return response

    async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse:
        resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True)
        return resp

Example 2: Translator Mapping

In translator/commands_from_c2.py:

commands = {
    # ... existing commands ...
    "ping": {"hex_code": 0x90, "input_type": "string"},
}

Example 3: C Header Definition

In comandos.h:

#define PING_CMD  0x90

Example 4: C Handler Implementation

Create ping.c:

#include "cazalla.h"
#include "comandos.h"
#include "paquete.h"

void PingHandler(PAnalizador analizadorTarea) {
    _inf("===== PROCESSING PING_CMD (0x%02X) =====", PING_CMD);
    
    // Read task UUID
    SIZE_T uuidLen = 36;
    PCHAR taskUuid = getString(analizadorTarea, &uuidLen);
    
    // Read message parameter
    SIZE_T messageLen = 0;
    PCHAR message = getString(analizadorTarea, &messageLen);
    
    // Create response
    PPaquete respuesta = nuevoPaquete(POST_RESPONSE, TRUE);
    addString(respuesta, taskUuid, FALSE);
    
    // Echo the message back
    PackageAddFormatPrintf(respuesta, FALSE, "Ping received: %.*s\n", (int)messageLen, message);
    PackageAddFormatPrintf(respuesta, FALSE, "Agent is alive and responding!\n");
    
    // Send response
    Analizador* resp = mandarPaquete(respuesta);
    liberarPaquete(respuesta);
    if (resp) liberarAnalizador(resp);
    
    if (analizadorTarea) liberarAnalizador(analizadorTarea);
    _inf("===== PING_CMD COMPLETED =====");
}

Example 5: Register in Dispatcher

In comandos.c, add to the dispatch chain:

#ifdef ENABLE_PING_CMD
        else if (tarea == PING_CMD) {
            _inf("===== PROCESSING PING_CMD (0x%02X) =====", tarea);
            PingHandler(analizadorTarea);
            _inf("===== PING_CMD COMPLETED =====");
        }
#endif

Example 6: Builder Mapping

In builder.py:

command_to_define = {
    # ... existing commands ...
    "ping": "ENABLE_PING_CMD",
}

🎯 Advanced Features

Adding Artifacts

Artifacts track system modifications for OPSEC and reporting:

// File write artifact
addArtifact(respuesta, "File Write", "C:\\path\\to\\file");

// Process create artifact
addArtifact(respuesta, "Process Create", "cmd.exe /c whoami");

// Network connection artifact
addArtifact(respuesta, "Network Connection", "Connection to 192.168.1.1:80");

// Registry write artifact
addArtifact(respuesta, "Registry Write", "HKEY_LOCAL_MACHINE\\...");

Adding Credentials

Automatically extract and report credentials:

// Plaintext password
addCredential(respuesta, "plaintext", "DOMAIN", "password123", "username");

// NTLM hash
addCredential(respuesta, "hash", "", "aad3b435b51404ee:hashvalue", "Administrator");

// Certificate
addCredential(respuesta, "certificate", "", "cert_data_base64", "account_name");

OPSEC Checking

Implement OPSEC warnings in Python:

async def opsec_pre(self, taskData: PTTaskMessageAllData) -> PTTaskOPSECPreTaskMessageResponse:
    message = "⚠️ OPSEC WARNING\n\n"
    message += "This command may be detected by:\n"
    message += "  • EDR/XDR solutions\n"
    message += "  • Network monitoring\n"
    
    return PTTaskOPSECPreTaskMessageResponse(
        TaskID=taskData.Task.ID,
        Success=True,
        OpsecPreBlocked=False,  # Set to True to block execution
        OpsecPreBypassRole="operator",
        OpsecPreMessage=message
    )

Process Browser Integration

To integrate with Mythic's Process Browser:

supported_ui_features = ["process_browser:list"]  # or "process_browser:kill"

Then in the translator, format process data as JSON matching Mythic's Process Browser format.

File Browser Integration

To integrate with Mythic's File Browser:

supported_ui_features = ["file_browser:list"]

The translator will automatically convert file listings to File Browser format.

🧪 Testing Your Command

1. Restart Mythic Agent Container

After adding your command, restart the Cazalla container:

cd ~/Mythic
./mythic-cli restart cazalla

2. Verify Command Appears

  1. Open Mythic UI
  2. Navigate to Payloads → Create Payload
  3. Select Cazalla
  4. Check that your command appears in the command selection list

3. Build Test Payload

  1. Select your command (and any others you need)
  2. Configure C2 profile
  3. Build the payload
  4. Check build logs for any compilation errors

4. Test Command Execution

  1. Deploy payload to test system
  2. Execute your command from Mythic UI
  3. Verify response is received correctly
  4. Check for any errors in agent logs

🐛 Common Issues and Solutions

Issue: Command Not Appearing in UI

Solution:

  • Verify Python file is in agent_functions/ directory
  • Check that class name ends with Command (e.g., PingCommand)
  • Restart Mythic agent container
  • Check Mythic logs for import errors

Issue: Compilation Errors

Solution:

  • Verify hex code is unique (check comandos.h)
  • Ensure #ifdef blocks are properly closed
  • Check that handler function is declared before use
  • Verify all includes are correct

Issue: Command Not Executing

Solution:

  • Verify command was selected during payload build
  • Check that hex code matches in translator and comandos.h
  • Verify handler is registered in comandos.c
  • Check agent logs for error messages

Issue: Response Not Received

Solution:

  • Verify response packet is created correctly
  • Check that task UUID is added to response
  • Ensure mandarPaquete() is called
  • Verify packet is properly formatted

📚 Reference: Command Hex Codes

Current hex code allocations in Cazalla:

  • 0x00: GET_TASKING
  • 0x01: POST_RESPONSE
  • 0x15: PROCESS_CMD (ps)
  • 0x20-0x28: File system commands (cd, ls, pwd, cp, mkdir, rm, cat, download, upload)
  • 0x29: SCREENSHOT_CMD
  • 0x30-0x31: Keylogging (start, stop)
  • 0x32-0x37: Token operations (list_tokens, steal_token, make_token, rev2self, whoami, kill)
  • 0x38: SLEEP_CMD
  • 0x40-0x42: Browser and SAM (browser_info, browser_dump, samdump)
  • 0x54: SHELL_CMD
  • 0x60-0x63: Network tunneling (socks, rpfwd)
  • 0x70-0x71: Code execution (inline_execute, inline_execute_assembly)
  • 0x80: EXIT_CMD
  • 0xF1: CHECKIN

Available ranges for new commands:

  • 0x43-0x53: Available
  • 0x55-0x5F: Available
  • 0x64-0x6F: Available
  • 0x72-0x7F: Available
  • 0x81-0xF0: Available (avoid 0xF1-F5, 0xF7-F8 which are used for markers)

Checklist

Use this checklist when adding a new command:

  • Python command file created in agent_functions/
  • Command class inherits from CommandBase
  • cmd attribute matches command name
  • builtin attribute set appropriately
  • Command added to translator commands dictionary
  • Hex code defined in comandos.h
  • C handler function implemented
  • Handler registered in comandos.c dispatch chain
  • Command added to builder.py command_to_define mapping
  • Handler wrapped in #ifdef ENABLE_XXX_CMD
  • Includes added if needed
  • Command tested in payload build
  • Command tested in execution
  • Documentation updated (optional)

Next Steps: After implementing your command, consider:

  • Adding OPSEC warnings for risky operations
  • Implementing artifact tracking for system modifications
  • Adding credential extraction if applicable
  • Writing unit tests for your command logic
  • Documenting your command in the commands reference