作者:HuanGMz@知道創宇404實驗室
時間:2021年5月11日
DataContractSerializer 是一個序列化工具,可以將 類實例序列化為xml內容。DataContractSerializer 與 XmlSerializer 有很多相似之處,比如 都將類型實例序列化為xml數據、在初始化序列化器時 都需要先傳入目標類型、都會依據目標類型 生成專門的動態代碼用于完成序列化和反序列化。不過 XmlSerializer生成的動態代碼可以單步跟進去,而 DataContractSerializer 生成的動態代碼無法查看,也就無從知道它反序列化的細節。
DataContractSerializer 的反序列化漏洞 與 XmlSerializer 的也很相似,都需要控制傳入的目標類型以及xml數據。但是在研究 Exchange 反序列化漏洞 CVE-2021-28482 時發現,原來 DataContractSerializer 還有一種漏洞情況:當目標類型不可控,但目標類型有字段為 object 類型 且 使用了DataContractResolver進行松散的類型解析 ,可以在該屬性位置插入任何 gadget。
DataContractSerializer 的兩種漏洞情形
構造函數之一:
public DataContractSerializer (Type type,
System.Collections.Generic.IEnumerable<Type> knownTypes,
int maxItemsInObjectGraph,
bool ignoreExtensionDataObject,
bool preserveObjectReferences,
System.Runtime.Serialization.IDataContractSurrogate dataContractSurrogate,
System.Runtime.Serialization.DataContractResolver dataContractResolver);
參數:
- type
序列化或反序列化的實例的類型。
指定該DataContractSerializer實例 用于對什么類進行序列化和反序列化。DataContractSerializer 會依據傳入的type 生成專門的動態代碼,并使用這些動態代碼完成序列化和反序列化。
- knownTypes
IEnumerable<Type> 類型,可以是 List<Type>
該參數用于告知序列化器: 目標類型中使用了哪些其他類型。在沒有dataContractResolver參數的情況下,該參數很有必要。如果沒有指定 dataContractResolver,又沒有指定 knownTypes,當目標類型中有其他未知類型時,就會報錯。
- maxItemsInObjectGraph
要序列化或反序列化的最大項數。 默認值為 MaxValue]屬性返回的值。
- ignoreExtensionDataObject
要在序列化和反序列化時忽略類型擴展提供的數據,則為 true;否則為 false。
- preserveObjectReferences
要使用非標準的 XML 結構來保留對象引用數據,則為 true;否則為 false。
- dataContractSurrogate
一個用于自定義序列化過程的 IDataContractSurrogate實現。
- dataContractResolver
對 DataContractResolver 抽象類的實現。
用于將 xsi:type 聲明映射到數據協定類型。
常見的DataContractSerializer 漏洞的原理是第一個參數 type 可控,此時我們可以讓DataContractSerializer 反序列化出我們想要的類型。當我們傳入特意構造的xml數據,使得DataContractSerializer 反序列化出一個 ObjectDataProvider 實例,又由于ObjectDataProvider 的特性,調用Process.Start() 函數,就能實現執行代碼。
但是DataContractSerializer 還有兩個重要的參數,knownTypes 和 dataContractResolver,他們都用于解決 在序列化或反序列化時,目標類型中包含其他未知類型的情形。其中,knownTypes 是一個 IEnumerable<Type>,直接記錄所有的未知類型,而dataContractResolver 是一個DataContractResolver 類的實現,該類定義了兩個函數 用于在序列化或反序列化時 完成xml數據中類型名稱與實際類型之間的轉換翻譯。
某些程序在實現DataContractResolver 類的時候,對類型的解析沒有任何限制,用戶可以在xml中指定節點類型為任意類型。此時,如果初始化 DataContractSerializer 時參數type(即目標類型)不可控,但目標類型中有一個字段為object 類型,我們就可以將這個object類型在xml中指定為任意類型,從而反序列化出任意類型。
DataContractResolver
public abstract class DataContractResolver
{
public abstract bool TryResolveType(Type type, Type declaredType, DataContractResolver knownTypeResolver, out XmlDictionaryString typeName, out XmlDictionaryString typeNamespace);
public abstract Type ResolveName(string typeName, string typeNamespace, Type declaredType, DataContractResolver knownTypeResolver);
}
繼承該類需要實現兩個函數 TryResolveType 和 ResolveName。
TryResolveType() 用于在序列化時獲取目標對象的類型,并返回字符串類型的 typeName 和 typeNamespace。
ResolveName() 用于在反序列化時 對xsi:type 屬性指定的類型進行解析,獲取對應的類型。
比如,CVE-2021-282482 中 EntityDataContractResolver : DataContractResolver 對這兩個方法的實現如下:
在ResolveName() 的輸入參數中,typeName 由 xml中的 xsi:type 來指定,但細節是怎樣的呢?
隨便寫一個測試程序,當xml如下(xsi 簡化為 i):
<TestClass xmlns="http://schemas.datacontract.org/2004/07/DataContractSerializerTest" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<propertyValues xmlns:c="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
<c:KeyValueOfstringanyType>
<c:Key>test</c:Key>
<c:Value i:type="System.Diagnostics.Process, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
</c:Value>
</c:KeyValueOfstringanyType>
</propertyValues>
</TestClass>
可以看到,進入 ResolveName() 時,typeName參數就是由 xsi:type 所指定,而typeNamespace 使用了默認xml命名空間。
再看 EntityDataContractResolver 的 ResolveName() 方法,直接調用Type.GetType() 來獲取目標類型。 這樣只要我們在xsi:type 中用類型的 程序集限定名稱 來指定,就可以不用考慮 未知類型的限制了。
注意 Type.GetType() 對參數的要求:
- typeName
要獲取的類型的程序集限定名稱。 請參閱 AssemblyQualifiedName。 如果該類型位于當前正在執行的程序集中或者 mscorlib.dll/System.Private.CoreLib.dll 中,則提供由命名空間限定的類型名稱就足夠了。
所謂程序集限定名稱是指:類型名稱(包括其命名空間),后跟一個逗號,然后是程序集的顯示名稱。比如:
"System.Diagnostics.Process, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
xml中有時會用類名與命名空間分開的方式指定類型,如下:
<test i:type="d:Process" xmlns:d="http://schemas.datacontract.org/2004/07/System.Diagnostics"></test>
如果此時在ResolveName 下斷點, 會發現 typeName為 Process,typeNameSpace 則由 xmlns:d 來指定。
再看 EntityDataContractResolver 的 ResolveName() 方法,此時會先調用 Assembly.Load() 來加載程序集,然后從這個程序集里獲取類型。Assembly.Load() 的參數要求為程序集名稱的長或短形式。那么我們可以創建正確的xml如下:
<TestClass xmlns="http://schemas.datacontract.org/2004/07/DataContractSerializerTest" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<propertyValues xmlns:c="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
<c:KeyValueOfstringanyType>
<c:Key>test</c:Key>
<c:Value i:type="d:System.Diagnostics.Process" xmlns:d="System.Diagnostics.Process, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
</c:Value>
</c:KeyValueOfstringanyType>
</propertyValues>
</TestClass>
測試程序
DataContract 和 DataMember 特性用于指定類型和字段可以使用 DataContractSerializer 進行序列化。
MyDataContractReslver 是對DataContractResolver 的實現,其對類型的解析沒有任何限制,存在安全性問題。
ProcessClass 是用于在序列化時替代 System.Diagnostics.Process,如果直接使用 System.Diagnostics.Process 會報錯。在生成樣本xml后,我們將其中的 ProcessClass 替換為 System.Diagnostics.Process 即可。
VulnerableClass 是模擬的可能被攻擊的類型,該類型中有一個字段為object 類型。
在上面的代碼中,我們以VulnerableClass 為目標類型進行序列化,并且將object類型的myvalue 字段賦值為了 ObjectDataProvider() 類實例,并且通過ObjectDataProvider 調用 ProcessClass 的 ClassMethod 方法。
生成的樣例xml如下:
我們對生成的xml進行修改,去掉無用的屬性、將其中的 i:type 替換為 程序集限定名稱、將ClassProcess 替換為 System.Diagnostics.Process 等,最終的payload 如下:

我們使用該payload 進行反序列化測試,成功彈出計算器。
DataContractSerializer 的第二種漏洞情形有兩個條件:目標類型不可控,但是類型中有object 字段 ,且使用了沒有類型限制的 DataContractResolver 。
在上面的payload 中,我們直接使用ObjectDataProvider,而沒有使用 ExpandedWrapper
這是因為 ExpandedWrapper 的使用情形是為了在目標類型可控時,在一個 type 參數中,同時告知 DataContractSerializer 多個類型,這里由于 DataContractResolver 的特點,我們可以直接在xml中指定類型,沒有必要再使用 ExpandedWrapper。
附錄
[如何查看DataContractSerializer 生成的動態代碼]
https://docs.microsoft.com/zh-cn/archive/blogs/curth/viewing-emitted-il
[Oleksandr Mirosh 的blackhat paper]
https://www.blackhat.com/docs/us-17/thursday/us-17-Munoz-Friday-The-13th-JSON-Attacks-wp.pdf
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1584/