An Unsafe Deserialization Vulnerability and Types of Deserialization - Tutorial Boy -->

An Unsafe Deserialization Vulnerability and Types of Deserialization


Deserialization

Unsafe Deserialization (also referred to as Insecure Deserialization) is a vulnerability wherein malformed and untrusted data input is insecurely deserialized by an application. It is exploited to hijack the logic flow of the application end might result in the execution of arbitrary code. Although this isn’t exactly a simple attack to employ, it featured in OWASP’s Top 10 most recent iteration as part of the Software and Data Integrity Failures risk, due to the severity of impact upon compromise.

The process of converting an object state or data structure into a storable or transmissible format is called serialization. Deserialization is its opposite - the process of extracting the serialized data to reconstruct the original object version.

Unsafe Deserialization issues arise when an attacker is able to pass ad hoc malicious data into user-supplied data to be deserialized. This could result in arbitrary object injection into the application that might influence the correct target behavior.

Impact

A successful Unsafe Deserialization attack can result in the full compromise of the confidentiality, integrity, and availability of the target system, and the oft-cited Equifax breach is probably the best example of the worst outcome that can arise. In Equifax’s case, an unsafe Java deserialization attack leveraging the Struts 2 framework resulted in remote code execution, which, in turn, led to the largest data breach in history.

Prevention

It is important to consider any development project from an architectural standpoint to determine when and where serialization is necessary. If it is unnecessary, consider using a simpler format when passing data.

In cases where it is impossible to forego serialization without disrupting the application’s operational integrity, developers can implement a range of defence-in-depth measures to mitigate the chances of being exploited.

  • Use serialization that only permits primitive data types.
  • Use a serialization library that provides cryptographic signature and encryption features to ensure serialized data are obtained untainted.
  • Authenticate before deserializing.
  • Use low privilege environments to isolate and run code that deserializes.

Finally, if possible, replace object serialization with data-only serialization formats, such as JSON.

Testing

Verify that serialization is not used when communicating with untrusted clients. If this is not possible, ensure that adequate integrity controls (and possibly encryption if sensitive data is sent) are enforced to prevent deserialization attacks including object injection.

OWASP ASVS: 1.5.2, 5.5.1, 5.5.3

Types of Deserializations

Unsafe Deserialization in .NET

Vulnerable Example

The .NET framework offers several instances of deserialization. Developers will likely be familiar with the following example, where some untrusted binary data is deserialized to create some objects:

[Serializable]
public class SomeClass
{
	public string SomeProperty { get; set; }
	public double SomeOtherProperty { get; set; }
}

class Program
{
	static void Main(string[] args)
	{
	   BinaryFormatter binaryFormatter = new BinaryFormatter();
	   MemoryStream memoryStream = new MemoryStream(File.ReadAllBytes("untrusted.file"));
	   SomeClass obj = (SomeClass)binaryFormatter.Deserialize(memoryStream);
	   Console.WriteLine(obj.SomeProperty);
	   Console.WriteLine(obj.SomeOtherProperty);
	}
}


The above program merrily deserializes not only instances of SomeClass (even though a class cast error is raised for other objects), but also might be enough to trigger dangerous behaviors. For example, a malicious user could leverage publicly available tools such as ysoserial.net to easily craft payloads that exploit the presence of external libraries, and thus build a chain of gadgets that eventually lead to RCE.

Alternatively, an attacker with knowledge of the source code of the application could attempt to locate dangerous classes in the code base. For example, suppose that somewhere in the application, the following class is defined:

[Serializable]
public class DangerousClass
{
    private string path;

	public DangerousClass(String path) {
		this.path = path;
	}

	public ~DangerousClass() {
		File.Delete(path)
	}
}


The attacker is then able to build such objects locally using an arbitrary path as a parameter, serialize it, and finally feed it to the vulnerable application. When said object is eventually removed from memory by the garbage collector, the attacker gains the ability to delete arbitrary files in the system.

Prevention

Never pass user-supplied input to BinaryFormatter; the documentation states this explicitly:

The BinaryFormatter type is dangerous and is not recommended for data processing. Applications should stop using BinaryFormatter as soon as possible, even if they believe the data they’re processing to be trustworthy. BinaryFormatter is insecure and can’t be made secure.

When possible, developers are encouraged to use other forms of data serialization, such as XML, JSON, or the BinaryReader and BinaryWriter classes. The latter is the recommended approach for binary serialization. For example, in the above scenario, the serialization phase could be implemented as:

var someObject = new SomeClass();
someObject.SomeProperty = "some value";
someObject.SomeOtherProperty = 3.14;

using (BinaryWriter writer = new BinaryWriter(File.Open("untrusted.file", FileMode.Create)))
{
    writer.Write(someObject.SomeProperty);
    writer.Write(someObject.SomeOtherProperty);
}


And in turn, the deserialization phase as:

var someObject = new SomeClass();
using (BinaryReader reader = new BinaryReader(File.Open("untrusted.file", FileMode.Open)))
{
	someObject.SomeProperty = reader.ReadString();
	someObject.SomeOtherProperty = reader.ReadDouble();
}

Unsafe Deserialization in Java

Java implements serialization natively for objects that implement the Serializable interface via the ObjectInputStream and ObjectOutputStream facilities. The binary format used directly references classes by name that are eventually loaded dynamically, provided that they are in the class path. This potentially allows instantiating objects of classes not initially intended by the developer, thus it is very important that untrusted data is not deserialized as is.

Developers may customize some aspects of the serialization process by providing callbacks such as writeReplace and readResolve. This could be exploited by an attacker to build chains by building complex objects that eventually lead to code execution or other actions on the target. Especially when complex and well-known libraries and frameworks are used, attackers may leverage publicly available tools such as ysoserial to easily craft the appropriate payload.

Vulnerable Example

The following Spring controller uses the data coming from the client request to deserialize an object:

@Controller
public class MyController {
    @RequestMapping(value = "/", method = GET)
    public String index(@CookieValue(value = "myCookie") String myCookieString) {
        // decode the Base64 cookie value
        byte[] myCookieBytes = Base64.getDecoder().decode(myCookieString);

        // use those bytes to deserialize an object
        ByteArrayInputStream buffer = new ByteArrayInputStream(myCookieBytes);
        try (ObjectInputStream stream = new ObjectInputStream(buffer)) {
            MyObject myObject = stream.readObject();

            // ...
        }
    }
}

Prevention

Never pass user-supplied input to the Java deserialization mechanism, and opt for data-only serialization formats such as JSON.

If the deserialization of untrusted data is really necessary, consider adopting an allow list approach to only allow objects of certain classes to be deserialized.

Since Java version 9, it has been possible to specify a deserialization filter in several ways. One example is to use the setObjectInputFilter method for ObjectInputStream objects before their use. The setObjectInputFilter method takes, as an argument, a method that implements the filtering logic. The following filter only allows one to deserialize instances of the MyClass class:

ObjectInputStream objectInputStream = new ObjectInputStream(buffer);
stream.setObjectInputFilter(MyFilter::myFilter);
Where:

public class MyFilter {
    static ObjectInputFilter.Status myFilter(ObjectInputFilter.FilterInfo info) {
        Class<?> serialClass = info.serialClass();
        if (serialClass != null) {
            return serialClass.getName().equals(MyClass.class.getName())
                    ? ObjectInputFilter.Status.ALLOWED
                    : ObjectInputFilter.Status.REJECTED;
        }
        return ObjectInputFilter.Status.UNDECIDED;
    }
}
Alternatively, it is possible to implement a similar solution by specializing the implementation of the ObjectInputStream object. The following snippet only allows one to deserialize instances of the MyClass class:

public class MyFilteringInputStream extends ObjectInputStream {
    public MyFilteringInputStream(InputStream inputStream) throws IOException {
        super(inputStream);
    }

    @Override
    protected Class<?> resolveClass(ObjectStreamClass objectStreamClass) throws IOException, ClassNotFoundException {
        if (!objectStreamClass.getName().equals(MyClass.class.getName())) {
            throw new InvalidClassException("Forbidden class", objectStreamClass.getName());
        }
        return super.resolveClass(objectStreamClass);
    }
}

It is then possible to invoke the deserialization in the usual way:

ObjectInputStream objectInputStream = new MyFilteringInputStream(buffer);
objectInputStream.readObject();

Unsafe Deserialization in NodeJS

Vulnerable example

Unlike PHP or Java, Node.js (JavaScript) does not provide advanced forms of object serialization, yet the JSON (JavaScript Object Notation) format is often used to convert JavaScript data object from/to a string representation. Before the relative recent addition of the JSON.parse method to ECMAScript, developers used to deserialize objects using the eval function. The following snippet illustrates this bad practice:

function myJSONParse(data) {
    return eval(`(${data})`);
}

If data is controlled by the attacker, it becomes trivial to inject arbitrary JavaScript code. For example, the following invocation executes a shell script that writes the output of the id command to /tmp/proof:

myJSONParse("require('child_process').exec('id > /tmp/proof')"

Prevention

The correct way to serialize and deserialize JavaScript objects is to use the provided JSON global object. For example:

const object = {foo: 123};
JSON.stringify(object) // '{"foo":123}'
JSON.parse('{"foo":123}') // { foo: 123 }

Unsafe Deserialization in PHP

PHP uses serialize() and unserialize() native functions to serialize and unserialize an object. For example, the following script creates an instance of the object FSResource, serializes it, and then prints the string representation of the object.

<?php

class FSResource {

    function __construct($path) {
        $this->path = $path;
        if (file_exists($path)) {
          $this->content = file_get_contents($path);
        }
    }

    function __destruct() {
        file_put_contents($this->path, $this->content);
    }

}

$resource = new FSResource('/tmp/file');
print(serialize($resource));

# Prints the following string representation:
# O:10:"FSResource":2:{s:4:"path";s:9:"/tmp/file";s:7:"content";s:0:"";}

The string representation can then be deserialized again to recreate the object instance and access its attributes.

$instance = unserialize('O:10:"FSResource":2:{s:4:"path";s:9:"/tmp/file";s:7:"content";s:0:"";}');
print($instance->path);

# Prints the path attribute:
# /tmp/file

Vulnerable Example

The exploitation of deserialization in PHP is called PHP Object Injection, which happens when user-controlled input is passed as the first argument of the unserialize() function. This is a vulnerable script.php:

<?php

$instance = unserialize($_GET["data"]);

To be exploitable, the vulnerable piece of code must have enough PHP code in scope to build a working POP chain, a chain of reusable PHP code that causes a meaningful impact when invoked. The chain usually starts by triggering __destroy() or __wakeup() PHP magic methods, called when the object is destroyed or deserialized, in order to call other gadgets to conduct malicious actions on the system.

If the class FSResource defined in the paragrah above is in scope, an attacker could send an HTTP request containing a serialized representation of an FSResource object that creates a malicious PHP file to path with an arbitrary content when the __destruct() magic method is called upon destruction.

http://localhost/script.php?data=O:10:%22FSResource%22:2:{s:4:%22path%22;s:9:%22shell.php%22;s:7:%22content%22;s:27:%22%3C?php%20system($_GET[%22cmd%22]);%22;}

The payload above decodes as O:10:"FSResource":2:{s:4:"path";s:9:"shell.php";s:7:"content";s:27:"<?php system($_GET["cmd"]);";} and, when deserialized, it creates the shell.php allowing the attacker to run arbitrary commands on the systems. More complex payloads can be built by chaining code from multiple classes or reusing public POP chains such as the ones included in the PHPGCC projects.

Prevention

Never use the unserialize() function on user-supplied input, and preferably use data-only serialization formats such as JSON. If you need to use PHP deserialization, a second optional parameter has been added in PHP 7 that enables you to specify an allow list of allowed classes.

Unsafe Deserialization in Python

Vulnerable example

Python provides a native solution for this problem - the pickle library. The following Flask endpoint provides an example where untrusted data is fed into the pickle.loads function:

import pickle

@app.route("/import_object", methods=['POST'])
def import_object():
    data = request.files.get('user_file').read()
    user_object = pickle.loads(data)
    store_in_database(user_object)
    return 'OK'


A malicious user could craft a payload that evaluates as code when unpickled. The Python program below outputs a payload that executes a system command when processed by pickle.loads:

import pickle
import os

class Pickle(object):
    def __reduce__(self):
        return os.system, ('id > /tmp/proof',)

o = Pickle()
p = pickle.dumps(o)
print(p)


The __reduce__ method provides the logic to unserialize/serialize the object. When a tuple is returned, the first element is a callable, and the second represents its argument. Thus, it is possible to execute system commands by using the os.system function. In the above case, the payload writes the output of the id command to /tmp/proof. Here is an example:

sf@secureflag.com:~$ python3 generate.py
b'\x80\x03cposix\nsystem\nq\x00X\x0f\x00\x00\x00id > /tmp/proofq\x01\x85q\x02Rq\x03.'
sf@secureflag.com:~$ python3
>>> import pickle
>>> pickle.loads(b'\x80\x03cposix\nsystem\nq\x00X\x0f\x00\x00\x00id > /tmp/proofq\x01\x85q\x02Rq\x03.')
0
sf@secureflag.com:~$ cat /tmp/proof
uid=1000(sf) gid=1000(sf) groups=1000(sf)

Prevention

The pickle library’s documentation discourages the unpickling of untrusted data and suggests using data-only serialization formats such as JSON.

If you really need to unserialize content from an untrusted source, consider implementing a message authentication code (MAC) to ensure the data integrity of the payload.

References

Unsafe Deserialization in Ruby



Ruby uses the Marshal library to serialize and unserialize objects. For example, the following script creates an instance of the object User, serializes it, and then prints the string representation of the object.

User = Struct.new(:name, :role)
user = User.new('Mike', :admin)
puts Marshal.dump(user).inspect

# Prints the following string representation:
# "\x04\bS:\tUser\a:\tnameI\"\tMike\x06:\x06ET:\trole:\nadmin"

The string representation can then be deserialized again to recreate the object instance and access its attributes.

user = Marshal.load("\x04\bS:\tUser\a:\tnameI\"\tMike\x06:\x06ET:\trole:\nadmin")
print(user.name);

# It prints the following string:
# Mike

Vulnerable Example

The exploitation of deserialization in Ruby happens when user-controlled input is passed as the first argument of the Marshal.load() function.

To be exploitable, the vulnerable piece of code must have enough Ruby code in scope to build a gadget chain, which means a chain of reusable code that causes a meaningful impact when invoked.

For example, assume that Marshal.load() deserializes user-provided data. An attacker could craft a malicious payload like the following one, which abuses an existing class to execute a command when deserialized.

class FSResource
  def initialize path
    @path    = path
  end

  def to_s
    open(@path).read
  end
end

# Craft the payload to execute `id` via the `open` function instead of opening a file
obj = FSResource.new('|id')
payload = Marshal.dump(obj)

# Unserializing the payload allows to execute arbitrary commands
serialized_obj = Marshal.load(payload)
puts serialized_obj

# It prints the output of id:
# uid=1002(admin) gid=1002(admin) groups=1002(admin)

A number of real code chains against Ruby and Ruby on Rails have been discovered and published by security researchers in the past.

References

Unsafe Deserialization in Scala

Scala (the same as Java) implements serialization natively for objects that implement the Serializable interface via the ObjectInputStream and ObjectOutputStream facilities. The binary format used directly references classes by name that are eventually loaded dynamically, provided that they are in the class path. This potentially allows instantiating objects of classes not initially intended by the developer, thus it is very important that untrusted data is not deserialized as is.

Developers may customize some aspects of the serialization process by providing callbacks such as writeReplace and readResolve. This could be exploited by an attacker to build chains by building complex objects that eventually lead to code execution or other actions on the target. Especially when complex and well-known libraries and frameworks are used, attackers may leverage publicly available tools such as ysoserial to easily craft the appropriate payload.

Vulnerable Example

The following Play controller uses the data coming from the client request to deserialize an object:

def handler() =
  AuthAction(parse.multipartFormData) { implicit request => {
    request.body.file("file") match {
      case Some(file) => {
        // deserialize data from Base64 file upload
        val base64Data = new String(Files.readAllBytes(Paths.get(file.ref.path.toString()))).trim()
        val data = Base64.getDecoder().decode(base64Data)
        val ois = new ObjectInputStream(new ByteArrayInputStream(data))
        val object = ois.readObject().asInstanceOf[MyClass]
        ois.close()

        // ...
      }
      case None => InternalServerError("...")
    }
  }
}

Prevention

Never pass user-supplied input to the Scala deserialization mechanism, and opt for data-only serialization formats such as JSON.

If the deserialization of untrusted data is really necessary, consider adopting an allow list approach to only allow objects of certain classes to be deserialized.

It is possible to specialize the implementation of the ObjectInputStream object. The following snippet only allows one to deserialize instances of the MyClass class:

class SafeInputStream(inputStream: InputStream) extends ObjectInputStream(inputStream) {
  override def resolveClass(objectStreamClass: java.io.ObjectStreamClass): Class[_] = {
    objectStreamClass.getName match {
      case "MyClass" | "scala.Some" | "scala.Option" => super.resolveClass(objectStreamClass)
      case _ => throw new InvalidClassException("Forbidden class", objectStreamClass.getName)
    }
  }
}

It is then possible to invoke the deserialization in the usual way:

val ois = new SafeInputStream(new ByteArrayInputStream(data))
val object = ois.readObject().asInstanceOf[MyClass]