Microsoft Exchange Deserialization RCE (CVE-2021–42321)
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 analyze 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 released 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 strict mode 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 strict mode 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 user configuration 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 user configuration from HTTP requests we found this document from Microsoft(link)
But from the public document, we can’t find how to set Stream into user configuration so we dive into the logic of EWS to see how we can set it
We clearly see that there’s an XML node name “BinaryData”, which sound like the serialization data we want to set
Basically, our request to EWS looks like this
Now we can set user configuration 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(link)
We finally achieve post-auth RCE:
- Create UserConfiguration with BinaryData as our Gadget Chain
- Request to EWS for GetClientAccessToken to trigger the Deserialization
About the gadget chain:
- We can embed any ysoserial.net gadget chain into System.Security.Claims.ClaimsPrincipal to trigger 2nd Deserialization
- Or we can use directly use the 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
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 ysoserial.net, the gadget ActivitySurrogateSelector can use to load a DLL via Assembly.Load() function. The whole idea is from the @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.
Next, we write a new plugin for ysoserial.net with ClaimsPrincipal (this is almost the same with ClaimsIdentity) then put the ActivitySurrogateSelector object into the 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
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.
How about XamlReader.Parse() ? XamlReader.Parse is a public/static also, so we can easily call it with TypeConfusedDelegate
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
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.