作者:HuanGMz@知道创宇404实验室
时间:2021年5月11日
DataContractSerializer 是一个序列化工具,可以将 类实例序列化为xml内容。DataContractSerializer 与 XmlSerializer 有很多相似之处,比如 都将类型实例序列化为xml数据、在初始化序列化器时 都需要先传入目标类型、都会依据目标类型 生成专门的动态代码用于完成序列化和反序列化。不过 XmlSerializer生成的动态代码可以单步跟进去,而 DataContractSerializer 生成的动态代码无法查看,也就无从知道它反序列化的细节。
DataContractSerializer 的反序列化漏洞 与 XmlSerializer 的也很相似,都需要控制传入的目标类型以及xml数据。但是在研究 Exchange 反序列化漏洞 CVE-2021-28482 时发现,原来 DataContractSerializer 还有一种漏洞情况:当目标类型不可控,但目标类型有字段为 object 类型 且 使用了DataContractResolver进行松散的类型解析 ,可以在该属性位置插入任何 gadget。
构造函数之一:
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);
参数:
序列化或反序列化的实例的类型。
指定该DataContractSerializer实例 用于对什么类进行序列化和反序列化。DataContractSerializer 会依据传入的type 生成专门的动态代码,并使用这些动态代码完成序列化和反序列化。
IEnumerable<Type> 类型,可以是 List<Type>
该参数用于告知序列化器: 目标类型中使用了哪些其他类型。在没有dataContractResolver参数的情况下,该参数很有必要。如果没有指定 dataContractResolver,又没有指定 knownTypes,当目标类型中有其他未知类型时,就会报错。
要序列化或反序列化的最大项数。 默认值为 MaxValue]属性返回的值。
要在序列化和反序列化时忽略类型扩展提供的数据,则为 true
;否则为 false
。
要使用非标准的 XML 结构来保留对象引用数据,则为 true
;否则为 false
。
一个用于自定义序列化过程的 IDataContractSurrogate实现。
对 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中指定为任意类型,从而反序列化出任意类型。
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() 对参数的要求:
要获取的类型的程序集限定名称。 请参阅 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 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1584/