前言
这个星期稍微学习了一下.NET
反序列化相关的知识,所以想写一篇文章系统的梳理一下.NET
安全的相关知识。在阅读了市面上的.NET
反序列化文章后,对比Java反序列化,虽然.NET
在Web开发中已经用的非常多了,但是针对其安全研究较少,只有部分师傅做过.NET
分析。市面上的文章质量一言难尽,鲜有文章能讲明白,讲清楚。
环境搭建
最常见的C#
开发写工具是Visual Studio
,作为微软爸爸的官方标配,其各种功能都很完善。但是对于安全研究人员(起码是对于我来说),用起来不太顺手,并且很多功能可能不太需要。
这里我推荐使用JetBrians
家的Rider
作为主力的开发工具和调试工具。对于我来说,Rider有这么几个点吸引我
-
统一的快捷键和UI,对于熟悉IDEA等JB家族IDE的师傅来说,VS的快捷键和JB的快捷键差别太大,用起来手感很差。
-
自带
C#
反编译和调试功能,很多师傅还在用dySpy来对没有代码的C#
dll文件进行调试,但是Rider和IDEA类似,继承了反编译工具和调试功能,用起来不要说太爽。下图就是SessionSecurityToken的反编译的结果,效果非常好! -
软件体积小,相较于VS动辄几十个G的安装大小,Rider的体积就小很多了。
这里非常推荐初学者用Rider进行C#代码的编写和调试,手感棒棒的!至于如何安装Rider,这个我相信绝大多数师傅应该都没问题,这里就不在多说。
一个常见的小问题
在写代码的时候会经常发现有一些类找不到,这个时候我们需要右键Dependencies并点击Reference。
之后,搜索你需要的依赖库,并Add就可以了。如果你的依赖库是一个dll文件,你可以点击下面的Add From并选择对应文件也可以添加。
一些与反序列化无关的知识点
在.NET中执行命令
在Java的CC链反序列化中,我们可以通过Runtime.getRuntime().exec()
执行命令。同样的,在.NET
中我们可以使用Process.Start()
去执行命令1,值得高兴的是Process.Start
是一个static方法,可以直接调用,不需要像在Java中调用getRuntime()
获取Instance。
Process.Start
有多个定义,其中最简单的就是上图中的。其有两个参数,第一个参数为fileName
一般为cmd.exe
,第二个参数是arguments
,通常为/c <command>
。代码写起来也很简单
using System.Diagnostics;
namespace Lesson01
{
internal class Program
{
private static void RunCommand(string cmd)
{
Process.Start("cmd.exe", "/c " + cmd);
}
public static void Main(string[] args)
{
RunCommand("calc.exe");
}
}
}
在.NET中加载dll
这里的dll不是指那些用C/C++语言编写的动态链接库,而是.NET
的依赖库,可以理解为Java中的class文件。类似于Java中调用TemplatesImpl加载class文件,在.NET
可以用Assembly.Load
直接加载dll文件。
ExploitClass
我们首先需要编写一个dll项目。在Rider中,我们右键解决方案名称,选择New Project,选择Class Library,最后点击Create
在生成的代码中,添加构造函数,这样我们的库就写完了。
using System.Diagnostics;
namespace ExploitClass
{
public class ExploitClass
{
public ExploitClass()
{
Process.Start("cmd.exe", "/c calc.exe");
}
}
}
Loader
在Loader中,我们需要使用Assembly.Load
加载我们上面编写的dll文件。我们可能需要在Loader Project中添加ExploitClass的引用。
这样之后我们可以直接用下面的代码获得ExploitClass的dll地址了。
typeof(ExploitClass.ExploitClass).Assembly.Location
Assembly.Load
的用法也很简单,第一个参数是dll的字节码即可。
var dllLocation = typeof(ExploitClass.ExploitClass).Assembly.Location;
Console.WriteLine("Dll Location: " + dllLocation);
var dll = File.ReadAllBytes(dllLocation);
Assembly.Load(dll).CreateInstance("ExploitClass.ExploitClass");
可以看到这种方法有一定的局限性,需要访问到这个ExploitClass
(这里用了CreateInstance
),才可以执行。为了解决这个问题,可以使用ModuleInit.Fody
这个库设置C# module的init函数
namespace ExploitClass
{
public static class ModuleInitializer
{
public static void Initialize()
{
System.Diagnostics.Process.Start("cmd.exe", "/c calc");
}
}
}
这样的话就不需要CreateInstance
了,在Assembly.Load
的时候就可以执行命令了。
.NET中的反序列化器
在笔者的学习过程中,见识了了很多反序列化器:
- 有以
Serializer
结尾的XmlSerializer
2,DataContractSerializer
3,JavaScriptSerializer
等; - 有以
Formatter
结尾的BinaryFormatter
,SoapFormatter
等; - 有以
Reader
等结尾的XamlReader
等; - 甚至第三方的如
JSON.NET
,
这些反序列化器各不相同。在此我们分类进行介绍。
Formatter序列化
以Formatter反序列化器作为入门篇可能是对初学者最方便的了,Formatter反序列化器是一个C#
支持的,非常标准的反序列化器。常见的Formatter序列化器有下面几种:
- BinaryFormatter:生成的序列化数据是二进制
- SoapFormatter:生成的数据用于Soap请求
- ObjectStateFormatter:用于生成ViewState这种状态
using (MemoryStream memoryStream = new MemoryStream())
{
// 序列化
var formatter = new BinaryFormatter(); // 可以替换为其他formatter
formatter.Serialize(memoryStream, myObject);
memoryStream.Position = 0;
// 反序列化
var myObject = formatter.Deserialize(memoryStream);
}
上面的代码展示了针对一个Object对象进行序列化和反序列化的过程。
用于控制序列化过程的注解
但是不是所有的类都可以被Formatter序列化器进行序列化,下面是一个标准的,可以使用Formatter序列器进行序列化的对象
[Serializable]
public class CustomObject1
{
[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");
}
}
所有支持Formatter序列化的对象都需要在类的声明位置标注[Serializable]
。在类当中可以指定[OnDeserializing]
,[OnDeserialized]
,[OnSerializing]
,[OnSerialized]
4种注解,来指定序列化和反序列化中的行为。当然,除了Formatter类之外,有一些其他的序列化器可以使用上述注解。
我们用这个代码对CustomObject1
进行序列化测试,可以观察到如下输出:
这说明在默认情况下,序列化的会先执行[OnSerializing]
标注的函数,在最后会执行[OnSerialized]
。反序列化的时候同理:会先执行[OnDeserializing]
标注的函数,在最后会执行[OnDeserialized]
。如果某个标注了[OnDeserializing]/[OnDeserialized]
函数在执行反序列化存在可以RCE的地方,那么就可以在反序列化的时候实现RCE。
ISerializable接口
除了上述的注解之外,如果对象实现了ISerializable
也可以通过构造函数和GetObjectData
函数控制序列化过程。下面展示了一个带有对应函数的类:
[Serializable]
public class CustomObject2: ISerializable
{
[NonSerialized]
private string _val;
public CustomObject2()
{
}
protected CustomObject2(SerializationInfo info, StreamingContext context)
{
Console.WriteLine("CustomObject2 constructor is called");
_val = info.GetString("foooo");
}
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
Console.WriteLine("GetObjectData");
info.AddValue("foooo", _val);
}
[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");
}
}
同样,我们对这个类进行序列化和反序列化,可以看到其调用顺序
OK,到此为止我们可以总结出一个流程
SurrogateSelector
在序列化的时候,我们可以通过设置fomatter的SurrogateSelector来指定序列化代理选择器。通过SurrogateSelector,可以自定义GetObjectData和SetObjectData从而控制序列化和反序列化流程。
通过设置代理器,可以将Formatter不支持序列化的对象进行序列化从而达到扩展Formatter的效果。
参考Y4er师傅的dotnet-serialize-1014,下面是一个没有标注序列化的类,在默认情况下是不能序列化的。
class Person
{
public string Name { get; set; }
public Person(string name)
{
Name = name;
}
public override string ToString()
{
return Name;
}
}
如果我们想序列化这个类,那就需要使用代理器了。下面是一个针对Person对象的序列化代理器,其设置了序列化该类的方法GetObjectData
,和反序列化该类的方法SetObjectData
。
class PersonSerializeSurrogate : ISerializationSurrogate
{
public void GetObjectData(Object obj, SerializationInfo info, StreamingContext context)
{
var p = (Person)obj;
info.AddValue("Name", p.Name);
}
public Object SetObjectData(Object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector)
{
var p = (Person)obj;
p.Name = info.GetString("Name");
return p;
}
}
我们通过设置代理,就可以序列化了
var formatter = new BinaryFormatter(); // 可以替换为其他formatter
SurrogateSelector ss = new SurrogateSelector(); // 创建selector
ss.AddSurrogate(typeof(Person), formatter.Context, new PersonSerializeSurrogate()); // 为Person对象设置代理器
formatter.SurrogateSelector = ss; // 设置代理选择器
formatter.Serialize(memoryStream, new Person("name"));
在设置完对象之后,序列化和反序列化的过程就有些不一样了,下面是Y4er师傅总结的流程。
Serializer序列化
Serializer
家族相对于Formatter
家族就复杂一些,比如说最常见的XmlSerializer
就不受上面注解的影响。
XmlSerializer
XmlSerializer进行序列化和反序列化的时候需要指定对应的ClassType,下面是XmlSerializer序列化和反序列化的标准代码:
XmlSerializer xmlSerializer = new XmlSerializer(typeof(XmlObject2));
using (MemoryStream memoryStream = new MemoryStream())
{
var obj = new XmlObject2();
TextWriter writer = new StreamWriter(memoryStream);
xmlSerializer.Serialize(writer, obj);
memoryStream.Position = 0;
Console.WriteLine(Encoding.UTF8.GetString(memoryStream.ToArray()));
XmlObject2 p1 = (XmlObject2)xmlSerializer.Deserialize(memoryStream);
}
我们可以看到在new XmlSerializer()
的时候,构造函数的第一个参数代表所序列化类的类型,这在一定程度上限制了XmlSerializer的利用场景。在研究XmlSerializer的序列化和反序列化的时候,
使用 XmlInclude或SoapInclude 特性静态指定非已知的类型
我经常遇到一个报错:使用 XmlInclude或SoapInclude 特性静态指定非已知的类型
,Y4er也在文章中提到了这个报错,不过他的解释我没怎么看懂,我这里在来详细说明一下。我们设想下面这个类:
public class XmlObject
{
public string Name;
public string Value;
}
public class XmlObject2
{
public object AnyObj;
}
当我们序列化下面这个对象的时候
var obj = new XmlObject2();
obj.AnyObj = new XmlObject2();
xmlSerializer.Serialize(writer, obj);
你会发现序列化的结果很正常:
<?xml version="1.0" encoding="utf-8"?>
<XmlObject2 xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<AnyObj xsi:type="XmlObject2" />
</XmlObject2>
但是,如果你讲AnyObj的值设置为new XmlObject()
,也就是:
var obj = new XmlObject2();
obj.AnyObj = new XmlObject();
xmlSerializer.Serialize(writer, obj);
你会惊奇的发现,上面的代码报错了。
我猜测:在创建序列化器的时候,需要指定一个类型,那么在序列化的过程中,序列化器会遍历对象所有的元素,如果某个元素超出了在创建序列化器指定的类型(比如说这里的XmlObject
),那么就会抛出这个报错。
那么解决这个问题的方式有2种:
- 在创建的时候把你需要的类加进去。
- 利用
XmlInclude
将你需要的类加进去。
第二种可以参考官方文档,这里不在赘述(未来也用不上)。第一种的实现方案看似很难,其实只需要利用好泛型。在.NET
的反序列化中常见的可以有很多泛型的类是ExpandedWrapper
。
var obj = new ExpandedWrapper<XmlObject, XmlObject2>();
obj.ProjectedProperty0 = new XmlObject2();
obj.ProjectedProperty0.AnyObj = new XmlObject();
我们将之前的代码改成这样,就可以成功序列化了。
<?xml version="1.0" encoding="utf-8"?>
<ExpandedWrapperOfXmlObjectXmlObject2 xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<ProjectedProperty0>
<AnyObj xsi:type="XmlObject" />
</ProjectedProperty0>
</ExpandedWrapperOfXmlObjectXmlObject2>
DataContractSerializer & DataContractJsonSerializer
这两个差别不大,合在一起说了。前一个出来的结果是XML类型,后一个是JSON类型。虽然DataContractSerializer
和XmlSerializer
都是Serializer
和XML
输出,但是两者可以说完全不同。下面是DataContractSerializer
的标准序列化和反序列化代码:
var s = new DataContractSerializer(typeof(DemoObject2));
using (MemoryStream memoryStream = new MemoryStream())
{
var obj = new DemoObject2();
obj.AnyObj = new DemoObject2();
s.WriteObject(memoryStream, obj);
memoryStream.Position = 0;
Console.WriteLine(Encoding.UTF8.GetString(memoryStream.ToArray()));
var p1 = s.ReadObject(memoryStream);
}
这居然用WriteObject和ReadObject,咋和Java一样呢?
【待续。。。】
参考文献
Footnotes
-
https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.start?view=net-7.0 ↩
-
https://learn.microsoft.com/en-us/dotnet/api/system.xml.serialization.xmlserializer?view=net-7.0 ↩
-
https://learn.microsoft.com/en-us/dotnet/api/system.runtime.serialization.datacontractserializer?view=net-7.0 ↩
-
https://github.com/Y4er/dotnet-deserialization/blob/main/dotnet-serialize-101.md ↩