1.关于BinaryFormatter
其实非要说的话,BinaryFormatter才是.NET里最经典的序列化与反序列化相关类,从名字就可以看出来,这玩意大概是用来把对象序列化成二进制数据或者把二进制数据反序列化成一个对象的。打开这个类的文档,一上来就能看到软子醒目的警示:
软子直接就告诉开发者这玩意不安全,建议大家别去用。这个类实现了IFormatter接口。
在软子文档里搜索和IFormatter接口沾边的类,还可以找到好几个:
既然BinaryFormatter是对象与二进制数据之间的相互转化,那么不难猜测SoapFormatter是对象与SOAP格式数据之间的相互转换,至于ObjectStateFormatter和LosFormatter也是同理,应该都是为了实现对象与某种特定格式数据之间的序列化与反序列化操作。后续也会慢慢学习这些别的Formatter的反序列化问题。
2.使用BinaryFormatter进行序列化与反序列化
这里就来到一个很有趣的问题了,为什么BinaryFormatter会造成反序列化问题?这个还是要先学习一下怎么用BinaryFormatter进行序列化与反序列化操作,这里使用Y4er师傅的代码。
代码有啥含义注释写好了,直接看图即可。我们尝试运行上述代码
可以看到,除了设置了[NonSerialized]特性的n2属性,其它属性均成功反序列化。但这只是最简单的流程,从这里并不能看出有什么能利用的点。你们.NET就没有像JAVA那边的readObject()或者PHP的__destruct()一样的方法吗?其实还真有,而且在.NET这边还要更复杂一些,我们来慢慢介绍。
首先我们来介绍GetObjectData()和反序列化构造方法两种特殊的方法,这两个方法可以简单类比为JAVA那边的writeObject()和readObject()方法。其它代码基本不做改动,我们来改动一下MyObject类:
具体的改造信息我们也写在注释里了,总之,当一个类实现ISerializable接口时,就可以实现GetObjectData()和反序列化构造方法,可以类比为JAVA那边的writeObject()和readObject(),我们运行一下试试:
上图的演示中还和SerializationInfo扯上了关系,在序列化和反序列化过程中,我们可以借助这个类的对象来存储一些信息
除了这些AddValue方法,还有个值得关注的SetType方法,后续搓链子的时候有用
可以看到,在序列化流程的时候会调用GetObjectData()方法,在反序列化流程时会先调用反序列化构造方法。现在我们就可以想到第一个BinaryFormatter反序列化可能的成因。如果一个实现了ISerializable接口的敏感类,在反序列化构造方法里读取了一些info里的变量值,并且进行了一些敏感操作,是不是就能被用于攻击呢?这个猜想我们先按下不表。我们再继续介绍BinaryFormatter反序列化中一些可能被我们利用的特性。
在序列化和反序列化的过程中还有一些很有趣的特性,分别对应四个回调事件
乍一看可能不知道是什么意思,但是我用PHP POP链里的一些魔术方法来举例就很明白了,比如这个OnDeserializingAttribute,在反序列化之前调用,优先级大于反序列化构造方法。是不是就有种__wakeup()和__destruct()的既视感?我们再改造一下上面的MyObject类,添加下面这些方法:
用我们上面提到的特性对方法进行标记。然后再运行一次序列化与反序列化流程,看看调用顺序
可以看到,在序列化时,方法的调用顺序是依次调用TestOnSerializing、GetObjectData、TestOnSerialized。而在反序列化时,方法的调用顺序是TestOnDeserializing、反序列化构造方法、TestOnDeserialized方法。这些都符合我们上面对这些特性的介绍。
因此现在我们又有一个设想,既然TestOnDeserializing、TestOnDeserialized这样的被标记的方法,在反序列化流程中也会自动调用,那么我们设想一些类里如果也有类似的被特性标记的特殊方法,并且这些方法也进行了敏感操作,是否也可以被用于反序列化攻击呢?这个也等后续再揭晓。关于这些OnDeserializing相关的方法,其实还有一个究极有趣的特性。
假设我们这个MyObject类不再实现ISerializable接口,按理说此时GetObjectData方法和反序列化构造方法都会失效对吧,那这些被特性标记的方法还能有效吗?
注意此时,GetObjectData方法和反序列化构造方法确实都失效了,不再调用,但这些被标记的方法却依然按顺序忠实履行自己的使命。那我们可以猜测,即使一个类没有实现ISerializable接口,但是其相关方法中存在一些敏感操作,依然有可能成为反序列化链子中的一环。
最后要介绍的一个特性是序列化代理选择器,还记得我们前面提到过,Formatter相关类都要实现IFormatter接口吗。
其中这个SurrogateSelector序列化代理选择器很有意思,因为它可以接管序列化与反序列化流程。我们BinaryFormatter类就有一个构造函数,接受一个IsurrogateSelector类型的传参
当然这么看还是很抽象,简单来说,我们可以自己写一个选择器类,在其中自定义序列化与反序列化方法。在使用BinaryFormatter类进行序列化与反序列化操作时,可以使用这个选择器类,就可以把原先正常流程中的GetObjectData和反序列化构造方法换成我们定义的别的方法。直接看代码演示。额外加一个选择器类
详情在注释里也写得很清楚了,那么我们要怎么在BinaryFormatter里使用选择器呢?参考如下代码:
运行,查看结果
可以看到,此时和MyObject类里的GetObjectData方法以及反序列化构造函数已经没啥关系了,取而代之的是选择器里的GetObjectData方法以及SetObjectData方法。
总之,对于这些Formatter在序列化与反序列化时的流程、优先级、生命周期,参考先知上Y4er师傅的文章,有一个非常直观的图:
覆盖了上面介绍的几种情况。
有了这样的基础知识,我们现在可以尝试找一些能利用的敏感类了。
3.TextFormattingRunProperties链
也即ysoserial.net里的这个gadget
我们先不看已有的分析,看看能不能通过我们上面的一些奇思妙想把这个链子跟出来。还是先看看软子的文档,看看这个类是干啥的
一看,好家伙,又是一个和WPF有关的类,想到之前对Xaml的利用,感觉确实有戏啊。跟进去看看
首先我们可以注意到,这个类是实现了ISerializable接口的,根据我们上面的奇思妙想,如果他的反序列化构造方法或者某些OnDeserializing相关方法里的代码最终能跟到敏感操作,就相当于整出了一条反序列化链子,就类似我们在JAVA那边从readObject()一路找最终和sink点串在一起的感觉。
那么TextFormattingRunProperties是否能满足我们的期望呢?跟进其反序列化构造方法:
这个反序列化构造方法中大量调用GetObjectFromSerializationInfo()方法,把一个字符串和SerializationInfo类的对象传入了,我们跟进这个方法:
可以看到,实际上是对info调用GetString()方法,也即我们前面介绍过的从SerializationInfo里提取序列化时存储的一些变量值。比如上面的
this.GetObjectFromSerializationInfo("ForegroundBrush", info)
实际上就是从info里取出ForegroundBrush变量的值,接着爆的来了。它把这个值,直接传入XamlReader.Parse(),这个方法我们在上一篇对XmlSerializer反序列化的分析中介绍过,看到它就等于RCE。
也就是说,我们只需要在序列化时,在info里塞一个ForegroundBrush(别的也可以,符合GetObjectFromSerializationInfo的调用就行),其值为我们之前介绍过的恶意Xaml数据,就可以搓出一条链子来啊。既然如此,我们也来手搓一个吧,直接改我们前面的MyObject类。
这里其他的都很好理解,唯一一个很有意思的点就在于info.SetType()这一步。因为读者朋友可能会好奇,我们这里是用MyObject类去生成序列化数据,这也不是调用的TextFormattingRunProperties类生成的序列化数据呀,这样生成出来能用吗?关键就在SetType()这个方法,它指定序列化对象的类型,我们将其指定为TextFormattingRunProperties类的Type类型。在序列化过程中,SerializationInfo不仅存储对象的字段和属性值,还需要知道对象的具体类型,以便在反序列化时能够正确地重建对象,GPT给出的解释如下:
然后我们还是老样子,从testxaml.txt里读取恶意的xamlpayload,然后借助MyObject构造函数给xamlpayload属性赋值,然后生成序列化数据,并进行反序列化,如果我们前面的分析正确,应该能打一个计算器出来
成功,这样我们就手搓了TextFormattingRunProperties这条链子,整体还是很简单的,只要对ObjectDataProvider链最终的sink点XamlReader.Parse()方法还有印象的话,几步就能跟完这条链子。当然这个链子也有一些限制TextFormattingRunProperties位于Microsoft.PowerShell.Editor.dll,该库是PowerShell的一部分,该PowerShell已预安装在从Windows Server 2008 R2和Windows 7开始的所有Windows版本中。
4.DataSet链
也即ysoserial.net里的这条链
这个链子很有趣,并且要基于前面提到的TextFormattingRunProperties链子。还一样我们先自己看看。软子给出了DataSet的源码,我们不用用dnspy硬看了。
https://github.com/dotnet/runtime/blob/5535e31a712343a63f5d7d796cd874e563e5ac14/src/libraries/System.Data.Common/src/System/Data/DataSet.cs
还是关注它的序列化构造函数
这里用this方法重载,把DataSet的info和context参数给到另一个同名方法(但是有三个形参)
关于这个方法,阅读完之后可以看到一个有趣的点,在它的最后,调用了一个DeserializeDataSet方法
抛开其他流程先不管,这个DeserializeDataSet是什么意思?难道这个方法还要再走一遍什么反序列化流程吗?跟进这个方法
这个方法又调用了
DeserializeDataSetSchema(info, context, remotingFormat, schemaSerializationMode);
和
DeserializeDataSetData(info, remotingFormat);
两个方法,我们先看前者:
我们可以发现这点真是究极有趣,它从info里提取Data.Tables_0属性的值,类型为byte[]数组,然后对这个数组进行BinaryFormatter反序列化!
也就是说,DataSet链子最终的sink点还是一个反序列化点,也就是说这是一条二次反序列化链,简直是JAVA那边的SignedObject链的精神续作。实际上,在.NET里,有大量这样的二次反序列化链。
既然我们大概弄清楚了链子的流程,那么来看看细节。要利用这个点实现二次反序列化,首先要保证remotingFormat形参的值不等于SerializationFormat.Xml的值,且schemaSerializationMode形参的值要等与SchemaSerializationMode.IncludeSchema的值,稍后我们要研究这些条件要如何满足。
在接下来,注意到在正式进入反序列化流程前,还有一个DeserializeDataSetProperties()方法,我们要看看这个方法里有没有什么异常处理之类的代码,可以看到,这个代码里主要是从info里获取一些变量并且进行赋值,所以这些我们在序列化里肯定是要添加到info里的
最后,后续要从info里取出DataSet.Tables.Count变量的值,这是一个整型的值,后续通过这个值去配置for循环条件。如果我们将其值设置为1,那么后续的
byte[] buffer = (byte[])info.GetValue(string.Format(CultureInfo.InvariantCulture, "DataSet.Tables_{0}", i), typeof(byte[]))!;
实际上就等效于:
byte[] buffer = (byte[])info.GetValue("DataSet.Tables_0", typeof(byte[]))!;
这样没问题,我们来研究remotingFormat和schemaSerializationMode两个形参如何赋值,一路跟到反序列化构造方法:
可以看到,两个关键形参,其中remotingFormat的默认值就是SerializationFormat.Xml。而schemaSerializationMode的默认值就是SchemaSerializationMode.IncludeSchema。那么schemaSerializationMode的值我们可以不用动,前面remotingFormat的值我们要想办法改掉,恰好后面就有代码能改
我们要想办法在info里设置一个DataSet.RemotingFormat变量,在反序列化流程中会检测这个变量是否存在,如果存在那么会将其值赋值给remotingFormat,这样我们就实现了remotingFormat的修改,可以令其不等于SerializationFormat.Xml
综上,我们就分析完这条链子了,下面还是开始手搓一下
老样子,在GetObjectData里进行一些info的设定,然后还是SetType为DataSet类的Type对象。
后面对info的一些设置,我们前面都分析过了,这里不再介绍了。
然后在序列化时,我们读取之前生成的TextFormattingRunProperties链子内容,将其保存到byte[]数组中。后续对myObject对象进行序列化与反序列化
成功RCE。
至此我们就分析完了DataSet这条链子,很容易将其类比为JAVA那边的SignedObject相关链子,一样的二次反序列化,此外如果能走这条链子,也相当于建立起了其他反序列化到BinaryFormatter反序列化的桥梁。
5.TypeConfuseDelegate链
这个链子又更有趣,有很多新东西。他名字里有个Delegate,也就是说这个链子要用到.NET里的委托机制。这里先来学习一下委托的用法。一个简单的委托代码如下:
其实就是把PrintString方法的引用传递给委托,此时调用委托实际上就相当于调用了PrintString方法,运行上述代码就可以看出来
委托还有一种有趣的写法,参考如下的代码:
我们可以使用Func类的构造函数去创建委托对象,例如上图的
Func<string, string, Process> startProcess = new Func<string, string, Process>(Process.Start);
这个直接等效于我们上面先创造委托,再把方法传入委托的过程,可以直接做好一个委托对象
其中
new Func<string, string, Process>(Process.Start);
创建了一个委托对象,该委托指代
Process.Start(string x,string y)
且返回值为Process的方法,这就是<string, string, Process>以及Process.Start的含义,我们创造了一个startProcess委托对象,随后可以通过
startProcess(string x,stirng y);
去调用委托背后对应的Process.Start方法,运行上述代码,成功弹出计算器
此外,如果某个方法没有返回值,还可以使用Action类型去创造委托,类似我们介绍的Func,感兴趣的读者可以自行研究
这个是最简单的委托,但是下面的链子还要用到一个多播委托的概念,请看下面的demo
简单来说,把多个不同的方法分别传给委托,生成对象,然后把这些对象用+号连接,生成的新对象就是多播委托,当我们调用多播委托时,实际上就是依次调用PrintString方法和WriteToFile方法
还有一种实现多播委托的方式是使用MulticastDelegate.Combine(),用法类似,我们得到多播委托对象后,对其调用DynamicInvoke并传入参数即可调用委托了。在这里还可以对多播委托对象调用GetInvocationList()方法,顾名思义这是个getter方法,可以获取InvocationList相关属性的值,而这个属性就保存了我们传入的多个委托对象
对委托对象再调用Method属性,可以拿到委托对象背后对应的方法,我们可以看到第一个委托对象对应PrintString方法,第二个对应WriteToFile方法
有了这些前置知识点的铺垫,可以来学习一下TypeConfuseDelegate这条有意思的链子了。这个链子涉及SortedSet和Comparer。
我们首先要找一个可能被利用的委托,这里仙贝们找的是Comparison<T> Delegate这个委托
可以看到,形参是泛型,我们操作空间比较大,虽然返回值类型是int,但是后续我们可以用多播委托的一些特性去绕过这个限制。
那接下来我们要研究一下在哪里会使用这个委托。这里找到ComparisonComparer<T>这个类,他就实现了Comparer<T>接口。它有一个_comparison属性,属性类型就正是我们上面提到的Comparsion<T>委托。并且ComparisonComparer<T>类构造方法刚好就能给_comparison属性赋值(这里懒得翻软子文档然后反编译代码了,直接偷Y4er佬的图吧)
并且最有意思的是
ComparisonComparer<T>
中有一个Compare(T x,T y)方法,这个方法中就会调用this._comparison(x,y),我们前面提到_comparison正是一个委托对象。这不就刚好是一个绝佳的利用点吗?
此外这里还有一个
Comparer<T>
它其中调用了
ComparisonComparer<T>
构造方法去进行_comparison的赋值,这样就比较方便了。
上面提到的这些类都是可以序列化与反序列化的,可以成为链子的一环。那么接下来,为了触发委托的调用,我们还得仰仗ComparisonComparer<T>中的Compare(T x,T y)方法,我们还得再找找什么地方调用了这个方法
SortedSet中有一个AddIfNotPresent()方法,其中就调用了Compare()方法。而comparer对象我们也是可控的,马上会见到。
哪里又调用了AddIfNotPresent()方法呢?答案是Add()方法
(顺带一提Y4er师傅反编译dll看到的内容是下面这样的,大同小异吧)
那么哪里又调用了Add()方法呢?在SortedSet类的OnDeserialization方法中可以看到相关调用:
欸,这就很有趣了,我们前面介绍过,形如OnDeserialization这样的方法,在反序列化过程中也是会被触发的,以往我们找的都是反序列化构造函数中的敏感操作作为入口,这里找的是OnDeserialization。
并且前面我们关心的comparer变量,在这里会发现也是可控的,反序列化时会从info里取出Comparer变量的值并赋值给comparer
此外,委托相关函数的参数也是可控的
从info里取出Items变量(也就是添加到SortedSet的值)的值,并且传入Add方法:
一路传给comparer.Compare()
我们前面分析过了,这个里面就是委托的调用了,所以最终item参数是和最后我们Process.Start()类的参数息息相关的。这样整个链子就分析完了。值得注意的是,SortedSet类的GetObjectData方法本身就会帮我们把上面这些东西添加好,所以我们就没必要自写GetObjectData了,老老实实做一个SortedSet对象出来就行
其中值得说的是这一行:
info.AddValue(ComparerName, comparer, typeof(IComparer<T>));
取comparer属性的值,对Comparer变量进行赋值
那么要怎么控制comparer呢?SortedSet有一个构造方法就能完成
我们来看看ysoserial.net是如何构造这个链子的
大部分东西我们都介绍过了,这里最最最有趣的一个点,可以看到一开始我们造恶意多播委托的时候,实际上没有恶意内容,而是用String类的Compare()方法作为委托引用,这个方法就完美满足前面提到的comparison委托的要求,两个参数(String类型),返回值类型为int。这是为什么呢?因为我们后续在设置最后命令执行的参数的时候,也要用到SortedSet类的Add方法,我们前面分析过这个方法会走到恶意流程的。因此一开始不能是恶意多播委托对象,否则在这里就会进入恶意流程。
我们是把整个“壳子”造出来之后,利用反射获取MulticastDelegate类的_invocationList属性,前面提到过,传入多播委托的不同委托对象就存在这个属性里,我们把这个属性的第二个键的键值修改为new Func<string,string,Process>(Process.Start),这样的形式就是委托对象,这个委托对象的含义是对Process.Start(string x,string y)的引用,返回值类型为Process。
看到上述操作,JAVA安全老玩家应该类目了,因为我们在CC链中这样的操作看的可太多了,比如涉及到map.put()的链子,由于在put()过程中也会走入恶意代码流程,所以也得采用反射改值的思想。
当然这里还有一个多播委托的特性值得介绍,还是会到前文。前文提到过过委托的返回值、形参和传入委托的方法的返回值和形参必须一致,你利用的这个Comparison的返回值是int
可传入委托的Process.Start(string x ,string y)方法,其返回值类型是Process,按理说这应该是不合法的才对
这里似乎就是多播委托的特性,原作者的解释是:
The only weird thing about this code is TypeConfuseDelegate. It’s a long standing issue that .NET delegates don’t always enforce their type signature, especially the return value. In this case we create a two entry multicast delegate (a delegate which will run multiple single delegates sequentially), setting one delegate to String::Compare which returns an int, and another to Process::Start which returns an instance of the Process class. This works, even when deserialized and invokes the two separate methods. It will then return the created process object as an integer, which just means it will return the pointer to the instance of the process object.
大意就是说你们.NET真是安安又全全,.NET长期存在一个问题,就是委托并没有想象中的严格和严谨,尤其是关于返回值类型。比如在这个案例中,将一个委托设置为String类的Compare方法,返回一个int,另一个设置为Process类的Start方法,返回Process类的一个实例。即使在反序列化并调用两个单独的方法时,这也是有效的。
最后,运行上述代码,RCE
6.总结
学习了一下Formatter序列化与反序列化点的流程,这里面有很多可以作为切入点的、在反序列化流程中会自动执行的方法,就有种PHP那边各种魔术方法的美感,感觉还是比JAVA的反序列化好玩一些的。和这个反序列化点一同学习的还有几个反序列化链子,也都很有意思。
如果以审计的视角来看,主要就看BinaryFormatter类的Deserialize方法接受的数据是否可控,比如本文演示中用到的这个反序列化函数就是很典型的反序列化点,从某个文件中读取文件内容为Stream,并传入BinaryFormatter类的Deserialize方法,实战中定位Deserialize方法,看看前文是否能控制它的传参即可。
7.参考
https://xz.aliyun.com/t/9591 //formatter序列化与反序列化生命周期分析,强烈推荐阅读
https://xz.aliyun.com/t/9593
https://www.freebuf.com/vuls/273253.html