Deserialization can be dangerous

  • Gérald Barré

This post is part of the series 'Vulnerabilities'. Be sure to check out the rest of the blog posts of the series!

Serialization is the process of converting an object into a stream of bytes to store the object or transmit it to memory, a database, or a file. It is very handy to store data and load them later or to transfer data between 2 systems. The .NET frameworks comes with multiple serializers including BinaryFormatter, JavaScriptSerializer, XmlSerializer, DataContractSerializer. While it is easy to serialize and deserialize data, you should take care of the options provided by the deserializer to not introduce a vulnerability in your code.

For instance, you can use the BinarySerializer to persist an object to a file.

static void Main(string[] args)
{
    var o = new Sample { Path = "test" };

    using (var stream = File.OpenWrite("output.bin"))
    {
        var binaryFormatter = new BinaryFormatter();
        binaryFormatter.Serialize(stream, collection);
    }

    using (var stream = File.OpenRead("output.bin"))
    {
        var binaryFormatter = new BinaryFormatter();
        var deserialized = binaryFormatter.Deserialize(stream);
    }
}

[Serializable]
class Sample
{
    public string Path { get; set; }
}

#Why is it dangerous?

However, the deserializer does not validate that the file has not been tampered. If you send a file to a server that contains a binary serialized data, you can create an instance of any type on the server. The deserializer will instantiate the object and assign the value of its properties. This code is an issue if the constructor and the properties can execute some undesirable code. The second way to execute code is to instantiate a class that implements IDisposable such as TempFileCollection. This class deletes the registered files on Dispose. So, once the GC calls the finalizer the files will be deleted. But you could do something more terrifying and maybe run any command on the machine. That's what is called Remote Code Execution (RCE). The binary serializer was the cause of multiple real life vulnerabilities such as in Microsoft Exchange Server (CVE-2018-8302) and more recently in Docker (CVE-2018-15514).

To sum up, deserialization executes code from different locations:

  • The constructor of each deserialized instance
  • The getter/setter of each property
  • The destructor when the garbage collection (GC) destroy the instance

#how to prevent that attack with common .NET serializers?

You can prevent this by implementing a custom SerializationBinder to only allow a reduced set of type to be instantiated:

using (var stream = File.OpenRead("output.bin"))
{
    var binaryFormatter = new BinaryFormatter();
    binaryFormatter.Binder = new CustomBinder();
    var deserialized = binaryFormatter.Deserialize(stream);
}

// Only allow known-types to be instantiated
class CustomBinder : SerializationBinder
{
    public override Type BindToType(string assemblyName, string typeName)
    {
        if (assemblyName == typeof(Sample).Assembly.FullName && typeName == typeof(Sample).FullName)
            return typeof(Sample);

        throw new ArgumentOutOfRangeException();
    }
}

If you are using ASP.NET, you may know that the ViewState is able to store any serializable object. This means it can also deserialize these objects. ASP.NET uses the BinarySerializer to store the ViewState. So, a malicious payload may be able to create an instance of any type on your server. You can prevent this by setting enableViewStateMac to true (on by default). This will first check that the payload has been generated by the server before deserializing it. You can read more about this parameter on the ASP.NET blog.

Other serializers may also be sensible to this attack. For instance, if you misconfigure Json.NET you may be vulnerable:

var settings = new JsonSerializerSettings
{
    // Limit the object graph we'll consume to a fixed depth. This prevents stackoverflow exceptions
    // from deserialization errors that might occur from deeply nested objects.
    MaxDepth = 32,

    // Setting this to None prevents Json.NET from loading malicious, unsafe, or security-sensitive types
    TypeNameHandling = TypeNameHandling.None,
};

JsonConvert.DeserializeObject<T>(jsonString, settings);

JavaScriptSerializer is also vulnerable if you do not set a type resolver:

var resolver = new CustomJavaScriptTypeResolver();
var serializer = new System.Web.Script.Serialization.JavaScriptSerializer(resolver);
serializer.RecursionLimit = 32;

#More generally, how to prevent that attack?

  • When you roundtrip state to a user-controlled space, sign the data. Thus, you can validate that the data has been generated by your application and it should not contain any malicious data.
  • Don't publish the signing key, so no one can generate the malicious payload.
  • Don't use any serialization formatter that allows an attacker to specify the object type to be deserialized. It may require some configuration for some serializers.

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?Buy Me A Coffee