简介
SoapFormatter位于System.Runtime.Serialization.Formatters.Soap.dll,以 SOAP 格式序列化和反序列化对象或连接对象的整个图,并实现了IRemotingFormatter、IFormatter接口。SOAP即Simple Object Access Protocol,简单对象访问协议,基于XML协议。SOAP是开放的协议,可以跨平台的其他程序也可以使用SoapFormatter序列化的文件,SoapFormatter与BinaryFormatter的区别是:SoapFormatter不能序列化泛型类型。与BinaryFormatter一样在序列化时不需要向序列化器指定序列化对象的类型。而XmlSerializer需要向XML序列化器指定序列化对象的类型。SoapFormatter的缺点是生成的文件较大,现在已很少使用。
01 SoapFormatter的序列化和反序列化
编写以下测试用例,[Serializable]标识表示此类可被序列化。
序列化并输出序列化后的xml和反序列化结果。
执行结果如下
02 ActivitySurrogateSelector
在SoapFormatter类的定义有一个SurrogateSelector 属性,SurrogateSelector即代理选择器,作用是帮助格式化程序选择要将序列化或反序列化进程委托给的序列化代理项。ISurrogateSelector定义如下。
ISurrogateSelector中ISerializationSurrogate是必须实现的接口,其中GetObjectData方法在对象序列化时进行调用,而SetObjectData方法用于反序列化。关于ISurrogateSelector和ISerializationSurrogate的详细介绍可以参考微软官网:https://learn.microsoft.com/zh-cn/dotnet/api/system.runtime.serialization.surrogateselector?view=net-6.0。我们这里只需要关注一点:代理选择器的判定在[Serializable]特性之前,可以实现将原本不能被序列化的类用来序列化和反序列化。
看到GetObjectData方法,其实已经可以接上《.net反序列化萌新入门--Json.Net》中的WindowsIdentity攻击链了,但这里我们要讲一个不同的攻击链ActivitySurrogateSelector。前面介绍了代理选择器,但在实际环境中我们无法指定代理选择器,也就无法正确的反序列化,这就用到了ActivitiySurrogateSelector,添加以下选择代理器,代码来自ysoserial的ActivitySurrogateSelectorGenerator.cs。
此时将测试用例中的[Serializable]标识删除,依然可以成功序列化,并且以下代码中soapFormatter2没有指定代理选择器也可以反序列化成功。代码中关闭DisableActivitySurrogateSelectorTypeCheck类型检查是为了绕过高版本框架对ActivitySurrogateSelector类滥用的限制。
使用dnspy分析序列化成功的原因,首先是soapFormatter.SurrogateSelector = new MySurrogateSelector()中代理选择器被赋值给this.m_surrogates。
在序列化时代理选择器传入ObjectWriter。
在ObjectWriter中,代理选择器用于WriteObjectInfo的序列化。
跟进WriteObjectInfo.Serialize(),对代理选择器进行判定后,代码最终走到了this.serializationSurrogate.GetObjectData()中。
这就来到了ActivitySurrogateSelector内实现的GetObjectData,在GetObjectData调用SetType将类型设置为子类ObjectSerializedRef。而ObjectSerializedRef是可以被序列化的。
总的来说,ActivitySurrogateSelector代理选择器中ObjectSurrogate.GetObjectData()将原本不可被序列化的对象存储到ObjectSerializedRef这个可以被序列化的类实例中,由此实现序列化原本不可序列化的类。如果我们可以加载自己的程序集,那么在new实例的时候触发构造函数就会执行恶意代码。作者将目标转向了LINQ类,如果我们替换了LINQ中的委托,通过替换委托来加载程序集并创建实例,那么触发LINQ之后就会执行恶意代码,构造攻击链:byte[] -> Assembly.Load -> Assembly -> Assembly.GetType -> Type[] -> Activator.CreateInstance。此攻击链中this.assemblyBytes = File.ReadAllBytes(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "e.dll")),使用Assembly.Load将恶意类加载到byte[]数组中,利用Select()方法拿到IEnumerable,通过创建委托配合SelectMany()拿到Assembly.GetTypes(),最后Activator.CreateInstance创建实例就完成了整个LINQ的链。
以上是作者一开始的思路,但在不同的.net框架中可能无法兼容,作者使用了System.Linq.Enumerable+WhereSelectEnumerableIterator`2修复这个问题用来兼容不同版本。
最终执行链为:Assembly.Load(byte[]).GetTypes().GetEnumerator().{MoveNext(),get_Current()} -> Activator.CreateInstance()
构造了LINQ链还有一个问题,因为LINQ的延迟执行特点,只有当我们枚举结果集合里的元素时,才会加载程序集并创建类型实例。作者又找到一种方法,使得在反序列化时执行ToString() 函数,然后找到一条链从ToString() 到 IEnumerable。
IEnumerable -> PagedDataSource -> ICollection
ICollection -> AggregateDictionary -> IDictionary
IDictionary -> DesignerVerb -> ToString
代码实现如下,使用PagedDataSource类将IEnumerable 类型转换为 ICollection类型,其中的dataSource字段为IEnumerable 类型。然后将 ICollection 类型转换为 IDictionary 类型,DesignerVerb 类型的ToString() 函数会枚举 IDictionary,需要使用反射插入IDictionary。
最后使用Hashtable启动整条链,在对Hashtable 类进行反序列化的时候,它将会重建密钥集。反射修改buckets字段的key值,将key是string的替换为verb,由此两个key相同,hash相同会异常,从而调用Environment.GetResourceString(),在GetResourceString()中string.Format会触发ToString()。
到这里整条链其实已经完成,可以执行命令但是会触发异常造成报错,所以作者又包装了一层System.Windows.Forms.AxHost.State,
可以看到PropertyBagBinary在反序列化时增加了try-catch处理,从而解决了报错的问题。
03 总结
本文介绍了SoapFormatter反序列化相关知识和ActivitySurrogateSelector攻击链,ActivitySurrogateSelectorFromFile链与ActivitySurrogateSelector基本相同,只是可以执行自己编写的程序集。从审计角度来看,如果能控制SoapFormatter.Deserialize()传入的XML数据就可以轻松实现反序列化。
参考
https://xz.aliyun.com/t/9595
https://zhuanlan.zhihu.com/p/333316520
https://learn.microsoft.com/zh-cn/dotnet/api/system.runtime.serialization.formatters.soap.soapformatter?redirectedfrom=MSDN&view=netframework-4.8.1
https://learn.microsoft.com/zh-cn/dotnet/api/system.runtime.serialization.surrogateselector?view=net-6.0