file upload completed
This commit is contained in:
@@ -0,0 +1,104 @@
|
|||||||
|
from mythic_container.MythicCommandBase import *
|
||||||
|
from mythic_container.MythicRPC import *
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
class UploadArguments(TaskArguments):
|
||||||
|
def __init__(self, command_line, **kwargs):
|
||||||
|
super().__init__(command_line, **kwargs)
|
||||||
|
self.args = [
|
||||||
|
CommandParameter(
|
||||||
|
name="file",
|
||||||
|
type=ParameterType.File,
|
||||||
|
description="File to upload to the target",
|
||||||
|
),
|
||||||
|
CommandParameter(
|
||||||
|
name="path",
|
||||||
|
type=ParameterType.String,
|
||||||
|
description="Path where the file should be saved on the target",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
async def parse_arguments(self):
|
||||||
|
if len(self.command_line) == 0:
|
||||||
|
raise ValueError("Must supply a file and path")
|
||||||
|
|
||||||
|
parts = self.command_line.split(" ", 1)
|
||||||
|
if len(parts) != 2:
|
||||||
|
raise ValueError("Must supply both file and path: upload <file_id> <path>")
|
||||||
|
|
||||||
|
self.add_arg("file", parts[0])
|
||||||
|
self.add_arg("path", parts[1])
|
||||||
|
|
||||||
|
async def parse_dictionary(self, dictionary_arguments):
|
||||||
|
if "file" in dictionary_arguments:
|
||||||
|
self.add_arg("file", dictionary_arguments["file"])
|
||||||
|
if "path" in dictionary_arguments:
|
||||||
|
# Normalize path: replace double backslashes with single backslash
|
||||||
|
path = dictionary_arguments["path"]
|
||||||
|
if path:
|
||||||
|
path = path.replace("\\\\", "\\")
|
||||||
|
self.add_arg("path", path)
|
||||||
|
|
||||||
|
|
||||||
|
class UploadCommand(CommandBase):
|
||||||
|
cmd = "upload"
|
||||||
|
needs_admin = False
|
||||||
|
help_cmd = "upload <file> <path>"
|
||||||
|
description = "Upload a file from the Mythic server to the target"
|
||||||
|
version = 1
|
||||||
|
author = "@KaseyaOFSTeam"
|
||||||
|
argument_class = UploadArguments
|
||||||
|
|
||||||
|
async def create_go_tasking(self, taskData: PTTaskMessageAllData) -> PTTaskCreateTaskingMessageResponse:
|
||||||
|
response = PTTaskCreateTaskingMessageResponse(
|
||||||
|
TaskID=taskData.Task.ID,
|
||||||
|
Success=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
file_id = taskData.args.get_arg("file")
|
||||||
|
path = taskData.args.get_arg("path")
|
||||||
|
|
||||||
|
# Get file information to extract filename
|
||||||
|
try:
|
||||||
|
file_search = await SendMythicRPCFileSearch(MythicRPCFileSearchMessage(
|
||||||
|
AgentFileId=file_id,
|
||||||
|
LimitByCallback=False,
|
||||||
|
TaskID=taskData.Task.ID
|
||||||
|
))
|
||||||
|
|
||||||
|
if file_search.Success and len(file_search.Files) > 0:
|
||||||
|
file_info = file_search.Files[0]
|
||||||
|
filename = file_info.Filename
|
||||||
|
|
||||||
|
# If path is empty, use current directory
|
||||||
|
if not path or path.strip() == "":
|
||||||
|
path = ".\\" + filename
|
||||||
|
# If path ends with backslash or is a directory, append filename
|
||||||
|
elif path.endswith("\\") or path.endswith("/"):
|
||||||
|
# Path is a directory, append filename
|
||||||
|
path = path.rstrip("\\/") + "\\" + filename
|
||||||
|
# Check if path is just a directory (no filename extension and ends with no backslash)
|
||||||
|
elif not os.path.basename(path) or (not path.endswith("\\") and os.path.isabs(path) and not os.path.splitext(os.path.basename(path))[1]):
|
||||||
|
# Try to check if it's a directory by seeing if basename has no extension
|
||||||
|
# This is a heuristic - if path has no extension and looks like a directory, append filename
|
||||||
|
if not os.path.splitext(os.path.basename(path))[1]:
|
||||||
|
# No extension, might be a directory
|
||||||
|
path = path.rstrip("\\/") + "\\" + filename
|
||||||
|
|
||||||
|
response.DisplayParams = f"file_id: {file_id}, path: {path}, filename: {filename}"
|
||||||
|
# Update the path argument with the final path (including filename if needed)
|
||||||
|
taskData.args.add_arg("path", path)
|
||||||
|
else:
|
||||||
|
# If we can't get file info, just use the path as-is
|
||||||
|
response.DisplayParams = f"file_id: {file_id}, path: {path}"
|
||||||
|
except Exception as e:
|
||||||
|
# If we can't get file info, just use the path as-is
|
||||||
|
response.DisplayParams = f"file_id: {file_id}, path: {path} (warning: could not get filename: {str(e)})"
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse:
|
||||||
|
resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True)
|
||||||
|
return resp
|
||||||
|
|
||||||
@@ -1202,3 +1202,270 @@ VOID DescargarFichero(PAnalizador argumentos) {
|
|||||||
end:
|
end:
|
||||||
LocalFree(rutaFichero);
|
LocalFree(rutaFichero);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper structure to store upload response data
|
||||||
|
typedef struct {
|
||||||
|
BOOL success;
|
||||||
|
UINT32 total_chunks;
|
||||||
|
UINT32 chunk_num;
|
||||||
|
PCHAR chunk_data_b64; // Base64 encoded chunk data
|
||||||
|
SIZE_T chunk_data_len;
|
||||||
|
} UploadResponseData;
|
||||||
|
|
||||||
|
// Helper function to extract upload response data from Mythic response
|
||||||
|
// Format: Binary response with upload response marker 0xF9 followed by [total_chunks:4][chunk_num:4][chunk_data_len:4][chunk_data_base64]
|
||||||
|
static BOOL extractUploadResponse(PAnalizador respuesta, UploadResponseData* out) {
|
||||||
|
if (!respuesta || !out || respuesta->tamano < 13) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for upload response marker 0xF9
|
||||||
|
PBYTE buffer = respuesta->buffer;
|
||||||
|
SIZE_T len = respuesta->tamano;
|
||||||
|
|
||||||
|
for (SIZE_T i = 0; i < len - 13; i++) { // At least: marker(1) + total_chunks(4) + chunk_num(4) + data_len(4)
|
||||||
|
if (buffer[i] == 0xF9) {
|
||||||
|
// Found marker, extract data
|
||||||
|
i++; // Skip marker
|
||||||
|
|
||||||
|
// Extract total_chunks
|
||||||
|
if (i + 4 > len) break;
|
||||||
|
out->total_chunks = (buffer[i] << 24) | (buffer[i+1] << 16) | (buffer[i+2] << 8) | buffer[i+3];
|
||||||
|
i += 4;
|
||||||
|
|
||||||
|
// Extract chunk_num
|
||||||
|
if (i + 4 > len) break;
|
||||||
|
out->chunk_num = (buffer[i] << 24) | (buffer[i+1] << 16) | (buffer[i+2] << 8) | buffer[i+3];
|
||||||
|
i += 4;
|
||||||
|
|
||||||
|
// Extract chunk_data length
|
||||||
|
if (i + 4 > len) break;
|
||||||
|
UINT32 chunk_data_len = (buffer[i] << 24) | (buffer[i+1] << 16) | (buffer[i+2] << 8) | buffer[i+3];
|
||||||
|
i += 4;
|
||||||
|
|
||||||
|
if (chunk_data_len == 0 || chunk_data_len > 1024 * 1024 || i + chunk_data_len > len) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract chunk_data (base64 string)
|
||||||
|
out->chunk_data_b64 = (PCHAR)LocalAlloc(LPTR, chunk_data_len + 1);
|
||||||
|
if (!out->chunk_data_b64) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy(out->chunk_data_b64, buffer + i, chunk_data_len);
|
||||||
|
out->chunk_data_b64[chunk_data_len] = '\0';
|
||||||
|
out->chunk_data_len = chunk_data_len;
|
||||||
|
out->success = TRUE;
|
||||||
|
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
VOID SubirFichero(PAnalizador argumentos) {
|
||||||
|
SIZE_T tamanoUuid = 36;
|
||||||
|
PCHAR tareaUuid = getString(argumentos, &tamanoUuid);
|
||||||
|
UINT32 nbArg = getInt32(argumentos);
|
||||||
|
|
||||||
|
_dbg("[upload] tenemos %d argumentos", nbArg);
|
||||||
|
if (nbArg < 2) {
|
||||||
|
PPaquete respuestaError = nuevoPaquete(POST_RESPONSE, TRUE);
|
||||||
|
addString(respuestaError, tareaUuid, FALSE);
|
||||||
|
PPaquete salidaError = nuevoPaquete(0, FALSE);
|
||||||
|
PackageAddFormatPrintf(salidaError, FALSE, "[upload] Error: Se requiere file_id y path\n");
|
||||||
|
addBytes(respuestaError, (PBYTE)salidaError->buffer, salidaError->length, TRUE);
|
||||||
|
mandarPaquete(respuestaError);
|
||||||
|
liberarPaquete(salidaError);
|
||||||
|
liberarPaquete(respuestaError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse arguments: file_id and path
|
||||||
|
SIZE_T tamano = 0;
|
||||||
|
PCHAR file_id = getString(argumentos, &tamano);
|
||||||
|
SIZE_T tamano2 = 0;
|
||||||
|
PCHAR rutaDestino = getString(argumentos, &tamano2);
|
||||||
|
|
||||||
|
_dbg("[upload] File ID: \"%s\", Ruta destino: \"%s\"", file_id, rutaDestino);
|
||||||
|
|
||||||
|
// Normalizar path y verificar que no termine en backslash
|
||||||
|
char fullPath[2048] = {0};
|
||||||
|
if (GetFullPathNameA(rutaDestino, sizeof(fullPath), fullPath, NULL) == 0) {
|
||||||
|
strncpy(fullPath, rutaDestino, sizeof(fullPath) - 1);
|
||||||
|
fullPath[sizeof(fullPath) - 1] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar si el path es un directorio (termina en \ o es un directorio existente)
|
||||||
|
SIZE_T pathLen = strlen(fullPath);
|
||||||
|
if (pathLen > 0 && (fullPath[pathLen - 1] == '\\' || fullPath[pathLen - 1] == '/')) {
|
||||||
|
// Path termina en backslash, es un directorio sin nombre de archivo
|
||||||
|
PPaquete respuestaError = nuevoPaquete(POST_RESPONSE, TRUE);
|
||||||
|
addString(respuestaError, tareaUuid, FALSE);
|
||||||
|
PPaquete salidaError = nuevoPaquete(0, FALSE);
|
||||||
|
PackageAddFormatPrintf(salidaError, FALSE, "[upload] Error: El path es un directorio. Especifique un nombre de archivo: %s\n", fullPath);
|
||||||
|
addBytes(respuestaError, (PBYTE)salidaError->buffer, salidaError->length, TRUE);
|
||||||
|
mandarPaquete(respuestaError);
|
||||||
|
liberarPaquete(salidaError);
|
||||||
|
liberarPaquete(respuestaError);
|
||||||
|
goto end;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar si el path existe y es un directorio
|
||||||
|
DWORD fileAttrib = GetFileAttributesA(fullPath);
|
||||||
|
if (fileAttrib != INVALID_FILE_ATTRIBUTES && (fileAttrib & FILE_ATTRIBUTE_DIRECTORY)) {
|
||||||
|
// El path existe y es un directorio
|
||||||
|
PPaquete respuestaError = nuevoPaquete(POST_RESPONSE, TRUE);
|
||||||
|
addString(respuestaError, tareaUuid, FALSE);
|
||||||
|
PPaquete salidaError = nuevoPaquete(0, FALSE);
|
||||||
|
PackageAddFormatPrintf(salidaError, FALSE, "[upload] Error: El path es un directorio existente. Especifique un nombre de archivo: %s\n", fullPath);
|
||||||
|
addBytes(respuestaError, (PBYTE)salidaError->buffer, salidaError->length, TRUE);
|
||||||
|
mandarPaquete(respuestaError);
|
||||||
|
liberarPaquete(salidaError);
|
||||||
|
liberarPaquete(respuestaError);
|
||||||
|
goto end;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crear archivo de destino
|
||||||
|
HANDLE hFile = CreateFileA(
|
||||||
|
fullPath,
|
||||||
|
GENERIC_WRITE,
|
||||||
|
0,
|
||||||
|
NULL,
|
||||||
|
CREATE_ALWAYS,
|
||||||
|
FILE_ATTRIBUTE_NORMAL,
|
||||||
|
NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hFile == INVALID_HANDLE_VALUE) {
|
||||||
|
DWORD error = GetLastError();
|
||||||
|
_err("[upload] No se pudo crear 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, "[upload] Error: No se pudo crear el archivo. Código: %lu\n", error);
|
||||||
|
addBytes(respuestaError, (PBYTE)salidaError->buffer, salidaError->length, TRUE);
|
||||||
|
mandarPaquete(respuestaError);
|
||||||
|
liberarPaquete(salidaError);
|
||||||
|
liberarPaquete(respuestaError);
|
||||||
|
goto end;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DWORD CHUNK_SIZE = 512 * 1024; // 512KB chunks
|
||||||
|
UINT32 totalChunks = 0;
|
||||||
|
UINT32 currentChunk = 1;
|
||||||
|
DWORD totalBytesWritten = 0;
|
||||||
|
|
||||||
|
// Solicitar chunks uno por uno
|
||||||
|
while (TRUE) {
|
||||||
|
_dbg("[upload] Solicitando chunk %lu", currentChunk);
|
||||||
|
|
||||||
|
// Crear paquete para solicitar chunk
|
||||||
|
PPaquete requestPaquete = nuevoPaquete(POST_RESPONSE, TRUE);
|
||||||
|
addString(requestPaquete, tareaUuid, FALSE);
|
||||||
|
|
||||||
|
// Agregar output vacío para chunks de upload
|
||||||
|
addInt32(requestPaquete, 0);
|
||||||
|
|
||||||
|
// Agregar upload request
|
||||||
|
addUploadRequest(requestPaquete, file_id, CHUNK_SIZE, currentChunk, fullPath);
|
||||||
|
|
||||||
|
// Enviar solicitud y recibir respuesta
|
||||||
|
PAnalizador respuesta = mandarPaquete(requestPaquete);
|
||||||
|
liberarPaquete(requestPaquete);
|
||||||
|
|
||||||
|
if (!respuesta) {
|
||||||
|
_err("[upload] Error: No se recibió respuesta para chunk %lu\n", currentChunk);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parsear respuesta de upload
|
||||||
|
UploadResponseData uploadResp = {0};
|
||||||
|
if (!extractUploadResponse(respuesta, &uploadResp)) {
|
||||||
|
_err("[upload] Error: No se pudo parsear respuesta de upload para chunk %lu\n", currentChunk);
|
||||||
|
liberarAnalizador(respuesta);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Primera respuesta: obtener total_chunks
|
||||||
|
if (totalChunks == 0) {
|
||||||
|
totalChunks = uploadResp.total_chunks;
|
||||||
|
_dbg("[upload] Total chunks: %lu", totalChunks);
|
||||||
|
|
||||||
|
PPaquete infoRespuesta = nuevoPaquete(POST_RESPONSE, TRUE);
|
||||||
|
addString(infoRespuesta, tareaUuid, FALSE);
|
||||||
|
PPaquete salidaInfo = nuevoPaquete(0, FALSE);
|
||||||
|
PackageAddFormatPrintf(salidaInfo, FALSE, "[upload] Iniciando upload: %s (%lu chunks)\n",
|
||||||
|
fullPath, totalChunks);
|
||||||
|
addBytes(infoRespuesta, (PBYTE)salidaInfo->buffer, salidaInfo->length, TRUE);
|
||||||
|
mandarPaquete(infoRespuesta);
|
||||||
|
liberarPaquete(salidaInfo);
|
||||||
|
liberarPaquete(infoRespuesta);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decodificar chunk_data de base64
|
||||||
|
PBYTE decodedData = NULL;
|
||||||
|
SIZE_T decodedLen = 0;
|
||||||
|
|
||||||
|
if (uploadResp.chunk_data_b64 && uploadResp.chunk_data_len > 0) {
|
||||||
|
if (!base64_decode(uploadResp.chunk_data_b64, &decodedData, &decodedLen)) {
|
||||||
|
_err("[upload] Error decodificando base64 para chunk %lu\n", currentChunk);
|
||||||
|
LocalFree(uploadResp.chunk_data_b64);
|
||||||
|
liberarAnalizador(respuesta);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escribir chunk al archivo
|
||||||
|
DWORD bytesWritten = 0;
|
||||||
|
if (!WriteFile(hFile, decodedData, (DWORD)decodedLen, &bytesWritten, NULL)) {
|
||||||
|
DWORD error = GetLastError();
|
||||||
|
_err("[upload] Error escribiendo chunk %lu. Código: %lu\n", currentChunk, error);
|
||||||
|
LocalFree(decodedData);
|
||||||
|
LocalFree(uploadResp.chunk_data_b64);
|
||||||
|
liberarAnalizador(respuesta);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
totalBytesWritten += bytesWritten;
|
||||||
|
_dbg("[upload] Chunk %lu/%lu escrito: %lu bytes (total: %lu bytes)",
|
||||||
|
currentChunk, totalChunks, bytesWritten, totalBytesWritten);
|
||||||
|
|
||||||
|
LocalFree(decodedData);
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalFree(uploadResp.chunk_data_b64);
|
||||||
|
liberarAnalizador(respuesta);
|
||||||
|
|
||||||
|
// Verificar si hemos recibido todos los chunks
|
||||||
|
if (currentChunk >= totalChunks) {
|
||||||
|
_dbg("[upload] Todos los chunks recibidos");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentChunk++;
|
||||||
|
}
|
||||||
|
|
||||||
|
CloseHandle(hFile);
|
||||||
|
|
||||||
|
// Enviar mensaje final con artifact
|
||||||
|
PPaquete respuestaFinal = nuevoPaquete(POST_RESPONSE, TRUE);
|
||||||
|
addString(respuestaFinal, tareaUuid, FALSE);
|
||||||
|
|
||||||
|
PPaquete salidaFinal = nuevoPaquete(0, FALSE);
|
||||||
|
PackageAddFormatPrintf(salidaFinal, FALSE, "[upload] Upload completado: %s (%lu bytes)\n",
|
||||||
|
fullPath, totalBytesWritten);
|
||||||
|
addBytes(respuestaFinal, (PBYTE)salidaFinal->buffer, salidaFinal->length, TRUE);
|
||||||
|
|
||||||
|
// Agregar artifact File Write (indica que se escribió un archivo)
|
||||||
|
addArtifact(respuestaFinal, "File Write", fullPath);
|
||||||
|
|
||||||
|
mandarPaquete(respuestaFinal);
|
||||||
|
liberarPaquete(salidaFinal);
|
||||||
|
liberarPaquete(respuestaFinal);
|
||||||
|
|
||||||
|
end:
|
||||||
|
return;
|
||||||
|
}
|
||||||
@@ -21,4 +21,6 @@ VOID LeerFichero(PAnalizador argumentos);
|
|||||||
|
|
||||||
VOID DescargarFichero(PAnalizador argumentos);
|
VOID DescargarFichero(PAnalizador argumentos);
|
||||||
|
|
||||||
|
VOID SubirFichero(PAnalizador argumentos);
|
||||||
|
|
||||||
#endif //FILESYSTEM_H
|
#endif //FILESYSTEM_H
|
||||||
@@ -52,6 +52,8 @@ BOOL handleGetTasking(PAnalizador obtenerTarea) {
|
|||||||
LeerFichero(analizadorTarea);
|
LeerFichero(analizadorTarea);
|
||||||
} else if (tarea == DOWNLOAD_CMD) {
|
} else if (tarea == DOWNLOAD_CMD) {
|
||||||
DescargarFichero(analizadorTarea);
|
DescargarFichero(analizadorTarea);
|
||||||
|
} else if (tarea == UPLOAD_CMD) {
|
||||||
|
SubirFichero(analizadorTarea);
|
||||||
} else if (tarea == START_SOCKS_CMD) {
|
} else if (tarea == START_SOCKS_CMD) {
|
||||||
_dbg("Received START_SOCKS_CMD");
|
_dbg("Received START_SOCKS_CMD");
|
||||||
startSocksHandler(analizadorTarea);
|
startSocksHandler(analizadorTarea);
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
#define RM_CMD 0x25
|
#define RM_CMD 0x25
|
||||||
#define CAT_CMD 0x26
|
#define CAT_CMD 0x26
|
||||||
#define DOWNLOAD_CMD 0x27
|
#define DOWNLOAD_CMD 0x27
|
||||||
|
#define UPLOAD_CMD 0x28
|
||||||
|
|
||||||
/* New SOCKS control commands (agent-side) */
|
/* New SOCKS control commands (agent-side) */
|
||||||
#define START_SOCKS_CMD 0x60
|
#define START_SOCKS_CMD 0x60
|
||||||
|
|||||||
@@ -712,3 +712,33 @@ VOID addDownloadChunk(PPaquete paquete, PCHAR file_id, UINT32 chunk_num, PBYTE c
|
|||||||
|
|
||||||
LocalFree(chunk_data_b64);
|
LocalFree(chunk_data_b64);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
VOID addUploadRequest(PPaquete paquete, PCHAR file_id, UINT32 chunk_size, UINT32 chunk_num, PCHAR full_path) {
|
||||||
|
if (!paquete || !file_id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload request marker (0xF8)
|
||||||
|
addByte(paquete, 0xF8);
|
||||||
|
|
||||||
|
// 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 size
|
||||||
|
addInt32(paquete, chunk_size);
|
||||||
|
|
||||||
|
// Chunk number (1-based)
|
||||||
|
addInt32(paquete, chunk_num);
|
||||||
|
|
||||||
|
// Full path length and value (optional, can be empty string)
|
||||||
|
if (!full_path) {
|
||||||
|
full_path = "";
|
||||||
|
}
|
||||||
|
SIZE_T path_len = strlen(full_path);
|
||||||
|
addInt32(paquete, (UINT32)path_len);
|
||||||
|
if (path_len > 0) {
|
||||||
|
addString(paquete, full_path, FALSE);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,4 +47,8 @@ VOID addCredential(PPaquete paquete, PCHAR credential_type, PCHAR realm, PCHAR c
|
|||||||
VOID addDownloadRegistration(PPaquete paquete, PCHAR full_path, UINT32 total_chunks, UINT32 chunk_size);
|
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);
|
VOID addDownloadChunk(PPaquete paquete, PCHAR file_id, UINT32 chunk_num, PBYTE chunk_data, SIZE_T chunk_data_len);
|
||||||
|
|
||||||
|
/* Upload helpers: for file upload from Mythic to agent */
|
||||||
|
/* Format for upload request: [0xF8 marker][file_id_len:4][file_id][chunk_size:4][chunk_num:4][full_path_len:4][full_path] */
|
||||||
|
VOID addUploadRequest(PPaquete paquete, PCHAR file_id, UINT32 chunk_size, UINT32 chunk_num, PCHAR full_path);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
@@ -75,61 +75,61 @@ class CazallaAgent(PayloadType):
|
|||||||
# Track encryption configuration
|
# Track encryption configuration
|
||||||
crypto_type_value = None
|
crypto_type_value = None
|
||||||
aespsk_key_b64 = ""
|
aespsk_key_b64 = ""
|
||||||
debug_output = [] # Collect all debug messages for "Applying configuration" step
|
debug_messages = [] # Collect all debug messages for "Applying configuration" step
|
||||||
|
|
||||||
debug_output.append(f"=== DEBUG INFO ===")
|
debug_messages.append(f"=== DEBUG INFO ===")
|
||||||
debug_output.append(f"UUID: {self.uuid}")
|
debug_messages.append(f"UUID: {self.uuid}")
|
||||||
debug_output.append(f"Number of C2 profiles: {len(self.c2info)}")
|
debug_messages.append(f"Number of C2 profiles: {len(self.c2info)}")
|
||||||
|
|
||||||
for c2 in self.c2info:
|
for c2 in self.c2info:
|
||||||
profile = c2.get_c2profile()
|
profile = c2.get_c2profile()
|
||||||
debug_output.append(f"\n--- C2 Profile: {profile} ---")
|
debug_messages.append(f"\n--- C2 Profile: {profile} ---")
|
||||||
|
|
||||||
params_dict = c2.get_parameters_dict()
|
params_dict = c2.get_parameters_dict()
|
||||||
if not params_dict:
|
if not params_dict:
|
||||||
debug_output.append("WARNING: get_parameters_dict() returned None or empty!")
|
debug_messages.append("WARNING: get_parameters_dict() returned None or empty!")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
debug_output.append(f"Parameters dict keys: {list(params_dict.keys())}")
|
debug_messages.append(f"Parameters dict keys: {list(params_dict.keys())}")
|
||||||
debug_output.append(f"Full params_dict: {params_dict}")
|
debug_messages.append(f"Full params_dict: {params_dict}")
|
||||||
|
|
||||||
for key, val in params_dict.items():
|
for key, val in params_dict.items():
|
||||||
debug_output.append(f"Processing parameter: {key} = {val} (type: {type(val).__name__})")
|
debug_messages.append(f"Processing parameter: {key} = {val} (type: {type(val).__name__})")
|
||||||
|
|
||||||
# Check if val is a dict that might contain enc_key
|
# Check if val is a dict that might contain enc_key
|
||||||
if isinstance(val, dict):
|
if isinstance(val, dict):
|
||||||
debug_output.append(f" -> Parameter {key} is a dict with keys: {list(val.keys())}")
|
debug_messages.append(f" -> Parameter {key} is a dict with keys: {list(val.keys())}")
|
||||||
|
|
||||||
# Handle AESPSK dict: contains 'value' (crypto_type), 'enc_key', and 'dec_key'
|
# Handle AESPSK dict: contains 'value' (crypto_type), 'enc_key', and 'dec_key'
|
||||||
if key == "AESPSK" or key == "aespsk":
|
if key == "AESPSK" or key == "aespsk":
|
||||||
# Extract crypto_type from 'value' field
|
# Extract crypto_type from 'value' field
|
||||||
if 'value' in val:
|
if 'value' in val:
|
||||||
crypto_type_value = val['value']
|
crypto_type_value = val['value']
|
||||||
debug_output.append(f" -> Found crypto_type in AESPSK['value']: {crypto_type_value}")
|
debug_messages.append(f" -> Found crypto_type in AESPSK['value']: {crypto_type_value}")
|
||||||
# Extract enc_key
|
# Extract enc_key
|
||||||
if 'enc_key' in val:
|
if 'enc_key' in val:
|
||||||
if val["enc_key"] is not None and val["enc_key"] != "":
|
if val["enc_key"] is not None and val["enc_key"] != "":
|
||||||
aespsk_key_b64 = val["enc_key"]
|
aespsk_key_b64 = val["enc_key"]
|
||||||
try:
|
try:
|
||||||
decoded_len = len(base64.b64decode(val['enc_key']))
|
decoded_len = len(base64.b64decode(val['enc_key']))
|
||||||
debug_output.append(f" -> Found enc_key in AESPSK: {val['enc_key'][:50]}... (decoded length: {decoded_len})")
|
debug_messages.append(f" -> Found enc_key in AESPSK: {val['enc_key'][:50]}... (decoded length: {decoded_len})")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
debug_output.append(f" -> ERROR decoding enc_key: {e}")
|
debug_messages.append(f" -> ERROR decoding enc_key: {e}")
|
||||||
else:
|
else:
|
||||||
debug_output.append(f" -> enc_key in AESPSK is None or empty")
|
debug_messages.append(f" -> enc_key in AESPSK is None or empty")
|
||||||
# Handle other dict parameters that might contain enc_key
|
# Handle other dict parameters that might contain enc_key
|
||||||
elif 'enc_key' in val:
|
elif 'enc_key' in val:
|
||||||
debug_output.append(f" -> Found enc_key in dict: {val['enc_key'][:50] if val['enc_key'] else 'None'}...")
|
debug_messages.append(f" -> Found enc_key in dict: {val['enc_key'][:50] if val['enc_key'] else 'None'}...")
|
||||||
if val["enc_key"] is not None and val["enc_key"] != "":
|
if val["enc_key"] is not None and val["enc_key"] != "":
|
||||||
aespsk_key_b64 = val["enc_key"]
|
aespsk_key_b64 = val["enc_key"]
|
||||||
try:
|
try:
|
||||||
decoded_len = len(base64.b64decode(val['enc_key']))
|
decoded_len = len(base64.b64decode(val['enc_key']))
|
||||||
debug_output.append(f" -> enc_key decoded length: {decoded_len}")
|
debug_messages.append(f" -> enc_key decoded length: {decoded_len}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
debug_output.append(f" -> ERROR decoding enc_key: {e}")
|
debug_messages.append(f" -> ERROR decoding enc_key: {e}")
|
||||||
else:
|
else:
|
||||||
aespsk_key_b64 = ""
|
aespsk_key_b64 = ""
|
||||||
debug_output.append(f" -> enc_key is None or empty")
|
debug_messages.append(f" -> enc_key is None or empty")
|
||||||
else:
|
else:
|
||||||
Config[key] = val
|
Config[key] = val
|
||||||
else:
|
else:
|
||||||
@@ -137,41 +137,41 @@ class CazallaAgent(PayloadType):
|
|||||||
# Log the crypto_type value and store it (in case it comes as a direct parameter)
|
# Log the crypto_type value and store it (in case it comes as a direct parameter)
|
||||||
if key == "crypto_type":
|
if key == "crypto_type":
|
||||||
crypto_type_value = val
|
crypto_type_value = val
|
||||||
debug_output.append(f" -> crypto_type set to: {val}")
|
debug_messages.append(f" -> crypto_type set to: {val}")
|
||||||
|
|
||||||
# Also check if the key itself is 'enc_key'
|
# Also check if the key itself is 'enc_key'
|
||||||
if key == "enc_key" and val and not isinstance(val, dict):
|
if key == "enc_key" and val and not isinstance(val, dict):
|
||||||
aespsk_key_b64 = str(val)
|
aespsk_key_b64 = str(val)
|
||||||
debug_output.append(f" -> Found enc_key as direct parameter: {aespsk_key_b64[:50]}...")
|
debug_messages.append(f" -> Found enc_key as direct parameter: {aespsk_key_b64[:50]}...")
|
||||||
break
|
break
|
||||||
|
|
||||||
# Determine encryption based on crypto_type and enc_key
|
# Determine encryption based on crypto_type and enc_key
|
||||||
debug_output.append(f"\n=== ENCRYPTION DECISION ===")
|
debug_messages.append(f"\n=== ENCRYPTION DECISION ===")
|
||||||
debug_output.append(f"crypto_type_value = {crypto_type_value} (type: {type(crypto_type_value).__name__ if crypto_type_value else 'None'})")
|
debug_messages.append(f"crypto_type_value = {crypto_type_value} (type: {type(crypto_type_value).__name__ if crypto_type_value else 'None'})")
|
||||||
debug_output.append(f"aespsk_key_b64 length = {len(aespsk_key_b64) if aespsk_key_b64 else 0}")
|
debug_messages.append(f"aespsk_key_b64 length = {len(aespsk_key_b64) if aespsk_key_b64 else 0}")
|
||||||
debug_output.append(f"crypto_type_value == 'aes256_hmac'? {crypto_type_value == 'aes256_hmac'}")
|
debug_messages.append(f"crypto_type_value == 'aes256_hmac'? {crypto_type_value == 'aes256_hmac'}")
|
||||||
debug_output.append(f"aespsk_key_b64 is truthy? {bool(aespsk_key_b64)}")
|
debug_messages.append(f"aespsk_key_b64 is truthy? {bool(aespsk_key_b64)}")
|
||||||
|
|
||||||
encryption_enabled = False
|
encryption_enabled = False
|
||||||
if crypto_type_value == "aes256_hmac" and aespsk_key_b64:
|
if crypto_type_value == "aes256_hmac" and aespsk_key_b64:
|
||||||
encryption_enabled = True
|
encryption_enabled = True
|
||||||
debug_output.append(f"✓ Encryption ENABLED: crypto_type={crypto_type_value}, enc_key present")
|
debug_messages.append(f"✓ Encryption ENABLED: crypto_type={crypto_type_value}, enc_key present")
|
||||||
else:
|
else:
|
||||||
debug_output.append(f"✗ Encryption DISABLED: crypto_type={crypto_type_value or 'none'}, enc_key={'present' if aespsk_key_b64 else 'missing'}")
|
debug_messages.append(f"✗ Encryption DISABLED: crypto_type={crypto_type_value or 'none'}, enc_key={'present' if aespsk_key_b64 else 'missing'}")
|
||||||
|
|
||||||
# Set encryption configuration
|
# Set encryption configuration
|
||||||
if encryption_enabled and aespsk_key_b64:
|
if encryption_enabled and aespsk_key_b64:
|
||||||
Config["ENCRYPTION_ENABLED"] = "1" # Use 1/0 instead of TRUE/FALSE for direct numeric use
|
Config["ENCRYPTION_ENABLED"] = "1" # Use 1/0 instead of TRUE/FALSE for direct numeric use
|
||||||
Config["AESPSK_KEY"] = aespsk_key_b64
|
Config["AESPSK_KEY"] = aespsk_key_b64
|
||||||
debug_output.append(f"\nSet Config[ENCRYPTION_ENABLED] = '1'")
|
debug_messages.append(f"\nSet Config[ENCRYPTION_ENABLED] = '1'")
|
||||||
debug_output.append(f"Set Config[AESPSK_KEY] = '{Config['AESPSK_KEY'][:20]}...' (length: {len(Config['AESPSK_KEY'])})")
|
debug_messages.append(f"Set Config[AESPSK_KEY] = '{Config['AESPSK_KEY'][:20]}...' (length: {len(Config['AESPSK_KEY'])})")
|
||||||
debug_output.append(f"Agent-side encryption ENABLED with AES256-HMAC")
|
debug_messages.append(f"Agent-side encryption ENABLED with AES256-HMAC")
|
||||||
else:
|
else:
|
||||||
Config["ENCRYPTION_ENABLED"] = "0" # Use 1/0 instead of TRUE/FALSE for direct numeric use
|
Config["ENCRYPTION_ENABLED"] = "0" # Use 1/0 instead of TRUE/FALSE for direct numeric use
|
||||||
Config["AESPSK_KEY"] = ""
|
Config["AESPSK_KEY"] = ""
|
||||||
debug_output.append(f"\nSet Config[ENCRYPTION_ENABLED] = '0'")
|
debug_messages.append(f"\nSet Config[ENCRYPTION_ENABLED] = '0'")
|
||||||
debug_output.append(f"Set Config[AESPSK_KEY] = '' (empty)")
|
debug_messages.append(f"Set Config[AESPSK_KEY] = '' (empty)")
|
||||||
debug_output.append(f"Agent-side encryption disabled")
|
debug_messages.append(f"Agent-side encryption disabled")
|
||||||
|
|
||||||
if "https://" in Config["callback_host"]:
|
if "https://" in Config["callback_host"]:
|
||||||
Config["ssl"] = True
|
Config["ssl"] = True
|
||||||
@@ -196,9 +196,9 @@ class CazallaAgent(PayloadType):
|
|||||||
config_path = f"{agent_build_path.name}/agent_code/cazalla/Config.h"
|
config_path = f"{agent_build_path.name}/agent_code/cazalla/Config.h"
|
||||||
|
|
||||||
# Replace placeholders in Config.h
|
# Replace placeholders in Config.h
|
||||||
debug_output.append(f"\n=== CONFIG.H REPLACEMENT ===")
|
debug_messages.append(f"\n=== CONFIG.H REPLACEMENT ===")
|
||||||
debug_output.append(f"Before replacement - ENCRYPTION_ENABLED='{Config.get('ENCRYPTION_ENABLED', 'NOT SET')}'")
|
debug_messages.append(f"Before replacement - ENCRYPTION_ENABLED='{Config.get('ENCRYPTION_ENABLED', 'NOT SET')}'")
|
||||||
debug_output.append(f"Before replacement - AESPSK_KEY='{Config.get('AESPSK_KEY', 'NOT SET')[:20] if Config.get('AESPSK_KEY') else 'NOT SET'}...'")
|
debug_messages.append(f"Before replacement - AESPSK_KEY='{Config.get('AESPSK_KEY', 'NOT SET')[:20] if Config.get('AESPSK_KEY') else 'NOT SET'}...'")
|
||||||
|
|
||||||
with open(config_path, "r+") as f:
|
with open(config_path, "r+") as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
@@ -215,8 +215,8 @@ class CazallaAgent(PayloadType):
|
|||||||
# Replace encryption settings
|
# Replace encryption settings
|
||||||
encryption_value = Config.get("ENCRYPTION_ENABLED", "0")
|
encryption_value = Config.get("ENCRYPTION_ENABLED", "0")
|
||||||
aespsk_value = Config.get("AESPSK_KEY", "")
|
aespsk_value = Config.get("AESPSK_KEY", "")
|
||||||
debug_output.append(f"Replacing %ENCRYPTION_ENABLED% with '{encryption_value}'")
|
debug_messages.append(f"Replacing %ENCRYPTION_ENABLED% with '{encryption_value}'")
|
||||||
debug_output.append(f"Replacing %AESPSK_KEY% with '{aespsk_value[:30] if aespsk_value else ''}...' (length: {len(aespsk_value)})")
|
debug_messages.append(f"Replacing %AESPSK_KEY% with '{aespsk_value[:30] if aespsk_value else ''}...' (length: {len(aespsk_value)})")
|
||||||
content = content.replace("%ENCRYPTION_ENABLED%", encryption_value)
|
content = content.replace("%ENCRYPTION_ENABLED%", encryption_value)
|
||||||
content = content.replace("%AESPSK_KEY%", aespsk_value)
|
content = content.replace("%AESPSK_KEY%", aespsk_value)
|
||||||
|
|
||||||
@@ -231,15 +231,15 @@ class CazallaAgent(PayloadType):
|
|||||||
import re
|
import re
|
||||||
match = re.search(r'#define ENCRYPTION_ENABLED\s+(\S+)', content_after)
|
match = re.search(r'#define ENCRYPTION_ENABLED\s+(\S+)', content_after)
|
||||||
if match:
|
if match:
|
||||||
debug_output.append(f"\n✓ Verified: Config.h has ENCRYPTION_ENABLED = {match.group(1)}")
|
debug_messages.append(f"\n✓ Verified: Config.h has ENCRYPTION_ENABLED = {match.group(1)}")
|
||||||
match = re.search(r'#define AESPSK_KEY\s+"([^"]*)"', content_after)
|
match = re.search(r'#define AESPSK_KEY\s+"([^"]*)"', content_after)
|
||||||
if match:
|
if match:
|
||||||
key_val = match.group(1)
|
key_val = match.group(1)
|
||||||
debug_output.append(f"✓ Verified: Config.h has AESPSK_KEY length = {len(key_val)}")
|
debug_messages.append(f"✓ Verified: Config.h has AESPSK_KEY length = {len(key_val)}")
|
||||||
|
|
||||||
# Send all debug info as part of "Applying configuration" step
|
# Send all debug info as part of "Applying configuration" step
|
||||||
final_output = f"All configuration setting applied (output={output_format}, debug_level={debug_level}, debug_output={debug_output})\n\n"
|
final_output = f"All configuration setting applied (output={output_format}, debug_level={debug_level}, debug_output={debug_output})\n\n"
|
||||||
final_output += "\n".join(debug_output)
|
final_output += "\n".join(debug_messages)
|
||||||
|
|
||||||
await SendMythicRPCPayloadUpdatebuildStep(MythicRPCPayloadUpdateBuildStepMessage(
|
await SendMythicRPCPayloadUpdatebuildStep(MythicRPCPayloadUpdateBuildStepMessage(
|
||||||
PayloadUUID=self.uuid,
|
PayloadUUID=self.uuid,
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
from mythic_container.MythicCommandBase import *
|
||||||
|
from mythic_container.MythicRPC import *
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
class UploadArguments(TaskArguments):
|
||||||
|
def __init__(self, command_line, **kwargs):
|
||||||
|
super().__init__(command_line, **kwargs)
|
||||||
|
self.args = [
|
||||||
|
CommandParameter(
|
||||||
|
name="file",
|
||||||
|
type=ParameterType.File,
|
||||||
|
description="File to upload to the target",
|
||||||
|
),
|
||||||
|
CommandParameter(
|
||||||
|
name="path",
|
||||||
|
type=ParameterType.String,
|
||||||
|
description="Path where the file should be saved on the target",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
async def parse_arguments(self):
|
||||||
|
if len(self.command_line) == 0:
|
||||||
|
raise ValueError("Must supply a file and path")
|
||||||
|
|
||||||
|
parts = self.command_line.split(" ", 1)
|
||||||
|
if len(parts) != 2:
|
||||||
|
raise ValueError("Must supply both file and path: upload <file_id> <path>")
|
||||||
|
|
||||||
|
self.add_arg("file", parts[0])
|
||||||
|
# Normalize path in parse_arguments too
|
||||||
|
path = parts[1]
|
||||||
|
if path:
|
||||||
|
# Replace all sequences of double backslashes with single backslash
|
||||||
|
while "\\\\" in path:
|
||||||
|
path = path.replace("\\\\", "\\")
|
||||||
|
self.add_arg("path", path)
|
||||||
|
|
||||||
|
async def parse_dictionary(self, dictionary_arguments):
|
||||||
|
if "file" in dictionary_arguments:
|
||||||
|
self.add_arg("file", dictionary_arguments["file"])
|
||||||
|
if "path" in dictionary_arguments:
|
||||||
|
# Normalize path: replace double backslashes with single backslash
|
||||||
|
path = dictionary_arguments["path"]
|
||||||
|
if path:
|
||||||
|
# Replace all sequences of double backslashes with single backslash
|
||||||
|
while "\\\\" in path:
|
||||||
|
path = path.replace("\\\\", "\\")
|
||||||
|
self.add_arg("path", path)
|
||||||
|
|
||||||
|
|
||||||
|
class UploadCommand(CommandBase):
|
||||||
|
cmd = "upload"
|
||||||
|
needs_admin = False
|
||||||
|
help_cmd = "upload <file> <path>"
|
||||||
|
description = "Upload a file from the Mythic server to the target"
|
||||||
|
version = 1
|
||||||
|
author = "@KaseyaOFSTeam"
|
||||||
|
argument_class = UploadArguments
|
||||||
|
|
||||||
|
async def create_go_tasking(self, taskData: PTTaskMessageAllData) -> PTTaskCreateTaskingMessageResponse:
|
||||||
|
response = PTTaskCreateTaskingMessageResponse(
|
||||||
|
TaskID=taskData.Task.ID,
|
||||||
|
Success=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
file_id = taskData.args.get_arg("file")
|
||||||
|
path = taskData.args.get_arg("path")
|
||||||
|
|
||||||
|
# Get file information to extract filename
|
||||||
|
try:
|
||||||
|
file_search = await SendMythicRPCFileSearch(MythicRPCFileSearchMessage(
|
||||||
|
AgentFileId=file_id,
|
||||||
|
LimitByCallback=False,
|
||||||
|
TaskID=taskData.Task.ID
|
||||||
|
))
|
||||||
|
|
||||||
|
if file_search.Success and len(file_search.Files) > 0:
|
||||||
|
file_info = file_search.Files[0]
|
||||||
|
filename = file_info.Filename
|
||||||
|
|
||||||
|
# If path is empty, use current directory + filename
|
||||||
|
if not path or path.strip() == "":
|
||||||
|
path = ".\\" + filename
|
||||||
|
# If path ends with backslash or forward slash, append filename
|
||||||
|
elif path.endswith("\\") or path.endswith("/"):
|
||||||
|
path = path.rstrip("\\/") + "\\" + filename
|
||||||
|
|
||||||
|
response.DisplayParams = f"file_id: {file_id}, path: {path}, filename: {filename}"
|
||||||
|
# Update the path argument with the final path (including filename if needed)
|
||||||
|
taskData.args.add_arg("path", path)
|
||||||
|
else:
|
||||||
|
response.DisplayParams = f"file_id: {file_id}, path: {path}"
|
||||||
|
except Exception as e:
|
||||||
|
response.DisplayParams = f"file_id: {file_id}, path: {path} (warning: could not get filename: {str(e)})"
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse:
|
||||||
|
resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True)
|
||||||
|
return resp
|
||||||
@@ -16,6 +16,7 @@ commands = {
|
|||||||
"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"},
|
"download": {"hex_code": 0x27, "input_type": "string"},
|
||||||
|
"upload": {"hex_code": 0x28, "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},
|
||||||
@@ -53,6 +54,14 @@ def responseTasking(tasks):
|
|||||||
data = command_code + task_id
|
data = command_code + task_id
|
||||||
if task["parameters"] != "":
|
if task["parameters"] != "":
|
||||||
parameters = json.loads(task["parameters"])
|
parameters = json.loads(task["parameters"])
|
||||||
|
# Normalize paths for upload command - replace double backslashes
|
||||||
|
if command_to_run == "upload" and "path" in parameters:
|
||||||
|
path = parameters["path"]
|
||||||
|
if path:
|
||||||
|
# Replace all sequences of double backslashes with single backslash
|
||||||
|
while "\\\\" in path:
|
||||||
|
path = path.replace("\\\\", "\\")
|
||||||
|
parameters["path"] = path
|
||||||
data += len(parameters).to_bytes(4,"big")
|
data += len(parameters).to_bytes(4,"big")
|
||||||
for param in parameters:
|
for param in parameters:
|
||||||
data += len(parameters[param]).to_bytes(4, "big")
|
data += len(parameters[param]).to_bytes(4, "big")
|
||||||
@@ -134,4 +143,19 @@ def responsePosting(responses):
|
|||||||
data += file_id_bytes
|
data += file_id_bytes
|
||||||
print(f"[DOWNLOAD] File ID agregado a respuesta binaria: {file_id}")
|
print(f"[DOWNLOAD] File ID agregado a respuesta binaria: {file_id}")
|
||||||
|
|
||||||
|
# Check if this response is for an upload request
|
||||||
|
# When Mythic responds to an upload request, it includes upload data directly in response
|
||||||
|
# Format from Mythic: {"chunk_data": "...", "chunk_num": 1, "total_chunks": 1, "file_id": "...", ...}
|
||||||
|
if "chunk_data" in response and "chunk_num" in response:
|
||||||
|
# Format: [0xF9 marker][total_chunks:4][chunk_num:4][chunk_data_len:4][chunk_data_base64]
|
||||||
|
data += bytes([0xF9]) # Upload response marker
|
||||||
|
data += response.get("total_chunks", 0).to_bytes(4, "big")
|
||||||
|
data += response.get("chunk_num", 0).to_bytes(4, "big")
|
||||||
|
|
||||||
|
chunk_data_b64 = response.get("chunk_data", "")
|
||||||
|
chunk_data_bytes = chunk_data_b64.encode('utf-8')
|
||||||
|
data += len(chunk_data_bytes).to_bytes(4, "big")
|
||||||
|
data += chunk_data_bytes
|
||||||
|
print(f"[UPLOAD] Upload response agregado: total_chunks={response.get('total_chunks', 0)}, chunk_num={response.get('chunk_num', 0)}, data_len={len(chunk_data_bytes)}")
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|||||||
@@ -189,6 +189,7 @@ def postResponse(data):
|
|||||||
credentials_data = []
|
credentials_data = []
|
||||||
download_registration = None
|
download_registration = None
|
||||||
download_chunk = None
|
download_chunk = None
|
||||||
|
upload_request = 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)
|
||||||
@@ -419,6 +420,64 @@ def postResponse(data):
|
|||||||
remaining_data = remaining_data[offset:]
|
remaining_data = remaining_data[offset:]
|
||||||
except:
|
except:
|
||||||
break
|
break
|
||||||
|
elif remaining_data[0] == 0xF8:
|
||||||
|
# Upload request marker found
|
||||||
|
# Format: [0xF8][file_id_len:4][file_id][chunk_size:4][chunk_num:4][full_path_len:4][full_path]
|
||||||
|
offset = 1
|
||||||
|
|
||||||
|
# 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_size
|
||||||
|
if len(remaining_data) < offset + 4:
|
||||||
|
break
|
||||||
|
chunk_size = int.from_bytes(remaining_data[offset:offset+4], 'big')
|
||||||
|
offset += 4
|
||||||
|
|
||||||
|
# Extract chunk_num
|
||||||
|
if len(remaining_data) < offset + 4:
|
||||||
|
break
|
||||||
|
chunk_num = int.from_bytes(remaining_data[offset:offset+4], 'big')
|
||||||
|
offset += 4
|
||||||
|
|
||||||
|
# Extract full_path
|
||||||
|
if len(remaining_data) < offset + 4:
|
||||||
|
break
|
||||||
|
path_len = int.from_bytes(remaining_data[offset:offset+4], 'big')
|
||||||
|
offset += 4
|
||||||
|
|
||||||
|
full_path = ""
|
||||||
|
if path_len > 0:
|
||||||
|
if len(remaining_data) < offset + path_len:
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
full_path = remaining_data[offset:offset+path_len].decode('cp850')
|
||||||
|
offset += path_len
|
||||||
|
except:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Store upload request info (will be used to construct Mythic JSON)
|
||||||
|
upload_request = {
|
||||||
|
"file_id": file_id,
|
||||||
|
"chunk_size": chunk_size,
|
||||||
|
"chunk_num": chunk_num,
|
||||||
|
"full_path": full_path
|
||||||
|
}
|
||||||
|
print(f"[UPLOAD] Request detectado: file_id={file_id}, chunk_num={chunk_num}, chunk_size={chunk_size}, path={full_path}")
|
||||||
|
|
||||||
|
remaining_data = remaining_data[offset:]
|
||||||
else:
|
else:
|
||||||
# No more markers
|
# No more markers
|
||||||
break
|
break
|
||||||
@@ -529,6 +588,21 @@ def postResponse(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'])}")
|
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'])}")
|
||||||
|
|
||||||
|
# Add upload request information if present
|
||||||
|
if upload_request:
|
||||||
|
# For upload requests, we ONLY send the upload info, no user_output
|
||||||
|
# Remove user_output if it was added
|
||||||
|
if "user_output" in jsonTask:
|
||||||
|
jsonTask.pop("user_output", None)
|
||||||
|
|
||||||
|
jsonTask["upload"] = {
|
||||||
|
"file_id": upload_request["file_id"],
|
||||||
|
"chunk_size": upload_request["chunk_size"],
|
||||||
|
"chunk_num": upload_request["chunk_num"],
|
||||||
|
"full_path": upload_request["full_path"]
|
||||||
|
}
|
||||||
|
print(f"[UPLOAD] Upload request agregado al JSON: file_id={upload_request['file_id']}, chunk_num={upload_request['chunk_num']}, chunk_size={upload_request['chunk_size']}")
|
||||||
|
|
||||||
resTaks.append(jsonTask)
|
resTaks.append(jsonTask)
|
||||||
|
|
||||||
dataJson = {"action": "post_response", "responses": resTaks}
|
dataJson = {"action": "post_response", "responses": resTaks}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
- [Artifacts Support](#artifacts-support)
|
- [Artifacts Support](#artifacts-support)
|
||||||
- [Credentials Support](#credentials-support)
|
- [Credentials Support](#credentials-support)
|
||||||
- [File Downloads Support](#file-downloads-support)
|
- [File Downloads Support](#file-downloads-support)
|
||||||
|
- [File Uploads Support](#file-uploads-support)
|
||||||
- [Development](#development)
|
- [Development](#development)
|
||||||
- [Troubleshooting](#troubleshooting)
|
- [Troubleshooting](#troubleshooting)
|
||||||
- [Credits](#credits)
|
- [Credits](#credits)
|
||||||
@@ -183,6 +184,7 @@ For more information, see the [Mythic documentation on customizing public agents
|
|||||||
| `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` |
|
| `download` | Download file from target to Mythic server | `download C:\path\to\file.txt` |
|
||||||
|
| `upload` | Upload file from Mythic server to target | `upload <file> C:\path\to\destination.txt` |
|
||||||
|
|
||||||
### Execution & Control
|
### Execution & Control
|
||||||
|
|
||||||
@@ -561,6 +563,7 @@ 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
|
||||||
|
- `upload` - reports destination file path when uploading files
|
||||||
- **File Delete**: Automatically reported for:
|
- **File Delete**: Automatically reported for:
|
||||||
- `rm` (delete) - reports deleted file/directory path
|
- `rm` (delete) - reports deleted file/directory path
|
||||||
- **File Read**: Automatically reported for:
|
- **File Read**: Automatically reported for:
|
||||||
@@ -573,7 +576,6 @@ Currently supported:
|
|||||||
- `cat` - read-only file operation (doesn't create artifacts, but may report credentials)
|
- `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 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`)
|
||||||
- **Process Inject**: When processes are injected into (e.g., `inject` command)
|
- **Process Inject**: When processes are injected into (e.g., `inject` command)
|
||||||
@@ -610,6 +612,7 @@ 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
|
||||||
|
- `upload`: File Write artifact with destination file path
|
||||||
- `rm`: File Delete artifact with deleted path
|
- `rm`: File Delete artifact with deleted path
|
||||||
- `download`: File Read artifact with downloaded file path
|
- `download`: File Read artifact with downloaded file path
|
||||||
|
|
||||||
@@ -882,6 +885,112 @@ For more information, see the [Mythic File Downloads documentation](https://docs
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 📤 File Uploads Support
|
||||||
|
|
||||||
|
Cazalla supports uploading files from the Mythic server to the target using chunked transfers, following [Mythic's File Upload specification](https://docs.mythic-c2.net/customizing/hooking-features/action-upload).
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- **Chunked File Transfers**: Large files are automatically split into chunks (512KB default) for efficient transfer
|
||||||
|
- **Automatic Path Normalization**: Automatically handles path formatting (normalizes double backslashes)
|
||||||
|
- **Smart Path Handling**: Automatically appends filename when path is a directory
|
||||||
|
- **Progress Tracking**: Mythic tracks upload progress (chunks sent/total chunks)
|
||||||
|
- **File Write Artifacts**: Automatically reports File Write artifacts when files are uploaded
|
||||||
|
|
||||||
|
### How It Works
|
||||||
|
|
||||||
|
1. **Command Execution**: When you execute `upload <file> C:\path\to\destination.txt`, the agent:
|
||||||
|
- Receives the `file_id` and destination path from Mythic
|
||||||
|
- Normalizes the destination path (handles double backslashes, directories, etc.)
|
||||||
|
- Creates the destination file
|
||||||
|
- Requests file chunks from Mythic sequentially
|
||||||
|
- Receives base64-encoded chunk data from Mythic
|
||||||
|
- Decodes and writes chunks to the destination file
|
||||||
|
- Reports a File Write artifact when complete
|
||||||
|
|
||||||
|
2. **Mythic Integration**: The upload is automatically:
|
||||||
|
- Tracked with progress in Mythic's UI
|
||||||
|
- Registered as a File Write artifact
|
||||||
|
- Visible in Mythic's Artifacts page
|
||||||
|
|
||||||
|
### Using the Upload Command
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Upload a file to the target
|
||||||
|
# Select file from Mythic UI file selector, then specify destination path
|
||||||
|
upload <file> C:\Users\localuser\Downloads\file.txt
|
||||||
|
|
||||||
|
# Uploads are chunked automatically - Mythic will show progress
|
||||||
|
```
|
||||||
|
|
||||||
|
### Upload Process
|
||||||
|
|
||||||
|
1. **Request**: Agent sends upload request to Mythic for each chunk:
|
||||||
|
- `file_id`: Unique identifier for the file to upload
|
||||||
|
- `chunk_num`: Automatically incremented (1, 2, 3, ...)
|
||||||
|
- `chunk_size`: Size requested per chunk (512KB default)
|
||||||
|
- `full_path`: Destination path on the target
|
||||||
|
|
||||||
|
2. **Response**: Mythic responds with chunk data:
|
||||||
|
- `total_chunks`: Total number of chunks for the file
|
||||||
|
- `chunk_num`: Current chunk number
|
||||||
|
- `chunk_data`: Base64-encoded chunk data
|
||||||
|
|
||||||
|
3. **Writing**: Agent decodes and writes each chunk sequentially
|
||||||
|
|
||||||
|
4. **Completion**: Once all chunks are received and written, the file is complete
|
||||||
|
|
||||||
|
### Example Upload Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
Mythic: Task created with file_id and path
|
||||||
|
Agent: [upload] Iniciando upload: C:\Users\file.txt (2 chunks)
|
||||||
|
Agent: [upload] Solicitando chunk requesting chunk 1
|
||||||
|
Mythic: Response with chunk 1/2 (512KB base64 data)
|
||||||
|
Agent: [upload] Chunk 1/2 escrito: 512000 bytes
|
||||||
|
Agent: [upload] Solicitando chunk requesting chunk 2
|
||||||
|
Mythic: Response with chunk 2/2 (512KB base64 data)
|
||||||
|
Agent: [upload] Chunk 2/2 escrito: 512000 bytes
|
||||||
|
Agent: [upload] Upload completado: C:\Users\file.txt (1024000 bytes)
|
||||||
|
Mythic: File Write artifact created
|
||||||
|
```
|
||||||
|
|
||||||
|
### Path Handling
|
||||||
|
|
||||||
|
The upload command includes smart path handling:
|
||||||
|
|
||||||
|
- **Normalization**: Automatically normalizes double backslashes (`C:\\\\Users` → `C:\Users`)
|
||||||
|
- **Directory Detection**: If the path is a directory, automatically appends the filename from Mythic
|
||||||
|
- **Full Path Resolution**: Uses `GetFullPathName` to resolve relative paths to absolute paths
|
||||||
|
- **Error Handling**: Clear error messages for invalid paths, access denied, or directory issues
|
||||||
|
|
||||||
|
### Implementation Details
|
||||||
|
|
||||||
|
The upload system uses:
|
||||||
|
- **Chunk Size**: 512KB per chunk (configurable in code)
|
||||||
|
- **Base64 Decoding**: Chunk data is automatically decoded from base64 before writing
|
||||||
|
- **Sequential Requests**: Agent requests chunks one at a time (1, 2, 3, ...)
|
||||||
|
- **Artifact Reporting**: Automatically reports "File Write" artifact with file path
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
|
||||||
|
The upload command handles various error scenarios:
|
||||||
|
- **Invalid Path**: Returns error if path is invalid or malformed
|
||||||
|
- **Directory as Path**: Detects and reports error if path is a directory without filename
|
||||||
|
- **Access Denied**: Returns Windows error code if file cannot be created/written
|
||||||
|
- **Path Not Found**: Returns error if parent directory does not exist
|
||||||
|
|
||||||
|
### Viewing Upload Artifacts
|
||||||
|
|
||||||
|
1. Navigate to your callback in the Mythic UI
|
||||||
|
2. Click the **Artifacts** icon (fingerprint) in the top navigation
|
||||||
|
3. Filter by "File Write" artifact type
|
||||||
|
4. View all files that were uploaded, including file paths and timestamps
|
||||||
|
|
||||||
|
For more information, see the [Mythic File Upload documentation](https://docs.mythic-c2.net/customizing/hooking-features/action-upload).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 🔧 Development
|
## 🔧 Development
|
||||||
|
|
||||||
### Project Structure
|
### Project Structure
|
||||||
|
|||||||
Reference in New Issue
Block a user