从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 命名空间中定义了三个类,分别为
TypedBinaryFormatterTypeSerializationFormatterTypeSoapFormatter其中在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)⇒trueChainedSerializationBinder.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