.NET反序列化漏洞之繞過 SerializationBinder 不安全的類型綁定
概述
很多 .NET 應用程序在修復 `BinaryFormatter` 、 `SoapFormatter` 、`LosFormatter` 、 `NetDataContractSerializer` 、`ObjectStateFormatter ` 等反序列化漏洞時,喜歡通過自定義 `SerializationBinder` 來限定類型,從而達到緩解反序列化攻擊的目的。歷史上很多 .NET 反序列化漏洞都采用了這種方法,但是我們查看微軟官方的警告說明:

使用 `SerializationBinder` 無法完全修復反序列化漏洞隱患。最近看到老外發了一篇相關文章,感覺很有價值,自己也深入研究總結了兩種不安全的 `SerializationBinder` 限定方式,下面分享給大家。
SerializationBinder 綁定限制
常見修復方式就是對 `BinaryFormatter` 反序列化過程綁定 `Binder` 對象,通過 `SerializationBinder` 來檢查反序列化類型。構建如下 `demo`。
反序列化操作如下:
using (var fileStream = new FileStream(file, FileMode.Open)){ BinaryFormatter formatter = new BinaryFormatter(); fileStream.Position = 0; formatter.Binder=new SafeDeserializationBinder(); formatter.Deserialize(fileStream);}
自定義 `SafeDeserializationBinder` 繼承于 `SerializationBinder` ,通過黑名單機制進行檢查,當發現存在惡意類型時,比如 `System.Data.DataSet` ,將阻斷反序列化過程:
public class SafeDeserializationBinder : SerializationBinder{ List blackTypeName = new List<string> { };
private void _AddBlackList() { blackTypeName.Add("System.Data.DataSet"); }
public override Type BindToType(string assemblyName, string typeName) { this._AddBlackList(); foreach (var t in blackTypeName) { if (typeName.Equals(t)) { //todo } } return Type.GetType(typeName); }}
Bypass 1 :無效的 null 返回值
大家很容易想到,當檢測到反序列化黑名單,直接返回 `null` :

這樣真的可以阻斷反序列化漏洞嗎?我們可以進行測試。利用 `YSoSerial.Net` 特定生成 `System.Data.DataSet` 的反序列化載荷:
ysoserial.exe -o raw -f BinaryFormatter -g DataSet -c calc >payload.txt

確實返回了 `null` ,但是發現最終還是執行了反序列化操作并觸發了 RCE:

為什么呢?下面調試分析一下原因。`BinaryFormatter` 反序列化時將調用 `ObjectReader#Bind` 來獲取 `Type` 類型:

首先調用自定義的 `SafeDeserializationBinder#BindToType` ,當返回 `null` 時,函數并沒有直接結束,而是繼續調用 `FastBindToType` 來獲取 `Type` 對象:

首先嘗試從 `typecache` 緩存中提取,程序首次調用獲取不到值,繼續判斷 `bSimpleAssembly` 的取值(默認始終為 `true`),進而嘗試調用 `GetSimplyNamedTypeFromAssembly` :

通過 `FormatterServices#GetTypeFromAssembly` 最終取到了 `type` 的值:

所以嘗試在 `SerializationBinder` 加載惡意 `Type` 時通過返回 `null` 是無法阻斷反序列化漏洞的。比如 Exchange CVE-2022-23277 就是由于在遇到黑名單時最終返回 `null` 從而導致被繞過。要想 `SerializationBinder` 有效,正確的做法是拋出異常,修改 `demo` 如下:

拋出異常將中斷后續處理流程,導致反序列化綁定的 `Type` 最終確定為 `null` ,從而無法觸發反序列化漏洞。
Bypass 2 :拋出異常真的安全嗎?
上面通過拋出異常的方式真的能夠完全修復漏洞嗎?答案是否定的。我們思考下既然反序列化操作可以通過 `BindToType` 檢查 `Type` 和 `Assembly` ,那么在生成序列化載荷時,就可能可以自定義 `Type` 和 `Assembly` ,查看 `YSoSerial.Net` 生成 `System.Data.DataSet` 的反序列化載荷的代碼段 ( `/Generators/DataSetGenerator.cs` ):

我們可以手動去修改 `Type` 的賦值過程,確保讓其不位于黑名單之中,比如:

重新編譯生成 `YSoSerial.Net` ,并再次生成新的 `DataSet` 反序列化載荷:

此時 `typeName` 并不在黑名單之中,所以不會拋出異常,但是卻成功返回了正確的 `Type` 類型,從而繞過檢查進而實現了 RCE :

比如 DevExpress CVE-2022-28684 反序列化漏洞就是通過類似上面這種方式實現 Bypass 的。
小結
通過 `SerializationBinder` 綁定 `Type` 類型來緩解反序列化漏洞,無論是直接返回 `null` 還是拋出異常,都存在被繞過的風險,最好的修復方式其實微軟官方已經給出了答案,那就是不要使用 `BinaryFormatter` 這類反序列化類:
