Process Browser options implemented

This commit is contained in:
marcos.luna
2025-11-04 13:02:01 +01:00
parent 47978f4259
commit 30c2d6390f
11 changed files with 616 additions and 60 deletions
@@ -171,6 +171,10 @@ BOOL handleGetTasking(PAnalizador obtenerTarea) {
_inf("===== PROCESSING WHOAMI_CMD (0x%02X) =====", tarea); _inf("===== PROCESSING WHOAMI_CMD (0x%02X) =====", tarea);
ObtenerUsuarioActual(analizadorTarea); ObtenerUsuarioActual(analizadorTarea);
_inf("===== WHOAMI_CMD COMPLETED ====="); _inf("===== WHOAMI_CMD COMPLETED =====");
} else if (tarea == KILL_CMD) {
_inf("===== PROCESSING KILL_CMD (0x%02X) =====", tarea);
MatarProceso(analizadorTarea);
_inf("===== KILL_CMD COMPLETED =====");
} else { } else {
_wrn("Tarea desconocida: 0x%02x", tarea); _wrn("Tarea desconocida: 0x%02x", tarea);
_wrn("Tarea %u: Comando desconocido, liberando recursos", i+1); _wrn("Tarea %u: Comando desconocido, liberando recursos", i+1);
@@ -53,6 +53,7 @@
#define MAKE_TOKEN_CMD 0x34 #define MAKE_TOKEN_CMD 0x34
#define REV2SELF_CMD 0x35 #define REV2SELF_CMD 0x35
#define WHOAMI_CMD 0x36 #define WHOAMI_CMD 0x36
#define KILL_CMD 0x37
/* Extension marker to carry SOCKS array in binary messages */ /* Extension marker to carry SOCKS array in binary messages */
#define SOCKS_BLOCK_MARKER 0xF5 #define SOCKS_BLOCK_MARKER 0xF5
@@ -910,3 +910,148 @@ VOID ObtenerUsuarioActual(PAnalizador argumentos) {
liberarPaquete(respuesta); liberarPaquete(respuesta);
} }
/**
* @brief Kill a process by PID
* Terminates a process using TerminateProcess API
*/
VOID MatarProceso(PAnalizador argumentos) {
// For input_type=string commands, format is: [UUID:length_prefixed][nbArg:4][param1:length_prefixed]...
SIZE_T tamanoUuid = 36;
PCHAR tareaUuid = getString(argumentos, &tamanoUuid);
UINT32 nbArg = getInt32(argumentos);
if (nbArg < 1) {
PPaquete respuestaError = nuevoPaquete(POST_RESPONSE, TRUE);
addString(respuestaError, tareaUuid, FALSE);
PPaquete salidaError = nuevoPaquete(0, FALSE);
PackageAddFormatPrintf(salidaError, FALSE, "[kill] Error: Se requiere PID\n");
addBytes(respuestaError, (PBYTE)salidaError->buffer, salidaError->length, TRUE);
mandarPaquete(respuestaError);
liberarPaquete(salidaError);
liberarPaquete(respuestaError);
return;
}
// PID comes as a string parameter (input_type=string means all params are strings)
SIZE_T pid_len = 0;
PCHAR pid_str = getString(argumentos, &pid_len);
// Convert PID string to integer
UINT32 pid = 0;
if (pid_str && pid_len > 0) {
// Null-terminate the PID string for atoi
char pid_null_term[16] = {0};
SIZE_T copy_len = pid_len < sizeof(pid_null_term) - 1 ? pid_len : sizeof(pid_null_term) - 1;
memcpy(pid_null_term, pid_str, copy_len);
pid_null_term[copy_len] = '\0';
pid = (UINT32)atoi(pid_null_term);
}
if (pid == 0) {
PPaquete respuestaError = nuevoPaquete(POST_RESPONSE, TRUE);
addString(respuestaError, tareaUuid, FALSE);
PPaquete salidaError = nuevoPaquete(0, FALSE);
PackageAddFormatPrintf(salidaError, FALSE, "[kill] Error: PID inválido o es 0\n");
addBytes(respuestaError, (PBYTE)salidaError->buffer, salidaError->length, TRUE);
mandarPaquete(respuestaError);
liberarPaquete(salidaError);
liberarPaquete(respuestaError);
return;
}
// Don't allow killing critical system processes (PIDs 0, 4, 8)
if (pid <= 4 || pid == 8) {
PPaquete respuestaError = nuevoPaquete(POST_RESPONSE, TRUE);
addString(respuestaError, tareaUuid, FALSE);
PPaquete salidaError = nuevoPaquete(0, FALSE);
PackageAddFormatPrintf(salidaError, FALSE, "[kill] Error: No se puede matar procesos críticos del sistema (PID: %lu)\n", pid);
addBytes(respuestaError, (PBYTE)salidaError->buffer, salidaError->length, TRUE);
mandarPaquete(respuestaError);
liberarPaquete(salidaError);
liberarPaquete(respuestaError);
return;
}
// First, verify process exists using PROCESS_QUERY_INFORMATION
_inf("[kill] Intentando matar proceso PID %lu", pid);
HANDLE hCheck = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pid);
if (!hCheck) {
DWORD checkError = GetLastError();
_err("[kill] Proceso PID %lu no existe (verificación inicial falló, error: %lu)", pid, checkError);
PPaquete respuestaError = nuevoPaquete(POST_RESPONSE, TRUE);
addString(respuestaError, tareaUuid, FALSE);
PPaquete salidaError = nuevoPaquete(0, FALSE);
PackageAddFormatPrintf(salidaError, FALSE, "[kill] Error: El proceso PID %lu no existe o ya terminó (error: %lu)\n", pid, checkError);
addBytes(respuestaError, (PBYTE)salidaError->buffer, salidaError->length, TRUE);
mandarPaquete(respuestaError);
liberarPaquete(salidaError);
liberarPaquete(respuestaError);
return;
}
_inf("[kill] Proceso PID %lu existe, verificando acceso de terminación...", pid);
CloseHandle(hCheck);
// Open process with PROCESS_TERMINATE access
HANDLE hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, pid);
if (!hProcess) {
DWORD error = GetLastError();
_err("[kill] OpenProcess(PROCESS_TERMINATE) falló para PID %lu, error: %lu", pid, error);
PPaquete respuestaError = nuevoPaquete(POST_RESPONSE, TRUE);
addString(respuestaError, tareaUuid, FALSE);
PPaquete salidaError = nuevoPaquete(0, FALSE);
// Provide more specific error messages
if (error == ERROR_INVALID_PARAMETER || error == 87) {
PackageAddFormatPrintf(salidaError, FALSE, "[kill] Error: El proceso PID %lu no existe o ya terminó (error: %lu)\n", pid, error);
} else if (error == ERROR_ACCESS_DENIED || error == 5) {
PackageAddFormatPrintf(salidaError, FALSE, "[kill] Error: Acceso denegado al proceso PID %lu. Se requieren privilegios de administrador (error: %lu)\n", pid, error);
} else {
PackageAddFormatPrintf(salidaError, FALSE, "[kill] Error: No se pudo abrir el proceso PID %lu, error: %lu\n", pid, error);
}
addBytes(respuestaError, (PBYTE)salidaError->buffer, salidaError->length, TRUE);
mandarPaquete(respuestaError);
liberarPaquete(salidaError);
liberarPaquete(respuestaError);
return;
}
_inf("[kill] Proceso PID %lu abierto con éxito, terminando...", pid);
// Terminate the process
BOOL success = TerminateProcess(hProcess, 1); // Exit code 1
DWORD terminateError = GetLastError();
CloseHandle(hProcess);
if (!success) {
_err("[kill] TerminateProcess falló para PID %lu, error: %lu", pid, terminateError);
} else {
_inf("[kill] TerminateProcess ejecutado exitosamente para PID %lu", pid);
}
PPaquete respuesta = nuevoPaquete(POST_RESPONSE, TRUE);
addString(respuesta, tareaUuid, FALSE);
PPaquete salida = nuevoPaquete(0, FALSE);
if (success) {
PackageAddFormatPrintf(salida, FALSE, "[kill] Proceso PID %lu terminado exitosamente\n", pid);
} else {
DWORD error = GetLastError();
PackageAddFormatPrintf(salida, FALSE, "[kill] Error al terminar proceso PID %lu, error: %lu\n", pid, error);
}
// Add output first (parser expects UUID -> output -> artifacts)
addBytes(respuesta, (PBYTE)salida->buffer, salida->length, TRUE);
// Add artifact for process termination AFTER output
if (success) {
char artifact_msg[256] = {0};
snprintf(artifact_msg, sizeof(artifact_msg), "Process Termination: PID %lu", pid);
addArtifact(respuesta, "Process Termination", artifact_msg);
}
mandarPaquete(respuesta);
liberarPaquete(salida);
liberarPaquete(respuesta);
}
@@ -11,6 +11,7 @@
#define STEAL_TOKEN_CMD 0x33 #define STEAL_TOKEN_CMD 0x33
#define MAKE_TOKEN_CMD 0x34 #define MAKE_TOKEN_CMD 0x34
#define REV2SELF_CMD 0x35 #define REV2SELF_CMD 0x35
#define KILL_CMD 0x37
// Forward declarations // Forward declarations
VOID ListarTokens(PAnalizador argumentos); VOID ListarTokens(PAnalizador argumentos);
@@ -18,6 +19,7 @@ VOID RobarToken(PAnalizador argumentos);
VOID CrearToken(PAnalizador argumentos); VOID CrearToken(PAnalizador argumentos);
VOID RevertirToken(PAnalizador argumentos); VOID RevertirToken(PAnalizador argumentos);
VOID ObtenerUsuarioActual(PAnalizador argumentos); VOID ObtenerUsuarioActual(PAnalizador argumentos);
VOID MatarProceso(PAnalizador argumentos);
#endif #endif
@@ -0,0 +1,115 @@
from mythic_container.MythicCommandBase import *
from mythic_container.MythicRPC import *
class KillArguments(TaskArguments):
def __init__(self, command_line, **kwargs):
super().__init__(command_line, **kwargs)
self.args = [
CommandParameter(
name="pid",
type=ParameterType.Number,
description="Process ID to kill",
parameter_group_info=[ParameterGroupInfo(
required=False,
ui_position=1
)]
),
# Process Browser parameters (optional, passed automatically by Mythic)
CommandParameter(
name="host",
type=ParameterType.String,
description="Host name (from Process Browser)",
parameter_group_info=[ParameterGroupInfo(
required=False,
ui_position=2
)]
),
CommandParameter(
name="process_id",
type=ParameterType.Number,
description="Process ID (from Process Browser, takes precedence over pid)",
parameter_group_info=[ParameterGroupInfo(
required=False,
ui_position=3
)]
),
CommandParameter(
name="architecture",
type=ParameterType.String,
description="Process architecture (from Process Browser)",
parameter_group_info=[ParameterGroupInfo(
required=False,
ui_position=4
)]
),
]
async def parse_arguments(self):
if len(self.command_line) == 0:
# Check if process_id is provided (from Process Browser)
if not self.get_arg("process_id") and not self.get_arg("pid"):
raise ValueError("Must supply a PID")
else:
try:
self.add_arg("pid", int(self.command_line))
except ValueError:
raise ValueError("PID must be a number")
async def parse_dictionary(self, dictionary_arguments):
self.load_args_from_dictionary(dictionary_arguments)
# If process_id is provided (from Process Browser), use it instead of pid
if self.get_arg("process_id") and not self.get_arg("pid"):
self.add_arg("pid", self.get_arg("process_id"))
class KillCommand(CommandBase):
cmd = "kill"
needs_admin = False
help_cmd = "kill <pid>"
description = "Terminate a process by PID"
version = 1
author = "@KaseyaOFSTeam"
attackmapping = ["T1489"]
supported_ui_features = ["process_browser:kill"]
argument_class = KillArguments
async def create_go_tasking(self, taskData: PTTaskMessageAllData) -> PTTaskCreateTaskingMessageResponse:
response = PTTaskCreateTaskingMessageResponse(
TaskID=taskData.Task.ID,
Success=True,
)
pid = taskData.args.get_arg("pid")
if pid:
response.DisplayParams = f"Killing process PID: {pid}"
else:
response.DisplayParams = "Killing process (no PID specified)"
return response
async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse:
resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True)
# Check if kill was successful by checking user_output
if response:
user_output = response.get("user_output", "")
# Check if output indicates success (doesn't contain error indicators)
if user_output and not any(err in user_output.lower() for err in ["error", "no se pudo", "falló", "failed"]):
# Get the PID that was killed
pid = task.args.get_arg("pid") or task.args.get_arg("process_id")
if pid:
# Get hostname
host = task.Callback.Host if task.Callback else ""
# Add removed_processes array to response (similar to removed_files for file browser)
# This tells Mythic Process Browser to remove this process from the cache
resp.ProcessResponse = response if isinstance(response, dict) else {}
if "removed_processes" not in resp.ProcessResponse:
resp.ProcessResponse["removed_processes"] = []
resp.ProcessResponse["removed_processes"].append({
"host": host,
"process_id": int(pid)
})
logging.info(f"[PROCESS_BROWSER] Marked process PID {pid} as removed in Process Browser")
return resp
@@ -5,7 +5,36 @@ from mythic_container.MythicRPC import *
class ListTokensArguments(TaskArguments): class ListTokensArguments(TaskArguments):
def __init__(self, command_line, **kwargs): def __init__(self, command_line, **kwargs):
super().__init__(command_line, **kwargs) super().__init__(command_line, **kwargs)
self.args = [] self.args = [
# Process Browser parameters (optional, passed automatically by Mythic)
CommandParameter(
name="host",
type=ParameterType.String,
description="Host name (from Process Browser)",
parameter_group_info=[ParameterGroupInfo(
required=False,
ui_position=1
)]
),
CommandParameter(
name="process_id",
type=ParameterType.Number,
description="Process ID to list tokens from (from Process Browser)",
parameter_group_info=[ParameterGroupInfo(
required=False,
ui_position=2
)]
),
CommandParameter(
name="architecture",
type=ParameterType.String,
description="Process architecture (from Process Browser)",
parameter_group_info=[ParameterGroupInfo(
required=False,
ui_position=3
)]
),
]
async def parse_arguments(self): async def parse_arguments(self):
pass pass
@@ -21,6 +50,7 @@ class ListTokensCommand(CommandBase):
description = "List all available tokens from running processes" description = "List all available tokens from running processes"
version = 1 version = 1
author = "@KaseyaOFSTeam" author = "@KaseyaOFSTeam"
supported_ui_features = ["process_browser:list_tokens"]
argument_class = ListTokensArguments argument_class = ListTokensArguments
async def create_go_tasking(self, taskData: PTTaskMessageAllData) -> PTTaskCreateTaskingMessageResponse: async def create_go_tasking(self, taskData: PTTaskMessageAllData) -> PTTaskCreateTaskingMessageResponse:
@@ -1,50 +1,62 @@
from mythic_container.MythicCommandBase import * from mythic_container.MythicCommandBase import *
import json import json
import logging
class PsArguments(TaskArguments):
class PsArguments(TaskArguments):
def __init__(self, command_line, **kwargs):
super().__init__(command_line, **kwargs) def __init__(self, command_line, **kwargs):
self.args = [] super().__init__(command_line, **kwargs)
self.args = []
async def parse_arguments(self):
if len(self.command_line) > 0: async def parse_arguments(self):
raise Exception("ps no debe lanzarse con parametros") if len(self.command_line) > 0:
raise Exception("ps no debe lanzarse con parametros")
class PsCommand(CommandBase):
cmd = "ps" class PsCommand(CommandBase):
needs_admin = False cmd = "ps"
help_cmd = "ps" needs_admin = False
description = "Lista los procesos del host." help_cmd = "ps"
version = 1 description = "Lista los procesos del host."
supported_ui_features = ["process_browser:list"] version = 1
author = "Kaseya OFSTeam" supported_ui_features = ["process_browser:list"]
argument_class = PsArguments author = "Kaseya OFSTeam"
browser_script = BrowserScript( argument_class = PsArguments
script_name="ps", author="Kaseya OFSTeam", for_new_ui=True browser_script = BrowserScript(
) script_name="ps", author="Kaseya OFSTeam", for_new_ui=True
attributes = CommandAttributes( )
builtin=False, attributes = CommandAttributes(
supported_os=[ SupportedOS.Windows ], builtin=False,
suggested_command=True supported_os=[ SupportedOS.Windows ],
) suggested_command=True
attackmapping = [] )
attackmapping = []
async def create_go_tasking(self, taskData: PTTaskMessageAllData) -> PTTaskCreateTaskingMessageResponse:
response = PTTaskCreateTaskingMessageResponse( async def create_go_tasking(self, taskData: PTTaskMessageAllData) -> PTTaskCreateTaskingMessageResponse:
TaskID=taskData.Task.ID, response = PTTaskCreateTaskingMessageResponse(
Success=True, TaskID=taskData.Task.ID,
) Success=True,
return response )
return response
async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse:
""" async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse:
Process response from agent. The translator automatically converts """
tab-separated process list to Process Browser JSON format. Process response from agent. The translator automatically converts
""" tab-separated process list to Process Browser JSON format.
resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True) We also ensure update_deleted=True is set for all processes to clear the cache.
# Note: The translator (commands_from_implant.py) handles conversion """
# from tab-separated format to Process Browser JSON format automatically resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True)
# Merge with existing response if present
if response and isinstance(response, dict):
resp.ProcessResponse = response
# Ensure processes have update_deleted=True to clear cache
if "processes" in resp.ProcessResponse and isinstance(resp.ProcessResponse["processes"], list):
for process in resp.ProcessResponse["processes"]:
if isinstance(process, dict):
process["update_deleted"] = True
logging.info(f"[PROCESS_BROWSER] Set update_deleted=True for {len(resp.ProcessResponse['processes'])} processes in process_response")
return resp return resp
@@ -11,22 +11,56 @@ class StealTokenArguments(TaskArguments):
type=ParameterType.Number, type=ParameterType.Number,
description="Process ID to steal token from", description="Process ID to steal token from",
parameter_group_info=[ParameterGroupInfo( parameter_group_info=[ParameterGroupInfo(
required=True, required=False,
ui_position=1 ui_position=1
)] )]
), ),
# Process Browser parameters (optional, passed automatically by Mythic)
CommandParameter(
name="host",
type=ParameterType.String,
description="Host name (from Process Browser)",
parameter_group_info=[ParameterGroupInfo(
required=False,
ui_position=2
)]
),
CommandParameter(
name="process_id",
type=ParameterType.Number,
description="Process ID (from Process Browser, takes precedence over pid)",
parameter_group_info=[ParameterGroupInfo(
required=False,
ui_position=3
)]
),
CommandParameter(
name="architecture",
type=ParameterType.String,
description="Process architecture (from Process Browser)",
parameter_group_info=[ParameterGroupInfo(
required=False,
ui_position=4
)]
),
] ]
async def parse_arguments(self): async def parse_arguments(self):
if len(self.command_line) == 0: if len(self.command_line) == 0:
raise ValueError("Must supply a PID") # Check if process_id is provided (from Process Browser)
try: if not self.get_arg("process_id") and not self.get_arg("pid"):
self.add_arg("pid", int(self.command_line)) raise ValueError("Must supply a PID")
except ValueError: else:
raise ValueError("PID must be a number") try:
self.add_arg("pid", int(self.command_line))
except ValueError:
raise ValueError("PID must be a number")
async def parse_dictionary(self, dictionary_arguments): async def parse_dictionary(self, dictionary_arguments):
self.load_args_from_dictionary(dictionary_arguments) self.load_args_from_dictionary(dictionary_arguments)
# If process_id is provided (from Process Browser), use it instead of pid
if self.get_arg("process_id") and not self.get_arg("pid"):
self.add_arg("pid", self.get_arg("process_id"))
class StealTokenCommand(CommandBase): class StealTokenCommand(CommandBase):
@@ -37,6 +71,7 @@ class StealTokenCommand(CommandBase):
version = 1 version = 1
author = "@KaseyaOFSTeam" author = "@KaseyaOFSTeam"
attackmapping = ["T1134.001"] attackmapping = ["T1134.001"]
supported_ui_features = ["process_browser:steal_token"]
argument_class = StealTokenArguments argument_class = StealTokenArguments
async def create_go_tasking(self, taskData: PTTaskMessageAllData) -> PTTaskCreateTaskingMessageResponse: async def create_go_tasking(self, taskData: PTTaskMessageAllData) -> PTTaskCreateTaskingMessageResponse:
@@ -27,6 +27,7 @@ commands = {
"make_token": {"hex_code": 0x34, "input_type": "string"}, "make_token": {"hex_code": 0x34, "input_type": "string"},
"rev2self": {"hex_code": 0x35, "input_type": None}, "rev2self": {"hex_code": 0x35, "input_type": None},
"whoami": {"hex_code": 0x36, "input_type": None}, "whoami": {"hex_code": 0x36, "input_type": None},
"kill": {"hex_code": 0x37, "input_type": "string"},
# Added SOCKS control commands # Added SOCKS control commands
"start_socks": {"hex_code": 0x60, "input_type": "int"}, "start_socks": {"hex_code": 0x60, "input_type": "int"},
"stop_socks": {"hex_code": 0x61, "input_type": None}, "stop_socks": {"hex_code": 0x61, "input_type": None},
@@ -1135,8 +1135,13 @@ def postResponse(data):
# Si detectamos una lista de procesos, también incluir el formato para Process Browser # Si detectamos una lista de procesos, también incluir el formato para Process Browser
if is_process_list and len(processes) > 0: if is_process_list and len(processes) > 0:
# Mark ALL processes with update_deleted=True to tell Mythic this is the complete list
# According to Mythic docs: "setting this to true tells Mythic to mark any process not returned in this process array as deleted"
# This effectively clears the cache by removing processes that no longer exist
for process in processes:
process["update_deleted"] = True
jsonTask["processes"] = processes jsonTask["processes"] = processes
print(f"[PROCESS_BROWSER] Respuesta convertida al formato Process Browser con {len(processes)} procesos") print(f"[PROCESS_BROWSER] Respuesta convertida al formato Process Browser con {len(processes)} procesos (update_deleted=True en TODOS los procesos para limpiar caché)")
# Detectar y convertir formato File Browser (formato: path\nD/F\tSIZE\tDATE\tNAME) # Detectar y convertir formato File Browser (formato: path\nD/F\tSIZE\tDATE\tNAME)
# El formato del agente es: primera línea es el path, luego líneas con D/F\tSIZE\tMM/DD/YYYY HH:MM:SS\tNAME # El formato del agente es: primera línea es el path, luego líneas con D/F\tSIZE\tMM/DD/YYYY HH:MM:SS\tNAME
+209 -3
View File
@@ -30,6 +30,7 @@
- [File Uploads Support](#file-uploads-support) - [File Uploads Support](#file-uploads-support)
- [Keylogging Support](#%EF%B8%8F-keylogging-support) - [Keylogging Support](#%EF%B8%8F-keylogging-support)
- [Token Support](#-token-support) - [Token Support](#-token-support)
- [Context Tracking](#-context-tracking)
- [Development](#development) - [Development](#development)
- [Troubleshooting](#troubleshooting) - [Troubleshooting](#troubleshooting)
- [Credits](#credits) - [Credits](#credits)
@@ -216,7 +217,13 @@ For more information, see the [Mythic documentation on customizing public agents
| `make_token` | Create a new token using credentials and impersonate it | `make_token domain username password [logon_type]` | | `make_token` | Create a new token using credentials and impersonate it | `make_token domain username password [logon_type]` |
| `rev2self` | Revert token impersonation back to original token | `rev2self` | | `rev2self` | Revert token impersonation back to original token | `rev2self` |
**Note**: The `ps` command integrates with Mythic's Process Browser, allowing unified process management across multiple callbacks. Process lists are automatically synchronized and viewable from the Process Browser UI. ### Process Management
| Command | Description | Example |
|---------|-------------|---------|
| `kill` | Terminate a process by PID (Process Browser supported) | `kill <pid>` |
**Note**: The `ps` command integrates with Mythic's Process Browser, allowing unified process management across multiple callbacks. Process lists are automatically synchronized and viewable from the Process Browser UI. The `kill` command also supports Process Browser integration, allowing you to terminate processes directly from the Process Browser UI.
### Network Operations ### Network Operations
@@ -473,6 +480,7 @@ Cazalla uses a custom binary protocol for efficient communication with Mythic.
| `0x81` | Agent → Mythic | POST_RESPONSE (send task output) | | `0x81` | Agent → Mythic | POST_RESPONSE (send task output) |
| `0xF5` | Bidirectional | SOCKS data block | | `0xF5` | Bidirectional | SOCKS data block |
| `0xF2` | Bidirectional | RPFWD data block | | `0xF2` | Bidirectional | RPFWD data block |
| `0xF0` | Agent → Mythic | Callback context data (cwd, impersonation_context, etc.) |
### Task Structure ### Task Structure
@@ -588,7 +596,7 @@ This enables:
## 📊 Process Browser Integration ## 📊 Process Browser Integration
Cazalla's `ps` command fully integrates with Mythic's Process Browser feature, providing unified process management across multiple callbacks on the same host. Cazalla fully integrates with Mythic's Process Browser feature, providing unified process management across multiple callbacks on the same host. Both `ps` and `kill` commands support Process Browser integration.
### Features ### Features
@@ -596,6 +604,8 @@ Cazalla's `ps` command fully integrates with Mythic's Process Browser feature, p
- **Process Hierarchy**: View process parent-child relationships when parent process IDs are available - **Process Hierarchy**: View process parent-child relationships when parent process IDs are available
- **Rich Process Data**: Includes process name, PID, PPID, architecture, user account, and session ID - **Rich Process Data**: Includes process name, PID, PPID, architecture, user account, and session ID
- **Automatic Synchronization**: Process lists are automatically synchronized with Mythic's Process Browser UI - **Automatic Synchronization**: Process lists are automatically synchronized with Mythic's Process Browser UI
- **Process Actions**: Kill processes directly from the Process Browser UI
- **Cache Management**: Process lists automatically clear stale entries using `update_deleted=True` flag
### Process Browser UI ### Process Browser UI
@@ -604,13 +614,19 @@ Access the Process Browser in Mythic:
1. Navigate to a callback in the Mythic UI 1. Navigate to a callback in the Mythic UI
2. Click the **PROCESSES** tab (next to the **CALLBACK** tab) 2. Click the **PROCESSES** tab (next to the **CALLBACK** tab)
3. View all processes from all callbacks on that host 3. View all processes from all callbacks on that host
4. Use the UI buttons for additional actions (inject, kill, etc.) if implemented 4. Use the UI buttons for process actions:
- **Kill**: Terminate a process directly from the Process Browser (uses `kill` command)
- **Steal Token**: Steal token from a process (uses `steal_token` command)
- **List Tokens**: List tokens from a process (uses `list_tokens` command)
### Implementation Details ### Implementation Details
#### `ps` Command
The `ps` command implements the `process_browser:list` supported UI feature: The `ps` command implements the `process_browser:list` supported UI feature:
- Process data is automatically converted to Mythic's Process Browser JSON format - Process data is automatically converted to Mythic's Process Browser JSON format
- The translator detects process list responses and formats them accordingly - The translator detects process list responses and formats them accordingly
- All processes are marked with `update_deleted=True` to clear stale entries from cache
- Process information includes: - Process information includes:
- `process_id`: Process identifier (required) - `process_id`: Process identifier (required)
- `name`: Process name (required) - `name`: Process name (required)
@@ -619,6 +635,22 @@ The `ps` command implements the `process_browser:list` supported UI feature:
- `user`: User account running the process - `user`: User account running the process
- `session_id`: Windows session ID - `session_id`: Windows session ID
#### `kill` Command
The `kill` command implements the `process_browser:kill` supported UI feature:
- Accepts `pid` or `process_id` parameter (from Process Browser)
- Automatically verifies process existence before termination
- Protects critical system processes (PIDs 0, 4, 8)
- Reports "Process Termination" artifact with PID
- Provides detailed error messages for different failure scenarios
#### Process Browser Parameters
When using Process Browser UI actions, the following parameters are automatically passed:
- `host`: Host name (from Process Browser)
- `process_id`: Process ID (from Process Browser, takes precedence over `pid`)
- `architecture`: Process architecture (from Process Browser)
For more information, see the [Mythic Process Browser documentation](https://docs.mythic-c2.net/customizing/hooking-features/process_list). For more information, see the [Mythic Process Browser documentation](https://docs.mythic-c2.net/customizing/hooking-features/process_list).
--- ---
@@ -694,6 +726,8 @@ Currently supported:
- `rm` (delete) - reports deleted file/directory path - `rm` (delete) - reports deleted file/directory path
- **File Read**: Automatically reported for: - **File Read**: Automatically reported for:
- `download` - reports file path when downloading files - `download` - reports file path when downloading files
- **Process Termination**: Automatically reported for:
- `kill` - reports terminated process PID (e.g., "Process Termination: PID 1234")
- **API Call**: Reported for: - **API Call**: Reported for:
- `screenshot` - GDI Screen Capture (GetDC, CreateCompatibleDC, CreateCompatibleBitmap, BitBlt, GetDIBits) - `screenshot` - GDI Screen Capture (GetDC, CreateCompatibleDC, CreateCompatibleBitmap, BitBlt, GetDIBits)
- `keylog_start` - Keyboard Hook (GetAsyncKeyState, GetForegroundWindow, GetWindowTextA, GetUserNameA) - `keylog_start` - Keyboard Hook (GetAsyncKeyState, GetForegroundWindow, GetWindowTextA, GetUserNameA)
@@ -1497,6 +1531,178 @@ For more information, see the [Mythic Tokens documentation](https://docs.mythic-
--- ---
## 📊 Context Tracking
Cazalla supports **Context Tracking** to display dynamic callback context information (like current working directory and impersonation context) as tabs in the Mythic UI. This feature enhances the operator experience by providing immediate visibility into the agent's current state.
### Overview
Context Tracking allows Mythic to display callback-specific information as dynamic tabs above the tasking area. These tabs update automatically as the agent's context changes, providing real-time visibility into:
- **Current Working Directory (`cwd`)**: The agent's current directory
- **Impersonation Context (`impersonation_context`)**: The currently impersonated user (if any)
### How It Works
1. **Initial Check-in**: The agent automatically includes `cwd` and `impersonation_context` in the initial check-in message
2. **Dynamic Updates**: Commands that change the context (like `cd`, `steal_token`, `rev2self`) automatically update and report the new context
3. **UI Display**: Mythic displays these fields as dynamic tabs above the tasking area
### Supported Context Fields
#### Current Working Directory (`cwd`)
- **Updated by**: `cd` command
- **Initial value**: Automatically set during check-in with the process's current directory
- **Display**: Shows the current directory path (e.g., `C:\Users\localuser\Downloads`)
#### Impersonation Context (`impersonation_context`)
- **Updated by**: `steal_token`, `make_token`, `rev2self`
- **Initial value**: Automatically set during check-in with the process's current user (format: `DOMAIN\username`)
- **Display**: Shows the currently impersonated user (e.g., `NT AUTHORITY\SYSTEM`)
### Commands That Update Context
#### `cd` Command
```bash
# Change directory - automatically updates cwd context
cd {"path":"C:\\Windows"}
```
After executing `cd`, the `cwd` tab in Mythic UI will update to show the new directory.
#### `steal_token` Command
```bash
# Steal token from a process - automatically updates impersonation_context
steal_token 1060
```
After stealing a token, the `impersonation_context` tab will show the impersonated user (e.g., `NT AUTHORITY\SYSTEM`).
#### `make_token` Command
```bash
# Create token with credentials - automatically updates impersonation_context
make_token DOMAIN username password
```
After creating a token, the `impersonation_context` tab will show the new user context.
#### `rev2self` Command
```bash
# Revert to original token - clears impersonation_context
rev2self
```
After reverting, the `impersonation_context` tab will be cleared (or show the original user).
### Technical Implementation
#### Binary Protocol
Context updates are sent using a special marker in task responses:
```
[0xF0 marker][field_name_len:4][field_name][field_value_len:4][field_value]...
```
- **Marker**: `0xF0` indicates callback context data
- **Field Name**: Name of the context field (e.g., `cwd`, `impersonation_context`)
- **Field Value**: The actual value to update
Multiple fields can be sent in a single response by repeating the pattern.
#### Check-in Format
During initial check-in, context fields are included directly in the check-in message and added to the root JSON:
```json
{
"action": "checkin",
"uuid": "...",
"host": "...",
"cwd": "C:\\Users\\localuser",
"impersonation_context": "CETP-WIN11-01\\localuser",
...
}
```
#### Task Response Format
In task responses, context updates are included in the `callback` key:
```json
{
"action": "post_response",
"responses": [{
"task_id": "...",
"completed": true,
"callback": {
"cwd": "C:\\Windows",
"impersonation_context": "NT AUTHORITY\\SYSTEM"
},
"user_output": "..."
}]
}
```
### UI Configuration
Context tabs are automatically displayed in the Mythic UI when context data is available. You can configure their appearance in Mythic settings:
1. Navigate to **Settings****Tasking Context Tabs**
2. Select which fields to display (`cwd`, `impersonation_context`)
3. Configure colors and styling if desired
### Example Usage
```bash
# 1. Agent checks in - tabs appear showing initial context
# Tab: "Dir: C:\Users\localuser\Downloads"
# Tab: "User: CETP-WIN11-01\localuser"
# 2. Change directory
cd {"path":"C:\\Windows"}
# Tab updates: "Dir: C:\Windows"
# 3. Steal SYSTEM token
steal_token 1060
# Tab updates: "User: NT AUTHORITY\SYSTEM"
# 4. Revert token
rev2self
# Tab updates: "User: CETP-WIN11-01\localuser"
```
### Debugging
To verify context tracking is working:
1. **Check agent logs** for messages like:
```
Checkin: Añadido cwd inicial: C:\Users\localuser
Checkin: Añadido impersonation_context inicial: CETP-WIN11-01\localuser
[steal_token] Contexto de impersonación actualizado: NT AUTHORITY\SYSTEM
```
2. **Check translator logs** for messages like:
```
[CHECKIN] Callback context: cwd = 'C:\Users\localuser'
[CHECKIN] Callback context: impersonation_context = 'CETP-WIN11-01\localuser'
[CALLBACK_CONTEXT] cwd = 'C:\Windows'
[CALLBACK_CONTEXT] impersonation_context = 'NT AUTHORITY\SYSTEM'
```
3. **Verify in Mythic UI**: Context tabs should appear above the tasking area and update automatically.
For more information, see the [Mythic Context Tracking documentation](https://docs.mythic-c2.net/customizing/hooking-features/context-tracking).
---
## 🔧 Development ## 🔧 Development
### Project Structure ### Project Structure