Attacker Value
Moderate
(1 user assessed)
Exploitability
Moderate
(1 user assessed)
User Interaction
None
Privileges Required
Low
Attack Vector
Adjacent_network
2

CVE-2022-21969

Disclosure Date: January 11, 2022
Exploited in the Wild
Add MITRE ATT&CK tactics and techniques that apply to this CVE.

Description

Microsoft Exchange Server Remote Code Execution Vulnerability

Add Assessment

1
Ratings
Technical Analysis

There is a nice writeup on this at https://medium.com/@frycos/searching-for-deserialization-protection-bypasses-in-microsoft-exchange-cve-2022-21969-bfa38f63a62d. The bug appears to be a deserialization bug that occurs when loading a specific file, however according to the demo video at https://gist.github.com/Frycos/a446d86d75c09330d77f37ca28923ddd it seems to be more of a local attack. That being said it would grant you an LPE to SYSTEM if you were able to trigger it. Furthermore Frycos mentions that he thinks Microsoft didn’t fix the root issue when he wrote the blog (as of January 12th 2022), so its possible the root issue wasn’t fixed, though Frycos mentioned he didn’t look into this further.

From https://twitter.com/MCKSysAr/status/1524518517990727683 it does seem like at the very least some exploitation attempts have been made to try exploit this although writing to C:\Program Files\Microsoft\Exchange Server\V15\UnifiedMessaging\voicemail to trigger the bug via making it process a voicemail has proven to be difficult to do. It does however note my tip, shown later in this writeup, of how to bypass the deny list by using System.Runtime.Remoting.ObjRef as was pointed out online, was valid.

What follows below is some of my notes that I wrote up a while back and never published. Hopefully they are of help to someone.

Overview

Background info

Deserialization vulnerability leading to RCE potentially.
Got a CVSS 3.1 score of 9.0 with a temporal score metric score of 7.8.

Interesting that it mentions the attack vector is Adjacent and the article notes that this may be only cause of the way that he exploited it and may indicate they didn’t fix the root issue.

Low attack complexity and low privileges required seems to indicate it may be authenticated but you don’t need many privileges??? I need to check on this further.

High impact on everything else suggest this is a full compromise; this would be in line with leaking the hash.

Affected

  • Microsoft Exchange Server 2019 Cumulative Update 11 prior to January 2022 security update.
  • Microsoft Exchange Server 2019 Cumulative Update 10 prior to January 2022 security update.
  • Microsoft Exchange Server 2016 Cumulative Update 22 prior to January 2022 security update.
  • Microsoft Exchange Server 2016 Cumulative Update 21 prior to January 2022 security update.
  • Microsoft Exchange Server 2013 Cumulative Update 23 prior to January 2022 security update.

Fixed By

KB5008631

Other vulns fixed in same patch

CVE-2022-21846 <– NSA reported this one.
CVE-2022-21855 <– Reported by Andrew Ruddick of MSRC.

Writeup Review

Original writeup: https://www.instapaper.com/read/1487196325

We have well known sinks in [[.NET]] whereby one can make deserialization calls from unprotected formatters such as BinaryFormatter. These formatters as noted in [[CVE-2021-42321]] don’t have any SerializationBinder or similar binders attached to them, which means that they are open to deserialize whatever they like, without any binder limiting them to what they can deserialize.

Initial search for vulnerabilities took place around Exchange’s Rpc functions, which use a binary protocol created by Microsoft for communication instead of using normal HTTP requests.

Looking around we can see Microsoft.Exchange.Rpc.ExchangeCertificates.ExchangeCertificateRpcServer contains several function prototypes:

// Microsoft.Exchange.Rpc.ExchangeCertificate.ExchangeCertificateRpcServer  
using System;  
using System.Security;  
using Microsoft.Exchange.Rpc;  
  
internal abstract class ExchangeCertificateRpcServer : RpcServerBase  
{  
    public unsafe static IntPtr RpcIntfHandle = (IntPtr)<Module>.IExchangeCertificate_v1_0_s_ifspec;  
  
    public abstract byte[] GetCertificate(int version, byte[] pInBytes);  
  
    public abstract byte[] CreateCertificate(int version, byte[] pInBytes);  
  
    public abstract byte[] RemoveCertificate(int version, byte[] pInBytes);  
  
    public abstract byte[] ExportCertificate(int version, byte[] pInBytes, SecureString password);  
  
    public abstract byte[] ImportCertificate(int version, byte[] pInBytes, SecureString password);  
  
    public abstract byte[] EnableCertificate(int version, byte[] pInBytes);  
  
    public ExchangeCertificateRpcServer()  
    {  
    }  
}

These are then implemented in Microsoft.Exchange.Servicelets.ExchangeCertificate.ExchangeCertificateServer.

// Microsoft.Exchange.Servicelets.ExchangeCertificate.ExchangeCertificateServer  
using System;  
using System.Security;  
using System.Security.AccessControl;  
using System.Security.Principal;  
using Microsoft.Exchange.Management.SystemConfigurationTasks;  
using Microsoft.Exchange.Rpc;  
using Microsoft.Exchange.Rpc.ExchangeCertificate;  
using Microsoft.Exchange.Servicelets.ExchangeCertificate;  
  
internal class ExchangeCertificateServer : ExchangeCertificateRpcServer  
{  
    internal const string RequestStoreName = "REQUEST";  
  
    private static ExchangeCertificateServer server;  
  
    public static bool Start(out Exception e)  
    {  
        e = null;  
        SecurityIdentifier securityIdentifier = new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null);  
        FileSystemAccessRule accessRule = new FileSystemAccessRule(securityIdentifier, FileSystemRights.Read, AccessControlType.Allow);  
        FileSecurity fileSecurity = new FileSecurity();  
        fileSecurity.SetOwner(securityIdentifier);  
        fileSecurity.SetAccessRule(accessRule);  
        try  
        {  
            server = (ExchangeCertificateServer)RpcServerBase.RegisterServer(typeof(ExchangeCertificateServer), fileSecurity, 1u, isLocalOnly: false);  
            return true;  
        }  
        catch (RpcException ex)  
        {  
            RpcException ex2 = (RpcException)(e = ex);  
            return false;  
        }  
    }  
  
    public static void Stop()  
    {  
        if (server != null)  
        {  
            RpcServerBase.StopServer(ExchangeCertificateRpcServer.RpcIntfHandle);  
            server = null;  
        }  
    }  
  
    public override byte[] CreateCertificate(int version, byte[] inputBlob)  
    {  
        return ExchangeCertificateServerHelper.CreateCertificate(ExchangeCertificateRpcVersion.Version1, inputBlob);  
    }  
  
    public override byte[] GetCertificate(int version, byte[] inputBlob)  
    {  
        return ExchangeCertificateServerHelper.GetCertificate(ExchangeCertificateRpcVersion.Version1, inputBlob);  
    }  
  
    public override byte[] RemoveCertificate(int version, byte[] inputBlob)  
    {  
        return ExchangeCertificateServerHelper.RemoveCertificate(ExchangeCertificateRpcVersion.Version1, inputBlob);  
    }  
  
    public override byte[] ExportCertificate(int version, byte[] inputBlob, SecureString password)  
    {  
        return ExchangeCertificateServerHelper.ExportCertificate(ExchangeCertificateRpcVersion.Version1, inputBlob, password);  
    }  
  
    public override byte[] ImportCertificate(int version, byte[] inputBlob, SecureString password)  
    {  
        return ExchangeCertificateServerHelper.ImportCertificate(ExchangeCertificateRpcVersion.Version1, inputBlob, password);  
    }  
  
    public override byte[] EnableCertificate(int version, byte[] inputBlob)  
    {  
        return ExchangeCertificateServerHelper.EnableCertificate(ExchangeCertificateRpcVersion.Version1, inputBlob);  
    }  
}

Examining these functions we can see a lot of them take a byte array input named byte[] inputBlob. If we follow the ImportCertificate() function here as an example we can see that the implementation will call into Microsoft.Exchange.Servicelets.ExchangeCertificate.ExchangeCertificateServerHelper, as is also true for the other functions.

// Microsoft.Exchange.Servicelets.ExchangeCertificate.ExchangeCertificateServerHelper  
using System;  
using System.Collections.Generic;  
using System.Management.Automation;  
using System.Security;  
using System.Security.Cryptography;  
using System.Security.Cryptography.X509Certificates;  
using System.Text;  
using Microsoft.Exchange.Data;  
using Microsoft.Exchange.Data.Common;  
using Microsoft.Exchange.Data.Directory;  
using Microsoft.Exchange.Data.Directory.Management;  
using Microsoft.Exchange.Data.Directory.SystemConfiguration;  
using Microsoft.Exchange.Extensions;  
using Microsoft.Exchange.Management.FederationProvisioning;  
using Microsoft.Exchange.Management.Metabase;  
using Microsoft.Exchange.Management.SystemConfigurationTasks;  
using Microsoft.Exchange.Management.Tasks;  
using Microsoft.Exchange.Net;  
using Microsoft.Exchange.Security.Cryptography.X509Certificates;  
using Microsoft.Exchange.Servicelets.ExchangeCertificate;  
  
internal class ExchangeCertificateServerHelper  
{  
   
... 
  
    public static byte[] ImportCertificate(ExchangeCertificateRpcVersion rpcVersion, byte[] inputBlob, SecureString password)  
    {  
        bool flag = false;  
        ExchangeCertificateRpc exchangeCertificateRpc = new ExchangeCertificateRpc(rpcVersion, inputBlob, null);  
        if (string.IsNullOrEmpty(exchangeCertificateRpc.ImportCert))  
        {  
            return ExchangeCertificateRpc.SerializeError(rpcVersion, Strings.ImportCertificateDataInvalid, ErrorCategory.ReadError);  
        }  
        Server server = null;  
        ITopologyConfigurationSession topologyConfigurationSession = DirectorySessionFactory.Default.CreateTopologyConfigurationSession(ConsistencyMode.IgnoreInvalid, ADSessionSettings.FromRootOrgScopeSet(), 1159, "ImportCertificate", "d:\\dbs\\sh\\e19dt\\1103_100001\\cmd\\c\\sources\\Dev\\Management\\src\\ServiceHost\\Servicelets\\ExchangeCertificate\\Program\\ExchangeCertificateServer.cs");  
        try  
        {  
            server = ManageExchangeCertificate.FindLocalServer(topologyConfigurationSession);  
        }  
        catch (LocalServerNotFoundException)  
        {  
            flag = true;  
        }  
        if (flag || !ManageExchangeCertificate.IsServerRoleSupported(server))  
        {  
            return ExchangeCertificateRpc.SerializeError(rpcVersion, Strings.RoleDoesNotSupportExchangeCertificateTasksException, ErrorCategory.InvalidOperation);  
        }  
        X509Store x509Store = new X509Store(StoreName.My, StoreLocation.LocalMachine);  
        try  
        {  
            x509Store.Open(OpenFlags.ReadWrite | OpenFlags.OpenExistingOnly);  
        }  
        catch (CryptographicException)  
        {  
            x509Store = null;  
        }  
        List<ServiceData> installed = new List<ServiceData>();  
        GetInstalledRoles(topologyConfigurationSession, server, installed);  
        try  
        {  
            byte[] array = null;  
            if (CertificateEnroller.TryAcceptPkcs7(exchangeCertificateRpc.ImportCert, out var thumbprint, out var untrustedRoot))  
            {  
                X509Certificate2Collection x509Certificate2Collection = x509Store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false);  
                if (x509Certificate2Collection.Count > 0)  
                {  
                    if (!string.IsNullOrEmpty(exchangeCertificateRpc.ImportDescription))  
                    {  
                        x509Certificate2Collection[0].FriendlyName = exchangeCertificateRpc.ImportDescription;  
                    }  
                    ExchangeCertificate exchangeCertificate = new ExchangeCertificate(x509Certificate2Collection[0]);  
                    UpdateServices(exchangeCertificate, server, installed);  
                    exchangeCertificateRpc.ReturnCert = exchangeCertificate;  
                }  
                return exchangeCertificateRpc.SerializeOutputParameters(rpcVersion);  
            }  
            if (untrustedRoot)  
            {  
                return ExchangeCertificateRpc.SerializeError(rpcVersion, Strings.ImportCertificateUntrustedRoot, ErrorCategory.ReadError);  
            }  
            try  
            {  
                array = Convert.FromBase64String(exchangeCertificateRpc.ImportCert);  
            }  
            catch (FormatException)  
            {  
                return ExchangeCertificateRpc.SerializeError(rpcVersion, Strings.ImportCertificateBase64DataInvalid, ErrorCategory.ReadError);  
            }  
            X509Certificate2 x509Certificate = null;  
            try  
            {  
                X509KeyStorageFlags x509KeyStorageFlags = X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet;  
                bool flag2 = password == null || password.Length == 0;  
                X509Certificate2Collection x509Certificate2Collection2 = new X509Certificate2Collection();  
                if (exchangeCertificateRpc.ImportExportable)  
                {  
                    x509KeyStorageFlags |= X509KeyStorageFlags.Exportable;  
                }  
                x509Certificate2Collection2.Import(array, flag2 ? null : password.AsUnsecureString(), x509KeyStorageFlags);  
                x509Certificate = ManageExchangeCertificate.FindImportedCertificate(x509Certificate2Collection2);  
            }  
            catch (CryptographicException)  
            {  
                return ExchangeCertificateRpc.SerializeError(rpcVersion, Strings.ImportCertificateDataInvalid, ErrorCategory.ReadError);  
            }  
            if (x509Certificate == null)  
            {  
                return ExchangeCertificateRpc.SerializeError(rpcVersion, Strings.ImportCertificateDataInvalid, ErrorCategory.ReadError);  
            }  
            if (!string.IsNullOrEmpty(exchangeCertificateRpc.ImportDescription))  
            {  
                x509Certificate.FriendlyName = exchangeCertificateRpc.ImportDescription;  
            }  
            if (x509Store.Certificates.Find(X509FindType.FindByThumbprint, x509Certificate.Thumbprint, validOnly: false).Count > 0)  
            {  
                return ExchangeCertificateRpc.SerializeError(rpcVersion, Strings.ImportCertificateAlreadyExists(x509Certificate.Thumbprint), ErrorCategory.WriteError);  
            }  
            x509Store.Add(x509Certificate);  
            ExchangeCertificate exchangeCertificate2 = new ExchangeCertificate(x509Certificate);  
            UpdateServices(exchangeCertificate2, server, installed);  
            exchangeCertificateRpc.ReturnCert = exchangeCertificate2;  
        }  
        finally  
        {  
            x509Store?.Close();  
        }  
        return exchangeCertificateRpc.SerializeOutputParameters(rpcVersion);  
    }  
  
...

We can see from this that most functions appear to be calling Microsoft.Exchange.Management.SystemConfigurationTasks.ExchangeCertificateRpc.ExchangeCertificateRpc(). This has some interesting code relevant to deserialization:

// Microsoft.Exchange.Management.SystemConfigurationTasks.ExchangeCertificateRpc  
using System.Collections.Generic;  
using Microsoft.Exchange.Rpc.ExchangeCertificate;  
  
public ExchangeCertificateRpc(ExchangeCertificateRpcVersion version, byte[] inputBlob, byte[] outputBlob)  
{  
    inputParameters = new Dictionary<RpcParameters, object>();  
    if (inputBlob != null)  
    {  
        switch (version)  
        {  
        case ExchangeCertificateRpcVersion.Version1:  
            inputParameters = (Dictionary<RpcParameters, object>)DeserializeObject(inputBlob, customized: false);  
            break;  
        case ExchangeCertificateRpcVersion.Version2:  
            inputParameters = BuildInputParameters(inputBlob);  
            break;  
        }  
    }  
    outputParameters = new Dictionary<RpcOutput, object>();  
    if (outputBlob != null)  
    {  
        switch (version)  
        {  
        case ExchangeCertificateRpcVersion.Version1:  
            outputParameters = (Dictionary<RpcOutput, object>)DeserializeObject(outputBlob, customized: false);  
            break;  
        case ExchangeCertificateRpcVersion.Version2:  
            outputParameters = BuildOutputParameters(outputBlob);  
            break;  
        }  
    }  
}

Here we can see that the byte[] inputBlob from earlier is passed to DeserializeObject(inputBlob, customized: false) in the case that ExchangeCertificateRpcVersion parameter passed in is ExchangeCertificateRpcVersion.Version1.

Okay so already we know we have one limitation in that we need to set the version parameter here to ExchangeCertificateRpcVersion.Version1 somehow.

Keeping this in mind lets explore further and look at the Microsoft.Exchange.Management.SystemConfigurationTasks.ExchangeCertificateRpc.DeserializeObject(inputBlob, customized:false) call implementation.

// Microsoft.Exchange.Management.SystemConfigurationTasks.ExchangeCertificateRpc  
using System.IO;  
using Microsoft.Exchange.Data.Serialization;  
using Microsoft.Exchange.Diagnostics;  
  
private object DeserializeObject(byte[] data, bool customized)  
{  
    if (data != null)  
    {  
        using (MemoryStream serializationStream = new MemoryStream(data))  
        {  
            bool strictModeStatus = Microsoft.Exchange.Data.Serialization.Serialization.GetStrictModeStatus(DeserializeLocation.ExchangeCertificateRpc);  
            return ExchangeBinaryFormatterFactory.CreateBinaryFormatter(DeserializeLocation.ExchangeCertificateRpc, strictModeStatus, allowedTypes, allowedGenerics).Deserialize(serializationStream);  
        }  
    }  
    return null;  
}

Interesting so we can see that we create a new MemoryStream object from our byte[] data parameter and use this to create a serialization stream of type MemoryStream. We then check using Microsoft.Exchange.Data.Serialization.Serialization.GetStrictModeStatus to see if DeserializeLocation.ExchangeCertificateRpc requires strict mode for deserialization or not and we set the boolean strictModeStatus to this result.

Finally we create a binary formatter using ExchangeBinaryFormatterFactory.CreateBinaryFormatter(DeserializeLocation.ExchangeCertificateRpc, strictModeStatus, allowedTypes, allowedGenerics) and then call its Deserialize() method on the serialized MemoryStream object we created earlier using byte[] data.

Note that before the November 2021 patch, this DeserializeObject function actually looked like this:

// Microsoft.Exchange.Management.SystemConfigurationTasks.ExchangeCertificateRpc  
using System.IO;  
using Microsoft.Exchange.Data.Serialization;  
using Microsoft.Exchange.Diagnostics;  
  
private object DeserializeObject(byte[] data, bool customized)  
{  
    if (data != null)  
    {  
        using (MemoryStream serializationStream = new MemoryStream(data))  
        {  
            BinaryFormatter binaryFormatter = new BinaryFormatter();
			if (customized)
			{
				binaryFormatter.Binder = new CustomizedSerializationBinder();
			}
			return binaryFormatter.Deserialize(memoryStream);
        }  
    }  
    return null;  
}

As we can see the earlier code here was using BinaryFormatter to deserialize the payload without using a proper SerializationBinder or really any protection at all for that matter.

Looking At DeserializeObject() Deeper

Lets look at the November 2022 edition of Microsoft.Exchange.Management.SystemConfigurationTasks.ExchangeCertificateRpc.DeserializeObject(inputBlob, customized:false) again:

// Microsoft.Exchange.Management.SystemConfigurationTasks.ExchangeCertificateRpc  
using System.IO;  
using Microsoft.Exchange.Data.Serialization;  
using Microsoft.Exchange.Diagnostics;  
  
private object DeserializeObject(byte[] data, bool customized)  
{  
    if (data != null)  
    {  
        using (MemoryStream serializationStream = new MemoryStream(data))  
        {  
            bool strictModeStatus = Microsoft.Exchange.Data.Serialization.Serialization.GetStrictModeStatus(DeserializeLocation.ExchangeCertificateRpc);  
            return ExchangeBinaryFormatterFactory.CreateBinaryFormatter(DeserializeLocation.ExchangeCertificateRpc, strictModeStatus, allowedTypes, allowedGenerics).Deserialize(serializationStream);  
        }  
    }  
    return null;  
}

What we want to check here now is the ExchangeBinaryFormatterFactor.CreateBinaryFormatter call. What does the code for this look like?

// 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)  
    };  
}

Ah our good old friend ChainedSerializationBinder and BinaryFormatter. Looks like we will need to create a BinaryFormatter serialized payload and ChainedSerializationBinder will be the validator.

As mentioned in the article to bypass this logic we need to ensure that strictMode is set to False and that we are not using any fully qualified assembly name in the deny list defined in Microsoft.Exchange.Diagnostics.ChainedSerializationBinder.GlobalDisallowedTypesForDeserialization, which will pretty much kill all publicly known .NET deserialization gadgets from ysoserial.NET.

For reference this is the code for ChainedSerializationBinder in November 2021 Update:

// 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();  
  
  
    protected static HashSet<string> GlobalDisallowedGenericsForDeserialization { get; private set; } = BuildGlobalDisallowedGenericsForDeserialization();  
  
  
    public ChainedSerializationBinder()  
    {  
        serializationOnly = true;  
    }  
  
    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;  
    }  
  
    public override void BindToName(Type serializedType, out string assemblyName, out string typeName)  
    {  
        string text = null;  
        string text2 = null;  
        InternalBindToName(serializedType, out assemblyName, out typeName);  
        if (assemblyName == null && typeName == null)  
        {  
            assemblyName = text;  
            typeName = text2;  
        }  
    }  
  
    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;  
    }  
  
    protected virtual Type InternalBindToType(string assemblyName, string typeName)  
    {  
        return LoadType(assemblyName, typeName);  
    }  
  
    protected Type LoadType(string assemblyName, string typeName)  
    {  
        Type type = null;  
        try  
        {  
            type = Type.GetType($"{typeName}, {assemblyName}");  
        }  
        catch (TypeLoadException)  
        {  
        }  
        catch (FileLoadException)  
        {  
        }  
        if (type == null)  
        {  
            string shortName = assemblyName.Split(',')[0];  
            try  
            {  
                type = Type.GetType($"{typeName}, {shortName}");  
            }  
            catch (TypeLoadException)  
            {  
            }  
            catch (FileLoadException)  
            {  
            }  
            if (type == null)  
            {  
                Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();  
                IEnumerable<Assembly> source = assemblies.Where((Assembly x) => shortName == x.FullName.Split(',')[0]);  
                Assembly assembly = (source.Any() ? source.First() : null);  
                if (assembly != null)  
                {  
                    type = assembly.GetType(typeName);  
                }  
                else  
                {  
                    Assembly[] array = assemblies;  
                    foreach (Assembly assembly2 in array)  
                    {  
                        try  
                        {  
                            type = assembly2.GetType(typeName);  
                            if (!(type != null))  
                            {  
                                continue;  
                            }  
                            return type;  
                        }  
                        catch  
                        {  
                        }  
                    }  
                }  
            }  
        }  
        return type;  
    }  
  
    protected virtual void InternalBindToName(Type serializedType, out string assemblyName, out string typeName)  
    {  
        assemblyName = null;  
        typeName = null;  
    }  
  
    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;  
            }  
        }  
    }  
  
    private static HashSet<string> BuildDisallowedGenerics()  
    {  
        return new HashSet<string> { typeof(SortedSet<>).GetGenericTypeDefinition().FullName };  
    }  
  
    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.Claims.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"  
        };  
    }  
  
    private static HashSet<string> BuildGlobalDisallowedGenericsForDeserialization()  
    {  
        return new HashSet<string>();  
    }  
}

Interesting to note that this doesn’t seem to contain the entries for System.Runtime.Remoting.ObjectRef which was a new gadget chain just added with https://github.com/pwntester/ysoserial.net/pull/115 that relies on a rouge .NET remoting server like https://github.com/codewhitesec/RogueRemotingServer. There is a writeup on this at https://codewhitesec.blogspot.com/2022/01/dotnet-remoting-revisited.html that explains more but this would allow RCE via a serialized payload attached to the rouge .NET remoting server.

Anyway so from earlier we know that the strict mode is determined via the line bool strictModeStatus = Microsoft.Exchange.Data.Serialization.Serialization.GetStrictModeStatus(DeserializeLocation.ExchangeCertificateRpc); so this provides our other bypass.

Lets check if the result of this is False or not:

So from here we can likely supply a System.Runtime.Remoting.ObjectRef, take advantage of the lack of strict checking on this, and get the whole exploit to work. The problem now is finding the whole chain to reach this vulnerable call and then trigger the deserialization.

January 2022 Patch Analysis

  • No adjustments to the ChainedSerializationBinder deny list at all.

Here is the Jan 2022 version of the deny list:

    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.Claims.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"  
        };  
    }

Looking at this in [[Meld]] shows that the deny list for ChainedSerializationBinder did not change between November 2021 and January 2022. So we could use System.Runtime.Remoting.ObjRef to bypass this deny list, potentially also allowing RCE on the latest version.

  • Removed Microsoft.Exchange.DxStore.Common.DxBinarySerializationUtil which seemed to have some options for doing unsafe deserialization.
using System;
using System.IO;
using FUSE.Weld.Base;
using Microsoft.Exchange.Diagnostics;
using Microsoft.Exchange.DxStore.Server;

namespace Microsoft.Exchange.DxStore.Common;

public static class DxBinarySerializationUtil
{
	private static readonly string[] allowedTypes = new string[101]
	{
		typeof(ExceptionUri).FullName,
		typeof(Ranges).FullName,
		typeof(Range).FullName,
		typeof(Target).FullName,
		typeof(CommonSettings).FullName,
		typeof(DataStoreStats).FullName,
		typeof(DxStoreAccessClientException).FullName,
		typeof(DxStoreAccessClientTransientException).FullName,
		typeof(DxStoreAccessReply).FullName,
		typeof(DxStoreAccessReply.CheckKey).FullName,
		typeof(DxStoreAccessReply.DeleteKey).FullName,
		typeof(DxStoreAccessReply.DeleteProperty).FullName,
		typeof(DxStoreAccessReply.ExecuteBatch).FullName,
		typeof(DxStoreAccessReply.GetAllProperties).FullName,
		typeof(DxStoreAccessReply.GetProperty).FullName,
		typeof(DxStoreAccessReply.GetPropertyNames).FullName,
		typeof(DxStoreAccessReply.GetSubkeyNames).FullName,
		typeof(DxStoreAccessReply.SetProperty).FullName,
		typeof(DxStoreAccessRequest).FullName,
		typeof(DxStoreAccessRequest.CheckKey).FullName,
		typeof(DxStoreAccessRequest.DeleteKey).FullName,
		typeof(DxStoreAccessRequest.DeleteProperty).FullName,
		typeof(DxStoreAccessRequest.ExecuteBatch).FullName,
		typeof(DxStoreAccessRequest.GetAllProperties).FullName,
		typeof(DxStoreAccessRequest.GetProperty).FullName,
		typeof(DxStoreAccessRequest.GetPropertyNames).FullName,
		typeof(DxStoreAccessRequest.GetSubkeyNames).FullName,
		typeof(DxStoreAccessRequest.SetProperty).FullName,
		typeof(DxStoreAccessServerTransientException).FullName,
		typeof(DxStoreBatchCommand).FullName,
		typeof(DxStoreBatchCommand.CreateKey).FullName,
		typeof(DxStoreBatchCommand.DeleteKey).FullName,
		typeof(DxStoreBatchCommand.DeleteProperty).FullName,
		typeof(DxStoreBatchCommand.SetProperty).FullName,
		typeof(DxStoreBindingNotSupportedException).FullName,
		typeof(DxStoreClientException).FullName,
		typeof(DxStoreClientTransientException).FullName,
		typeof(DxStoreCommand).FullName,
		typeof(DxStoreCommand.ApplySnapshot).FullName,
		typeof(DxStoreCommand.CreateKey).FullName,
		typeof(DxStoreCommand.DeleteKey).FullName,
		typeof(DxStoreCommand.DeleteProperty).FullName,
		typeof(DxStoreCommand.DummyCommand).FullName,
		typeof(DxStoreCommand.ExecuteBatch).FullName,
		typeof(DxStoreCommand.PromoteToLeader).FullName,
		typeof(DxStoreCommand.SetProperty).FullName,
		typeof(DxStoreCommand.UpdateMembership).FullName,
		typeof(DxStoreCommand.VerifyStoreIntegrity).FullName,
		typeof(DxStoreCommand.VerifyStoreIntegrity2).FullName,
		typeof(DxStoreCommandConstraintFailedException).FullName,
		typeof(DxStoreInstanceClientException).FullName,
		typeof(DxStoreInstanceClientTransientException).FullName,
		typeof(DxStoreInstanceComponentNotInitializedException).FullName,
		typeof(DxStoreInstanceKeyNotFoundException).FullName,
		typeof(DxStoreInstanceNotReadyException).FullName,
		typeof(DxStoreInstanceServerException).FullName,
		typeof(DxStoreInstanceServerTransientException).FullName,
		typeof(DxStoreInstanceStaleStoreException).FullName,
		typeof(DxStoreManagerClientException).FullName,
		typeof(DxStoreManagerClientTransientException).FullName,
		typeof(DxStoreManagerGroupNotFoundException).FullName,
		typeof(DxStoreManagerServerException).FullName,
		typeof(DxStoreManagerServerTransientException).FullName,
		typeof(DxStoreReplyBase).FullName,
		typeof(DxStoreRequestBase).FullName,
		typeof(DxStoreSerializeException).FullName,
		typeof(DxStoreServerException).FullName,
		typeof(DxStoreServerFault).FullName,
		typeof(DxStoreServerTransientException).FullName,
		typeof(HttpReply).FullName,
		typeof(HttpReply.DxStoreReply).FullName,
		typeof(HttpReply.ExceptionReply).FullName,
		typeof(HttpReply.GetInstanceStatusReply).FullName,
		typeof(HttpRequest).FullName,
		typeof(HttpRequest.DxStoreRequest).FullName,
		typeof(HttpRequest.GetStatusRequest).FullName,
		typeof(HttpRequest.GetStatusRequest.Reply).FullName,
		typeof(HttpRequest.PaxosMessage).FullName,
		typeof(InstanceGroupConfig).FullName,
		typeof(InstanceGroupMemberConfig).FullName,
		typeof(InstanceGroupSettings).FullName,
		typeof(InstanceManagerConfig).FullName,
		typeof(InstanceSnapshotInfo).FullName,
		typeof(InstanceStatusInfo).FullName,
		typeof(LocDescriptionAttribute).FullName,
		typeof(PaxosBasicInfo).FullName,
		typeof(PaxosBasicInfo.GossipDictionary).FullName,
		typeof(ProcessBasicInfo).FullName,
		typeof(PropertyNameInfo).FullName,
		typeof(PropertyValue).FullName,
		typeof(ReadOptions).FullName,
		typeof(ReadResult).FullName,
		typeof(WcfTimeout).FullName,
		typeof(WriteOptions).FullName,
		typeof(WriteResult).FullName,
		typeof(WriteResult.ResponseInfo).FullName,
		typeof(GroupStatusInfo).FullName,
		typeof(GroupStatusInfo.NodeInstancePair).FullName,
		typeof(InstanceMigrationInfo).FullName,
		typeof(KeyContainer).FullName,
		typeof(DateTimeOffset).FullName
	};

	private static readonly string[] allowedGenerics = new string[6] { "System.Collections.Generic.ObjectEqualityComparer`1", "System.Collections.Generic.EnumEqualityComparer`1", "System.Collections.Generic.EqualityComparer`1", "System.Collections.Generic.GenericEqualityComparer`1", "System.Collections.Generic.KeyValuePair`2", "System.Collections.Generic.List`1" };

	public static void Serialize(MemoryStream ms, object obj)
	{
		ExchangeBinaryFormatterFactory.CreateSerializeOnlyFormatter().Serialize(ms, obj);
	}

	public static object DeserializeUnsafe(Stream s)
	{
		return ExchangeBinaryFormatterFactory.CreateBinaryFormatter(DeserializeLocation.HttpBinarySerialize).Deserialize(s);
	}

	public static object Deserialize(Stream s)
	{
		return DeserializeSafe(s);
	}

	public static object DeserializeSafe(Stream s)
	{
		return ExchangeBinaryFormatterFactory.CreateBinaryFormatter(DeserializeLocation.SwordFish_AirSync, strictMode: false, allowedTypes, allowedGenerics).Deserialize(s);
	}
}
  • Added in Microsoft.Exchange.DxStore.Common.IDxStoreDynamicConfig.cs which has the following code:
namespace Microsoft.Exchange.DxStore.Common;

public interface IDxStoreDynamicConfig
{
	bool IsRemovePublicKeyToken { get; }

	bool IsSerializerIncompatibleInitRemoved { get; }

	bool EnableResolverTypeCheck { get; }

	bool EnableResolverTypeCheckException { get; }
}

Exploit Chain

Lets start at the deserialization chain and work backwards.

Microsoft.Exchange.Management.SystemConfigurationTasks.ExchangeCertificateRpc.DeserializeObject
Microsoft.Exchange.Management.SystemConfigurationTasks.ExchangeCertificateRpc.ExchangeCertificateRpc(ExchangeCertificateRpcVersion version, byte[] inputBlob, byte[] outputBlob)
Microsoft.Exchange.Servicelets.ExchangeCertificate.ExchangeCertificateServerHelper.GetCertificate(int version, byte[] inputBlob)
Microsoft.Exchange.Servicelets.ExchangeCertificate.ExchangeCertificateServer.GetCertificate(int version, byte[] inputBlob)

We can then use the Get-ExchangeCertificate commandlet from https://docs.microsoft.com/en-us/powershell/module/exchange/get-exchangecertificate?view=exchange-ps and set a breakpoint inside Microsoft.Exchange.ExchangeCertificateServicelet.dll specifically within the Microsoft.Exchange.Servicelets.ExchangeCertificate.GetCertificate handler.

Unfortunately it seems like the current way things work we are sending a ExchangeCertificateRpcVersion rpcVersion with a version of Version2.

Exploited process is Microsoft.Exchange.ServiceHost.exe which runs as NT AUTHORITY\SYSTEM.

CVSS V3 Severity and Metrics
Base Score:
9.0 Critical
Impact Score:
6
Exploitability Score:
2.3
Vector:
CVSS:3.1/AV:A/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H
Attack Vector (AV):
Adjacent_network
Attack Complexity (AC):
Low
Privileges Required (PR):
Low
User Interaction (UI):
None
Scope (S):
Changed
Confidentiality (C):
High
Integrity (I):
High
Availability (A):
High

General Information

Vendors

  • microsoft

Products

  • exchange server 2013,
  • exchange server 2016,
  • exchange server 2019

Exploited in the Wild

Reported by:

Additional Info

Technical Analysis