1.XmlSerializer反序列化点以及ObjectDataProvider链

2024-10-08 18:05   中国  

1.前言

本系列记录一下关于.NET安全中反序列化相关的安全知识吧,网上的很多关于这方面的文章感觉都有些年头了,主要侧重于审计层面,跟随前辈的脚步来研究一下。

2.关于XmlSerializer

首先我们就什么都不介绍,就先看这个名字,XmlSerializer,一个处理Xml格式数据的方法。稍有经验的安全从业者都知道,一旦涉及到Xml格式数据处理,那漏洞可真是多如牛毛。普通的XXE这类漏洞就不说了,在JAVA安全中,更是有相当多案例,例如经典的Xstream反序列化等等。所以当我们一看到一个方法,同时和XML、序列化与反序列化沾边,应该马上能感受到这个危险程度。

但是我们先不看安全,还是先单纯看看XmlSerializer应该怎么玩吧。我们先写一个简单的代码来演示,我将代码写在ashx文件里方便演示。这里我先写一个简单的MyClass

里面有IdNameDescription三个属性。然后我们来写一下序列化和反序列化流程

可以看到,这里我们先创建MyClass类实例,对其属性赋值,然后调用XmlSerializer()方法。这里第一个细节,那就是XmlSerializer类构造函数接受一个Type类型的传参,这个可以简单类比为JAVA那边的class对象(详见ASP.NET的反射机制)。后续我们无论是Xml序列化还是反序列化,都是要根据这个Type来的,比如这边我要对MyClass类进行序列化与反序列化,就可以通过typeof(MyClass)来获取。除此之外还有一些方法,比如

Type.GetType(“命名空间+类名) //类似JAVA那边的forName()

object.Type //object为目标类的对象,类似JAVA那边的getClass()

这里引入第一个隐藏的知识点,那就是后续我们在反序列化利用的时候,至少这个Type我们得控制,要控制为目标类的Type类型。

那么继续,后面的代码其实就是将MyClass类的对象序列化成XML格式的数据,输出。然后尝试调用Deserialize()方法,将XML数据重新还原成一个对象(是不是感觉越来越危险了),并且输出对象的Name属性的值。

我们运行上述代码看看:

那么这就是序列化和反序列化的过程,你看出什么了吗?说实话,我也没看出来,所以我们得改造一下代码。首先改造一下MyClass

MyClass写一个构造函数,输出一个值,并且属性在getset时都执行一些操作,输出值(可以类比理解为JavaBean中的gettersetter概念)。这样在序列化和反序列化过程中,我们就能知道每个方法每个属性的赋值和调用了。

我们重新写一个ashx文件,读取testxml.txt里的内容并进行xml反序列化操作

Testxml.txt文件里就是之前提到的序列化数据

然后我们访问一下

我们可以发现一个令人毛骨悚然的事实,在反序列化过程中,实际上会依次调用MyClass类的构造函数创建对象,并依次调用每个属性的set进行赋值(可以理解为调用了setter?)。根据JAVA安全那边的经验,我们现在已经可以猜到这个过程有多危险了。接下来,只需要尝试寻找一些在这个过程中就能触发敏感操作的类,去调用即可,那么,有这样的类吗?

3.ObjectDataProvider

联想一下上面的条件,现在我们需要一个类,通过调用这个类的构造函数创建对象,然后再调用set赋值的过程中就能触发敏感操作的类,.NET里有这样的类吗?前辈们找到的是ObjectDataProvider这个类,网上能找到很多关于它在开发方面用法的文章

搜索这个类的相关信息,我们可以知道这玩意是在WPF里使用的,这个用法就很类似CC链里的InvokerTransformer。我们也来玩玩这个类,这里注意调用这个类的时候得是C# WPF项目,网上很多关于XmlSerializer反序列化的文章都没有细说这一点,在这里研究了半天,还是看开发那边的教程才知道的,这个类在System.Windows.Data命名空间,要么创建一个WPF项目,要么创造一个.NET Framework控制台应用,然后在程序集配置里手动添加System.Windows.Data命名空间对应的dll文件(后续会提到)

由于我好奇WPF的代码长啥样,创建了个WPF项目看看,师傅们要复现不一定选择WPF,因为后续要用到的另一个类又和WCF相关,又要换来换去的,还是直接选择.NET Framework控制台应用吧

这里实例化ObjectDataProvider类的对象,然后对其MethodParameters属性赋值,这个值就是后续要调用方法的参数。然后对其MethodName属性赋值,即要调用的方法名,最后对ObjectInstance属性赋值,值为目标类的对象,这里最终实现的效果就是调用System.Diagnostics命名空间下的Process类的Start方法,经典的.NET命令执行函数

运行上述代码,成功弹出计算器

很好,现在似乎就可以串起来了,我们如果把上面这个ObjectDataProvider类的对象进行一个XML序列化,是不是就能拿到一个用于RCEPayload了?一不做二不休,我们来试试

可以看到在序列化的时候这里就有一个异常

System.InvalidOperationException:“There was an error generating the XML document.”InvalidOperationException: The type System.Diagnostics.Process was not expected. Use the XmlInclude or SoapInclude attribute to specify types that are not known statically.

InvalidOperationException: 不应是类型 System.Windows.Data.ObjectDataProvider。使用 XmlInclude SoapInclude 特性静态指定非已知的类型。

这个要怎么办呢,看了各种仙贝对这个问题的分析,大概的结论是ObjectDataProviderObjectInstance还要承载一个恶意类的对象,XML序列化就不支持了。

反编译ObjectDataProvider所处的dll PresentationFramework.dll,找到对应的属性,发现这是一个object类型的属性

我们自己简单模拟一下这段代码,看看会不会触发一样地异常信息

发现触发异常,如果把object改为目标类的类型Test2,则可以正常反序列化

很奇葩的是,当我去掉下面这一行代码的时候

o.ObjectInstance = new Process();

它还真就能运行下去

总之我们姑且认为就是这个原因,这个object类型的属性在序列化过程中在干扰我们,xml也许期望一个具体的类型。那我们应该怎么办呢?仙贝们找到的方法是用ExpandedWrapper<TExpandedElement, TProperty0>来进行包装:

所以我们可以用如下代码进行测试

当然这里有个小细节,就是导入命名空间的时候会出错

using System.Windows.Data;using System.Data.Services.Internal;

这里解决方法是右键添加引用

点击浏览,然后找到命名空间对应的dll文件进行导入,导入完之后就好啦

总之,我们可以先来试试用ExpandedwrapperObjectDataProvider进行包装的效果

还是成功,那我们再试试把这个Expandedwrapper的对象进行序列化呢?感觉离成功很近了

但是还是不行啊,用ExpandedWrapper直接封装System.Diagnostics.Process是没法序列化的。这里我写了一个恶意类,其中有一个恶意方法。我们看看不直接调用System.Diagnostics.Process,而是通过自己写的恶意类间接调用能不能行

这回就成了,总之比较关键的是这个用于包装的类Expandedwrapper,但是对于其具体的工作原理和实现还是有点迷糊,现在大概推断是它相当于把原先模糊的object类型修改为了目标类类型?说不定是错误理解,改天再详细研究一下XmlSerializerExpandedwrapper的细节。

我们还是把上面生成的序列化数据放到一个文件里

然后运行下面这个反序列化流程

成功rce。

总之,我们大概研究了一下ObjectDataProviderExpandedwrapper的使用。目前其实已经可以进行一些敏感操作了,比如你发现目标有一个用于执行命令的工具类,就可以通过上述这个攻击链生成payload去攻击了。但还是很不爽,还是要靠开发赏饭吃,我们能不能用.NET自带的类再组一下攻击链,彻底摆脱这个限制呢?

4.ResourceDictionary

又是一个很有趣的类,介绍这个类之前,还是先尽量从开发的视角去看一些问题。首先我们前面介绍了ObjectDataProvider这个类,知道这个类是和WPF密切相关的,当我们搜索WPF ObjectDataProvider相关关键词学习用法的时候,你会发现很多开发方向的师傅都提到了这玩意一般情况下是和xaml配合使用的

你会发现,像我们上面那样单独调用Objectdataprovider反而是异类,这玩意的真正主场是在xaml,可以在Xaml里去调用任意类的特定方法。我们都不需要看别的东西,直接参考上面那个案例,其实大概就知道要怎么构造一个恶意xaml内容了,无非就是用上面的这个写法去调用Process类的Start方法,并且给参数传入呗。最后构造出的xaml代码和上面只有一点点差异:

还是有点差异的,不过很好理解,首先是:

xmlns:b="clr-namespace:System;assembly=mscorlib"xmlns:c="clr-namespace:System.Diagnostics;assembly=system"

这两行是拿到SystemSystem.Diagnostics命名空间,并且分别设置别名为bc。后续在ObjectType中调用System.Diagnostics命名空间中的Process类中的Start方法。然后再往后,这个方法接受String类型的传参,因此从System命名空间中拿到String,其键值就是要执行的命令。

我们的xaml payload就构造好了,一旦上述xaml内容被解析执行,应该就能RCE了。那么下一个问题,我们要怎么动态解析执行这个xaml内容呢?那我们直接搜索一下啊:

通过参考微软官方文档以及开发相关的教程可以知道,想要动态解析执行xaml格式的内容,主要是通过XamlReader类中的Load()方法和Parse()方法,Parse()方法也只是把Load()方法包装了一下,支持传入string类型的xaml内容直接执行。

所以我们不妨来试一下:

这里读取testxaml.txt中的内容,转为字符串形式,然后传入XamlReader.Parse()方法,txt中的内容即为我们上面构造好的xaml payload

成功RCE

那么现在我们可以思考一个问题,我们上面这样的调用方式,并不是直接对ObjectDataProviderProcess类进行序列化与反序列化操作。而是通过对xaml的解析去间接调用ObjectDataProvider并进一步调用Process。这样是否能规避XmlSerializer的限制呢?终于来到最后一步了。

5.链子串联

终于来到最后一步了,现在我们的思路其实很清晰了,原本我们是直接通过实例化ObjectDataProvider尝试去调用开发者自写的恶意类,因为直接调用Process类会失败。那现在我们可以尝试用ObjectDataProvider去调用XamlReader.Parse()方法,通过这个方法去解析执行xaml内容,看看这样能不能绕过种种限制呢?我们来手搓一个链子试试。

得到如下内容:

接下来激动人心的时刻到了,我们用xmlSerializer类的反序列化流程尝试对上述payload进行反序列化

成功弹出计算器,至此,我们的整个反序列化流程就成功实现了。

6.复盘与总结

总的来说还是一个非常有趣的漏洞。先从XmlSerializer在反序列化过程中的性质入手,去寻找一些可能被利用的类,这点和我们在JAVA安全研究中的一些思想很类似。我们找到一个有趣的类,ObjectDataProvider,类似JAVA CC链中的Invokertransformer,都是可以起到类似反射调用任意方法的效果。为了将这个类改造为允许通过XmlSerializer进行序列化的格式,我们需要通过Expandedwrapper进行包装。但这样仍然不能直接调用Process进行反序列化,只能间接调用。于是换一个思路,用ObjectDataProvider调用XamlReader类的Parse()方法,去加载恶意xaml代码去进行RCE。这个流程中一些关键方法的调用栈为:

System.Xml.Serialization.XmlSerializer#Deserialize  System.Windows.Data.ObjectDataProvider#ObjectDataProvider    System.Windows.Markup.XamlReader#Parse      System.Windows.Data.ObjectDataProvider#ObjectDataProvider        System.Diagnostics.Process#Start

如果单纯想研究在代码审计中要如何发现XmlSerializer反序列化问题,主要关注两个地方,一个是初始化XmlSerializer的时候,Type属性是否可控。另一个则是反序列化的xml内容是否可控,上面我们是直接用typeof()传入属性类型去获取的type,根据仙贝们的经验,实战中想要利用的话,比较常见的是用Type.GetType()去获取type,所以我们生成序列化payload的时候,还可以顺手把这个Type输出一下

System.Data.Services.Internal.ExpandedWrapper`2[[System.Windows.Markup.XamlReader, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35],[System.Windows.Data.ObjectDataProvider, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35]]

当然这只是手搓链子的场景,实战中用ysoserial.net生成payload来打就行,这里顺便介绍一下ysoserial.net的基础使用

说明里提到了ObjectDataProvider这条链子,并且指明了它适用的反序列化点类型,也就是本文的XmlSerializer,可以看到这条利用链很多反序列化点都能用得上。使用如下命令生成payload

ysoserial.exe -f XmlSerializer -g ObjectDataProvider -o raw -c "calc" -t

-f参数指定反序列化点类型,-g参数指定gadget类型,我们这里是ObjectDataProvider链子,然后-o指定以原始格式输出,-c指定要执行的命令类型

这里注意到type类型是输出在root节点的,猜测是.net开发者的习惯或者某些约定俗成的规矩?从xmlroot节点获取type?不懂。

随后我们假设下面这个环境:

注意这个GetType接受的传参是否可控,且注意上下文中能否控制xml内容,若都可以,则可以尝试打XmlSerializer反序列化漏洞。总之和我们上面搞的东西是一毛一样的。

此外,上面我们还学到一些敏感方法,那就是XamlReader类的LoadParse方法,审计的时候如果看到相关方法,也可以注意其接受的内容是否可控,如果可以的话则直接传入恶意Xaml内容即可实现RCE

最后一点感叹,在学习这个漏洞的过程中,我参考的最多的不是安全方面的文章,而是开发方面的文章,很多知识点对于安全研究者来说可能很新鲜而且比较难以理解,但是熟悉开发的师傅看到这些知识点可能就会想“卧槽,这不是常识吗”,比如本文中和WPF开发有关的一些知识点,早就有无数开发者写过相关的文章了。也难怪一些开发功底扎实的师傅转行安全之后很快就能发现一些更深层更有趣的漏洞,还是要有一个扎实的代码功底,多学习各种开发相关的知识才行。

7.参考

https://www.freebuf.com/articles/web/197400.htmlhttps://xz.aliyun.com/t/9592https://www.cnblogs.com/Ivan1ee/p/16255289.htmlhttps://www.cnblogs.com/Peter-Luo/p/12150734.htmlhttps://www.cnblogs.com/T-ARF/p/12369534.htmlhttps://www.cnblogs.com/AtTheMoment/p/15065650.htmlhttps://www.cnblogs.com/nice0e3/p/16942833.htmlhttps://www.freebuf.com/articles/network/323531.htmlhttps://www.freebuf.com/articles/network/323531.html

HW专项行动小组
大师!教我打攻防
 最新文章