从CVE-2021-42321学.NET deserialize gadgets
文章目录
CVE-2021-42321
影响版本
- Exchange Server 2016 CU21/CU22
- Exchange Server 2019 CU10/CU11
攻击流程为EWS操作
|
|
在 Microsoft.Exchange.Compliance.Serialization.Formatters
命名空间中定义了三个类,分别为
TypedBinaryFormatter
TypeSerializationFormatter
TypeSoapFormatter
其中在TypeBinaryFormatter
中定义的反序列化方法内容为
|
|
可以发现传入的 SerializationBinder
实际上并没有用到,一般来说这个东西都用于反序列化时限定类型(针对 BinaryFormatter、SoapFormatter、LosFormatter、NetDataContractSerializer、ObjectStateFormatter
)
继续跟入 CreateBinaryFormatter
→ Microsoft.Exchange.Diagnostics.CreateBinaryFormatter
|
|
虽然直接返回了一个 BinaryFormatter
的实例,但是其中仍然初始化了一个Binder,跟入其中查看一下定义,首先可以发现在其中定了几个HashSet,从名字就能看出来用处
比如在 GlobalDisallowedTypesForDeserialization
中可以看到定义了非常多常见的 gadgets
回头看一下构造函数中收到的参数
|
|
因为继承了抽象类 SerializationBinder
,所以关注重写后的 BindToType
|
|
InternalBindToType
中是一个正常的获取Type的流程,可以看到如果获取到的Type不为null,会走一次验证,来判断这个类型是否可以被序列化
|
|
只要不是
InvalidOperationException
的异常就OK
!this.strictMode
⇒true
!this.allowedTypesForDeserialization.Contains(fullName)
⇒true
ChainedSerializationBinder.GlobalDisallowedTypesForDeserialization.Contains(fullName)
⇒ ??? 需要从黑名单中找一条 漏网之鱼
|
|
手误将 System.Security.Claims.ClaimsPrincipal
写错了,补丁diff
Gadget#1 System.Security.Claims.ClaimsPrincipal
直接跟入这个类,发现实现了 Iserializable
接口,并且定了一个使用了 OnDeserializedAttribute
来自定义反序列化的流程的函数,查看关于反序列化的定义
|
|
继续跟入 DeserializeIdentities
|
|
使用了一个裸的 binaryFormatter
,对传入值base64解码后直接反序列化,所以在这里可以做一次二次反序列化实现RCE
|
|
最新版 ysoerial.net > 1.34
中已经内置了这条gadget,可以直接生成
Gadget#2 TypeConfuseDelegate
注意到 GlobalDisallowedTypesForDeserialization
也不包含 SortedSet
,所以还可以使用 TypeConfuseDelegate
这条gadget进行攻击
SortedSet
这个类用来将其中的元素按照一定规则进行排序,排序的方法需要自己创建一个用来规定如何排序的类作为比较器,必须实现 IComparer
接口,这个接口只有一个函数 Compare
,简单用法如下所示
这里有一个小细节,如果当前集合中的元素 ≥ 2,也就是调用第二次或者更多次 Add
函数的时候,会调用一次 Comparer.Compare
,并且将两次传入的值作为参数传入( 后面要考
首先跟入 SortedSet
的构造函数,可以发现他接受一个类型为 IComparer<T>
的参数,这个参数将作为比较器在后续的流程中被使用,而如果没有传入的话,会有一个默认值 Comparer<T>.Default
来看看这个默认比较器的定义是什么
|
|
关注到其中有一个 public 函数 Create
,如果传入类型为 Comparison<T>
的参数 comparison
不为null,那么将会把这个参数直接传入 ComparisonComparer<T>
作为实例化参数,首先参数类型 Comparison<T>
本身是一个委托,并且函数签名和 SortedSet
实现的接口 IComparer.Compare
完全一致
所以自然需要关注一下 ComparisonComparer<T>
究竟是什么东西,为什么会返回他
可以发现这是一个继承了 Comparer<T>
的比较器,并且可以被序列化,最后重写的比较函数中,将构造函数中传入的参数直接进行调用,在上面的流程中可以知道,传入的参数其实是一个委托
至此参数调用流程就清晰了,回过头来看一下 SortedSet<T>
,首先这是一个可以被序列化的类,另外还实现了 IDeserializationCallback
,这里关注一下完成反序列化后如何进行callback
|
|
回顾之前所有的分析,比较函数签名为 int function(string x, string y)
,如果这个函数可控,有一个签名非常相似的函数 Process.Start
,如果可以比较函数设置为他,那么在第二次Add的时候便可以调用实现一次命令执行,但是这里存在一个问题,那就是这个函数返回值为 Process
,与比较函数的签名返回类型 int
不匹配,直接赋值会导致异常,所以这里需要使用到多播委托 MulticastDelegate
而在官方文档中已经指出,其实程序中使用 deletegate
做声明的委托,就是派生自 MulticastDelegate
多播委托其实就是将多个委托合并起来成为一个列表,然后将他作为一个新的委托进行返回,跟入到委托合并,也就是 Delegate.Combine
因为继承自 MulticastDelegate
,所以最终实现其实在 MulticastDelegate.CombineImpl
,这个函数非常长,所以只需要知道他最终返回的是一个新的委托,并且合并中的关键点
这其中有两个变量
_invocationList
- 按顺序存放了将要执行的委托列表,私有变量,无法直接赋值
_invocationCount
- 委托列表的数量
虽然多播委托提供了一个可供调用的公开函数用来合并两个委托,但是仍然需要保证两个委托返回类型相同,但是这里可以发现一件事,其实在合并的时候,委托列表将会被转换为 object[]
用来保证两个委托合并的正常进行,那么可以考虑在构造的时候,使用反射直接修改这个值,强行插入一条 Process.Start
,由于这里需要一条委托,而 Process.Start
是一个函数,那么可以考虑使用 Func
构造一个接受两个 string
参数,返回类型为 Process
的泛型委托
|
|
构造方法
|
|
将 SortedSet<string> s
进行序列化后便可以得到一条用来弹出计算器的payload,如果对这个值进行反序列化,将会触发 Process.Start
并且执行 cmd.exe /c calc