Dangerous Pickles — Malicious Python Serialization

By Evan Sangaline | October 17, 2017
Follow @sangaline

What’s so dangerous about pickles?

Those pickles are very dangerous pickles. I literally can’t begin to tell you how really dangerous they are. You have to trust me on that. It’s important, Ok?

“Explosive Disorder” by Pan Telare

Before we get elbow deep in opcodes here, let’s cover a little background. The Python standard library has a module called pickle that is used for serializing and deserializing objects. Except it’s not called serializing and deserializing, it’s pickling and unpickling.

As someone who still suffers from the occasional nightmare about using Boost Serialization in C++, I’ve got to say that pickling is pretty great. No matter what you throw at it, it pretty much Just Works™. Not just with builtin types either—in most cases, you can serialize your own classes without needing to write any serialization picklization methods. Even “gotcha” objects like recursive data structures—which would cause a crash with the similar marshal module—are handled seamlessly.

To give a quick example for anybody who isn’t already familiar with the pickle module,

import pickle

# start with any instance of a Python type
original = { 'a': 0, 'b': [1, 2, 3] }

# turn it into a string
pickled = pickle.dumps(my_dict)

# turn it back into an identical object
identical = pickle.loads(pickled)

is all you need in most cases. It really is pretty great… but a darkness lurks beneath the surface.

One of the first lines of the pickle module’s documentation reads:

Warning: The pickle module is not secure against erroneous or maliciously constructed data. Never unpickle data received from an untrusted or unauthenticated source.

I’ve read that warning countless times, and often wondered in passing exactly what such maliciously constructed data would look like. Recently, I decided that it was time to finally find out. I’m really glad I did.

My quest to maliciously construct some data has led me down a path where I’ve learned a lot about how the pickling protocol works, discovered some really cool pickle debugging methods, and uncovered some pretty sassy comments in the Python source code. If you stick with me on this, you’ll hopefully share in those same benefits (and be sending people your own malicious pickle files in no time). Fair warning: this will get a bit technical… the only real prerequisite is knowing a little Python, but some basic familiarity with assembly wouldn’t hurt.

Not Much of a Pickle Bomb

I started out by reading through the pickle module’s documentation, hoping for any hints towards becoming an elite hacker, when one line immediately jumped out at me:

The module pickletools contains tools for analyzing data streams generated by pickle. pickletools source code has extensive comments about opcodes used by pickle protocols.

Opcodes? I wasn’t exactly expecting the pickle implementation to be

def dumps(obj):
    return obj.__repr__()

def loads(pickled):
    # Warning: the `pickle` module is not secure...
    return eval(pickled)

but I also wasn’t really expecting it to define it’s own low-level machine language either. Luckily, the other parts of that line are very true: the pickletools module is incredibly helpful in figuring out how the protocol works. Plus its code comments can be pretty funny.

Take, for example, the question of which protocol version we should focus on. There are five in total as of Python 3.6, numbered from 0 to 4. OK, protocol 0 is kind of the obvious choice because it’s described as “human-readable” in the docs, but the pickletools source code offers some additional insight:

Pickle opcodes never go away, not even when better ways to do a thing get invented. The repertoire of the PM just keeps growing over time… “Opcode bloat” isn’t so much a subtlety as a source of wearying complication.

It turns out that each new protocol is a superset of the previous, so even aside from protocol 0 being “human-readable”—which doesn’t really matter that much if we’re decompiling to instructions anyway—it also has the smallest number of allowable opcodes. That sounds ideal if the goal is to learn just enough to understand how malicious pickle files can be formed.

If you’re confused about this whole opcodes thing right now, don’t worry. We’ll bring this back to Python for now, and then I’ll explain things in detail from the perspective of how the opcodes equate to Python code a little later. Let’s make a nice simple Python class, no opcodes required.

class Bomb:
    def __init__(self, name):
        self.name = name

    def __getstate__(self):
        return self.name

    def __setstate__(self, state):
        self.name = state
        print(f'Bang! From, {self.name}.')

bomb = Bomb('Evan')

The __setstate__() and __getstate__() methods are used by the pickle module to serialize and deserialize classes. You often don’t need to define these yourself because the default implementations will just serialize the instance’s __dict__. As you can see, I’ve defined them explicitly here so that I can sneak in a little surprise for when the Bomb object is deserialized.

Let’s try it out and see if our deserialization surprise code works. We’ll pickle and unpickle the object with

import pickle

pickled_bomb = pickle.dumps(bomb, protocol=0)
unpickled_bomb = pickle.loads(pickled_bomb)

which outputs

Bang! From, Evan.

exactly according to plan! There’s just one problem: if we try to deserialize this pickled_bomb string in a context where Bomb isn’t defined then it won’t work. Instead, we’ll get the following error.

AttributeError: Can't get attribute 'Bomb' on <module '__main__'>

It turns out that we can only run our custom __setstate__() method if the unpickling context already has access to the code with our malicious print statement. If we have control over code that our victim is already running, then why even bother with this pickle stuff at all? We could just write our malicious code into any other method that they might use and we’re good to go. That’s exactly right, I just wanted to show this explicitly.

After all, it’s not totally unreasonable to suspect that Python might support pickling byte code for an object’s deserialization method. The marshal module can serialize methods for example, and many third party pickle alternatives, like marshmallow, dill, and pyro, also support function serialization. This, however, is not what that ominous warning in the pickle documentation is about. We’ll need to dig a little deeper in order to figure out what the real dangers of deserialization are.

Decompiling a Pickle

The time has come for us to try to figure out how pickling actually works. Let’s start by taking a look at our pickled_bomb object from the previous section.

b'ccopy_reg\n_reconstructor\np0\n(c__main__\nBomb\np1\nc__builtin__\nobject\np2\nNtp3\nRp4\nVEvan\np5\nb.'

Wait… we used protocol 0, right? If that’s “human-readable” then call me Floyd Mayweather Jr.

It’s OK though, the pickletools source code is supposed to have “extensive comments about opcodes used by pickle protocols.” Surely looking there will help us figure it out!

I despair of documenting this accurately and comprehensibly – you really have to read the pickle code to find all the special cases.

A comment in the pickletools source code

Oh, God. What have we gotten ourselves into?

Jokes aside, the pickletools source code actually is extremely well commented. The tools themselves are pretty darn helpful too. For example, they include a method for disassembling a pickle called pickletools.dis(). That will help us translate our pickle into something a little easier to understand.

To disassemble our pickled_bomb string, we simply have to run

import pickletools

pickletools.dis(pickled_bomb)

which will output

    0: c    GLOBAL     'copy_reg _reconstructor'
   25: p    PUT        0
   28: (    MARK
   29: c        GLOBAL     '__main__ Bomb'
   44: p        PUT        1
   47: c        GLOBAL     '__builtin__ object'
   67: p        PUT        2
   70: N        NONE
   71: t        TUPLE      (MARK at 28)
   72: p    PUT        3
   75: R    REDUCE
   76: p    PUT        4
   79: V    UNICODE    'Evan'
   85: p    PUT        5
   88: b    BUILD
   89: .    STOP
highest protocol among opcodes = 0

This should seem fairly familiar if you’ve ever dealt with any assembly languages (e.g. x86, Dalvik, CLR). It’s no biggie if you haven’t though, we’ll walk through this step-by-step. For now, just know that that the capitalized words, like GLOBAL, PUT, and MARK, are opcodes, or instructions, which are interpreted similarly to functions in higher-level languages. The stuff to the right of them are like arguments to those functions, and the stuff to the left just relates to how they were encoded in the original “human-readable” string.

Before we do that whole step-by-step thing though, let’s introduce another really nice utility provided by pickletools: pickletools.optimize(). This method removes unused opcodes from the pickle, so it will produce a simpler—but otherwise equivalent—pickle. We can disassemble an optimized version of pickled_bomb by running

pickled_bomb = pickletools.optimize(pickled_bomb)
pickletools.dis(pickled_bomb)

which produces a shorter and sweeter sequence of instructions.

    0: c    GLOBAL     'copy_reg _reconstructor'
   25: (    MARK
   26: c        GLOBAL     '__main__ Bomb'
   41: c        GLOBAL     '__builtin__ object'
   61: N        NONE
   62: t        TUPLE      (MARK at 25)
   63: R    REDUCE
   64: V    UNICODE    'Evan'
   70: b    BUILD
   71: .    STOP
highest protocol among opcodes = 0

You’ll notice that this is basically the original with all of the PUT opcodes removed. This brings us down to only 10 instruction steps that we need to understand. In a minute, we’ll go through these one by one and manually “decompile” them into Python code.

During unpickling, these opcodes are normally interpreted by something called the Pickle Machine (PM). Any pickle is effectively a program that runs on the PM in much the same way that compiled Java code runs on the Java Virtual Machine (JVM). In order to decompile our pickle code, we’ll need to first understand a little bit about how the PM works.

The PM has two areas where data can be stored and interacted with: the memo and the stack. The memo is for longterm storage and can be thought of as a Python dictionary mapping integers to objects. The stack is like a Python list which many operations interact with by appending or popping things. We can emulate these two data areas in Python as follows.

# the PM's longterm memory/storage
memo = {}
# the PM's stack, which most opcodes interact with
stack = []

During unpickling, the PM reads in a pickle program and performs each instruction in sequence. It terminates whenever it reaches a STOP opcode; whatever object is on top of the stack at that point is the final result of unpickling. Using our emulated memo and stack storage areas, let’s try to translate our pickle into Python… instruction by instruction.

  1. GLOBAL pushes either a class or a function on the stack given it’s module and name as arguments. Note that the disassembler message here is slightly misleading because copy_reg was renamed copyreg in Python 3.

    # Push a global object (module.attr) on the stack.
    #  0: c    GLOBAL     'copy_reg _reconstructor'
    from copyreg import _reconstructor
    stack.append(_reconstructor)
    
  2. MARK pushes a special markobject onto the stack so that we can later use it to specify a slice of the stack. We’ll just use the string “MARK” here to represent a markobject.

    # Push markobject onto the stack.
    # 25: (    MARK
    stack.append('MARK')
    
  3. GLOBAL again, but this time the module is __main__ so we don’t need to actually perform the import.

    # Push a global object (module.attr) on the stack.
    # 26: c        GLOBAL     '__main__ Bomb'
    stack.append(Bomb)
    
  4. GLOBAL again, we also don’t need to explicitly import object.

    # Push a global object (module.attr) on the stack.
    # 41: c        GLOBAL     '__builtin__ object'
    stack.append(object)
    
  5. NONE just pushes None to the stack.

    # Push None on the stack.
    # 61: N        NONE
    stack.append(None)
    
  6. TUPLE is a little bit trickier. Remember how we added that “MARK” to the stack previously? This operation is going to remove everything in the stack since that “MARK” and place it in a tuple. It will then remove the “MARK”, and replace it with the tuple.

    # Build a tuple out of the topmost stack slice, after markobject.
    # 62: t        TUPLE      (MARK at 28)
    last_mark_index = len(stack) - 1 - stack[::-1].index('MARK')
    mark_tuple = tuple(stack[last_mark_index + 1:])
    stack = stack[:last_mark_index] + [mark_tuple]
    

    It might be helpful to see explicitly how this mutates the stack.

    # the stack before the TUPLE operation:
    [<function copyreg._reconstructor>, 'MARK', __main__.Bomb, object, None]
    # the stack after the TUPLE operation:
    [<function copyreg._reconstructor>, (__main__.Bomb, object, None)]
    
  7. REDUCE removes the last two things from the stack. It then calls the object that was second to last using positional expansion of the thing that was last and places the result onto the stack. That’s kind of a mouthful, but it’s easy to understand in code.

    # Push an object built from a callable and an argument tuple.
    # 63: R    REDUCE
    args = stack.pop()
    callable = stack.pop()
    stack.append(callable(*args))
    
  8. UNICODE just pushes a unicode string onto the stack (and a particularly fine unicode string it is, too!).

    # Push a Python Unicode string object.
    # 64: V    UNICODE    'Evan'
    stack.append(u'Evan')
    
  9. BUILD pops the last thing off of the stack and then passes it as an argument to __setstate__() on the newly last thing on the stack.

    # Finish building an object, via __setstate__ or dict update.
    # 70: b    BUILD
    arg = stack.pop()
    stack[-1].__setstate__(arg)
    
  10. STOP just means that whatever is on top of the stack is our final result.

    # Stop the unpickling machine.
    # 71: .    STOP
    unpickled_bomb = stack[-1]
    

Phew, we did it! I’m not sure if our code is the most Pythonic… but it does emulate how the PM would do things. You might have noticed that we never used memo. Remember all of those PUT opcodes that were removed by pickletools.optimize()? Those would have involved interactions with memo, but they weren’t actually required in this simple example.

We’re over the hump here, but let’s try to simplify the code a bit to make what it’s doing a little more obvious. There are really only three operations here where anything other than data shuffling happens: the import of _reconstructor in instruction 1, the call to _reconstructor in instruction 7, and the call to __setstate__() in instruction 9. If we manage the data shuffling in our heads, then we can express this in just three lines of Python.

# Instruction 1, where `_reconstructor` was imported
from copyreg import _reconstructor

# Instruction 7, where `_reconstructor` was called
unpickled_bomb = _reconstructor(cls=Bomb, base=object, state=None)
# Instruction 9, where `__setstate__` was called
unpickled_bomb.__setstate__('Evan')

A look inside the source code for copyreg._reconstructor() reveals that it’s basically just calling object.__new__(Bomb). Using this fact, we can simplify this even further to just two lines.

unpickled_bomb = object.__new__(Bomb)
unpickled_bomb.__setstate__('Evan')

Congratulations, you just decompiled a pickle.

An Actual Pickle Bomb

I’m no pickle expert, but at this point I think I can see the basic picture of how one could construct a malicious pickle. The GLOBAL opcode can be used to import any function we want—os.system and __builtin__.eval seem like charming candidates—and then REDUCE can be used to execute it with an arbitrary argument. Except… wait, what’s this?

If not isinstance(callable, type), REDUCE complains unless the callable has been registered with the copyreg module’s safe_constructors dict, or the callable has a magic __safe_for_unpickling__ attribute with a true value. I’m not sure why it does this, but I’ve sure seen this complaint often enough when I didn’t want to <wink>.

Wink. Wink. The pickletools documentation seems to suggest that only whitelisted callables can be executed by REDUCE. This had me worried for a minute when I first read this, but upon Googling for “safe_constructors”, I quickly found PEP 307 from 2003.

In previous versions of Python, unpickling would do a “safety check” on certain operations, refusing to call functions or constructors that weren’t marked as “safe for unpickling” by either having an attribute __safe_for_unpickling__ set to 1, or by being registered in a global registry, copy_reg.safe_constructors.

This feature gives a false sense of security: nobody has ever done the necessary, extensive, code audit to prove that unpickling untrusted pickles cannot invoke unwanted code, and in fact bugs in the Python 2.2 pickle.py module make it easy to circumvent these security measures.

We firmly believe that, on the Internet, it is better to know that you are using an insecure protocol than to trust a protocol to be secure whose implementation hasn’t been thoroughly checked. Even high quality implementations of widely used protocols are routinely found flawed; Python’s pickle implementation simply cannot make such guarantees without a much larger time investment. Therefore, as of Python 2.3, all safety checks on unpickling are officially removed, and replaced with this warning:

Warning: Do not unpickle data received from an untrusted or unauthenticated source.

Hello darkness, our old friend. This is where it all began.

So that’s it, we’ve got all of our key ingredients and there are no false senses of security to protect against what we’re about to do. Let’s start by writing our bomb out in assembly:

# add a function to the stack to execute arbitrary python
GLOBAL     '__builtin__ eval'
# mark the start of our args tuple
MARK
    # add the Python code that we want to execute to the stack
    UNICODE    'print("Bang! From, Evan.")'
    # wrap that code into a tuple so it can be parsed by REDUCE
    TUPLE
# call `eval()` with our Python code as an argument
REDUCE
# STOP is required to be valid PM code
STOP

Now to convert this into an actual pickle, we need to replace each opcode with its corresponding ASCII code: c for GLOBAL, ( for MARK, V for UNICODE, t for TUPLE, R for REDUCE, and . for STOP. Note that these are the same characters that were to the left of the opcodes in the pickletools.dis() output from before. Arguments are parsed after each opcode according to a combination of position and newline delimitation. Each argument is placed either directly after its corresponding opcode or the previous argument, and it is read continuously until a newline character is encountered. Translating our above assembly into pickle machine code therefore gives us:

c__builtin__
eval
(Vprint("Bang! From, Evan.")
tR.

Finaaaallly, we can try this out:

# Run me at home!
# I'm safe, I promise!
pickled_bomb = b'c__builtin__\neval\n(Vprint("Bang! From, Evan.")\ntR.'
pickle.loads(pickled_bomb)

and…

Bang! From, Evan.

I know that you have no reason to believe me, but that actually worked on the first try.

It’s pretty easy to see from here how somebody might come up with a more malicious argument to eval(). The PM can be coerced into doing literally anything that Python code can do, including running system commands via os.system().

All Good Things Must Come to an End

I set out to discover how to make a dangerous pickle, and I accidentally learned how pickles work in the process. I’ve got to say, I found the whole Pickle Machine thing pretty fun to dig into. The pickletools source code was a huge help in figuring this stuff out, and I highly recommend it if you’re interested in learning more about the pickle protocol or the PM.

If you ever need somebody to exploit a vulnerable protocol to inject malicious shellcode, then please don’t hesitate to get in touch with us. Just kidding… we do data stuff. But if you’re looking for some help with web scraping, machine learning, or anything else that programmers can do for money, then seriously do get in touch!

Suggested Articles

If you enjoyed this article, then you might also enjoy these related ones.

A Brief Tour of Grouping and Aggregating in Pandas

By Andre Perunicic
on October 13, 2017

Learn how to use pandas to easily slice up a dataset and quickly extract useful statistics.

Read more

Analyzing One Million robots.txt Files

By Evan Sangaline
on September 19, 2017

Insights gathered from analyzing the robots.txt files of Alexa's top one million domains.

Read more

Fantasy Football for Hackers

By Evan Sangaline
on September 7, 2017

Building a draft strategy from the ground up.

Read more

Comments