From 30c2d6390f91c2cf13fd1b25bbc739fe3d449f8e Mon Sep 17 00:00:00 2001 From: "marcos.luna" Date: Tue, 4 Nov 2025 13:02:01 +0100 Subject: [PATCH] Process Browser options implemented --- .../cazalla/agent_code/cazalla/comandos.c | 4 + .../cazalla/agent_code/cazalla/comandos.h | 1 + .../cazalla/agent_code/cazalla/tokens.c | 145 ++++++++++++ .../cazalla/agent_code/cazalla/tokens.h | 2 + .../cazalla/cazalla/agent_functions/kill.py | 115 ++++++++++ .../cazalla/agent_functions/list_tokens.py | 32 ++- .../cazalla/cazalla/agent_functions/ps.py | 110 +++++---- .../cazalla/agent_functions/steal_token.py | 47 +++- .../cazalla/translator/commands_from_c2.py | 1 + .../translator/commands_from_implant.py | 7 +- README.md | 212 +++++++++++++++++- 11 files changed, 616 insertions(+), 60 deletions(-) create mode 100644 Payload_Type/cazalla/cazalla/agent_functions/kill.py diff --git a/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.c b/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.c index 61dd435..700d3cd 100644 --- a/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.c +++ b/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.c @@ -171,6 +171,10 @@ BOOL handleGetTasking(PAnalizador obtenerTarea) { _inf("===== PROCESSING WHOAMI_CMD (0x%02X) =====", tarea); ObtenerUsuarioActual(analizadorTarea); _inf("===== WHOAMI_CMD COMPLETED ====="); + } else if (tarea == KILL_CMD) { + _inf("===== PROCESSING KILL_CMD (0x%02X) =====", tarea); + MatarProceso(analizadorTarea); + _inf("===== KILL_CMD COMPLETED ====="); } else { _wrn("Tarea desconocida: 0x%02x", tarea); _wrn("Tarea %u: Comando desconocido, liberando recursos", i+1); diff --git a/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.h b/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.h index 0208765..97d2284 100644 --- a/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.h +++ b/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.h @@ -53,6 +53,7 @@ #define MAKE_TOKEN_CMD 0x34 #define REV2SELF_CMD 0x35 #define WHOAMI_CMD 0x36 +#define KILL_CMD 0x37 /* Extension marker to carry SOCKS array in binary messages */ #define SOCKS_BLOCK_MARKER 0xF5 diff --git a/Payload_Type/cazalla/cazalla/agent_code/cazalla/tokens.c b/Payload_Type/cazalla/cazalla/agent_code/cazalla/tokens.c index 40794a0..6f50fe2 100644 --- a/Payload_Type/cazalla/cazalla/agent_code/cazalla/tokens.c +++ b/Payload_Type/cazalla/cazalla/agent_code/cazalla/tokens.c @@ -910,3 +910,148 @@ VOID ObtenerUsuarioActual(PAnalizador argumentos) { 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); +} + diff --git a/Payload_Type/cazalla/cazalla/agent_code/cazalla/tokens.h b/Payload_Type/cazalla/cazalla/agent_code/cazalla/tokens.h index eb1cf06..f478793 100644 --- a/Payload_Type/cazalla/cazalla/agent_code/cazalla/tokens.h +++ b/Payload_Type/cazalla/cazalla/agent_code/cazalla/tokens.h @@ -11,6 +11,7 @@ #define STEAL_TOKEN_CMD 0x33 #define MAKE_TOKEN_CMD 0x34 #define REV2SELF_CMD 0x35 +#define KILL_CMD 0x37 // Forward declarations VOID ListarTokens(PAnalizador argumentos); @@ -18,6 +19,7 @@ VOID RobarToken(PAnalizador argumentos); VOID CrearToken(PAnalizador argumentos); VOID RevertirToken(PAnalizador argumentos); VOID ObtenerUsuarioActual(PAnalizador argumentos); +VOID MatarProceso(PAnalizador argumentos); #endif diff --git a/Payload_Type/cazalla/cazalla/agent_functions/kill.py b/Payload_Type/cazalla/cazalla/agent_functions/kill.py new file mode 100644 index 0000000..3e64902 --- /dev/null +++ b/Payload_Type/cazalla/cazalla/agent_functions/kill.py @@ -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 " + 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 + diff --git a/Payload_Type/cazalla/cazalla/agent_functions/list_tokens.py b/Payload_Type/cazalla/cazalla/agent_functions/list_tokens.py index 4f0f330..36e82cf 100644 --- a/Payload_Type/cazalla/cazalla/agent_functions/list_tokens.py +++ b/Payload_Type/cazalla/cazalla/agent_functions/list_tokens.py @@ -5,7 +5,36 @@ from mythic_container.MythicRPC import * class ListTokensArguments(TaskArguments): def __init__(self, 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): pass @@ -21,6 +50,7 @@ class ListTokensCommand(CommandBase): description = "List all available tokens from running processes" version = 1 author = "@KaseyaOFSTeam" + supported_ui_features = ["process_browser:list_tokens"] argument_class = ListTokensArguments async def create_go_tasking(self, taskData: PTTaskMessageAllData) -> PTTaskCreateTaskingMessageResponse: diff --git a/Payload_Type/cazalla/cazalla/agent_functions/ps.py b/Payload_Type/cazalla/cazalla/agent_functions/ps.py index e48b8ec..67f2c17 100644 --- a/Payload_Type/cazalla/cazalla/agent_functions/ps.py +++ b/Payload_Type/cazalla/cazalla/agent_functions/ps.py @@ -1,50 +1,62 @@ -from mythic_container.MythicCommandBase import * -import json - - -class PsArguments(TaskArguments): - - def __init__(self, command_line, **kwargs): - super().__init__(command_line, **kwargs) - self.args = [] - - async def parse_arguments(self): - if len(self.command_line) > 0: - raise Exception("ps no debe lanzarse con parametros") - - -class PsCommand(CommandBase): - cmd = "ps" - needs_admin = False - help_cmd = "ps" - description = "Lista los procesos del host." - version = 1 - supported_ui_features = ["process_browser:list"] - author = "Kaseya OFSTeam" - argument_class = PsArguments - browser_script = BrowserScript( - script_name="ps", author="Kaseya OFSTeam", for_new_ui=True - ) - attributes = CommandAttributes( - builtin=False, - supported_os=[ SupportedOS.Windows ], - suggested_command=True - ) - attackmapping = [] - - 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: - """ - 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) - # Note: The translator (commands_from_implant.py) handles conversion - # from tab-separated format to Process Browser JSON format automatically +from mythic_container.MythicCommandBase import * +import json +import logging + + +class PsArguments(TaskArguments): + + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [] + + async def parse_arguments(self): + if len(self.command_line) > 0: + raise Exception("ps no debe lanzarse con parametros") + + +class PsCommand(CommandBase): + cmd = "ps" + needs_admin = False + help_cmd = "ps" + description = "Lista los procesos del host." + version = 1 + supported_ui_features = ["process_browser:list"] + author = "Kaseya OFSTeam" + argument_class = PsArguments + browser_script = BrowserScript( + script_name="ps", author="Kaseya OFSTeam", for_new_ui=True + ) + attributes = CommandAttributes( + builtin=False, + supported_os=[ SupportedOS.Windows ], + suggested_command=True + ) + attackmapping = [] + + 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: + """ + Process response from agent. The translator automatically converts + tab-separated process list to Process Browser JSON format. + We also ensure update_deleted=True is set for all processes to clear the cache. + """ + 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 \ No newline at end of file diff --git a/Payload_Type/cazalla/cazalla/agent_functions/steal_token.py b/Payload_Type/cazalla/cazalla/agent_functions/steal_token.py index 7dbd26c..b2937c8 100644 --- a/Payload_Type/cazalla/cazalla/agent_functions/steal_token.py +++ b/Payload_Type/cazalla/cazalla/agent_functions/steal_token.py @@ -11,22 +11,56 @@ class StealTokenArguments(TaskArguments): type=ParameterType.Number, description="Process ID to steal token from", parameter_group_info=[ParameterGroupInfo( - required=True, + 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: - raise ValueError("Must supply a PID") - try: - self.add_arg("pid", int(self.command_line)) - except ValueError: - raise ValueError("PID must be a number") + # 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 StealTokenCommand(CommandBase): @@ -37,6 +71,7 @@ class StealTokenCommand(CommandBase): version = 1 author = "@KaseyaOFSTeam" attackmapping = ["T1134.001"] + supported_ui_features = ["process_browser:steal_token"] argument_class = StealTokenArguments async def create_go_tasking(self, taskData: PTTaskMessageAllData) -> PTTaskCreateTaskingMessageResponse: diff --git a/Payload_Type/cazalla/translator/commands_from_c2.py b/Payload_Type/cazalla/translator/commands_from_c2.py index c39e939..1eef579 100644 --- a/Payload_Type/cazalla/translator/commands_from_c2.py +++ b/Payload_Type/cazalla/translator/commands_from_c2.py @@ -27,6 +27,7 @@ commands = { "make_token": {"hex_code": 0x34, "input_type": "string"}, "rev2self": {"hex_code": 0x35, "input_type": None}, "whoami": {"hex_code": 0x36, "input_type": None}, + "kill": {"hex_code": 0x37, "input_type": "string"}, # Added SOCKS control commands "start_socks": {"hex_code": 0x60, "input_type": "int"}, "stop_socks": {"hex_code": 0x61, "input_type": None}, diff --git a/Payload_Type/cazalla/translator/commands_from_implant.py b/Payload_Type/cazalla/translator/commands_from_implant.py index 93eef01..7715226 100644 --- a/Payload_Type/cazalla/translator/commands_from_implant.py +++ b/Payload_Type/cazalla/translator/commands_from_implant.py @@ -1135,8 +1135,13 @@ def postResponse(data): # Si detectamos una lista de procesos, también incluir el formato para Process Browser 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 - 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) # 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 diff --git a/README.md b/README.md index 91f28eb..d4a5ce0 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ - [File Uploads Support](#file-uploads-support) - [Keylogging Support](#%EF%B8%8F-keylogging-support) - [Token Support](#-token-support) +- [Context Tracking](#-context-tracking) - [Development](#development) - [Troubleshooting](#troubleshooting) - [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]` | | `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 ` | + +**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 @@ -473,6 +480,7 @@ Cazalla uses a custom binary protocol for efficient communication with Mythic. | `0x81` | Agent → Mythic | POST_RESPONSE (send task output) | | `0xF5` | Bidirectional | SOCKS data block | | `0xF2` | Bidirectional | RPFWD data block | +| `0xF0` | Agent → Mythic | Callback context data (cwd, impersonation_context, etc.) | ### Task Structure @@ -588,7 +596,7 @@ This enables: ## 📊 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 @@ -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 - **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 +- **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 @@ -604,13 +614,19 @@ Access the Process Browser in Mythic: 1. Navigate to a callback in the Mythic UI 2. Click the **PROCESSES** tab (next to the **CALLBACK** tab) 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 +#### `ps` Command + The `ps` command implements the `process_browser:list` supported UI feature: - Process data is automatically converted to Mythic's Process Browser JSON format - 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_id`: Process identifier (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 - `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). --- @@ -694,6 +726,8 @@ Currently supported: - `rm` (delete) - reports deleted file/directory path - **File Read**: Automatically reported for: - `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: - `screenshot` - GDI Screen Capture (GetDC, CreateCompatibleDC, CreateCompatibleBitmap, BitBlt, GetDIBits) - `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 ### Project Structure