1.SoapFormatter基本使用
这玩意在序列化与反序列化的基本使用上和之前学习的BinaryFormatter还是很类似的,只不过它是用于SOAP数据和对象之间的互相转换。实战中还是关注SoapFormatter类的Deserialize()方法中接受的数据是否可控即可,后文的代码很多都使用Y4er师傅给出的演示代码。
这里进行一下序列化与反序列化操作
可以看到把对象序列化为了SOAP格式的数据,并且也可以反序列化回一个对象。这个点就没太多可以介绍的。
当然,假如有两个类,一个Person可以反序列化,另一个Person2不能
且Person中还有属性是Person2类的对象,这时候无法将Person进行序列化(常识啦)
2.ActivitySurrogateSelector链
由James Forshaw巨神发现的这个链子,难以想象什么样的脑子能从头到尾独自发现这条链子,总之先膜拜一下。看到这条链子的名字,SurrogateSelector,不难猜测它也是一个参与序列化与反序列化流程的选择器,那么这条链子有什么NB之处呢?我们先来看看一般的选择器的使用,其实在BinaryFormatter类的学习中我们已经掌握这个知识点了。
这里是一个无法被序列化与反序列化的类,我们写一个选择器类,在GetObjectData和SetObjectData写好序列化与反序列化逻辑
然后我们使用如下代码来实验
我们的思路是这样的,首先对Person类使用选择器类进行序列化,生成一个序列化数据stm,然后还是用选择器类进行反序列化。这个过程毫无意外能成功对吧。
然后用普通的SoapFormatter反序列化流程,尝试对上面生成的序列化数据stm进行反序列化。结果如下:
可见,fmt使用我们的PersonSerializeSurrogate选择器生成的序列化数据,同样也只能由他来反序列化,用一个没有使用选择器的SoapFormatter进行反序列化会出错。
理论上来说,选择器的存在可以帮助我们对一些原本无法序列化与反序列化的类进行操作,可以拓宽攻击面。然而现实环境中,应该很难看到有开发者在序列化与反序列化过程中使用选择器,就算有,这个选择器也不是我们能控制的。
这也是为什么ActivitySurrogateSelector这条链从头到尾都很NB,开头的切入点就很震撼人心,James Forshaw找到了一个特殊的选择器,我们在序列化的时候可以使用这个选择器去序列化任意一个类,并且在反序列化过程中,不使用选择器依然可以反序列化成功。
我们来看看案例:
老样子,先写一个正常来说无法被序列化与反序列化的类
这里我们写一个选择器,这个选择器实际上用反射获取ActivitySurrogateSelector类的ObjectSurrogate内部类,因此我们这个选择器实际上是ObjectSurrogate选择器。
接着好玩的地方来了
这里我们用ObjectSurrogate作为选择器生成序列化数据stm,注意反序列化时使用的是普通的SoapFormatter,按照上面的演示,反序列化时没有使用选择器,理论上来说会失败对吧。然而这里是可以反序列化成功的,这是为什么呢?
找到ObjectSurrogate类
https://github.com/microsoft/referencesource/blob/4fe4349175f4c5091d972a7e56ea12012f1e7170/System.Workflow.ComponentModel/AuthoringOM/Serializer/ActivitySurrogateSelector.cs#L117
可以看到,这个选择器类的SetObjectData方法根本就是空的,也就是说微软在设计这个选择器的时候,本来就没想着让你在反序列化流程中使用它。那么它的序列化流程就很关键了
可以看到,序列化的时候,实际上是序列化成ObjectSerializedRef类型,并且把目标类(也即我们尝试序列化的那个类)的各种信息都存到info里去了。
也就是说,我们的目标类的各种信息,在序列化的时候由ObjectSurrogate的GetObjectData方法存到ObjectSerializedRef这个类里面去了,而这个类又是一个可序列化与反序列化的类:
因此这个类起到了一个中转站的作用,在反序列化的时候实际上是对这个类进行反序列化,从中拿到原本的目标类的各种信息。这里借用https://xz.aliyun.com/t/13002中的一张图:
可以直观看到目标类中的参数名参数值等各种信息其实都被存起来了
https://www.zcgonvh.com/post/weaponizing_CVE-2020-0688_and_about_dotnet_deserialize_vulnerability.html#:~:text=%E5%9C%A8.net%E6%97%A0%E9%99%90%E5%88%B6%E5%8F%8D%E5%BA%8F
这篇文章里也提到了这一点
因此,借助ObjectSurrogate,或者说借助ObjectSerializedRef,我们可以对任意类进行序列化与反序列化,这样自由度就很高了,因为这样很多原本没有[Serializable]特性,无法参与序列化与反序列化的类,此时也可以成为我们反序列化链的一部分!
3.ActivitySurrogateSelector链之Assembly.Load与LINQ
在接着学习后面的内容之前,还是要先了解一下.NET里的Assembly.Load()机制以及它在安全中的应用。
对于JAVA安全比较熟悉的小伙伴都知道,在JAVA里我们可以通过defineClass()方法进行动态加载字节码,这对于加载恶意代码来说非常便利,那么.NET里有没有类似的功能呢?我们随便拉一个.NET的冰蝎马或者哥斯拉马看看就知道了
可以看到,这里用Assembly.Load()加载了一串byte[]类型的数据,然后对其调用CreateInstance(),然后再调用Equals方法,就代码执行了。
如果我们把Assembly.Load()看作defineClass(),把CreateInstance()类比为newInstance(),我们就能很好理解了。实际上这个Assembly.Load()有多个重载方法,我们即可以直接指定一个dll、exe文件,也可以指定程序集名称,也可以把dll读取为byte[]数组再通过Load()进行加载。此外,还有几个类似的Load()方法,比如LoadFrom()、LoadFile()感兴趣的读者可以自己研究。
这里我们分别用下面几个代码编译成exe来演示,如下代码没有命名空间,有一个EvalClass1类,且无参构造函数里调用Process.Start()启动计算器
对应TestEval1.exe
如下代码有一个Test命名空间,其中有一个EvalClass2类,且无参构造函数里调用Process.Start()启动计算器
对应TestEval2.exe
如下代码有两个命名空间Test和Testx,其中Test命名空间里有EvalClass2类,Testx命名空间里有Testx类
对应TestEvalx.exe
我们分别尝试使用Assembly.Load()去加载上述exe,看看过程有哪些异同。首先是TestEval1.exe
过程已经写在注释里了,运行代码,成功弹出计算器
那么对于TestEval2.exe呢?上述代码还能成功吗
文件换成TestEval2.exe,要加载的类换成EvalClass2,可以发现并没有成功。这是因为TestEval2是有一个命名空间Test的,CreateInstance()里面必须要写上完整的类名:
写好带上命名空间的完整类名,还是可以成功调用构造方法
对于TestEvalx.exe这种带有多个命名空间的同样也是如此
除了在Assembly.CreateInstance()里指定完整类名,其实还有一种方法,那就是用GetType()和Activator.CreateInstance()组合,什么意思呢?还是以TestEvalx.exe为例:
注意这里我们调用的是Activator.CreateInstance(),它接受Type类型的对象,我们先用GetType读取Testx.EvalClassx的Type对象,然后传入Activator.CreateInstance()
一样起到弹计算器的效果
此外,除了GetType,还有一个方法是GetTypes(),不接受传参,并且返回值类型是Type[]
不难推断是把目标里所有的Type都取出来,并存在一个Type[]里,如下图我们遍历type并输出FullName属性
这样我们就会使用Assembly.Load()以及其他几个相关方法了(也顺带简单分析了一下冰蝎和哥斯拉.NET马的实现原理),那么这和本文又有什么关系呢?我们很容易想到,上文中的这个流程:
Assembly testAssebly = System.Reflection.Assembly.Load(fileBytes);
Type evalType = testAssebly.GetType("Testx.EvalClassx");
Activator.CreateInstance(evalType);
就类似CC3中的sink点,如果.NET也有些类存在上述这些操作,或者说也存在什么委托之类的行为,能够间接执行上述操作,是不是也可以作为sink点呢?
James Forshaw又开始展示丰富的经验了,他是借助LINQ类去包装我们上述操作的,为什么?我们可以使用下面的代码先大概了解一下LINQ的玩法(参考https://xz.aliyun.com/t/13002):
可以看到,我们在调用LINQ的Where和Select方法的时候,传入的东西是我们自定义的方法IsEven、DoubleToString,看起来这里也是有委托机制的,理论上来说,通过IsEven筛选2的倍数,然后通过DoubleToString对满足2的倍数的数再x2,会输出4 8 12 16 20
运行一下,果真如此,我们可以去看看LINQ的一些源码
比如上面提到的Where()方法,第二个参数确实是接受一个委托类型,委托方法的返回值是bool,传参类型是泛型,也即符合下面这个方法
而SELECT()方法如下:
其第二个参数也是一个委托。并且SELECT()最终返回的结果是IEnumerable<TResult>类型。
看起来,我们可以用LINQ的这几个方法去包装我们上述Assembly.Load()的流程?包装的流程如下:
这里其实也有点烧脑,主要是中间这一步不好理解。
//这一步是生成对public virtual Type[] GetTypes()的委托
Func<Assembly, IEnumerable<Type>> map_type = (Func<Assembly, IEnumerable<Type>>)Delegate.CreateDelegate(typeof(Func<Assembly, IEnumerable<Type>>), typeof(Assembly).GetMethod("GetTypes"));
var e2 = e1.SelectMany(map_type); //由于GetTypes()返回的是Ienumerable<Type>集合,我们将其传入SelectMany(),刚好满足需求
我猜测是想通过GetTypes()去生成IEnumerable<Type>,方便下一步传入Activator.CreateInstance()。但是Select()第二个参数的委托的格式刚好不支持返回IEnumerable<Type>,SelectMany()第二个参数的委托格式是Func<TSource, IEnumerable<TResult>>,委托的返回值类型就是IEnumerable<TResult>,刚好满足需求
但是这里不能和上面一样直接写成Assembly.GetTypes,而且GetTypes本身也不需要传入参数啊,编译器会报错说无法推断类型参数,要我们显式指定类型
所以才需要用Delegate.CreateDelegate()去造一个委托对象并且显示指定类型?网上的参考资料说“开放委托不仅不会存储对象实例,而且还会增加一个Assembly参数,这正是我们需要的”。(.NET的各种委托特性真是乱七八糟的)
然后最后一步调用Activator.CreateInstance()就比较ez。因为入参是Type类型,刚好和public static object CreateInstance(Type type)对应的上。
总之按照上面的步骤,先相当于把list<byte[]>里面的属性传入Assembly.Load(),然后得到IEnumerable<Assembly>,接着相当于对Assembly调用GetTypes()方法得到IEnumerable<Type>(里面有多个Type,具体来说是Test和Testx),最后相当于把这些Type依次传入Activator.CreateInstance(),得到IEnumerable<object>(也有多个对象)。注意我们现在这里说的返回值IEnumerable<Assembly>啥的都是Select()或者SelectMany()的返回值,并非委托方法的返回值。
当然前面这里纯属参考网上已有的文章+一点个人理解,对LINQ和这些乱七八糟的委托并不是很熟悉,欢迎大手子指出错误。
总之,这样我们就造好了一个LINQ,最后的e3对象就是。我们可以触发一下LINQ,看看能不能弹计算器
注意到,能弹,而且能弹两个,因为我们Test和Testx两个命名空间里分别有两个类的构造函数可以弹计算器,前面得到的IEnumerable<Type>里就分别存储Test.EvalClass2和Testx.EvalClassx,依次传入Activator.CreateInstance(),自然就会弹两次。
那么现在又有一个问题了,我们虽然造好了一个恶意LINQ对象e3,并且手动去触发LINQ确实可以RCE,但是我们应该如何在反序列化流程中触发LINQ呢?
4.LINQ触发链
似乎没有找到文章在这一步细说了,那就由我这个小菜鸟斗胆分析一下(
怎么触发LINQ的执行,我们上面用的代码是这样的:
这个对e3调用foreach背后的细节是什么呢?其实它大概的工作原理是这样的:
1.获取迭代器:当执行上述循环的时候,会隐式调用e3对象的GetEnumerator()方法获取一个迭代器
2.迭代元素:一旦得到了迭代器,foreach 循环会使用该迭代器的 MoveNext() 方法来逐步遍历元素,并通过 Current 属性访问每个元素。
3.循环结束:当 MoveNext() 返回 false 时,循环结束。
而MoveNext()就是一个可以触发LINQ的操作,因此我们可以写成这样:
先对e3调用GetEnumerator()方法获取迭代器,再执行MoveNext()方法
如图,我们的恶意代码还是能够执行。
OK,理论上来说,我们只要找到一个点,能够对e3对象执行foreach()操作,或者说,这个点能调用e3对象的迭代器来进行foreach(),就可以进一步延长链子。有这样的点吗?
关注到PagedDataSource 类
它的GetEnumerator()方法的实现很有意思,因为它实际上返回的是它类中的dataSource属性的迭代器
而PagedDataSource 类的dataSource方法就是IEnumerable类型,恰好可以存入我们的e3那个对象
如果我们将这个dataSource赋值为e3对象,并且如果某个功能点对PagedDataSource类的对象进行foreach操作,实际上这个foreach拿到的是e3对象的迭代器,那么后续MoveNext()自然也是对e3对象的迭代器而言的,这样就会触发链子,我们可以做个实验:
如图,只要尝试遍历pds,实际上就相当于遍历e3,触发RCE。
那么接下来我们得再找一个点,这个点要存在一个遍历操作,并且要针对pds进行遍历。注意到AggregateDictionary类里有这样一段代码:
这里会对本类的_dictionaries属性进行遍历,从格式上看是符合我们的要求的,其_dictionaries属性是ICollection类型
恰好可以存放我们前面提到的PagedDataSource的对象
且_dictionaries通过该类的构造方法赋值,对我们写链子很友好
因此链子延长,我们如果把_dictionaries属性赋值为PagedDataSource的对象,在下面这段操作里就会触发对PagedDataSource的遍历,实际上相当于触发对e3的遍历
那么再有一个问题就是,这个
public virtual object this[object key]
是什么鬼东西?我们要怎么触发它?
这个实际上是C#中索引器的实现,我们可以用如下代码简单的演示一下要怎么触发索引器
如图,实际上,当一个类里存在索引器,我们只要采用如下写法:
某个类的对象["随机字符"] //取值操作
就会触发目标类的索引器get
那么接下来的目的就很简单了,我们只需要找到一个类,其中有属性可以存放AggregateDictionary类的对象,并且有形如下面这样的代码:
AggregateDictionary类的对象["随机字符"]
就可以触发AggregateDictionary类的索引器,进一步触发遍历,从而走通链子。注意到DesignerVerb类
https://github.com/dotnet/runtime/blob/5535e31a712343a63f5d7d796cd874e563e5ac14/src/libraries/System.ComponentModel.TypeConverter/src/System/ComponentModel/Design/DesignerVerb.cs
其Text属性的get里就有一个Properties[“Text”],是符合格式的写法,那么Properties是否可以被控制为AggregateDictionary类的对象呢?来到DesignerVerb类的父类MenuCommand
Properties有这样的定义,如果_properties存在,那么其值等于_properties的值,如果不存在则为new HybridDictionary()的值
再关注_properties,注意到它和Properties恰好是IDictionary类型
因此其恰好可以存放AggregateDictionary类的对象
那么再接下来,需要找到一个地方调用Text的get
这个就很简单了,注意到DesignerVerb类的ToString()方法,其中调用了Text,也就是有个取值操作,自然触发get:
我们跟到了ToString()方法,对PHP和JAVA熟悉的小伙伴都知道,__toString()和toString(),是太多链子的重要一环了,在.NET里会是如此吗?待会再来分析。
总之,我们现在可以先把链子搞一下了:
注释里写的很清楚了,运行,成功弹窗
当然只要是把verb当成字符串处理就会触发ToString,和php还有JAVA那边是一样的,比如这样:
调用Format这样的字符串处理方法就会触发
这里我看的时候还有个小疑惑,就是这一段
typeof(MenuCommand).GetField("properties", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(verb, dict);
MenuCommand里的明明是_properties和Properties,为什么这里是获取properties赋值?然而当我查看MenuCommand的Field的时候,我沉默了,确实只有properties
基础还是有点不扎实,不知道这是为啥,是某个.NET特性?(难道是会自动把_去掉?
总之,这个链子的大体就构造好了,但是还没完!现在的问题又变成,在反序列化流程中,要如何触发DesignerVerb类的ToString()方法?
5.触发ToString()
接下来就是要研究怎么在反序列化里触发ToString(),在JAVA里我们有BadAttributeValueExpException类的readObject()方法帮我们触发toString(),.NET里能不能找一个这样的反序列化入口来救救我们呢?答案是有的
在HashTable里,有这样一个方法,GetResourceString,接受一个string类型的key传参和一个object类型的values传参,并且会把values和字符串用Format做一个处理
也就是说,只要key和values可控,我们就能用这个方法续杯链子,那么这个方法在哪里调用了呢?在Insert()方法里
Block_15
注意到这里拼接的字符,Argument_AddingDuplicate,意为添加重复项,其实这里我们就可以猜测当key相同的时候,就会触发GetResourceString,并且把重复的key作为参数传入这个方法
这里就进行了一个调用,如何走到Block_15?这里看起来很复杂,不过我们前面推断过了,主要就是做了一个Key是否重复的判断
我们可以先做一个简单的小测试,下面这个代码调用HashTable的Add方法,会触发Insert
然后用dnspy进行调试
下几个断点,然后选择相关的exe文件(也即我们上面的测试代码)
由于有重复键,进入Block_15,这里会抛出异常
然后进入GetResourceString,可以看到values就是我们的key1键
如果将key1换成前面做好的DesignerVerb类对象verb,就能触发DesignerVerb的ToString()方法走通链子
那么现在我们知道,只要触发这个Insert(),就能触发键的比较,进而走到异常处理里的Format,进而触发ToString。先小小验证一下:
可行,那么问题又来了。除了这个Add(),还有什么地方能触发Insert()呢?最好是和序列化与反序列化有关的。
于是我们看到HashTable的OnDeserialization方法,一个能在反序列化时触发的特性方法:
在这个方法的底下,我们可以注意到一个
没错,在HashTable反序列化过程中,会触发Insert()
所以最后我们只需要把两个相同的verb封装到HashTable里就行,当然这里我们还是一样,一开始添加不同的key,后续用反射改成相同的,防止生成payload时就触发链子(java老玩家类目)
最后,由于前文提到的链子中的LINQ部分是不能序列化与反序列化的,所以生成序列化数据的时候,我们得用最开始提到的ObjectSurrogate选择器去把整个恶意HashTable对象包装在ObjectSerializedRef,这样LINQ这部分不能正常序列化与反序列化的对象才能在反序列化时正常触发。总之,这样生成出来的序列化数据,就是payload了,这样整个链子差不多完成了。
但是,你以为这又完了?上面的链子再利用的时候会爆500异常。这个链子还能再延申。
6.AxHostState链
为了解决异常问题,这个链子还可以继续延申,用到AxHostState链
进入AxHost类的State内部类
跟入这个类的反序列化构造方法
其中有一个Read()方法的调用,传入MemoryStream()类型,其值最终可以追溯到info里的PropertyBagBinary变量,就很可疑
跟进Read()方法,会发现它内部就是一个
(Hashtable)binaryFormatter.Deserialize(stream);
经典的binaryFormatter反序列化点,经典的二次反序列化
并且这里好就好在有异常处理,所以把我们前面做好的ActivitySurrogateSelector链包装在这里,由AxHostState这个二次反序列化去执行ActivitySurrogateSelector,就不会有异常了。
7.ActivitySurrogateSelectorFromFile链
三个和ActivitySurrogateSelector链相关的链子之一
其实这个链子的本质还是ActivitySurrogateSelector链,只是ysoserial.net为了提高这个链子的自由度所写的变体,我们前面提到ActivitySurrogateSelector链的sink点本质上是对Assembly.Load()的利用,而这玩意可以加载任意dll或exe的“字节码”,因此这个ActivitySurrogateSelectorFromFile链其实就是让用户自己指定你写好的dll文件,最后生成的payload一旦执行就是运行你dll里的代码
8.ActivitySurrogateDisableTypeCheck
三个和ActivitySurrogateSelector链相关的链子之一
这个链也特别有意思。在dotnet4.8以上版本,微软也知道ActivitySurrogateSelector的牛逼了,任何类都能参与序列化与反序列化,这谁顶得住啊。所以加了个修复,网上大部分文章都说是在这个
GetObjectData()
里面加了AppSettings.DisableActivitySurrogateSelectorTypeCheck,默认为false,所以导致不通过
所以我们上面的演示代码中才会有一行这个:
就是为了可以正常生成序列化数据。但是我的疑问又来了,如果只在GetObjectData这里加,只会影响序列化流程啊,如果我在序列化之前给这玩意用反射或者别的什么方法强行改掉,先成功生成一个序列化数据,后续反序列化时不也一样能行吗?但事实上微软并没有这么笨(
我们前面提到序列化这玩意本质是上是把目标对象的各种信息存到ObjectSerializedRef里,反序列化的时候实际上也是通过ObjectSerializedRef
在4.8以上,这个检查参数同样加在ObjectSerializedRef的OnDeserialization方法中。因此反序列化流程同样受到AppSettings.DisableActivitySurrogateSelectorTypeCheck的干扰。怎么样才能绕过这玩意呢?
我们前面提到,反射改值,是的如果我们在正式的ActivitySurrogateSelector链之前,用个别的链子触发反射修改AppSettings.DisableActivitySurrogateSelectorTypeCheck不就行了?一说到这个,就可以想到之前的ObjectDataProvider相关利用:
其实就是把前面我们使用的这行代码改成了反射的形式(反射获取Set方法)
然后借助Xaml(ObjectDataProvider)相关链去执行
9.结语
综上我们就学习了SoapFormatter反序列化以及ActivitySurrogateSelector相关链。绝大部分篇幅都是在分析ActivitySurrogateSelector这条链子,这一条链子就展现出James Forshaw巨佬恐怖的水平,每一步要么是.NET的奇妙特性,要么需要对开发方面的很多知识有深刻理解,要么就是奇思妙想,一边分析复现一边学习花了我接近一天时间,不知道要学到什么时候才有机会独立挖出这样一条链子。总之,NB。
10.参考
也是感谢各位大师傅的文章和demo,不然自己调试代码得花更长时间
https://xz.aliyun.com/t/9595
https://www.zcgonvh.com/post/weaponizing_CVE-2020-0688_and_about_dotnet_deserialize_vulnerability.html#:~:text=%E5%9C%A8.net%E6%97%A0%E9%99%90%E5%88%B6%E5%8F%8D%E5%BA%8F
https://www.cnblogs.com/zpchcbd/p/17184631.html
https://paper.seebug.org/1418/#0x21
https://xz.aliyun.com/t/13002?time__1311=GqmhBKYK7IPRx05DK7SiKou1pDcDiTpD#toc-12