diff --git a/README.md b/README.md
index 94fffa4..e3fa996 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
[](https://github.com/Meltedd/VisualSploit/releases)
[](LICENSE)
-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 evaluating it with Visual Studio, `dotnet`, or CI can be enough to run the payload without user interaction.
+Weaponizes MSBuild project files to run embedded shellcode. Given a supported MSBuild XML file (`.csproj`, `.vbproj`, `.proj`, imported `.props`/`.targets`, or `Directory.Build.props/targets`) and a shellcode blob, VisualSploit injects a loader that fires whenever MSBuild evaluates that file. Cloning a backdoored repo and evaluating it with Visual Studio, `dotnet`, or CI can be enough to run the payload without user interaction.

@@ -38,13 +38,16 @@ Cloned files carry no [MOTW](https://learn.microsoft.com/en-us/windows/win32/sec
## Targets
-| Target file | Fires when |
-|-----------------------------|-----------------------------------------------|
-| `*.csproj` / `*.vbproj` | The project is opened, restored, or built |
-| `Directory.Build.props` | Any project in or below its directory is opened, restored, or built |
-| `Directory.Build.targets` | Any project in or below its directory is built |
+| Target file | Fires when |
+|-----------------------------------|-----------------------------------------------|
+| `*.csproj` / `*.vbproj` | The project is opened, restored, or built |
+| `*.proj` | The build file is run by MSBuild |
+| `Directory.Build.props` | Any project in or below its directory is opened, restored, or built |
+| `Directory.Build.targets` | Any project in or below its directory is built |
+| Existing `*.props` / `*.targets` | A project or build imports the file |
`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 across developer machines, CI runners, and devcontainers that evaluate MSBuild.
+Other `.props` and `.targets` files must already be imported by a project or build.
## Usage
diff --git a/VisualSploit.Tests/MSBuildTests.cs b/VisualSploit.Tests/MSBuildTests.cs
index 396b4be..6415a64 100644
--- a/VisualSploit.Tests/MSBuildTests.cs
+++ b/VisualSploit.Tests/MSBuildTests.cs
@@ -86,6 +86,31 @@ public void Merges_into_existing_csproj_preserving_property_and_item_groups()
Assert.Contains(root.Elements(), e => e.Name.LocalName == "Target");
}
+ [Theory]
+ [InlineData(".proj")]
+ [InlineData(".props")]
+ [InlineData(".targets")]
+ public void Merges_into_existing_msbuild_file_preserving_project_contents(string extension)
+ {
+ var target = Path.Combine(_dir, $"Existing{extension}");
+ File.WriteAllText(target, """
+
+
+ Value
+
+
+ """);
+
+ Inject(Cfg(target));
+
+ var doc = XDocument.Load(target);
+ var root = doc.Root!;
+
+ Assert.Equal("Value", root.Descendants().First(e => e.Name.LocalName == "Keep").Value);
+ Assert.Contains(root.Elements(), e => e.Name.LocalName == "UsingTask");
+ Assert.Contains(root.Elements(), e => e.Name.LocalName == "Target");
+ }
+
[Fact]
public void Appends_generated_target_after_existing_InitialTargets()
{
@@ -145,6 +170,16 @@ public void Throws_when_csproj_target_does_not_exist()
Assert.Throws(() => Inject(Cfg(target)));
}
+ [Theory]
+ [InlineData("missing.proj")]
+ [InlineData("missing.props")]
+ [InlineData("missing.targets")]
+ public void Throws_when_missing_non_Directory_Build_msbuild_file_does_not_exist(string filename)
+ {
+ var target = Path.Combine(_dir, filename);
+ Assert.Throws(() => Inject(Cfg(target)));
+ }
+
[Fact]
public void Throws_when_existing_target_root_is_not_Project()
{
diff --git a/VisualSploit.Tests/ProgramTests.cs b/VisualSploit.Tests/ProgramTests.cs
index fa9b214..3f714d8 100644
--- a/VisualSploit.Tests/ProgramTests.cs
+++ b/VisualSploit.Tests/ProgramTests.cs
@@ -61,6 +61,31 @@ public void Output_includes_condition_when_configured()
Assert.Contains("Condition=\"'$(Configuration)' == 'Release'\"", output);
}
+ [Theory]
+ [InlineData(".proj")]
+ [InlineData(".props")]
+ [InlineData(".targets")]
+ public void Accepts_existing_msbuild_target_extension(string extension)
+ {
+ var output = RunWithOutputFileForTarget(extension);
+
+ Assert.Contains("
RunWithOutputFile(shellcode: [0xC3], extraArgs);
- string RunWithOutputFile(byte[] shellcode, params string[] extraArgs)
+ string RunWithOutputFile(byte[] shellcode, params string[] extraArgs) =>
+ RunWithOutputFileForTarget(shellcode, ".csproj", extraArgs);
+
+ string RunWithOutputFileForTarget(string targetExtension, params string[] extraArgs) =>
+ RunWithOutputFileForTarget([0xC3], targetExtension, extraArgs);
+
+ string RunWithOutputFileForTarget(byte[] shellcode, string targetExtension, params string[] extraArgs)
{
- var (target, shellcodePath) = WriteInputs(shellcode);
+ var (target, shellcodePath) = WriteInputs(shellcode, targetExtension);
var output = Path.Combine(_dir, $"{Guid.NewGuid():N}.csproj");
var args = new List { target, shellcodePath, "--output", output, "-s", "42" };
@@ -89,9 +120,9 @@ string RunWithOutputFile(byte[] shellcode, params string[] extraArgs)
return File.ReadAllText(output);
}
- (string target, string shellcodePath) WriteInputs(byte[] shellcode)
+ (string target, string shellcodePath) WriteInputs(byte[] shellcode, string targetExtension = ".csproj")
{
- var target = Path.Combine(_dir, $"{Guid.NewGuid():N}.csproj");
+ var target = Path.Combine(_dir, $"{Guid.NewGuid():N}{targetExtension}");
var shellcodePath = Path.Combine(_dir, $"{Guid.NewGuid():N}.bin");
File.WriteAllText(target, """
diff --git a/VisualSploit/Program.cs b/VisualSploit/Program.cs
index e263867..cc37930 100644
--- a/VisualSploit/Program.cs
+++ b/VisualSploit/Program.cs
@@ -8,7 +8,7 @@ internal static int Main(string[] args)
{
var targetArg = new Argument("target")
{
- Description = "Target .csproj, .vbproj, Directory.Build.props, or Directory.Build.targets"
+ Description = "Target .csproj, .vbproj, .proj, .props, .targets, or Directory.Build.props/targets"
};
targetArg.Validators.Add(r =>
{
@@ -156,13 +156,12 @@ Config BuildConfig(ParseResult ctx)
{
var ext = Path.GetExtension(filename);
if (ext.Equals(".csproj", StringComparison.OrdinalIgnoreCase) ||
- ext.Equals(".vbproj", StringComparison.OrdinalIgnoreCase))
+ ext.Equals(".vbproj", StringComparison.OrdinalIgnoreCase) ||
+ ext.Equals(".proj", StringComparison.OrdinalIgnoreCase) ||
+ ext.Equals(".props", StringComparison.OrdinalIgnoreCase) ||
+ ext.Equals(".targets", StringComparison.OrdinalIgnoreCase))
return null;
- if (filename.Equals("Directory.Build.props", StringComparison.OrdinalIgnoreCase) ||
- filename.Equals("Directory.Build.targets", StringComparison.OrdinalIgnoreCase))
- return null;
-
- return "Target must be one of: *.csproj, *.vbproj, Directory.Build.props, Directory.Build.targets";
+ return "Target must be one of: *.csproj, *.vbproj, *.proj, *.props, *.targets, Directory.Build.props, Directory.Build.targets";
}
}