.Net 反序列化学习之 DataContractSerializer

 

作者:HuanGMz@知道创宇404实验室

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 生成专门的动态代码,并使用这些动态代码完成序列化和反序列化。

  • knownTypesIEnumerable<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<student, ObjectDataProvider>。

这是因为 ExpandedWrapper 的使用情形是为了在目标类型可控时,在一个 type 参数中,同时告知 DataContractSerializer 多个类型,这里由于 DataContractResolver 的特点,我们可以直接在xml中指定类型,没有必要再使用 ExpandedWrapper。

 

附录

[如何查看DataContractSerializer 生成的动态代码]

https://docs.microsoft.com/zh-cn/archive/blogs/curth/viewing-emitted-il/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/https://www.blackhat.com/docs/us-17/thursday/us-17-Munoz-Friday-The-13th-JSON-Attacks-wp.pdf

(完)