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:
- Python Command Definition - Defines the command interface in Mythic
- Translator Mapping - Maps command names to hex codes for agent communication
- C Agent Implementation - Implements the actual command logic
- Command Dispatcher Registration - Registers the handler in the command dispatcher
- 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 toTrueif command requires administrator privilegesattackmapping: 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). Checkcomandos.hfor existing codesinput_type:"string"- String parameter"int"- Integer parameterNone- No parameters- Special types:
"bof_special","assembly_special","socks_special"(see existing commands)
Hex Code Allocation:
0x00-0x1F: Reserved for core commands0x20-0x5F: File system and standard commands0x60-0x6F: Network tunneling commands0x70-0x7F: Code execution commands0x80+: 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, ¶m1Len);
// 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 buffergetInt32(analizador): Read 32-bit integergetByte(analizador): Read single bytegetBytes(analizador, &len): Read byte arraynuevoPaquete(type, add_uuid): Create response packetaddString(paquete, str, copy): Add string to packetPackageAddFormatPrintf(paquete, copy, format, ...): Add formatted outputaddArtifact(paquete, type, data): Add artifact (file write, process create, etc.)addCredential(paquete, type, realm, credential, account): Add credentialmandarPaquete(paquete): Send packet to MythicliberarPaquete(paquete): Free packet memoryliberarAnalizador(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
elseblock 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
- Open Mythic UI
- Navigate to Payloads → Create Payload
- Select Cazalla
- Check that your command appears in the command selection list
3. Build Test Payload
- Select your command (and any others you need)
- Configure C2 profile
- Build the payload
- Check build logs for any compilation errors
4. Test Command Execution
- Deploy payload to test system
- Execute your command from Mythic UI
- Verify response is received correctly
- 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
#ifdefblocks 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_TASKING0x01: POST_RESPONSE0x15: PROCESS_CMD (ps)0x20-0x28: File system commands (cd, ls, pwd, cp, mkdir, rm, cat, download, upload)0x29: SCREENSHOT_CMD0x30-0x31: Keylogging (start, stop)0x32-0x37: Token operations (list_tokens, steal_token, make_token, rev2self, whoami, kill)0x38: SLEEP_CMD0x40-0x42: Browser and SAM (browser_info, browser_dump, samdump)0x54: SHELL_CMD0x60-0x63: Network tunneling (socks, rpfwd)0x70-0x71: Code execution (inline_execute, inline_execute_assembly)0x80: EXIT_CMD0xF1: CHECKIN
Available ranges for new commands:
0x43-0x53: Available0x55-0x5F: Available0x64-0x6F: Available0x72-0x7F: Available0x81-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 cmdattribute matches command namebuiltinattribute set appropriately- Command added to translator
commandsdictionary - Hex code defined in
comandos.h - C handler function implemented
- Handler registered in
comandos.cdispatch chain - Command added to
builder.pycommand_to_definemapping - Handler wrapped in
#ifdef ENABLE_XXX_CMD - Includes added if needed
- Command tested in payload build
- Command tested in execution
- Documentation updated (optional)
🔗 Related Documentation
- Commands Reference - Complete list of all commands
- Getting Started - Installation and setup
- Features Overview - Detailed feature explanations
- OPSEC Guide - Operational security considerations
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