It’s been a while since I have written a blog post, please accept my sincerest apologies. This is because a lot of fun stuff that I’ve recently done is going to be presented during conferences.
Please treat this post as a small introduction to my upcoming Hexacon 2023 talk titled “Exploiting Hardened .NET Deserialization: New Exploitation Ideas and Abuse of Insecure Serialization”. The entire talk and research was inspired by two small research projects, one of which focused on issues in SolarWinds deserialization.
In this blog post, I would like to present four old vulnerabilities that were fixed within the last year:
— CVE-2022-38108
— CVE-2022-36957
— CVE-2022-36958
— CVE-2022-36964
A small part of the Hexacon talk will show how I have bypassed patches to some of these vulnerabilities. Right now, we will focus on the original issues.
CVE-2022-38108
This vulnerability was already mentioned in this blog post. Let me reintroduce it to you in more detail.
Several SolarWinds services communicate with each other through a RabbitMQ instance, which is accessible through port 5671/TCP. Credentials are required to access it. However:
— High-privileged users were able to extract those credentials through SolarWinds Orion Platform.
— I later found CVE-2023-33225, which allowed low-privileged users to extract those credentials.
This vulnerability targeted the SolarWinds Information Service. In order to deliver an AMQP message to the Information Service, the Routing-Key
of the message must be set to SwisPubSub
.
<img alt="" height="452" src="https://images.squarespace-cdn.com/content/v1/5894c269e4fcb5e65a1ed623/cc95e220-68b2-4f8b-af96-d355aaa0676a/amqp-1.png?format=1000w" width="662" />
<p><em>Figure 1 - Routing-Key in AMQP message</em></p>
Now, let’s verify how SolarWinds handles those messages! We can start with the EasyNetQ.Consumer.HandleBasicDeliver
method:
At [1]
, the code retrieves the properties of the AMQP message. Those properties are controlled by the attacker who sends the message.
At [2]
, it creates an execution context, containing both the AMQP message properties and the message body.
At [3]
, it executes a task to consume the message.
This leads us to the Consume
method:
At [1]
, EasyNetQ.DefaultMessageSerializationStrategy.DeserializeMessage
is called. It accepts the message properties and the message body as input. The interesting stuff happens here.
At [1]
, we can see something really intriguing. A method named DeSerialize
is called and it returns an output of type Type
. As an input, it accepts the Type
property from the message. That’s right – we can control messageType
type through an AMQP message property!
At [2]
, it calls BytesToMessage
, which accepts both the attacker-controlled type and the message body as input.
At [1]
, the message body is decoded as a UTF-8 string. It is expected to contain JSON-formatted data.
At [2]
, the deserialization is performed. We control both the target type and the serialized payload.
At [3]
, it can be seen that the TypeNameHandling
deserialization setting is set to Auto
.
We have more than we need to achieve remote code execution here! To do that, we have to send an AMQP message with the Type
property set to a dangerous type.
<img alt="" height="390" src="https://images.squarespace-cdn.com/content/v1/5894c269e4fcb5e65a1ed623/607626ca-6fbe-4c67-b7b2-88e5fda9e2e1/amqp-2.png?format=1000w" width="1070" />
<p><em>Figure 2 - Deserialization Type control through AMQP properties</em></p>
In the message body, we must deliver the corresponding JSON.NET gadget. I have used a simple WindowsPrincipal
gadget from ysoserial.net, which is a bridge for the internally stored BinaryFormatter gadget. Upon the JSON deserialization, the RCE will be achieved through the underlying BinaryFormatter deserialization.
RCE achieved!
CVE-2022-36957
In the previous vulnerability, we were able to fully control the target deserialization type through the AMQP property. When I find such a vulnerability, I like to ask myself the following question: “What does a legitimate message look like?” I often check the types that are being deserialized during typical product operation. It sometimes leads to interesting findings.
I quickly realized that SolarWinds sends messages of one type only:
SolarWinds.MessageBus.Models.Indication
Let’s take a moment to analyze this type:
At [1]
and [2]
, we can see two public members of type SolarWinds.MessageBus.Models.PropertyBag
. The fun begins here.
At [1]
, you can see the definition of the class in question, SolarWinds.MessageBus.Models.PropertyBag
.
At [2]
, a custom converter is registered for this class - SolarWinds.MessageBus.Models. PropertyBagJsonConverter
. It implements the ReadJson
method, which will be called during deserialization.
At [1]
, the code iterates over the JSON properties.
At [2]
, a JSON value is retrieved and casted to the JObject
type.
At [3]
, a Type
is retrieved on the basis of the value stored in the t
key.
At [4]
, the object stored in the v
key is deserialized, where we control the target deserialization type (again)!
You can see that we are again able to control the deserialization type! This type is delivered through the t
JSON key and the serialized payload is delivered through the v
key.
Let’s have a look at a fragment of a legitimate message:
We can take any property, for instance: IndicationId
. Then, we need to:
• Set the value of the t
key to the name of a malicious type.
• Put a malicious serialized payload in the value of the v
key.
As the JSON deserialization settings are set to TypeNameHandling.Auto
, it is enough to deliver something like this:
Now, let’s imagine that the first bug described above, CVE-2022-38108, got fixed by hardcoding of the target deserialization type to SolarWinds.MessageBus.Models.Indication
. After all, this is the only legitimate type to be deserialized. That fix would not be enough, because SolarWinds.MessageBus.Models.Indication
can be used to deliver an inner object, with an attacker-controlled type. We have a second RCE through control of the type here.
CVE-2022-36958
SolarWinds defines some inner methods/operations called “SWIS verbs”. Those verbs can be either:
a) Invoked directly through the API.
b) Invoked indirectly through the Orion Platform Web UI (Orion Platform invokes verbs internally).
There are several things that we need to know about SWIS verbs:
• They are invoked using a payload within an XML structure.
• They accept arguments of predefined types.
For instance, consider the Orion.AgentManagement.Agent.Deploy
verb. It accepts 12 arguments. The following screenshot presents those arguments and their corresponding types.
<img alt="" height="826" src="https://images.squarespace-cdn.com/content/v1/5894c269e4fcb5e65a1ed623/d2927010-6dc5-466d-9d96-6211e08f3eec/verb-1.png?format=1000w" width="1562" />
<p><em>Figure 3 - Arguments for Orion.AgentManagement.Agent.Deploy</em></p>
The handling of arguments is performed by the method SolarWinds.InformationService.Verb. VerbExecutorContext.UnpackageParameters(XmlElement[], Stream)
:
At [1]
, the Type
is retrieved for the given verb argument.
At [2]
, a DataContractSerializer
is initialized with the retrieved argument type.
At [3]
and [4]
, the argument is deserialized.
We know that we are dealing with a DataContractSerializer
. We cannot control the deserialization types though. My first thought was: I had already found some abusable PropertyBag
classes. Maybe there are more to be found here?
It quickly turned out to be a good direction. There are multiple SWIS verbs that accept arguments of a type named SolarWinds.InformationService.Addons.PropertyBag
. We can provide arbitrary XML to be deserialized to an object of this type. Let’s investigate!
At [1]
, the ReadXml
method is defined. It will be called during deserialization.
At [2]
, the code iterates over the provided items.
At [3]
, the key
element is retrieved. If present, the code continues.
At [4]
, the value of the type
element is retrieved. One may safely assume where it leads.
At [5]
, the value
element is retrieved.
At [6]
, the Deserialize
method is called, and the data contained in both the value
and type
tags are provided as input.
At [7]
, the serialized payload and type name are passed to the SolarWinds.InformationService.Serialization.SerializationHelper.Deserialize
method.
Again, both the type and the serialized payload are controlled by the attacker. Let’s check this deserialization method.
At [1]
, the code checks if the provided type is cached.
If not, the type is retrieved from a string at [2]
.
At [3]
, the static DeserializeFromStrippedXml
is called.
As you can see, the static DeserializeFromStrippedXml
method retrieves a serializer object by calling SerializationHelper.serializerCache.GetSerializer(type)
. Then, it calls the (non-static) DeserializeFromStrippedXml(string)
method on the retrieved serializer object.
Let’s see how the serializer is retrieved.
At [1]
, the code tries to retrieve the serializer from a cache. In case of a cache miss, it retrieves the serializer by calling GetSerializerInternal
([2]
), so our investigation continues with GetSerializerInternal
.
At [3]
, an XmlTypeMapping
is retrieved on the basis of the attacker-controlled type. It does not implement any security measures. It is only used to retrieve some basic information about the given type.
At [4]
, an XmlStrippedSerializer
object is initialized. Four arguments are supplied to the constructor:
• A new XmlSerializer
instance, where the type of the serializer is controlled by the attacker(!).
• The XsdElementName
of the target type, obtained from the XmlTypeMapping
.
• The Namespace
of the type, also obtained from the XmlTypeMapping
.
• The type itself.
So far, we have two crucial facts:
• We are switching deserializers. The overall SWIS verb payload and arguments are deserialized with a DataContractSerializer
. However, our PropertyBag
object will eventually be deserialized with an XmlSerializer
.
• We fully control the type provided to the XmlSerializer
constructor, which is a key condition for exploitation.
It seems that we have it, another RCE through type control in deserialization. As XmlSerializer
can be abused through the ObjectDataProvider
, we can set the target deserialization type to the following:
System.Data.Services.Internal.ExpandedWrapper`
2[[System.Web.UI.LosFormatter, System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a],[System.Windows.Data.ObjectDataProvider, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35]], System.Data.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089`
However, let’s analyze the XmlStrippedSerializer.DeserializeFromStrippedXml(String)
before celebrating.
Something unusual is happening here. At [1]
, a new XML string is being created. It has the following structure:
<XsdElementName xmlns=’Namespace’>ATTACKER-XML</XsdElementName>
To sum up:
• The attacker’s XML gets wrapped with a tag derived from the delivered type (see GetSerializerInternal
method).
• Moreover, the retrieved Namespace
is inserted into the xmlns
attribute.
The attacker controls a major fragment of the final XML and controls the type. However, due to the custom XML wrapping, the ysoserial.net
gadget will not work out of the box. The generated gadget looks like this:
The first tag is equal to ExpandedWrapperOfLosFormatterObjectDataProvider
. This tag will be automatically generated by the DeserializeFromStrippedXml
method, thus we need to remove it from the generated payload! When we do so, the following XML will be passed to the XmlSerializer.Deserialize
method:
We still have a major issue here. Can you spot it?
When you compare both the original ysoserial.net gadget and our current gadget, one big difference can be spotted:
• The original gadget defines two namespaces in the root tag: xsi
and xsd
.
• The current gadget contains an empty xmlns
attribute only.
The ObjectInstance
tag relies on the xsi
namespace. Consequently, deserialization will fail.
Luckily, the namespace does not have to be defined in the root tag specifically. Accordingly, we can fix our gadget by defining both namespaces in the ProjectedProperty0
tag. The final gadget is as follows:
In this way, we get a third RCE, where we fully control the target deserialization type!
Here is a fragment of the API request, where the malicious SWIS verb argument is defined:
CVE-2022-36964
Technically, this issue is identical to CVE-2022-36958. However, it exists in a different class that shares the same implementation of the ReadXml
method. In this case, the vulnerable class is SolarWinds.InformationService.Contract2.PropertyBag
.
An argument of this type is accepted by the TestAlertingAction
SWIS verb, thus this issue is exploitable through the API.
This class may appear familiar to some of you. I already abused that same class with JSON.NET deserialization in CVE-2021-31474. Almost one and a half years later, I realized that this class can be abused in a totally different way as well.
Summary
In this blog post, I have shown you four different deserialization vulnerabilities in SolarWinds where the attacker could control the type of the deserialized object. One of them was particularly interesting, because DataContractSerializer
could be used to ultimately reach XmlSerializer
. During my Hexacon 2023 talk, I will show you some of the patches applied to the described issues and I will show you how I have bypassed them by using custom deserialization gadgets.
I hope you liked this writeup. Until my next post, you can follow me @chudypb and follow the team on Twitter, Mastodon, LinkedIn, or Instagram for the latest in exploit techniques and security patches.
Article Link: Zero Day Initiative — Finding Deserialization Bugs in the SolarWind Platform