17.SerializationBinder安全问题与DataSetTypeSpoof链

2024-11-03 14:20   广东  

1.SerializationBinder的安全问题

前面一直都是纯分析反序列化链,还没学习过相关的防御机制,这不就来了吗。这里用Y4er师傅的demo学习一下:

这里写了一个RCE类用于后续测试

这里写了一个MyBinder,实现SerializationBinder,并且实现BindToType()方法

这里获取反序列化时的Type,并且判断是否等于RCE,如果是返回一个null,否则就原样返回。这里看起来是期望实现一个过滤Type的功能,拦截RCE类的反序列化(然而这样的写法是有问题的,后面再说)

然后我们要怎么使用MyBinder呢?

注意反序列化过程中,设置Binder属性。

然后我们来试试,理论上来说,我们期望MyBinder()帮我们拦截掉RCE类的反序列化,然而运行之后发现情况并不是这样:

我们发现,确实触发了BindToType内部的判断,提示can’t deseriliza rce class,但是也确实触发了RCE内部的ToString(),也就是说,我们的BindToType拦截了个寂寞

这是为什么呢?答案是在调用完BindToType后,如果值为null,那么会进入一个FastBindToType()方法

这个方法里经过一系列操作后还是能拿到typeName,可以看到拿到了Serialize.RCE

然后这里进入

GetSimplyNamedTypeFromAssembly()

之后,依然可以正常得到目标类的Type。总结一下,当BindToType返回null的时候,又进入FastBindToType(),由这个方法返回type,而这个方法可以得到最终的Type并返回,所以实际上反序列化流程中还是能正常拿到Type,并不是开发者以为的是null。因此我们只需要设法让BindToType返回null,实际上就可以绕过它的限制。

那么要怎么样才是正确使用BindToType的姿势呢?答案是不返回null,直接抛出异常。


2.CVE-2022-23277

上面这个姿势在实战中有过应用,exchange的binaryformatter都有一个binder,BindToType()方法实现如下

这里会先把assemblyName和typeName传入InternalBindToType,得到Type,如果Type不为null,将type传入

ValidateTypeToDeserialize()

方法进行校验,所以这里的绕过方法就是想办法让

InternalBindToType()

返回null,并且让后续的FastBindToType()能够正常拿到type就行。

InternalBindToType调用LoadType

注意这里的try catch,想办法在这里触发异常,进入catch,就等于InternalBindToType()返回为空了,如果在此处进入异常,就会导致BindToType()返回null,实现绕过的目的,而这就涉及DataSetTypeSpoof链子


3.DataSetTypeSpoof

序列化过程中使用上述写法生成序列化数据,这样在LoadType的

Type.GetType(string.Format("{0}, {1}",typeName,assemblyName))

就会抛出异常

这里我们本地模仿着写一个类似的环境来测试:

这里我们的目标也是设法触发下面这段代码的异常,导致返回null

Type.GetType(String.Format("{0}, {1}", typeName, assemblyName));

否则后续判断type是DataSet就要抛出异常阻止反序列化流程了。

这里我们先生成一个普通的DataSet链子进行测试:

这里就被BindToType拦截了

我们这里换成DataSetTypeSpoof链试试

成功RCE。

发现确实会进入异常处理,导致返回null

来看看DateSet和DataSetTypeSpoof有什么不同

上面的是正常的DataSet链,下面的是DataSetTypeSpoof链,区别这里已经给出来了

主要差别就是到底是直接用

SetType(typeof())

指定反序列化时的类型,还是用

AssemblyName+FullTypeName

(并且FullTypeName使用AssemblyQualifiedName获取TypeName)

那么他们又有什么区别呢?

这又涉及到一个很有趣的知识,借用这篇文章里提到的信息:

https://exp10it.io/2024/02/dotnet-serializationbinder-%E7%BB%95%E8%BF%87/

可以看到,这里有多个获取TypeName的方法,不过每个输出都不太一样

我想表达什么意思呢?我们看前面的普通的DataSet链,是使用

info.SetType(typeof(System.Data.DataSet))

进行Type的设置的,其TypeName正是System.Data.DataSet

因此如果我是开发者,我可能有一个思路就是在BindToType里拿到typeName,判断是否为黑名单System.Data.DataSet,如果是就抛出异常。

然而设定typeName并不只有info.SetType()这种方式,typeName也不是只有一种格式。还有种方式就是我们前面提到的通过FullTypeName进行赋值,也即DataSetTypeSpoof中的写法,然后赋值的typeName的格式也和普通的格式不一样,我们通过AssemblyQualifiedName拿到的typeName格式如下:

System.Data.DataSet, System.Data, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089

(这样使用AssemblyQualifiedName输出的typeName是完全体,和普通的typeof不一样)

可以看到,这时候如果使用Equals对typeName进行简单的黑名单判断,那直接就会造成一个黑名单的绕过。甚至于,如果开发者对TypeName的处理规则不完善,遇到更复杂的TypeName的时候,确实有可能引发各种乱七八糟的异常,如果遇到异常后返回一个null,那就更危险了。

除此之外,.NET还支持解析一些畸形的类型名称:

开发者未必知道这些特性,因此可以结合上面这些特性去绕过一些简单的匹配规则,或者在某些情况下触发异常。


4.总结

所以我们可以尝试总结一些Binder的绕过场景:

所以我们可以尝试总结一些Binder的绕过场景:

①如果开发者写的BindToType()规则里匹配到恶意类就返回null,那么直接相当于一个无效的Binder,没有任何作用。有时候虽然不是明着返回一个null,但是也可以设法让其返回一个null(比如使用一些畸形的数据触发异常,如果catch为空则相当于返回null),比如CVE-2022-23277的例子。

②即使使用异常处理也绝非万无一失,还要关注BindToType()中对typeName的匹配规则,typeName有多种格式,多种写法,多种设定方法,且有办法构造畸形数据,对typeName的黑名单匹配规则稍有纰漏就容易产生绕过,也可能触发一些异常导致BindToType最终返回null最终导致绕过。


5.参考

https://y4er.com/posts/dotnet-deserialize-bypass-binder/#%E6%80%BB%E7%BB%93https://exp10it.io/2024/02/dotnet-serializationbinder-%E7%BB%95%E8%BF%87/

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