c#反序列化学习之TypeConfuseDelegate – 作者:宽字节安全实验室

反序列化漏洞在很多语言中都存在。之前一直在研究java的反序列化漏洞,但是想检验一下对于java反序列化漏洞的理解,于是我学习c#反序列化漏洞。当然,我也不是专业c#开发,基本没有正规接触过c#,所以有些地方在所难免有纰漏。

1. 反序列化基础

对象的序列化与反序列化的作用在于将一个对象的状态从当前CLR(java叫JVM)传输至另外一端CLR。该功能一般与rpc远程调用所搭配以降低开发难度。比如weblogic的T3 rmi协议,就是客户端将对象通过序列化传输到服务端。然后调用对象的某些方法。序列化的过程中,只存储对象的属性。例如一个学生类。

class student{
public String name;
public int age;
public String getName(){
returnname;
}
}

在这里我们先暂时不考虑继承自serializable接口。序列化的话,只会传输name与age这两个属性。至于方法则不会传输。在反序列化的时候,读取到name与age的属性,并将这两个属性赋值给相应的类生成的对象。你可以认为反序列化的代码如下

Class clazz = readClassNameFromTxt();
Student s = clazz.newInstance();
s.name = readStringFromTxt();
s.age = readIntFromTxt()

至于怎么存储对象的值,则由序列化的方式决定。例如xml序列化组件,fastjson序列化组件,ObjectInputStream序列化等等。在c#则是BinaryFormatter等,原理都是一样。

那么你想问,既然在序列化的过程中传输对象的属性,那么是怎样造成反序列化漏洞的?在反序列化过程中,一般都会调用某个特殊方法以满足特殊的反序列化需求。在java中就是readObject方法,c#就是OnDeserialization方法。在这些特殊的方法中又调用了某些类的操作,一步一步最终执行代码。这就是反序列化漏洞

和java一样,c#序列化的话,类需要继承自ISerializable接口才可以被序列化。为了保证序列化中还原的对象相同。一般在双方会确保序列化的对象的类是否是同一个。在java是serialVersionUID,c#类型版本由SerializationInfo中保存的AssemblyName决定,其规则遵循clr默认程序集发现和加载策略

2. 委托

基本原理懂了,那我们拿TypeConfusedDelegate这个gadget来分析。在学习这个gadget之前,我们了解一下c#委托。C# 中的委托(Delegate)类似于 C 或 C++ 中函数的指针。委托(Delegate)是存有对某个方法的引用的一种引用类型变量。引用可在运行时被改变。示例代码如下

static int num = 10;
public static int AddNum(int p)
{
num += p;
returnnum;
}

public static int getNum()
{
returnnum;
}

static void Main(string[] args)
{
// 创建委托实例
NumberChanger nc1 = new NumberChanger(AddNum);
// 使用委托对象调用方法
nc1(25);
Console.WriteLine("Value of Num: {0}", getNum());
}

其实委托就是c++的函数的指针。c#为了照顾以前c++程序员的开发习惯,但是又不想在c#中直接让程序员操作函数指针。于是搞了一个委托出来。查看c#的字节码,可以看到委托其实是生成一个类,继承自System.muitiDelegate。调用委托,执行这个类的Invoke方法。

图片[1]-c#反序列化学习之TypeConfuseDelegate – 作者:宽字节安全实验室-安全小百科

至于这个Invoke方法则是运行的时候动态生成的。但是我在查阅msdn中,发现一篇2001年的杂志介绍了委托的实现原理。

https://docs.microsoft.com/en-us/archive/msdn-magazine/2001/june/net-delegates-part-2

Invoke方法的伪代码如下

图片[2]-c#反序列化学习之TypeConfuseDelegate – 作者:宽字节安全实验室-安全小百科

其实就是执行函数指针所指向的函数。

c#还支持多重委托。也就是在一个委托中,最终执行两个委托。原理类似。一会在生成exp中介绍原理。

委托其实与多态的作用都差不多,大家可以思考一下c#与java的区别。

3. SortedSet

c# SortedSet是一个很重要的数据结构。看字面意思,就是一个有序的集合。也就是说,再向SortedSet添加元素的时候,SortedSet会调用排序算法,将你的所添加的元素添加到合适的位置。既然这里,你肯定会问,是怎么排序的?所以c#的SortedSet的构造函数中,有一个参数就是传入排序算法的委托。

代码如下

Comparison<string> da = new Comparison<string>(String.Compare);
IComparer<string> comp = Comparer<string>.Create(da);
var sortedSet = new SortedSet<string>(comp);

在向SortedSet添加数据的时候,排序算法会调用String.Compare方法。

图片[3]-c#反序列化学习之TypeConfuseDelegate – 作者:宽字节安全实验室-安全小百科

也就是遍历SortedSet中所有元素,并一一与添加的元素通过Compare对比。

下面我们看一下SortedSet反序列化的过程。在OnDeserialization方法中,首先在序列化流中还原Compare,然后再还原SortedSet的每个元素,并调用Add添加到实例化后的SortedSet中。

图片[4]-c#反序列化学习之TypeConfuseDelegate – 作者:宽字节安全实验室-安全小百科

到现在我们明白了,既然在c#中Comparison是一个委托。那么按照java中ysoserial的流程,首先将正常委托添加到SortedSet,防止在添加数据的时候不小心触发恶意委托。然后再通过反射修改成恶意委托,将这个SortedSet序列化,在反序列化的过程中不就可以触发恶意委托了吗。原理如此,我们继续往下分析反序列化payload生成

4. 反序列化payload生成

我们看一下委托的数据结构

图片[5]-c#反序列化学习之TypeConfuseDelegate – 作者:宽字节安全实验室-安全小百科

我们只需要通过反射替换Method即可。但是失败了,因为c#通过反射去修改委托实在是太麻烦了。稍有不慎就异常退出。但是我们通过c#修改多重委托就很方便。在调用多重委托时,会按顺序执行多种委托中的每一个委托。多重委托的数据结构如下

图片[6]-c#反序列化学习之TypeConfuseDelegate – 作者:宽字节安全实验室-安全小百科

很简单,我们只需要在sortedSet添加完数据后,再通过反射,将_invocationList的中任意一个委托修改成恶意委托即可。c#中有一个万能委托,可以指向任何函数。只需要替换成他即可。代码如下

FieldInfo fi=
typeof(MulticastDelegate).GetField("_invocationList", BindingFlags.NonPublic | BindingFlags.Instance);
object[] invoke_list = d.GetInvocationList() ;
// Modify the invocation list to add Process::Start(string, string)
invoke_list[1] = new Func<string, string, Process>(Process.Start);
fi.SetValue(d, invoke_list);

最终成功弹出计算器,堆栈如下

图片[7]-c#反序列化学习之TypeConfuseDelegate – 作者:宽字节安全实验室-安全小百科

来源:freebuf.com 2021-05-17 14:43:48 by: 宽字节安全实验室

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发

请登录后发表评论