前几天看到了这篇文章,记录一下。
先来一个demo,用SerializationBinder限制一下反序列化的类型。
1using System;
2using System.IO;
3using System.Runtime.Serialization;
4using System.Runtime.Serialization.Formatters.Binary;
5
6namespace Serialize
7{
8 internal class Program
9 {
10 static void Main(string[] args)
11 {
12 BinaryFormatter binaryFormatter = new BinaryFormatter();
13 MemoryStream memoryStream = new MemoryStream();
14 RCE calc = new RCE("calc");
15 binaryFormatter.Serialize(memoryStream, calc);
16
17
18 memoryStream.Position = 0;
19 binaryFormatter.Binder = new MyBinder();
20 object v = binaryFormatter.Deserialize(memoryStream);
21 Console.WriteLine(v);
22 Console.ReadKey();
23 }
24 }
25
26 [Serializable]
27 class RCE
28 {
29 public string cmd;
30
31 public RCE(string cmd)
32 {
33 this.cmd = cmd;
34 }
35
36 public override string ToString()
37 {
38 return $"exec cmd:{cmd}";
39 }
40 }
41 class MyBinder : SerializationBinder
42 {
43 public override Type BindToType(string assemblyName, string typeName)
44 {
45 Console.WriteLine($"assemblyName:{assemblyName},typeName:{typeName}.");
46 Type typeToDeserialize = Type.GetType(String.Format("{0}, {1}", typeName, assemblyName));
47
48 if (typeToDeserialize.Equals(typeof(RCE)))
49 {
50 //throw new Exception("can't deseriliza rce class.");
51 Console.WriteLine("can't deseriliza rce class.");
52 return null;
53 }
54 return typeToDeserialize;
55 }
56 }
57}
解释下代码,有一个RCE的类,通过反序列化cmd字段,然后触发他的tostring方法就可以rce执行命令。
在main函数中,我们先new了一个没有用binder的BinaryFormatter来序列化执行calc命令的RCE对象,在反序列化的时候,绑定了Binder实例做反序列化的类型判断。
在Binder中
通过Type.GetType拿到类型和typeof(RCE)进行比较,如果反序列化类型等于RCE,那么直接返回null,否则返回正确的type。
此时运行一下
发现Binder并没有起作用,calc命令仍然赋值给了RCE的cmd字段。why?
dnspy调试断在binder的return上然后下一步发现
在调用完m_binder.BindToType(assemblyString, typeString)
之后,如果type为空,dotnet会帮我们再次处理类型,也就是FastBindToType()
FastBindToType先从typecache中获取程序集,如果拿不到程序集就尝试进行加载程序集获取type。
其中bSimpleAssembly值取自FEassemblyFormat
而FEassemblyFormat是InternalFE的一个字段
通过 binaryFormatter.AssemblyFormat = System.Runtime.Serialization.Formatters.FormatterAssemblyStyle.Simple
我们赋值bSimpleAssembly,如果不赋值默认值也为FormatterAssemblyStyle.Simple
,所以bSimpleAssembly默认为true,接着看
通过 ObjectReader.ResolveSimpleAssemblyName 解析程序集,然后ObjectReader.GetSimplyNamedTypeFromAssembly(assembly, typeName, ref type)
从程序集中拿type
在断点的地方已经拿到了RCE类的type
最终反序列化仍然拿到了RCE的type
而并没有受限于binder的类型绑定。
其实上文的demo中我已经给了修复的方法,当加载不允许的程序集type时应该直接抛出异常,而不是返回null。
在BlueHat中也提到过 https://www.slideshare.net/MSbluehat/dangerous-contents-securing-net-deserialization
本地没有环境,直接用原作者的图了
exchange的binaryformatter都用到了ChainedSerializationBinder,上图是其实现。
在InternalBindToType返回空值时,不进行ValidateTypeToDeserialize导致黑名单完全不起作用。
InternalBindToType转发到LoadType函数
通过重写GetObjectData让序列化时自定义AssemblyName和FullTypeName
这样在LoadType的Type.GetType(string.Format("{0}, {1}",typeName,assemblyName))
就会抛出异常
抛了异常但是被catch捕获之后相当于LoadType返回了null,那么接着ValidateTypeToDeserialize失效,从而交由FastBindToType获取type,绕过了binder。
当binder返回null值时,binder对反序列化的类型校验不起作用。
文笔垃圾,措辞轻浮,内容浅显,操作生疏。不足之处欢迎大师傅们指点和纠正,感激不尽。