February 2, 2024

【.NET 安全】ASP.NET Deserialization 01

参考文献

https://github.com/Y4er/dotnet-deserialization/
一代人一代人传下来的文献。学。

01-环境搭建

搭建牢记于心,一开始以为很简单,但是对刚入手一门语言的人菜鸡还是太勉强了,花了也有2小时去研究一下包管理、lib添加以及一系列的兼容问题 all in one,就不写了。
笔者使用的编译器是Rider,被JB全家桶磨平了,用JB只有一次和无数次。
image.png
个人感觉挺不错的,体验感不错。比Visual studio美观(

02-基础序列化与反序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;

namespace DonNET_Deserialization;

public class NormalSerialize
{
[Serializable]
public class Myclass
{
public int n1;
[NonSerialized] public int n2;
public String str;
}

class Program
{
public static void BinaryFormatterSerialize(string file, object o)
{
BinaryFormatter binaryFormatter = new BinaryFormatter();
FileStream fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None);
binaryFormatter.Serialize(fileStream, o);
fileStream.Close();
Console.WriteLine($"serialize object {o} to file {file}.");
}

public static object BinaryFormatterDeserialFromFile(string file)
{
IFormatter formatter = new BinaryFormatter();
Stream stream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read);
object o = formatter.Deserialize(stream);
stream.Close();
return o;
}

static void Main(string[] args)
{
try
{
Myclass myObject = new Myclass();
myObject.n1 = 1;
myObject.n2 = 2;
myObject.str = "Boogipop";

BinaryFormatterSerialize("1.bin", myObject);
Myclass myObject1 = (Myclass)BinaryFormatterDeserialFromFile("1.bin");

Console.WriteLine($"n1:{myObject1.n1}");
Console.WriteLine($"NonSerialized n2:{myObject1.n2}");
Console.WriteLine($"str:{myObject1.str}");
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
}
}
}

image.png
运行结果如上,这里我们需要加上Serializable注解,并且可以发现添加了NonSerialized注解的字段并没有参与序列化,因此反序列化输出时它的值为0
image.png
它的序列化文件格式如上

03-Formatters

Formatter就是你需要进行什么样子的序列化和反序列化,在DOTNET中,Formatter大概分为以下几类

其中XmlSerializer和JsonSerializer咱们在不久后的将来还会见到的XD。本文主要讲一些基础内容知识。
image.png
其中Formatters都像BinaryFormatter一样实现了2个类IFormatter、IRemotingFormatter
其中IRemotiongFormatter主要提供RPC服务,它也实现IFormatter接口
image.png
因此我们只需要关注IFormatter即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using System.IO;
using System.Runtime.InteropServices;

#nullable disable
namespace System.Runtime.Serialization
{
[ComVisible(true)]
public interface IFormatter
{

object Deserialize(Stream serializationStream);


void Serialize(Stream serializationStream, object graph);


ISurrogateSelector SurrogateSelector { get; set; }
SerializationBinder Binder { get; set; }
StreamingContext Context { get; set; }
}
}

IFormatter类的定义如上,他有三个属性

这里引用Y4er师傅的介绍。

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

其中我们需要重点注意的就是ISurrogateSelector,代理选择器。

04-序列化和反序列化生命周期

这里还是以BinaryFormatter为例子,介绍一下DOTNET反序列化和序列化的生命周期。上面介绍了IFormatter的三个属性,而这三个属性具体含义和例子是这样的

1.首先确定formatter是否有代理选择器,如果有则检查代理选择器要处理的对象类型是否和给定的对象类型一致,如果一致,代理选择器会调用ISerializable.GetObjectData()
2.如果没有代理选择器,或者代理选择器不处理该对象类型,则检查对象是否有[Serializable]特性。如果不能序列化则抛出异常
3.检查该对象是否实现ISerializable接口,如果实现就调用其GetObjectData方法
4.如果没实现ISerializable接口就使用默认的序列化策略,序列化所以没标记[NonSerialized]的字段

用流程图表示就是下面这样

并且序列化和反序列化有以下四个回调事件

特性 调用关联的方法时 典型用法
OnDeserializingAttribute 反序列化之前 初始化可选字段的默认值。
OnDeserializedAttribute 反序列化之后 根据其他字段的内容修改可选字段值。
OnSerializingAttribute 序列化之前 准备序列化。 例如,创建可选数据结构。
OnSerializedAttribute 序列化之后 记录序列化事件。

04.1-代理器模式

和魔术方法一样,他们是以注解的形式写在方法上的。现在准备以下代码做测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Security.Permissions;

namespace NormalSerialize
{
[Serializable]
public class MyObject : ISerializable
{
public string str { get; set; }
public MyObject()
{
}
//实现了ISerializable接口的类必须包含有序列化构造函数,否则会出错。
protected MyObject(SerializationInfo info, StreamingContext context)
{
Console.WriteLine("MyObject(SerializationInfo info, StreamingContext context)");
str = info.GetString("str");
}

[SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)]
public virtual void GetObjectData(SerializationInfo info, StreamingContext context)
{
Console.WriteLine("GetObjectData of MyObject.class");
info.AddValue("str", str, typeof(string));
}

[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");
}
}
class MySerializationSurrogate : ISerializationSurrogate
{
public void GetObjectData(object obj, SerializationInfo info, StreamingContext context)
{
Console.WriteLine("GetObjectData of ISerializationSurrogate");
info.AddValue("str", ((MyObject)obj).str);
}

public object SetObjectData(object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector)
{
Console.WriteLine("SetObjectData of ISerializationSurrogate");
MyObject m = new MyObject();
m.str = (string)info.GetValue("str", typeof(string));
return m;
}
}
class Program
{
static void Main(string[] args)
{
try
{
MyObject myObject = new MyObject();
myObject.str = "hello";

using (MemoryStream memoryStream = new MemoryStream())
{
// 构建formatter
BinaryFormatter binaryFormatter = new BinaryFormatter();

// 设置序列化代理选择器
SurrogateSelector ss = new SurrogateSelector();
ss.AddSurrogate(typeof(MyObject), binaryFormatter.Context, new MySerializationSurrogate());
// 赋值给formatter 这里是否设置代理选择器决定了序列化的生命周期
binaryFormatter.SurrogateSelector = ss;
// 序列化
binaryFormatter.Serialize(memoryStream, myObject);
// 重置stream
memoryStream.Position = 0;
myObject = null;
// 反序列化
myObject = (MyObject)binaryFormatter.Deserialize(memoryStream);
Console.WriteLine(myObject.str); // hello
}

}
catch (Exception e)
{
Console.WriteLine(e.StackTrace);
}
}
}
}

可以看到这里首先准备了一个MyObject类,实现了ISerializable接口,并且准备了一个代理器MySerializationSurrogate,并且在序列化的时候指定了代理器,因此我们的流程应该是

04.2-非代理器&&ISerializable模式

这一次我们删除代理器设置。将binaryFormatter.SurrogateSelector = ss;注释
image.png
结果如上,顺序变成了

04.3-纯注解模式

当我们不继承ISerializable接口后,我们会发现他的流程变得更加简单了。
image.png
它不会调用GetObjectData也不会实例化对象。单纯只执行我们上面说的基本四个生命周期。

04.4-SerializationInfo

在上述流程中我们经常会见到SerializationInfo这个类,那么这个类有什么用呢?
image.png
首先它的成员变量就是我们序列化对象的成员属性、类型、数量和名称。其次他给定了我们获取和修改成员属性的方法。这一点在上述的GetObjectData也看到了。
image.png
image.png

05-Dotnet的命令执行

在ASP.NET CORE里,命令执行的方式比较严格。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using System.Diagnostics;

namespace DonNET_Deserialization;

public class Command
{
public static void Main(string[] args)
{
var start = Process.Start("cmd.exe", "/c calc");
var process = new Process();
process.StartInfo.FileName = "cmd.exe";
process.StartInfo.Arguments = "/c calc";
process.Start();
}
}

在csharp中调用系统命令必须指定你的filename和argument,而不是单单输入一个calc那么简单。假如我们需要获取命令执行的回显可以这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using System;
using System.Diagnostics;

namespace DonNET_Deserialization;

public class Command
{
public static void Main(string[] args)
{
var process = new Process();
process.StartInfo.FileName = "cmd.exe";
process.StartInfo.Arguments = "/c whoami";
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.UseShellExecute = false;
process.Start();
var processStandardOutput = process.StandardOutput.ReadToEnd();
Console.WriteLine(processStandardOutput);
}
}

image.png
其中RedirectStandardOutput和UseShellExecute需要注意一下

  • RedirectStandardOutput:
    • 当设置为 true 时,表示重定向标准输出流,即将进程的标准输出流(通常是在控制台窗口中显示的信息)连接到 Process.StandardOutput 流中,以便你的程序可以读取进程的输出。
  • UseShellExecute:
    • 当设置为 false 时,表示不使用操作系统的 shell 启动进程。相反,它允许你直接启动可执行文件,命令行等,而不需要借助 shell。

06-ObjectDataProvider

先介绍一下这个类,他是我们gadgets其中的一环,我们需要了解一下他为什么可以命令执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using System;
using System.Diagnostics;
using System.Windows.Data;

namespace DonNET_Deserialization;

public class Command
{
public static void Main(string[] args)
{
var objectDataProvider = new ObjectDataProvider();
objectDataProvider.MethodName = "Start";
objectDataProvider.MethodParameters.Add("cmd.exe");
objectDataProvider.MethodParameters.Add("/c calc");
objectDataProvider.ObjectInstance = new Process();
}
}

image.png
运行上述实例后就是可以命令执行。流程如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public object ObjectInstance
{
get
{
return this._instanceProvider == null ? this._objectInstance : (object) this._instanceProvider;
}
set
{
if (this._mode == ObjectDataProvider.SourceMode.FromType)
throw new InvalidOperationException(System.Windows.SR.Get("ObjectDataProviderCanHaveOnlyOneSource"));
this._mode = value == null ? ObjectDataProvider.SourceMode.NoSource : ObjectDataProvider.SourceMode.FromInstance;
if (this.ObjectInstance == value)
return;
if (value != null)
{
this._constructorParameters.SetReadOnly(true);
this._constructorParameters.ClearInternal();
}
else
this._constructorParameters.SetReadOnly(false);
value = this.TryInstanceProvider(value);
if (!this.SetObjectInstance(value) || this.IsRefreshDeferred)
return;
this.Refresh();
}
}

在Objectdataprovideer的属性ObjectInstance的Setter方法存在一个Refresh刷新
image.png
然后会调用Queryworkr方法
image.png
然后会调用InvokeMethodOnInstance方法
image.png
image.png
最后执行了命令。也就是核心是该类的setter方法。

07-小结

fe7659a5153950a5dcc050d2b80892d3.jpg

About this Post

This post is written by Boogipop, licensed under CC BY-NC 4.0.

#反序列化#.NET