Some notes about Microsoft Exchange Deserialization RCE (CVE-2021–42321)

8 min readNov 19, 2021


Vietnamese version:


It’s been several months since our last story about ProxyShell Exploit and recently Exchange was pwned again at Tianfu Cup 2021. We’re very excited about that Exploit and we’re waiting for Tuesday Patch of MS Exchange this month to analyse it.

There’s already a blog analysis about CVE-2021–42321 and just published yesterday. But we think that blog didn’t cover enough technical information and highlight notes so we decided to write this blog in English to let everyone understand what happened inside this CVE!

From the advisory of Microsoft, it stated that this CVE is a post-auth RCE. We just wonder that is a pre-auth RCE because it costs $200.000 when you have a successful demonstration at Tianfu Cup 2021. But with the patch from MS we only know that MS patch the post-auth RCE, maybe MS let the customer have time to patch the post-auth RCE and later release another patch for an auth bypass vulnerability?

If we look carefully at the advisory of Microsoft we can notice that only Exchange 2016 CU 21,22 and Exchange 2019 CU 10,11 . This means the only recent latest version of Exchange 2016,2019 are vulnerable to this CVE

Microsoft also release a patch for Exchange 2016,2019 before the Tianfu Cup happened and Exchange was pwned after this patch, so we need to diff the patch October and November 2021.

After decompiling with DnSpy and diffing with Win Merge we got about 275 files were changed (╯°□°)╯︵ ┻━┻.

The patch almost patching for Deserialization Sink inside Exchange, so we can be sure that this time it’s a deserialization vulnerability.


We noticed that some files were removed

There’s a funny point inside TypedBinaryFormatter is when deserialize the binder value was not passed correctly to ExchangeBinaryFormatterFactory . As we already know that SerializationBinder is used to control the actual types used during serialization and deserialization. This can be a mechanism deserialization vulnerability but in TypedBinaryFormatter we have a chance for it again :) .


Without the binder variable pass to ExchangeBinaryFormatterFactory.CreateBinaryFormatter() , ExchangeBinaryFormatterFactory will use ChainedSerializationBinder as SerializationBinder to validate actual types used during deserialization


When passing the argument from TypedBinaryFormatter.Deserialize() to ExchangeBinaryFormatterFactory.CreateBinaryFormatter() Exchange initialize ChainedSerializationBinder (implement SerializationBinder) with:

  • strictMode = false
  • allowList = System.DelegateSerializationHolder
  • allowedGenerics = null

Let’s have a look at ChainedSerializationBinder.BindToType() which will validate the class for Deserialization.


For Block 1:

  • If the strictMode is False and our class is not in allow list, and in blacklist , Exchange will throw InvalidOperationException
  • In this function, it only catches BlockedDeserializationException so InvalidOperationException will not be caught, so if Exchange through InvalidOperationException our deserialization chain will be broken

For Block 2:

  • If there’s a BlockedDeserializationException while ValidateTypeToDeserialize() was thrown, it will be caught in catch block but Exchange only throws that Exception when the value of the flag is True, but remembered that the strictMode value was passed to ChainedSerializationBinder is always False ! So there’s Exchange will catch BlockedDeserializationException safely and Exchange didn’t affect our Deserialization can continue without crashing.

Alright, now let’s have a look at ChainedSerializationBinder’s blacklist. It’s a big mistake of Exchange developers …

When deserialize ClaimsPrincipal, it also triggers another BinaryFormatter.Deserialize() without any filter. At this time, we can be sure that we found the Sink of this CVE



With Dnspy we tracing back to find where we can trigger deserialization

After some handy steps we found that, we can go from OrgExtensionSerializer.TryDeserialize() to ClientExtensionCollectionFormatter.Deserialize()

It gets the Stream from userConfiguration to deserialize it and sound like with a normal user we can set that setting also.

After a little bit of playing around with how can we set the userConfiguration from HTTP request we found this document from Microsoft

But from the public document, we can’t find how to set Stream into userConfiguration so we dive into the logic of EWS to see how we can set it

We clearly see that there’s a XML node name “BinaryData”, which sound like the serialization data we want to set


Basically, our request to EWS looks like this

sample request for setting BinaryData

Now we can set userConfiguration but how to trigger the actual deserialization sink?

Tracing back from OrgExtensionSerializer.TryDeserialize() we can reach to GetClientAccessToken

With this API from MS documents, now we can trigger the deserialization sink

sample request for GetClientAccessToken


We finally achieve post-auth RCE:

  1. Create UserConfiguration with BinaryData as our Gadget Chain
  2. Request to EWS for GetClientAccessToken to trigger the Deserialization

About the gadget chain:

  • We can embed any gadget chain into System.Security.Claims.ClaimsPrincipal to trigger 2nd Deserialization
  • Or we can use directly use TypeConfusedDelegate Chain (This chain is not in the blacklist of ChainedSerializationBinder and I don’t know why :> )


We already can pop calc on our Lab environment but how about the actual environment with up to date Windows Defender?

This is what we got :)

Since ProxyLogon, ProxyShell, and till now some EDRs,AV,sysmon and Microsoft Windows Defender try to catch and prevent process spawn from w3wp.exe process. This also annoys us but we need some improvements to overcome it!

As we know that we can use ClaimsPrincipal or TypeConfusedDelegate gadget chain to achieve post-auth RCE, but directly executing cmd.exe is not a good idea

With some improvements from @zcgonvh to , gadget ActivitySurrogateSelector can use to load a DLL via Assembly.Load() function. The whole idea is from @zcgonvh blog also.

This helps us achieve mem-shell when exploiting .net deserialization. Instead of calling Process.Start() now we move on eval JScript. With JScript, we can do many things with scripting instead of spawning cmd.exe / powershell.exe.

A snippet of code to eval JScript

Next, we write a new plugin for with ClaimsPrincipal (this is almost the same with ClaimsIdentity) then put ActivitySurrogateSelector object into ClaimsPrincipal gadget chain.

But ActivitySurrogateSelector has some limitations with .NET 4.8 and later

Shout out to @monoxgas for ActivitySurrogateDisableTypeCheck gadget chain to disable the protection for ActivitySurrogateSelector

Everything we need now is to put ActivitySurrogateDisableTypeCheck into ClaimsPrincipal

We’ve been laughing for hours while making this meme

The main idea of ActivitySurrogateDisableTypeCheck gadget chain is when deserializing it will call GetObjectFromSerializationInfo() and finnally reach to XamlReader.Parse() and run our Xaml payload to set value for DisableActivitySurrogateSelectorTypeCheck

Our Xaml payload was executed but …

There’s an InvalidCastException was thrown before we can set DisableActivitySurrogateSelectorTypeCheck to True, and this Exception was not caught by Exchange so this chain won’t work and we cannot change DisableActivitySurrogateSelectorTypeCheck because of crashing w3wp.exe process!

So we can’t use ActivitySurrogateDisableTypeCheck gadget chain. Now we need to find another gadget chain or another way to call XamlReader.Parse()

Remember that we can use TypeConfusedDelegate ?

With TypeConfusedDelegate we can invoke the method arbitrarily ( ͡° ͜ʖ ͡°)

This is how we call Process.Start()

How about XamlReader.Parse() ? XamlReader.Parse is a public/static also, so we can easily call it with TypeConfusedDelegate

from TypeConfusedDelegate -> XamlReader.Parse()

Finally, everything works as our expected, we can change DisableActivitySurrogateSelectorTypeCheck to True to overcome the limitation of .NET and later inject DLL to achieve mem-shell with Jscript to bypass the detection

PoC video:

PoC: We don’t think we will …

Final Thought

This exploit only work for

  • Microsoft Exchange 2019 CU10, 11
  • Microsoft Exchange 2016 CU21, 22

And didn’t affect another version, who knows what happened behind the scenes with a typo mistake on ChainedSerializationBinder ¯\_(ツ)_/¯