dotnet反序列化之并不安全的SerializationBinder

前几天看到了这篇文章,记录一下。

使用SerializationBinder

先来一个demo,用SerializationBinder限制一下反序列化的类型。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;

namespace Serialize
{
    internal class Program
    {
        static void Main(string[] args)
        {
            BinaryFormatter binaryFormatter = new BinaryFormatter();
            MemoryStream memoryStream = new MemoryStream();
            RCE calc = new RCE("calc");
            binaryFormatter.Serialize(memoryStream, calc);


            memoryStream.Position = 0;
            binaryFormatter.Binder = new MyBinder();
            object v = binaryFormatter.Deserialize(memoryStream);
            Console.WriteLine(v);
            Console.ReadKey();
        }
    }

    [Serializable]
    class RCE
    {
        public string cmd;

        public RCE(string cmd)
        {
            this.cmd = cmd;
        }

        public override string ToString()
        {
            return $"exec cmd:{cmd}";
        }
    }
    class MyBinder : SerializationBinder
    {
        public override Type BindToType(string assemblyName, string typeName)
        {
            Console.WriteLine($"assemblyName:{assemblyName},typeName:{typeName}.");
            Type typeToDeserialize = Type.GetType(String.Format("{0}, {1}", typeName, assemblyName));

            if (typeToDeserialize.Equals(typeof(RCE)))
            {
                //throw new Exception("can't deseriliza rce class.");
                Console.WriteLine("can't deseriliza rce class.");
                return null;
            }
            return typeToDeserialize;
        }
    }
}

解释下代码,有一个RCE的类,通过反序列化cmd字段,然后触发他的tostring方法就可以rce执行命令。

在main函数中,我们先new了一个没有用binder的BinaryFormatter来序列化执行calc命令的RCE对象,在反序列化的时候,绑定了Binder实例做反序列化的类型判断。

在Binder中

https://y4er.com/img/uploads/dotnet-deserialize-bypass-binder/1.png

通过Type.GetType拿到类型和typeof(RCE)进行比较,如果反序列化类型等于RCE,那么直接返回null,否则返回正确的type。

此时运行一下

https://y4er.com/img/uploads/dotnet-deserialize-bypass-binder/2.png

发现Binder并没有起作用,calc命令仍然赋值给了RCE的cmd字段。why?

不起作用的Binder

dnspy调试断在binder的return上然后下一步发现

https://y4er.com/img/uploads/dotnet-deserialize-bypass-binder/3.png

在调用完m_binder.BindToType(assemblyString, typeString)之后,如果type为空,dotnet会帮我们再次处理类型,也就是FastBindToType()

https://y4er.com/img/uploads/dotnet-deserialize-bypass-binder/4.png

FastBindToType先从typecache中获取程序集,如果拿不到程序集就尝试进行加载程序集获取type。

其中bSimpleAssembly值取自FEassemblyFormat

https://y4er.com/img/uploads/dotnet-deserialize-bypass-binder/5.png

而FEassemblyFormat是InternalFE的一个字段

https://y4er.com/img/uploads/dotnet-deserialize-bypass-binder/6.png

通过 binaryFormatter.AssemblyFormat = System.Runtime.Serialization.Formatters.FormatterAssemblyStyle.Simple我们赋值bSimpleAssembly,如果不赋值默认值也为FormatterAssemblyStyle.Simple,所以bSimpleAssembly默认为true,接着看

https://y4er.com/img/uploads/dotnet-deserialize-bypass-binder/7.png

通过 ObjectReader.ResolveSimpleAssemblyName 解析程序集,然后ObjectReader.GetSimplyNamedTypeFromAssembly(assembly, typeName, ref type)从程序集中拿type

在断点的地方已经拿到了RCE类的type

https://y4er.com/img/uploads/dotnet-deserialize-bypass-binder/8.png

最终反序列化仍然拿到了RCE的type

https://y4er.com/img/uploads/dotnet-deserialize-bypass-binder/9.png

而并没有受限于binder的类型绑定。

如何正确使用binder?

其实上文的demo中我已经给了修复的方法,当加载不允许的程序集type时应该直接抛出异常,而不是返回null。

https://y4er.com/img/uploads/dotnet-deserialize-bypass-binder/10.png

在BlueHat中也提到过 https://www.slideshare.net/MSbluehat/dangerous-contents-securing-net-deserialization

https://y4er.com/img/uploads/dotnet-deserialize-bypass-binder/11.png

CVE-2022-23277 of exchange

本地没有环境,直接用原作者的图了

https://y4er.com/img/uploads/dotnet-deserialize-bypass-binder/12.png

exchange的binaryformatter都用到了ChainedSerializationBinder,上图是其实现。

在InternalBindToType返回空值时,不进行ValidateTypeToDeserialize导致黑名单完全不起作用。

InternalBindToType转发到LoadType函数

https://y4er.com/img/uploads/dotnet-deserialize-bypass-binder/13.png

通过重写GetObjectData让序列化时自定义AssemblyName和FullTypeName

https://y4er.com/img/uploads/dotnet-deserialize-bypass-binder/14.png

这样在LoadType的Type.GetType(string.Format("{0}, {1}",typeName,assemblyName))就会抛出异常

https://y4er.com/img/uploads/dotnet-deserialize-bypass-binder/15.png

抛了异常但是被catch捕获之后相当于LoadType返回了null,那么接着ValidateTypeToDeserialize失效,从而交由FastBindToType获取type,绕过了binder。

总结

当binder返回null值时,binder对反序列化的类型校验不起作用。

参考

  1. https://codewhitesec.blogspot.com/2022/06/bypassing-dotnet-serialization-binders.html
  2. https://testbnull.medium.com/note-nhanh-v%E1%BB%81-binaryformatter-binder-v%C3%A0-cve-2022-23277-6510d469604c

文笔垃圾,措辞轻浮,内容浅显,操作生疏。不足之处欢迎大师傅们指点和纠正,感激不尽。